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