mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Merge pull request #8164 from AndrolGenhald/encapsed-literal-strings
Encapsed literal strings
This commit is contained in:
commit
b3038f0936
@ -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;
|
||||
@ -103,6 +104,7 @@ class AssertionFinder
|
||||
'is_iterable' => ['iterable'],
|
||||
'is_countable' => ['countable'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets all the type assertions in a conditional
|
||||
*
|
||||
@ -1499,50 +1501,35 @@ class AssertionFinder
|
||||
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;
|
||||
|
||||
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
|
||||
)
|
||||
&& $conditional->right->getArgs()
|
||||
&& ($conditional instanceof BinaryOp\Smaller || $conditional instanceof BinaryOp\SmallerOrEqual)
|
||||
) {
|
||||
$min_count = $conditional->left->value +
|
||||
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0);
|
||||
$assignment_to = self::ASSIGNMENT_TO_LEFT;
|
||||
$compare_to = $conditional->left;
|
||||
$comparison_adjustment = $conditional instanceof BinaryOp\Smaller ? 1 : 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::ASSIGNMENT_TO_LEFT;
|
||||
// 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 = $compare_to->value + $comparison_adjustment;
|
||||
|
||||
return $assignment_to;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -52,6 +52,8 @@ use function strlen;
|
||||
*/
|
||||
class ConcatAnalyzer
|
||||
{
|
||||
private const MAX_LITERALS = 64;
|
||||
|
||||
/**
|
||||
* @param Union|null $result_type
|
||||
*/
|
||||
@ -155,39 +157,35 @@ class ConcatAnalyzer
|
||||
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 = [];
|
||||
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) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ use Psalm\Type\Atomic\TFloat;
|
||||
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;
|
||||
@ -327,7 +328,7 @@ class CastAnalyzer
|
||||
|| $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();
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression;
|
||||
|
||||
use PhpParser;
|
||||
use PhpParser\Node\Scalar\EncapsedStringPart;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
@ -10,10 +11,16 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
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\Atomic\TNonspecificLiteralString;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function assert;
|
||||
use function in_array;
|
||||
|
||||
class EncapsulatedStringAnalyzer
|
||||
@ -29,20 +36,16 @@ class EncapsulatedStringAnalyzer
|
||||
|
||||
$all_literals = true;
|
||||
|
||||
foreach ($stmt->parts as $part) {
|
||||
if ($part instanceof PhpParser\Node\Scalar\EncapsedStringPart
|
||||
&& $part->value
|
||||
) {
|
||||
$non_empty = true;
|
||||
}
|
||||
$literal_string = "";
|
||||
|
||||
foreach ($stmt->parts as $part) {
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$part_type = $statements_analyzer->node_data->getType($part);
|
||||
|
||||
if ($part_type) {
|
||||
if ($part_type !== null) {
|
||||
$casted_part_type = CastAnalyzer::castStringAttempt(
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
@ -52,6 +55,28 @@ class EncapsulatedStringAnalyzer
|
||||
|
||||
if (!$casted_part_type->allLiterals()) {
|
||||
$all_literals = false;
|
||||
} elseif (!$non_empty) {
|
||||
// Check if all literals are nonempty
|
||||
$non_empty = true;
|
||||
foreach ($casted_part_type->getAtomicTypes() as $atomic_literal) {
|
||||
if (!$atomic_literal instanceof TLiteralInt
|
||||
&& !$atomic_literal instanceof TNonspecificLiteralInt
|
||||
&& !$atomic_literal instanceof TLiteralFloat
|
||||
&& !$atomic_literal instanceof TNonEmptyNonspecificLiteralString
|
||||
&& !($atomic_literal instanceof TLiteralString && $atomic_literal->value !== "")
|
||||
) {
|
||||
$non_empty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($literal_string !== null) {
|
||||
if ($casted_part_type->isSingleLiteral()) {
|
||||
$literal_string .= $casted_part_type->getSingleLiteral()->value;
|
||||
} else {
|
||||
$literal_string = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($statements_analyzer->data_flow_graph
|
||||
@ -82,16 +107,30 @@ class EncapsulatedStringAnalyzer
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($part instanceof EncapsedStringPart) {
|
||||
if ($literal_string !== null) {
|
||||
$literal_string .= $part->value;
|
||||
}
|
||||
$non_empty = $non_empty || $part->value !== "";
|
||||
} else {
|
||||
$all_literals = false;
|
||||
$literal_string = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($non_empty) {
|
||||
if ($all_literals) {
|
||||
if ($literal_string !== null) {
|
||||
$new_type = Type::getString($literal_string);
|
||||
} elseif ($all_literals) {
|
||||
$new_type = new Union([new TNonEmptyNonspecificLiteralString()]);
|
||||
} else {
|
||||
$new_type = new Union([new TNonEmptyString()]);
|
||||
}
|
||||
|
||||
} elseif ($all_literals) {
|
||||
$new_type = new Union([new TNonspecificLiteralString()]);
|
||||
}
|
||||
if (isset($new_type)) {
|
||||
assert($new_type instanceof Union);
|
||||
$new_type->parent_nodes = $stmt_type->parent_nodes;
|
||||
$stmt_type = $new_type;
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ class FilterVarReturnTypeProvider implements FunctionReturnTypeProviderInterface
|
||||
|
||||
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'])
|
||||
) {
|
||||
|
@ -7,6 +7,9 @@ namespace Psalm\Type\Atomic;
|
||||
*/
|
||||
class TFalse extends TBool
|
||||
{
|
||||
/** @var false */
|
||||
public $value = false;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'false';
|
||||
|
@ -7,6 +7,9 @@ namespace Psalm\Type\Atomic;
|
||||
*/
|
||||
class TTrue extends TBool
|
||||
{
|
||||
/** @var true */
|
||||
public $value = true;
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'true';
|
||||
|
@ -270,6 +270,7 @@ class Union implements TypeNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return non-empty-array<string, Atomic>
|
||||
*/
|
||||
public function getAtomicTypes(): array
|
||||
@ -1302,6 +1303,34 @@ class Union implements TypeNode
|
||||
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) {
|
||||
@ -1329,6 +1358,32 @@ class Union implements TypeNode
|
||||
|| isset($this->types['true']);
|
||||
}
|
||||
|
||||
public function isSingleLiteral(): bool
|
||||
{
|
||||
return count($this->types) === 1
|
||||
&& count($this->literal_int_types)
|
||||
+ count($this->literal_string_types)
|
||||
+ count($this->literal_float_types) === 1
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TLiteralInt|TLiteralString|TLiteralFloat
|
||||
*/
|
||||
public function getSingleLiteral()
|
||||
{
|
||||
if (!$this->isSingleLiteral()) {
|
||||
throw new InvalidArgumentException("Not a single literal");
|
||||
}
|
||||
|
||||
return ($literal = reset($this->literal_int_types)) !== false
|
||||
? $literal
|
||||
: (($literal = reset($this->literal_string_types)) !== false
|
||||
? $literal
|
||||
: reset($this->literal_float_types))
|
||||
;
|
||||
}
|
||||
|
||||
public function hasLiteralString(): bool
|
||||
{
|
||||
return count($this->literal_string_types) > 0;
|
||||
|
@ -686,6 +686,50 @@ class BinaryOperationTest extends TestCase
|
||||
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'],
|
||||
],
|
||||
'encapsedPossiblyEmptyLiteralString' => [
|
||||
'<?php
|
||||
/** @var "foo"|"" */
|
||||
$foo = "";
|
||||
/** @var "bar"|"" */
|
||||
$bar = "";
|
||||
$interpolated = "{$foo}{$bar}";
|
||||
',
|
||||
'assertions' => ['$interpolated===' => 'literal-string'],
|
||||
],
|
||||
'literalIntConcatCreatesLiteral' => [
|
||||
'<?php
|
||||
/**
|
||||
|
@ -418,6 +418,35 @@ class EmptyTest extends TestCase
|
||||
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>'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user