Skip to content

Commit

Permalink
More interpolation and concatenation improvements.
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrolGenhald committed Jun 25, 2022
1 parent 450409f commit 2559222
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Psalm\Internal\Analyzer\Statements\Expression;

use PhpParser;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\BinaryOp\Equal;
use PhpParser\Node\Expr\BinaryOp\Greater;
use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual;
Expand Down Expand Up @@ -103,6 +104,7 @@ class AssertionFinder
'is_iterable' => ['iterable'],
'is_countable' => ['countable'],
];

/**
* Gets all the type assertions in a conditional
*
Expand Down Expand Up @@ -1499,50 +1501,35 @@ protected static function hasNonEmptyCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_count
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
if ($conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& $conditional->left->getArgs();

$operator_greater_than_or_equal =
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual;

if ($left_count
&& $conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $operator_greater_than_or_equal
&& $conditional->right->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
? 0
: 1
)
&& $conditional->left->getArgs()
&& ($conditional instanceof BinaryOp\Greater || $conditional instanceof BinaryOp\GreaterOrEqual)
) {
$min_count = $conditional->right->value +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0);

return self::ASSIGNMENT_TO_RIGHT;
}

$right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall
$assignment_to = self::ASSIGNMENT_TO_RIGHT;
$compare_to = $conditional->right;
$comparison_adjustment = $conditional instanceof BinaryOp\Greater ? 1 : 0;
} elseif ($conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->parts[0]) === 'count'
&& $conditional->right->getArgs();

$operator_less_than_or_equal =
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual;
&& $conditional->right->getArgs()
&& ($conditional instanceof BinaryOp\Smaller || $conditional instanceof BinaryOp\SmallerOrEqual)
) {
$assignment_to = self::ASSIGNMENT_TO_LEFT;
$compare_to = $conditional->left;
$comparison_adjustment = $conditional instanceof BinaryOp\Smaller ? 1 : 0;
} else {
return false;
}

if ($right_count
&& $conditional->left instanceof PhpParser\Node\Scalar\LNumber
&& $operator_less_than_or_equal
&& $conditional->left->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 0 : 1
)
// TODO get node type provider here somehow and check literal ints and int ranges
if ($compare_to instanceof PhpParser\Node\Scalar\LNumber
&& $compare_to->value > (-1 * $comparison_adjustment)
) {
$min_count = $conditional->left->value +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0);
$min_count = $compare_to->value + $comparison_adjustment;

return self::ASSIGNMENT_TO_LEFT;
return $assignment_to;
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TLowercaseString;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Atomic\TNonspecificLiteralInt;
use Psalm\Type\Atomic\TNonspecificLiteralString;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TString;
Expand All @@ -52,6 +54,8 @@
*/
class ConcatAnalyzer
{
private const MAX_LITERALS = 64;

/**
* @param Union|null $result_type
*/
Expand Down Expand Up @@ -155,39 +159,35 @@ public static function analyze(
self::analyzeOperand($statements_analyzer, $left, $left_type, 'Left', $context);
self::analyzeOperand($statements_analyzer, $right, $right_type, 'Right', $context);

// If one of the types is a single int or string literal, and the other
// type is all string or int literals, combine them into new literal(s).
// If both types are specific literals, combine them into new literals
$literal_concat = false;

if (($left_type->allStringLiterals() || $left_type->allIntLiterals())
&& ($right_type->allStringLiterals() || $right_type->allIntLiterals())
) {
$literal_concat = true;
$result_type_parts = [];

foreach ($left_type->getAtomicTypes() as $left_type_part) {
assert($left_type_part instanceof TLiteralString || $left_type_part instanceof TLiteralInt);
foreach ($right_type->getAtomicTypes() as $right_type_part) {
assert($right_type_part instanceof TLiteralString || $right_type_part instanceof TLiteralInt);
$literal = $left_type_part->value . $right_type_part->value;
if (strlen($literal) >= $config->max_string_length) {
// Literal too long, use non-literal type instead
$literal_concat = false;
break 2;
if ($left_type->allSpecificLiterals() && $right_type->allSpecificLiterals()) {
$left_type_parts = $left_type->getAtomicTypes();
$right_type_parts = $right_type->getAtomicTypes();
$combinations = count($left_type_parts) * count($right_type_parts);
if ($combinations < self::MAX_LITERALS) {
$literal_concat = true;
$result_type_parts = [];

foreach ($left_type->getAtomicTypes() as $left_type_part) {
foreach ($right_type->getAtomicTypes() as $right_type_part) {
$literal = $left_type_part->value . $right_type_part->value;
if (strlen($literal) >= $config->max_string_length) {
// Literal too long, use non-literal type instead
$literal_concat = false;
break 2;
}

$result_type_parts[] = new TLiteralString($literal);
}

$result_type_parts[] = new TLiteralString($literal);
}
}

if (!empty($result_type_parts)) {
if ($literal_concat && count($result_type_parts) < 64) {
if ($literal_concat) {
assert(count($result_type_parts) === $combinations);
assert(count($result_type_parts) !== 0); // #8163
$result_type = new Union($result_type_parts);
} else {
$result_type = new Union([new TNonEmptyNonspecificLiteralString]);
}

return;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
Expand Down Expand Up @@ -327,7 +328,7 @@ public static function castStringAttempt(
|| $atomic_type instanceof TInt
|| $atomic_type instanceof TNumeric
) {
if ($atomic_type instanceof TLiteralInt) {
if ($atomic_type instanceof TLiteralInt || $atomic_type instanceof TLiteralFloat) {
$castable_types[] = new TLiteralString((string) $atomic_type->value);
} elseif ($atomic_type instanceof TNonspecificLiteralInt) {
$castable_types[] = new TNonspecificLiteralString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Atomic\TNonspecificLiteralInt;
use Psalm\Type\Union;

use function in_array;
Expand All @@ -33,12 +37,6 @@ public static function analyze(
$literal_string = "";

foreach ($stmt->parts as $part) {
if ($part instanceof PhpParser\Node\Scalar\EncapsedStringPart
&& $part->value
) {
$non_empty = true;
}

if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) {
return false;
}
Expand All @@ -55,11 +53,22 @@ public static function analyze(

if (!$casted_part_type->allLiterals()) {
$all_literals = false;
} elseif (!$non_empty) {
// Check if all literals are nonempty
foreach ($casted_part_type->getAtomicTypes() as $atomic_literal) {
$non_empty = $atomic_literal instanceof TLiteralInt
|| $atomic_literal instanceof TNonspecificLiteralInt
|| $atomic_literal instanceof TLiteralFloat
|| $atomic_literal instanceof TNonEmptyNonspecificLiteralString
|| ($atomic_literal instanceof TLiteralString && $atomic_literal->value !== "")
;
}
}

if ($literal_string !== null) {
if ($casted_part_type->isSingleLiteral()) {
$literal_string .= $casted_part_type->getSingleLiteral();
$literal_string .= $casted_part_type->getSingleLiteral()->value;
if (!$non_empty && $literal_string !== "") {}
} else {
$literal_string = null;
}
Expand Down Expand Up @@ -97,6 +106,7 @@ public static function analyze(
if ($literal_string !== null) {
$literal_string .= $part->value;
}
$non_empty = $non_empty || $part->value !== "";
} else {
$all_literals = false;
$literal_string = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev

if (isset($atomic_type->properties['options'])
&& $atomic_type->properties['options']->hasArray()
&& ($options_array = $atomic_type->properties['options']->getAtomicTypes()['array'])
&& ($options_array = $atomic_type->properties['options']->getAtomicTypes()['array'] ?? null)
&& $options_array instanceof TKeyedArray
&& isset($options_array->properties['default'])
) {
Expand Down
2 changes: 2 additions & 0 deletions src/Psalm/Type/Atomic/TFalse.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/
class TFalse extends TBool
{
public bool $value = false;

public function __toString(): string
{
return 'false';
Expand Down
2 changes: 2 additions & 0 deletions src/Psalm/Type/Atomic/TTrue.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/
class TTrue extends TBool
{
public bool $value = true;

public function __toString(): string
{
return 'true';
Expand Down
29 changes: 29 additions & 0 deletions src/Psalm/Type/Union.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ public function replaceTypes(array $types): void
}

/**
* @psalm-mutation-free
* @return non-empty-array<string, Atomic>
*/
public function getAtomicTypes(): array
Expand Down Expand Up @@ -1302,6 +1303,34 @@ public function allIntLiterals(): bool
return true;
}

/**
* @psalm-assert-if-true array<
* array-key,
* TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue
* > $this->getAtomicTypes()
*/
public function allSpecificLiterals(): bool
{
foreach ($this->types as $atomic_key_type) {
if (!$atomic_key_type instanceof TLiteralString
&& !$atomic_key_type instanceof TLiteralInt
&& !$atomic_key_type instanceof TLiteralFloat
&& !$atomic_key_type instanceof TFalse
&& !$atomic_key_type instanceof TTrue
) {
return false;
}
}

return true;
}

/**
* @psalm-assert-if-true array<
* array-key,
* TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue
* > $this->getAtomicTypes()
*/
public function allLiterals(): bool
{
foreach ($this->types as $atomic_key_type) {
Expand Down
34 changes: 34 additions & 0 deletions tests/BinaryOperationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,40 @@ function foo(string $s1): string {
return "Hello $s1 $s2";
}',
],
'encapsedStringIsInferredAsLiteral' => [
'<?php
$int = 1;
$float = 2.3;
$string = "foobar";
$interpolated = "{$int}{$float}{$string}";
',
'assertions' => ['$interpolated===' => '"12.3foobar"'],
],
'concatenatedStringIsInferredAsLiteral' => [
'<?php
$int = 1;
$float = 2.3;
$string = "foobar";
$concatenated = $int . $float . $string;
',
'assertions' => ['$concatenated===' => '"12.3foobar"'],
],
'encapsedNonEmptyNonSpecificLiteralString' => [
'<?php
/** @var non-empty-literal-string */
$string = "foobar";
$interpolated = "$string";
',
'assertions' => ['$interpolated===' => 'non-empty-literal-string'],
],
'concatenatedNonEmptyNonSpecificLiteralString' => [
'<?php
/** @var non-empty-literal-string */
$string = "foobar";
$concatenated = $string . "";
',
'assertions' => ['$concatenated===' => 'non-empty-literal-string'],
],
'literalIntConcatCreatesLiteral' => [
'<?php
/**
Expand Down
29 changes: 29 additions & 0 deletions tests/TypeReconciliation/EmptyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,35 @@ function foo(array $arr): void {
if (empty($arr["a"])) {}
}'
],
'SKIPPED-countWithLiteralIntVariable' => [ // #8163
'<?php
$c = 1;
/** @var list<int> */
$arr = [1];
assert(count($arr) === $c);
',
'assertions' => ['$arr===' => 'non-empty-list<int>'],
],
'SKIPPED-countWithIntRange' => [ // #8163
'<?php
/** @var int<1, max> */
$c = 1;
/** @var list<int> */
$arr = [1];
assert(count($arr) === $c);
',
'assertions' => ['$arr===' => 'non-empty-list<int>'],
],
'SKIPPED-countEmptyWithIntRange' => [ // #8163
'<?php
/** @var int<0, max> */
$c = 1;
/** @var list<int> */
$arr = [1];
assert(count($arr) === $c);
',
'assertions' => ['$arr===' => 'list<int>'],
],
];
}

Expand Down

0 comments on commit 2559222

Please sign in to comment.