1
0
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:
Matt Brown 2021-06-14 23:24:09 -04:00
parent c3fdfc5795
commit 9dde8eed9d
13 changed files with 494 additions and 321 deletions

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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;
}
/**

View File

@ -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,

View File

@ -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();

View 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';
}
}

View File

@ -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

View File

@ -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
) {

View File

@ -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
* )

View File

@ -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' => [

View File

@ -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";
}',
],
];
}

View File

@ -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',
]
],
];
}