1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +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,27 +187,30 @@ class ConcatAnalyzer
$left_type,
$numeric_type
);
$right_uint = Type::getPositiveInt();
$right_uint->addType(new Type\Atomic\TLiteralInt(0));
$right_is_uint = UnionTypeComparator::isContainedBy(
$codebase,
$right_type,
$right_uint
);
if ($left_is_numeric && $right_is_uint) {
$result_type = Type::getNumericString();
return;
if ($left_is_numeric) {
$right_uint = Type::getPositiveInt();
$right_uint->addType(new Type\Atomic\TLiteralInt(0));
$right_is_uint = UnionTypeComparator::isContainedBy(
$codebase,
$right_type,
$right_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,8 +95,27 @@ class ScalarTypeComparator
}
if ($container_type_part instanceof TNonspecificLiteralString) {
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced = true;
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,292 +897,24 @@ class TypeCombiner
}
if ($type instanceof TString) {
if ($type instanceof TCallableString && isset($combination->value_types['callable'])) {
return null;
}
if (isset($combination->value_types['array-key'])) {
return null;
}
if ($type instanceof Type\Atomic\TTemplateParamClass) {
$combination->value_types[$type_key] = $type;
} elseif ($type instanceof Type\Atomic\TClassString) {
if (!$type->as_type) {
$combination->class_string_types['object'] = new TObject();
} else {
$combination->class_string_types[$type->as] = $type->as_type;
}
} elseif ($type instanceof TLiteralString) {
if ($combination->strings !== null && count($combination->strings) < $literal_limit) {
$combination->strings[$type_key] = $type;
} else {
$shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : [];
$combination->strings = null;
if (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof TNumericString
&& \is_numeric($type->value)
) {
// do nothing
} elseif (isset($combination->value_types['class-string'])
&& $type instanceof TLiteralClassString
) {
// do nothing
} elseif ($type instanceof TLiteralClassString) {
$type_classlikes = $codebase
? self::getClassLikes($codebase, $type->value)
: [];
$mutual = array_intersect_key($type_classlikes, $shared_classlikes);
if ($mutual) {
$first_class = array_keys($mutual)[0];
$combination->class_string_types[$first_class] = new TNamedObject($first_class);
} else {
$combination->class_string_types['object'] = new TObject();
}
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof TNonspecificLiteralString
) {
// do nothing
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof Type\Atomic\TLowercaseString
&& \strtolower($type->value) === $type->value
) {
// do nothing
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof Type\Atomic\TNonFalsyString
&& $type->value
) {
// do nothing
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof Type\Atomic\TNonEmptyString
&& $type->value !== ''
) {
// do nothing
} else {
$combination->value_types['string'] = new TString();
}
}
} else {
$type_key = 'string';
if (!isset($combination->value_types['string'])) {
if ($combination->strings) {
if ($type instanceof TNumericString) {
$has_non_numeric_string = false;
foreach ($combination->strings as $string_type) {
if (!\is_numeric($string_type->value)) {
$has_non_numeric_string = true;
break;
}
}
if ($has_non_numeric_string) {
$combination->value_types['string'] = new TString();
} else {
$combination->value_types['string'] = $type;
}
$combination->strings = null;
} elseif ($type instanceof Type\Atomic\TLowercaseString) {
$has_non_lowercase_string = false;
foreach ($combination->strings as $string_type) {
if (\strtolower($string_type->value) !== $string_type->value) {
$has_non_lowercase_string = true;
break;
}
}
if ($has_non_lowercase_string) {
$combination->value_types['string'] = new TString();
} else {
$combination->value_types['string'] = $type;
}
$combination->strings = null;
} elseif ($type instanceof Type\Atomic\TNonEmptyString) {
$has_empty_string = false;
foreach ($combination->strings as $string_type) {
if (!$string_type->value) {
$has_empty_string = true;
break;
}
}
if ($has_empty_string) {
$combination->value_types['string'] = new TString();
} else {
$combination->value_types['string'] = $type;
}
$combination->strings = null;
} elseif ($type instanceof TNonspecificLiteralString) {
$combination->value_types['string'] = $type;
$combination->strings = null;
} else {
$has_non_literal_class_string = false;
$shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : [];
foreach ($combination->strings as $string_type) {
if (!$string_type instanceof TLiteralClassString) {
$has_non_literal_class_string = true;
break;
}
}
if ($has_non_literal_class_string ||
!$type instanceof TClassString
) {
$combination->value_types[$type_key] = new TString();
} else {
if (isset($shared_classlikes[$type->as]) && $type->as_type) {
$combination->class_string_types[$type->as] = $type->as_type;
} else {
$combination->class_string_types['object'] = new TObject();
}
}
}
} else {
$combination->value_types[$type_key] = $type;
}
} elseif (get_class($combination->value_types['string']) !== TString::class) {
if (get_class($type) === TString::class) {
$combination->value_types['string'] = $type;
} elseif ($combination->value_types['string'] instanceof TTraitString
&& $type instanceof TClassString
) {
$combination->value_types['trait-string'] = $combination->value_types['string'];
$combination->value_types['class-string'] = $type;
unset($combination->value_types['string']);
} elseif (get_class($combination->value_types['string']) !== get_class($type)) {
if (get_class($type) === TNonEmptyString::class
&& get_class($combination->value_types['string']) === TNumericString::class
) {
$combination->value_types['string'] = $type;
} elseif (get_class($type) === TNumericString::class
&& get_class($combination->value_types['string']) === TNonEmptyString::class
) {
// do nothing
} elseif ((get_class($type) === TNonEmptyString::class
|| get_class($type) === TNumericString::class)
&& get_class($combination->value_types['string']) === TNonFalsyString::class
) {
$combination->value_types['string'] = $type;
} elseif (get_class($type) === TNonFalsyString::class
&& (get_class($combination->value_types['string']) === TNonEmptyString::class
|| get_class($combination->value_types['string']) === TNumericString::class)
) {
// do nothing
} elseif ((get_class($type) === TNonEmptyString::class
|| get_class($type) === TNonFalsyString::class)
&& get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class
) {
$combination->value_types['string'] = new TNonEmptyString();
} elseif ((get_class($combination->value_types['string']) === TNonEmptyString::class
|| get_class($combination->value_types['string']) === TNonFalsyString::class)
&& get_class($type) === TNonEmptyLowercaseString::class
) {
$combination->value_types['string'] = new TNonEmptyString();
} elseif (get_class($type) === TLowercaseString::class
&& get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class
) {
$combination->value_types['string'] = $type;
} elseif (get_class($combination->value_types['string']) === TLowercaseString::class
&& get_class($type) === TNonEmptyLowercaseString::class
) {
//no-change
} else {
$combination->value_types['string'] = new TString();
}
}
}
$combination->strings = null;
}
self::scrapeStringProperties(
$type_key,
$type,
$combination,
$codebase,
$literal_limit
);
return null;
}
if ($type instanceof TInt) {
if (isset($combination->value_types['array-key'])) {
return null;
}
$had_zero = isset($combination->ints['int(0)']);
if ($type instanceof TLiteralInt) {
if ($type->value === 0) {
$had_zero = true;
}
if ($combination->ints !== null && count($combination->ints) < $literal_limit) {
$combination->ints[$type_key] = $type;
} else {
$combination->ints[$type_key] = $type;
$all_nonnegative = !array_filter(
$combination->ints,
function ($int): bool {
return $int->value < 0;
}
);
$combination->ints = null;
if (!isset($combination->value_types['int'])) {
$combination->value_types['int'] = $all_nonnegative ? new TPositiveInt() : new TInt();
} elseif ($combination->value_types['int'] instanceof TPositiveInt
&& !$all_nonnegative
) {
$combination->value_types['int'] = new TInt();
}
}
} else {
if ($type instanceof TPositiveInt) {
if ($combination->ints) {
$all_nonnegative = !array_filter(
$combination->ints,
function ($int): bool {
return $int->value < 0;
}
);
if ($all_nonnegative) {
$combination->value_types['int'] = $type;
} else {
$combination->value_types['int'] = new TInt();
}
} elseif (!isset($combination->value_types['int'])) {
$combination->value_types['int'] = $type;
}
} else {
$combination->value_types['int'] = $type;
}
$combination->ints = null;
}
if ($had_zero
&& isset($combination->value_types['int'])
&& $combination->value_types['int'] instanceof TPositiveInt
) {
if ($combination->ints === null) {
$combination->ints = ['int(0)' => new TLiteralInt(0)];
} elseif ($type instanceof TLiteralInt && $type->value < 0) {
$combination->ints = null;
$combination->value_types['int'] = new TInt();
}
}
self::scrapeIntProperties(
$type_key,
$type,
$combination,
$literal_limit
);
return null;
}
@ -1216,6 +949,317 @@ class TypeCombiner
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) {
if (!$type->as_type) {
$combination->class_string_types['object'] = new TObject();
} else {
$combination->class_string_types[$type->as] = $type->as_type;
}
} elseif ($type instanceof TLiteralString) {
if ($combination->strings !== null && count($combination->strings) < $literal_limit) {
$combination->strings[$type_key] = $type;
} else {
$shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : [];
$combination->strings = null;
if (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof TNumericString
&& \is_numeric($type->value)
) {
// do nothing
} elseif (isset($combination->value_types['class-string'])
&& $type instanceof TLiteralClassString
) {
// do nothing
} elseif ($type instanceof TLiteralClassString) {
$type_classlikes = $codebase
? self::getClassLikes($codebase, $type->value)
: [];
$mutual = array_intersect_key($type_classlikes, $shared_classlikes);
if ($mutual) {
$first_class = array_keys($mutual)[0];
$combination->class_string_types[$first_class] = new TNamedObject($first_class);
} else {
$combination->class_string_types['object'] = new TObject();
}
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof TNonspecificLiteralString
) {
// do nothing
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof Type\Atomic\TLowercaseString
&& \strtolower($type->value) === $type->value
) {
// do nothing
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof Type\Atomic\TNonFalsyString
&& $type->value
) {
// do nothing
} elseif (isset($combination->value_types['string'])
&& $combination->value_types['string'] instanceof Type\Atomic\TNonEmptyString
&& $type->value !== ''
) {
// do nothing
} else {
$combination->value_types['string'] = new TString();
}
}
} else {
$type_key = 'string';
if (!isset($combination->value_types['string'])) {
if ($combination->strings) {
if ($type instanceof TNumericString) {
$has_non_numeric_string = false;
foreach ($combination->strings as $string_type) {
if (!\is_numeric($string_type->value)) {
$has_non_numeric_string = true;
break;
}
}
if ($has_non_numeric_string) {
$combination->value_types['string'] = new TString();
} else {
$combination->value_types['string'] = $type;
}
$combination->strings = null;
} elseif ($type instanceof Type\Atomic\TLowercaseString) {
$has_non_lowercase_string = false;
foreach ($combination->strings as $string_type) {
if (\strtolower($string_type->value) !== $string_type->value) {
$has_non_lowercase_string = true;
break;
}
}
if ($has_non_lowercase_string) {
$combination->value_types['string'] = new TString();
} else {
$combination->value_types['string'] = $type;
}
$combination->strings = null;
} elseif ($type instanceof Type\Atomic\TNonEmptyString) {
$has_empty_string = false;
foreach ($combination->strings as $string_type) {
if (!$string_type->value) {
$has_empty_string = true;
break;
}
}
if ($has_empty_string) {
$combination->value_types['string'] = new TString();
} else {
$combination->value_types['string'] = $type;
}
$combination->strings = null;
} elseif ($type instanceof TNonspecificLiteralString) {
$combination->value_types['string'] = $type;
$combination->strings = null;
} else {
$has_non_literal_class_string = false;
$shared_classlikes = $codebase ? self::getSharedTypes($combination, $codebase) : [];
foreach ($combination->strings as $string_type) {
if (!$string_type instanceof TLiteralClassString) {
$has_non_literal_class_string = true;
break;
}
}
if ($has_non_literal_class_string ||
!$type instanceof TClassString
) {
$combination->value_types[$type_key] = new TString();
} else {
if (isset($shared_classlikes[$type->as]) && $type->as_type) {
$combination->class_string_types[$type->as] = $type->as_type;
} else {
$combination->class_string_types['object'] = new TObject();
}
}
}
} else {
$combination->value_types[$type_key] = $type;
}
} elseif (get_class($combination->value_types['string']) !== TString::class) {
if (get_class($type) === TString::class) {
$combination->value_types['string'] = $type;
} elseif ($combination->value_types['string'] instanceof TTraitString
&& $type instanceof TClassString
) {
$combination->value_types['trait-string'] = $combination->value_types['string'];
$combination->value_types['class-string'] = $type;
unset($combination->value_types['string']);
} elseif (get_class($combination->value_types['string']) !== get_class($type)) {
if (get_class($type) === TNonEmptyString::class
&& get_class($combination->value_types['string']) === TNumericString::class
) {
$combination->value_types['string'] = $type;
} elseif (get_class($type) === TNumericString::class
&& get_class($combination->value_types['string']) === TNonEmptyString::class
) {
// do nothing
} elseif ((get_class($type) === TNonEmptyString::class
|| get_class($type) === TNumericString::class)
&& get_class($combination->value_types['string']) === TNonFalsyString::class
) {
$combination->value_types['string'] = $type;
} elseif (get_class($type) === TNonFalsyString::class
&& (get_class($combination->value_types['string']) === TNonEmptyString::class
|| get_class($combination->value_types['string']) === TNumericString::class)
) {
// do nothing
} elseif ((get_class($type) === TNonEmptyString::class
|| get_class($type) === TNonFalsyString::class)
&& get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class
) {
$combination->value_types['string'] = new TNonEmptyString();
} elseif ((get_class($combination->value_types['string']) === TNonEmptyString::class
|| get_class($combination->value_types['string']) === TNonFalsyString::class)
&& get_class($type) === TNonEmptyLowercaseString::class
) {
$combination->value_types['string'] = new TNonEmptyString();
} elseif (get_class($type) === TLowercaseString::class
&& get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class
) {
$combination->value_types['string'] = $type;
} elseif (get_class($combination->value_types['string']) === TLowercaseString::class
&& get_class($type) === TNonEmptyLowercaseString::class
) {
//no-change
} else {
$combination->value_types['string'] = new TString();
}
}
}
$combination->strings = null;
}
}
private static function scrapeIntProperties(
string $type_key,
Atomic $type,
TypeCombination $combination,
int $literal_limit
): void {
if (isset($combination->value_types['array-key'])) {
return;
}
$had_zero = isset($combination->ints['int(0)']);
if ($type instanceof TLiteralInt) {
if ($type->value === 0) {
$had_zero = true;
}
if ($combination->ints !== null && count($combination->ints) < $literal_limit) {
$combination->ints[$type_key] = $type;
} else {
$combination->ints[$type_key] = $type;
$all_nonnegative = !array_filter(
$combination->ints,
function ($int): bool {
return $int->value < 0;
}
);
$combination->ints = null;
if (!isset($combination->value_types['int'])) {
$combination->value_types['int'] = $all_nonnegative
? new TPositiveInt()
: new TNonspecificLiteralInt();
} elseif ($combination->value_types['int'] instanceof TPositiveInt
&& !$all_nonnegative
) {
$combination->value_types['int'] = new TInt();
}
}
} else {
if ($type instanceof TPositiveInt) {
if ($combination->ints) {
$all_nonnegative = !array_filter(
$combination->ints,
function ($int): bool {
return $int->value < 0;
}
);
if ($all_nonnegative) {
$combination->value_types['int'] = $type;
} else {
$combination->value_types['int'] = new TInt();
}
} 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;
}
$combination->ints = null;
}
if ($had_zero
&& isset($combination->value_types['int'])
&& $combination->value_types['int'] instanceof TPositiveInt
) {
if ($combination->ints === null) {
$combination->ints = ['int(0)' => new TLiteralInt(0)];
} elseif ($type instanceof TLiteralInt && $type->value < 0) {
$combination->ints = null;
$combination->value_types['int'] = new TInt();
}
}
}
/**
* @return array<string, bool>
*/

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