mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add support for literal-int annotations as well
This commit is contained in:
parent
c3fdfc5795
commit
9dde8eed9d
@ -146,10 +146,8 @@ class ConcatAnalyzer
|
||||
// type is all string or int literals, combine them into new literal(s).
|
||||
$literal_concat = false;
|
||||
|
||||
if ((($left_type->isSingleStringLiteral() || $left_type->isSingleIntLiteral())
|
||||
&& ($right_type->allStringLiterals() || $right_type->allIntLiterals()))
|
||||
|| (($right_type->isSingleStringLiteral() || $right_type->isSingleIntLiteral())
|
||||
&& ($left_type->allStringLiterals() || $left_type->allIntLiterals()))
|
||||
if (($left_type->allStringLiterals() || $left_type->allIntLiterals())
|
||||
&& ($right_type->allStringLiterals() || $right_type->allIntLiterals())
|
||||
) {
|
||||
$literal_concat = true;
|
||||
$result_type_parts = [];
|
||||
@ -170,7 +168,7 @@ class ConcatAnalyzer
|
||||
}
|
||||
|
||||
if (!empty($result_type_parts)) {
|
||||
if ($literal_concat) {
|
||||
if ($literal_concat && count($result_type_parts) < 64) {
|
||||
$result_type = new Type\Union($result_type_parts);
|
||||
} else {
|
||||
$result_type = new Type\Union([new Type\Atomic\TNonEmptyNonspecificLiteralString]);
|
||||
@ -189,6 +187,8 @@ class ConcatAnalyzer
|
||||
$left_type,
|
||||
$numeric_type
|
||||
);
|
||||
|
||||
if ($left_is_numeric) {
|
||||
$right_uint = Type::getPositiveInt();
|
||||
$right_uint->addType(new Type\Atomic\TLiteralInt(0));
|
||||
$right_is_uint = UnionTypeComparator::isContainedBy(
|
||||
@ -197,19 +197,20 @@ class ConcatAnalyzer
|
||||
$right_uint
|
||||
);
|
||||
|
||||
if ($left_is_numeric && $right_is_uint) {
|
||||
if ($right_is_uint) {
|
||||
$result_type = Type::getNumericString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$lowercase_type = clone $numeric_type;
|
||||
$lowercase_type->addType(new Type\Atomic\TLowercaseString());
|
||||
$left_is_lowercase = UnionTypeComparator::isContainedBy(
|
||||
|
||||
$all_lowercase = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$left_type,
|
||||
$lowercase_type
|
||||
);
|
||||
$right_is_lowercase = UnionTypeComparator::isContainedBy(
|
||||
) && UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$right_type,
|
||||
$lowercase_type
|
||||
@ -217,34 +218,31 @@ class ConcatAnalyzer
|
||||
|
||||
$non_empty_string = Type::getNonEmptyString();
|
||||
|
||||
$left_is_non_empty = UnionTypeComparator::isContainedBy(
|
||||
$has_non_empty = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$left_type,
|
||||
$non_empty_string
|
||||
);
|
||||
$right_is_non_empty = UnionTypeComparator::isContainedBy(
|
||||
) || UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$right_type,
|
||||
$non_empty_string
|
||||
);
|
||||
|
||||
if ($left_is_lowercase && $right_is_lowercase) {
|
||||
$result_type = $left_is_non_empty || $right_is_non_empty
|
||||
? Type::getNonEmptyLowercaseString()
|
||||
: Type::getLowercaseString();
|
||||
$all_literals = $left_type->allLiterals() && $right_type->allLiterals();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($left_is_non_empty || $right_is_non_empty) {
|
||||
if ($left_type->allLiterals() && $right_type->allLiterals()) {
|
||||
if ($has_non_empty) {
|
||||
if ($all_literals) {
|
||||
$result_type = new Type\Union([new Type\Atomic\TNonEmptyNonspecificLiteralString]);
|
||||
} elseif ($all_lowercase) {
|
||||
$result_type = Type::getNonEmptyLowercaseString();
|
||||
} else {
|
||||
$result_type = Type::getNonEmptyString();
|
||||
}
|
||||
} else {
|
||||
if ($left_type->allLiterals() && $right_type->allLiterals()) {
|
||||
if ($all_literals) {
|
||||
$result_type = new Type\Union([new Type\Atomic\TNonspecificLiteralString]);
|
||||
} elseif ($all_lowercase) {
|
||||
$result_type = Type::getLowercaseString();
|
||||
} else {
|
||||
$result_type = Type::getString();
|
||||
}
|
||||
|
@ -372,6 +372,8 @@ class CastAnalyzer
|
||||
) {
|
||||
if ($atomic_type instanceof Type\Atomic\TLiteralInt) {
|
||||
$castable_types[] = new Type\Atomic\TLiteralString((string) $atomic_type->value);
|
||||
} elseif ($atomic_type instanceof Type\Atomic\TNonspecificLiteralInt) {
|
||||
$castable_types[] = new Type\Atomic\TNonspecificLiteralString();
|
||||
} else {
|
||||
$castable_types[] = new Type\Atomic\TNumericString();
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString;
|
||||
use Psalm\Type\Atomic\TNonEmptyString;
|
||||
use Psalm\Type\Atomic\TNonFalsyString;
|
||||
use Psalm\Type\Atomic\TNonspecificLiteralInt;
|
||||
use Psalm\Type\Atomic\TNonspecificLiteralString;
|
||||
use Psalm\Type\Atomic\TNumeric;
|
||||
use Psalm\Type\Atomic\TNumericString;
|
||||
@ -94,9 +95,28 @@ class ScalarTypeComparator
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TNonspecificLiteralString) {
|
||||
if ($input_type_part instanceof TString) {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TNonspecificLiteralInt
|
||||
&& ($input_type_part instanceof TLiteralInt
|
||||
|| $input_type_part instanceof TNonspecificLiteralInt)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TNonspecificLiteralInt) {
|
||||
if ($input_type_part instanceof TInt) {
|
||||
if ($atomic_comparison_result) {
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ use Psalm\Type\Atomic\TNonEmptyLowercaseString;
|
||||
use Psalm\Type\Atomic\TNonEmptyMixed;
|
||||
use Psalm\Type\Atomic\TNonEmptyString;
|
||||
use Psalm\Type\Atomic\TNonFalsyString;
|
||||
use Psalm\Type\Atomic\TNonspecificLiteralInt;
|
||||
use Psalm\Type\Atomic\TNonspecificLiteralString;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
use Psalm\Type\Atomic\TNumericString;
|
||||
@ -896,14 +897,73 @@ class TypeCombiner
|
||||
}
|
||||
|
||||
if ($type instanceof TString) {
|
||||
if ($type instanceof TCallableString && isset($combination->value_types['callable'])) {
|
||||
self::scrapeStringProperties(
|
||||
$type_key,
|
||||
$type,
|
||||
$combination,
|
||||
$codebase,
|
||||
$literal_limit
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($combination->value_types['array-key'])) {
|
||||
if ($type instanceof TInt) {
|
||||
self::scrapeIntProperties(
|
||||
$type_key,
|
||||
$type,
|
||||
$combination,
|
||||
$literal_limit
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof TFloat) {
|
||||
if ($type instanceof TLiteralFloat) {
|
||||
if ($combination->floats !== null && count($combination->floats) < $literal_limit) {
|
||||
$combination->floats[$type_key] = $type;
|
||||
} else {
|
||||
$combination->floats = null;
|
||||
$combination->value_types['float'] = new TFloat();
|
||||
}
|
||||
} else {
|
||||
$combination->floats = null;
|
||||
$combination->value_types['float'] = $type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof TCallable && $type_key === 'callable') {
|
||||
if (($combination->value_types['string'] ?? null) instanceof TCallableString) {
|
||||
unset($combination->value_types['string']);
|
||||
} elseif (!empty($combination->array_type_params) && $combination->all_arrays_callable) {
|
||||
$combination->array_type_params = [];
|
||||
} elseif (isset($combination->value_types['callable-object'])) {
|
||||
unset($combination->value_types['callable-object']);
|
||||
}
|
||||
}
|
||||
|
||||
$combination->value_types[$type_key] = $type;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function scrapeStringProperties(
|
||||
string $type_key,
|
||||
Atomic $type,
|
||||
TypeCombination $combination,
|
||||
?Codebase $codebase,
|
||||
int $literal_limit
|
||||
): void {
|
||||
if ($type instanceof TCallableString && isset($combination->value_types['callable'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($combination->value_types['array-key'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type instanceof Type\Atomic\TTemplateParamClass) {
|
||||
$combination->value_types[$type_key] = $type;
|
||||
} elseif ($type instanceof Type\Atomic\TClassString) {
|
||||
@ -1108,13 +1168,16 @@ class TypeCombiner
|
||||
|
||||
$combination->strings = null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof TInt) {
|
||||
private static function scrapeIntProperties(
|
||||
string $type_key,
|
||||
Atomic $type,
|
||||
TypeCombination $combination,
|
||||
int $literal_limit
|
||||
): void {
|
||||
if (isset($combination->value_types['array-key'])) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
$had_zero = isset($combination->ints['int(0)']);
|
||||
@ -1139,7 +1202,9 @@ class TypeCombiner
|
||||
$combination->ints = null;
|
||||
|
||||
if (!isset($combination->value_types['int'])) {
|
||||
$combination->value_types['int'] = $all_nonnegative ? new TPositiveInt() : new TInt();
|
||||
$combination->value_types['int'] = $all_nonnegative
|
||||
? new TPositiveInt()
|
||||
: new TNonspecificLiteralInt();
|
||||
} elseif ($combination->value_types['int'] instanceof TPositiveInt
|
||||
&& !$all_nonnegative
|
||||
) {
|
||||
@ -1163,6 +1228,17 @@ class TypeCombiner
|
||||
}
|
||||
} elseif (!isset($combination->value_types['int'])) {
|
||||
$combination->value_types['int'] = $type;
|
||||
} elseif (get_class($combination->value_types['int']) !== get_class($type)) {
|
||||
$combination->value_types['int'] = new TInt();
|
||||
}
|
||||
} elseif ($type instanceof TNonspecificLiteralInt) {
|
||||
if ($combination->ints || !isset($combination->value_types['int'])) {
|
||||
$combination->value_types['int'] = $type;
|
||||
} elseif (isset($combination->value_types['int'])
|
||||
&& get_class($combination->value_types['int'])
|
||||
!== get_class($type)
|
||||
) {
|
||||
$combination->value_types['int'] = new TInt();
|
||||
}
|
||||
} else {
|
||||
$combination->value_types['int'] = $type;
|
||||
@ -1182,38 +1258,6 @@ class TypeCombiner
|
||||
$combination->value_types['int'] = new TInt();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof TFloat) {
|
||||
if ($type instanceof TLiteralFloat) {
|
||||
if ($combination->floats !== null && count($combination->floats) < $literal_limit) {
|
||||
$combination->floats[$type_key] = $type;
|
||||
} else {
|
||||
$combination->floats = null;
|
||||
$combination->value_types['float'] = new TFloat();
|
||||
}
|
||||
} else {
|
||||
$combination->floats = null;
|
||||
$combination->value_types['float'] = $type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof TCallable && $type_key === 'callable') {
|
||||
if (($combination->value_types['string'] ?? null) instanceof TCallableString) {
|
||||
unset($combination->value_types['string']);
|
||||
} elseif (!empty($combination->array_type_params) && $combination->all_arrays_callable) {
|
||||
$combination->array_type_params = [];
|
||||
} elseif (isset($combination->value_types['callable-object'])) {
|
||||
unset($combination->value_types['callable-object']);
|
||||
}
|
||||
}
|
||||
|
||||
$combination->value_types[$type_key] = $type;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,6 +53,7 @@ class TypeTokenizer
|
||||
'lowercase-string' => true,
|
||||
'non-empty-lowercase-string' => true,
|
||||
'positive-int' => true,
|
||||
'literal-int' => true,
|
||||
'boolean' => true,
|
||||
'integer' => true,
|
||||
'double' => true,
|
||||
|
@ -266,6 +266,9 @@ abstract class Atomic implements TypeNode
|
||||
case 'non-empty-literal-string':
|
||||
return new Type\Atomic\TNonEmptyNonspecificLiteralString();
|
||||
|
||||
case 'literal-int':
|
||||
return new Type\Atomic\TNonspecificLiteralInt();
|
||||
|
||||
case 'false-y':
|
||||
return new TAssertionFalsy();
|
||||
|
||||
|
29
src/Psalm/Type/Atomic/TNonspecificLiteralInt.php
Normal file
29
src/Psalm/Type/Atomic/TNonspecificLiteralInt.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Psalm\Type\Atomic;
|
||||
|
||||
/**
|
||||
* Denotes the `literal-int` type, where the exact value is unknown but
|
||||
* we know that the int is not from user input
|
||||
*/
|
||||
class TNonspecificLiteralInt extends TInt
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'literal-int';
|
||||
}
|
||||
|
||||
public function getKey(bool $include_extra = true) : string
|
||||
{
|
||||
return 'int';
|
||||
}
|
||||
|
||||
public function canBeFullyExpressedInPhp(int $php_major_version, int $php_minor_version): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getAssertionString(bool $exact = false): string
|
||||
{
|
||||
return 'int';
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ class TNonspecificLiteralString extends TString
|
||||
|
||||
public function getKey(bool $include_extra = true) : string
|
||||
{
|
||||
return 'literal-string';
|
||||
return 'string';
|
||||
}
|
||||
|
||||
public function canBeFullyExpressedInPhp(int $php_major_version, int $php_minor_version): bool
|
||||
|
@ -1240,6 +1240,7 @@ class Union implements TypeNode
|
||||
&& !$atomic_key_type instanceof TLiteralInt
|
||||
&& !$atomic_key_type instanceof TLiteralFloat
|
||||
&& !$atomic_key_type instanceof Atomic\TNonspecificLiteralString
|
||||
&& !$atomic_key_type instanceof Atomic\TNonspecificLiteralInt
|
||||
&& !$atomic_key_type instanceof Atomic\TFalse
|
||||
&& !$atomic_key_type instanceof Atomic\TTrue
|
||||
) {
|
||||
|
@ -571,7 +571,10 @@ function rtrim(string $str, string $character_mask = " \t\n\r\0\x0B") : string {
|
||||
* @return (
|
||||
* $glue is non-empty-string
|
||||
* ? ($pieces is non-empty-array
|
||||
* ? ($pieces is array<literal-string> ? non-empty-literal-string : non-empty-string)
|
||||
* ? ($pieces is array<literal-string|literal-int>
|
||||
* ? ($glue is literal-string ? non-empty-literal-string : non-empty-string)
|
||||
* : non-empty-string
|
||||
* )
|
||||
* : string)
|
||||
* : string
|
||||
* )
|
||||
|
@ -920,9 +920,12 @@ class ArrayFunctionCallTest extends TestCase
|
||||
'implodeNonEmptyArrayAndString' => [
|
||||
'<?php
|
||||
$l = ["a", "b"];
|
||||
$a = implode(":", $l);',
|
||||
$k = [1, 2, 3];
|
||||
$a = implode(":", $l);
|
||||
$b = implode(":", $k);',
|
||||
[
|
||||
'$a===' => 'non-empty-literal-string',
|
||||
'$b===' => 'non-empty-literal-string',
|
||||
]
|
||||
],
|
||||
'key' => [
|
||||
|
@ -201,10 +201,8 @@ class BinaryOperationTest extends TestCase
|
||||
return $arg;
|
||||
}
|
||||
|
||||
/** @var "a"|"b" */
|
||||
$foo = "a";
|
||||
/** @var "c"|"d" */
|
||||
$bar = "c";
|
||||
$foo = rand(0, 1) ? "a" : "b";
|
||||
$bar = rand(0, 1) ? "c" : "d";
|
||||
$baz = $foo . $bar;
|
||||
foobar($baz);
|
||||
',
|
||||
@ -598,6 +596,49 @@ class BinaryOperationTest extends TestCase
|
||||
return "Hello $s1 $s2";
|
||||
}',
|
||||
],
|
||||
'literalIntConcatCreatesLiteral' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param literal-string $s1
|
||||
* @param literal-int $s2
|
||||
* @return literal-string
|
||||
*/
|
||||
function foo(string $s1, int $s2): string {
|
||||
return $s1 . $s2;
|
||||
}',
|
||||
],
|
||||
'literalIntConcatCreatesLiteral2' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param literal-int $s1
|
||||
* @return literal-string
|
||||
*/
|
||||
function foo(int $s1): string {
|
||||
return "foo" . $s1;
|
||||
}',
|
||||
],
|
||||
'encapsedStringWithIntIncludingLiterals' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param literal-int $s1
|
||||
* @param literal-int $s2
|
||||
* @return literal-string
|
||||
*/
|
||||
function foo(int $s1, int $s2): string {
|
||||
return "Hello $s1 $s2";
|
||||
}',
|
||||
],
|
||||
'encapsedStringWithIntIncludingLiterals2' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param literal-int $s1
|
||||
* @return literal-string
|
||||
*/
|
||||
function foo(int $s1): string {
|
||||
$s2 = "foo";
|
||||
return "Hello $s1 $s2";
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -752,6 +752,34 @@ class TypeCombinationTest extends TestCase
|
||||
'literal-string',
|
||||
]
|
||||
],
|
||||
'combineLiteralIntAndNonspecificLiteral' => [
|
||||
'literal-int',
|
||||
[
|
||||
'literal-int',
|
||||
'5',
|
||||
]
|
||||
],
|
||||
'combineNonspecificLiteralAndLiteralInt' => [
|
||||
'literal-int',
|
||||
[
|
||||
'5',
|
||||
'literal-int',
|
||||
]
|
||||
],
|
||||
'combineNonspecificLiteralAndPositiveInt' => [
|
||||
'int',
|
||||
[
|
||||
'positive-int',
|
||||
'literal-int',
|
||||
]
|
||||
],
|
||||
'combinePositiveAndLiteralInt' => [
|
||||
'int',
|
||||
[
|
||||
'literal-int',
|
||||
'positive-int',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user