From c18a155f77014366f3d1759ccb1877ea87a20983 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 01/10] Support type annotations for class consts (fixes #942). --- config.xsd | 1 + .../issues/UnresolvableConstant.md | 22 ++++ .../UnresolvableConstantException.php | 23 ++++ .../FunctionLike/ReturnTypeAnalyzer.php | 32 +++-- .../Analyzer/FunctionLikeAnalyzer.php | 37 ++++-- .../Fetch/ClassConstFetchAnalyzer.php | 6 +- src/Psalm/Internal/Codebase/ClassLikes.php | 10 +- .../Reflector/ClassLikeNodeScanner.php | 90 +++++++++----- .../Type/SimpleAssertionReconciler.php | 5 + src/Psalm/Internal/Type/TypeExpander.php | 9 +- src/Psalm/Issue/CodeIssue.php | 6 +- src/Psalm/Issue/UnresolvableConstant.php | 8 ++ src/Psalm/Storage/ClassConstantStorage.php | 16 +++ tests/ConstantTest.php | 117 ++++++++++++++++++ tests/Template/ClassTemplateExtendsTest.php | 5 +- .../TypeReconciliation/ArrayKeyExistsTest.php | 1 - 16 files changed, 329 insertions(+), 59 deletions(-) create mode 100644 docs/running_psalm/issues/UnresolvableConstant.md create mode 100644 src/Psalm/Exception/UnresolvableConstantException.php create mode 100644 src/Psalm/Issue/UnresolvableConstant.php diff --git a/config.xsd b/config.xsd index ad48f587e59..917968bd0f2 100644 --- a/config.xsd +++ b/config.xsd @@ -458,6 +458,7 @@ + diff --git a/docs/running_psalm/issues/UnresolvableConstant.md b/docs/running_psalm/issues/UnresolvableConstant.md new file mode 100644 index 00000000000..4a4ce88f63c --- /dev/null +++ b/docs/running_psalm/issues/UnresolvableConstant.md @@ -0,0 +1,22 @@ +# UnresolvableConstant + +Emitted when Psalm cannot resolve a constant used in `key-of` or `value-of`. Note that `static::CONST` is considered +unresolvable for `key-of` and `value-of`, since the literal keys and values can't be resolved due to the possibility +of being overridden by child classes. + +```php + + */ + public function bar(): string + { + return self::BAR[0]; + } +} +``` diff --git a/src/Psalm/Exception/UnresolvableConstantException.php b/src/Psalm/Exception/UnresolvableConstantException.php new file mode 100644 index 00000000000..3295f663f99 --- /dev/null +++ b/src/Psalm/Exception/UnresolvableConstantException.php @@ -0,0 +1,23 @@ +class_name = $class_name; + $this->const_name = $const_name; + } +} diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 5382c4e02a1..6f68fc02250 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -11,6 +11,7 @@ use Psalm\CodeLocation; use Psalm\Config; use Psalm\Context; +use Psalm\Exception\UnresolvableConstantException; use Psalm\Internal\Analyzer\ClassAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\InterfaceAnalyzer; @@ -40,6 +41,7 @@ use Psalm\Issue\MixedInferredReturnType; use Psalm\Issue\MixedReturnTypeCoercion; use Psalm\Issue\MoreSpecificReturnType; +use Psalm\Issue\UnresolvableConstant; use Psalm\IssueBuffer; use Psalm\StatementsSource; use Psalm\Storage\FunctionLikeStorage; @@ -821,15 +823,27 @@ public static function checkReturnType( return null; } - $fleshed_out_return_type = TypeExpander::expandUnion( - $codebase, - $storage->return_type, - $classlike_storage->name ?? null, - $classlike_storage->name ?? null, - $parent_class, - true, - true - ); + try { + $fleshed_out_return_type = TypeExpander::expandUnion( + $codebase, + $storage->return_type, + $classlike_storage->name ?? null, + $classlike_storage->name ?? null, + $parent_class, + true, + true + ); + } catch (UnresolvableConstantException $e) { + IssueBuffer::maybeAdd( + new UnresolvableConstant( + "Could not resolve constant {$e->class_name}::{$e->const_name}", + $storage->return_type_location + ), + $storage->suppressed_issues, + true + ); + $fleshed_out_return_type = $storage->return_type; + } if ($fleshed_out_return_type->check( $function_like_analyzer, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 459db1133bd..551e7e8b73c 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -10,6 +10,7 @@ use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Context; +use Psalm\Exception\UnresolvableConstantException; use Psalm\FileManipulation; use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeAnalyzer; use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeCollector; @@ -38,6 +39,7 @@ use Psalm\Issue\MissingThrowsDocblock; use Psalm\Issue\ReferenceConstraintViolation; use Psalm\Issue\ReservedWord; +use Psalm\Issue\UnresolvableConstant; use Psalm\Issue\UnusedClosureParam; use Psalm\Issue\UnusedParam; use Psalm\IssueBuffer; @@ -1013,17 +1015,30 @@ private function processParams( if ($function_param->type) { $param_type = clone $function_param->type; - $param_type = TypeExpander::expandUnion( - $codebase, - $param_type, - $context->self, - $context->self, - $this->getParentFQCLN(), - true, - false, - false, - true - ); + try { + $param_type = TypeExpander::expandUnion( + $codebase, + $param_type, + $context->self, + $context->self, + $this->getParentFQCLN(), + true, + false, + false, + true + ); + } catch (UnresolvableConstantException $e) { + if ($function_param->type_location !== null) { + IssueBuffer::maybeAdd( + new UnresolvableConstant( + "Could not resolve constant {$e->class_name}::{$e->const_name}", + $function_param->type_location + ), + $storage->suppressed_issues, + true + ); + } + } if ($function_param->type_location) { if ($param_type->check( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php index dc6fa069753..b9dcdee3d66 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php @@ -252,7 +252,9 @@ public static function analyze( $fq_class_name, $stmt->name->name, $class_visibility, - $statements_analyzer + $statements_analyzer, + [], + $stmt->class->parts[0] === "self" ); } catch (InvalidArgumentException $_) { return true; @@ -378,7 +380,7 @@ public static function analyze( ); } - if ($first_part_lc !== 'static' || $const_class_storage->final) { + if ($first_part_lc !== 'static' || $const_class_storage->final || $class_constant_type->from_docblock) { $stmt_type = clone $class_constant_type; $statements_analyzer->node_data->setType($stmt, $stmt_type); diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index aad785051c0..3d93f7a2d9a 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1632,7 +1632,8 @@ public function getClassConstantType( string $constant_name, int $visibility, ?StatementsAnalyzer $statements_analyzer = null, - array $visited_constant_ids = [] + array $visited_constant_ids = [], + bool $static_binding = false, ): ?Union { $class_name = strtolower($class_name); @@ -1659,15 +1660,18 @@ public function getClassConstantType( } if ($constant_storage->unresolved_node) { - $constant_storage->type = new Union([ConstantTypeResolver::resolve( + $constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve( $this, $constant_storage->unresolved_node, $statements_analyzer, $visited_constant_ids )]); + if ($constant_storage->type === null || !$constant_storage->type->from_docblock) { + $constant_storage->type = $constant_storage->inferred_type; + } } - return $constant_storage->type; + return $static_binding ? $constant_storage->inferred_type : $constant_storage->type; } elseif (isset($storage->enum_cases[$constant_name])) { return new Union([new TEnumCase($storage->name, $constant_name)]); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 977d7888e96..790f201ebb8 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1208,6 +1208,7 @@ private function visitClassConstDeclaration( $existing_constants = $storage->constants; $comment = $stmt->getDocComment(); + $var_comment = null; $deprecated = false; $description = null; $config = $this->config; @@ -1220,19 +1221,31 @@ private function visitClassConstDeclaration( } $description = $comments->description; + + try { + $var_comments = CommentAnalyzer::getTypeFromComment( + $comment, + $this->file_scanner, + $this->aliases, + [], + $this->type_aliases + ); + + $var_comment = array_pop($var_comments); + } catch (IncorrectDocblockException $e) { + $storage->docblock_issues[] = new MissingDocblockType( + $e->getMessage(), + new CodeLocation($this->file_scanner, $stmt, null, true) + ); + } catch (DocblockParseException $e) { + $storage->docblock_issues[] = new InvalidDocblock( + $e->getMessage(), + new CodeLocation($this->file_scanner, $stmt, null, true) + ); + } } foreach ($stmt->consts as $const) { - $const_type = SimpleTypeInferer::infer( - $this->codebase, - new NodeDataProvider(), - $const->value, - $this->aliases, - null, - $existing_constants, - $fq_classlike_name - ); - if (isset($storage->constants[$const->name->name]) || isset($storage->enum_cases[$const->name->name]) ) { @@ -1246,6 +1259,35 @@ private function visitClassConstDeclaration( continue; } + $inferred_type = SimpleTypeInferer::infer( + $this->codebase, + new NodeDataProvider(), + $const->value, + $this->aliases, + null, + $existing_constants, + $fq_classlike_name + ); + + $type_location = null; + if ($var_comment !== null && $var_comment->type !== null) { + $const_type = $var_comment->type; + + if ($var_comment->type_start !== null + && $var_comment->type_end !== null + && $var_comment->line_number !== null + ) { + $type_location = new DocblockTypeLocation( + $this->file_scanner, + $var_comment->type_start, + $var_comment->type_end, + $var_comment->line_number + ); + } + } else { + $const_type = $inferred_type; + } + $storage->constants[$const->name->name] = $constant_storage = new ClassConstantStorage( $const_type, $stmt->isProtected() @@ -1259,30 +1301,22 @@ private function visitClassConstDeclaration( ) ); + $constant_storage->inferred_type = $inferred_type; + $constant_storage->type_location = $type_location; + $constant_storage->stmt_location = new CodeLocation( $this->file_scanner, $const ); - if ($const_type - && $const->value instanceof Concat - && $const_type->isSingle() - && get_class($const_type->getSingleAtomic()) === TString::class + if ($inferred_type + && !( + $const->value instanceof Concat + && $inferred_type->isSingle() + && get_class($inferred_type->getSingleAtomic()) === TString::class + ) ) { - // Prefer unresolved type over inferred string from concat, so that it can later be resolved to literal. - $const_type = null; - } - - if ($const_type) { - $existing_constants[$const->name->name] = new ClassConstantStorage( - $const_type, - $stmt->isProtected() - ? ClassLikeAnalyzer::VISIBILITY_PROTECTED - : ($stmt->isPrivate() - ? ClassLikeAnalyzer::VISIBILITY_PRIVATE - : ClassLikeAnalyzer::VISIBILITY_PUBLIC), - null - ); + $existing_constants[$const->name->name] = $constant_storage; } else { $unresolved_const_expr = ExpressionResolver::getUnresolvedClassConstExpr( $const->value, diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index aaf7ce5e3c2..7a9223c548a 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -1652,6 +1652,11 @@ private static function reconcileInArray( ): Union { $new_var_type = clone $assertion->type; + if ($new_var_type->isSingle() && $new_var_type->getSingleAtomic() instanceof TClassConstant) { + // Can't do assertion on const with non-literal type + return $existing_var_type; + } + $intersection = Type::intersectUnionTypes($new_var_type, $existing_var_type, $codebase); if ($intersection === null) { diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 41fbb5e9f03..c4b26fab047 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -5,6 +5,7 @@ use Psalm\Codebase; use Psalm\Exception\CircularReferenceException; use Psalm\Storage\Assertion\IsType; +use Psalm\Exception\UnresolvableConstantException; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; @@ -347,7 +348,11 @@ public static function expandAtomic( $return_type->fq_classlike_name = $self_class; } - if ($evaluate_class_constants && $codebase->classOrInterfaceExists($return_type->fq_classlike_name)) { + if ($evaluate_class_constants) { + if (!$codebase->classOrInterfaceExists($return_type->fq_classlike_name)) { + throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name); + } + try { $class_constant_type = $codebase->classlikes->getClassConstantType( $return_type->fq_classlike_name, @@ -374,6 +379,8 @@ public static function expandAtomic( return array_values($const_type_atomic->type_params[1]->getAtomicTypes()); } } + } else { + throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name); } } diff --git a/src/Psalm/Issue/CodeIssue.php b/src/Psalm/Issue/CodeIssue.php index c6d55215edd..0b8035efd09 100644 --- a/src/Psalm/Issue/CodeIssue.php +++ b/src/Psalm/Issue/CodeIssue.php @@ -10,7 +10,9 @@ abstract class CodeIssue { + /** @var int */ public const ERROR_LEVEL = -1; + /** @var int<0, max> */ public const SHORTCODE = 0; /** @@ -88,8 +90,8 @@ public function toIssueData(string $severity): IssueData $snippet_bounds[1], $location->getColumn(), $location->getEndColumn(), - (int) static::SHORTCODE, - (int) static::ERROR_LEVEL, + static::SHORTCODE, + static::ERROR_LEVEL, $this instanceof TaintedInput ? $this->getTaintTrace() : null, diff --git a/src/Psalm/Issue/UnresolvableConstant.php b/src/Psalm/Issue/UnresolvableConstant.php new file mode 100644 index 00000000000..8c08586d44d --- /dev/null +++ b/src/Psalm/Issue/UnresolvableConstant.php @@ -0,0 +1,8 @@ + [ + ' [ + ' [], 'php_version' => '8.1', ], + 'returnValueofNonExistantConstant' => [ + ' + */ + public function bar(): string + { + return self::BAR[0]; + } + } + ', + 'error_message' => 'UnresolvableConstant', + ], + 'returnValueofStaticConstant' => [ + ' + */ + public function bar(): string + { + return static::BAR[0]; + } + } + ', + 'error_message' => 'UnresolvableConstant', + ], + 'takeKeyofNonExistantConstant' => [ + ' $key + */ + public function bar(int $key): string + { + return static::BAR[$key]; + } + } + ', + 'error_message' => 'UnresolvableConstant', + ], + 'takeKeyofStaticConstant' => [ + ' $key + */ + public function bar(int $key): string + { + return static::BAR[$key]; + } + } + ', + 'error_message' => 'UnresolvableConstant', + ], + 'SKIPPED-keyofSelfConstDoesntImplyKeyofStaticConst' => [ + ' */ + public const CONST = [1, 2, 3]; + + /** + * @param key-of $key + */ + public function bar(int $key): int + { + return static::CONST[$key]; + } + } + ', + 'error_message' => 'MixedArrayAccess', + ], ]; } } diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index f30f842a1e9..1200e53195d 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -28,6 +28,7 @@ public function providerValidCodeParse(): iterable */ abstract class Tuple { + /** @var int */ const ARITY = 0; /** @@ -36,7 +37,7 @@ abstract class Tuple */ public function arity(): int { - return (int)static::ARITY; + return static::ARITY; } /** @@ -74,7 +75,7 @@ public function __construct($_0) { */ public function arity(): int { - return (int)static::ARITY; + return static::ARITY; } /** diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 457a5898de8..5eef13b14d3 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -202,7 +202,6 @@ function foo(array $array = []): void { 'assertArrayKeyExistsRefinesType' => [ 'code' => ' */ public const DAYS = [ 1 => "mon", 2 => "tue", From df314658a53bdb242d725e212fcb5c963bc045eb Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 02/10] Fix trailing comma. --- src/Psalm/Internal/Codebase/ClassLikes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 3d93f7a2d9a..9993cbf4873 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1633,7 +1633,7 @@ public function getClassConstantType( int $visibility, ?StatementsAnalyzer $statements_analyzer = null, array $visited_constant_ids = [], - bool $static_binding = false, + bool $static_binding = false ): ?Union { $class_name = strtolower($class_name); From c877ce031ea0d4a1ef4582d6402dedd5ec1ff71b Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 03/10] Use inferred type instead of annotated type where possible. --- .../Internal/Analyzer/ClassLikeAnalyzer.php | 34 +++++++++++++ .../Fetch/ClassConstFetchAnalyzer.php | 2 +- src/Psalm/Internal/Codebase/ClassLikes.php | 4 +- src/Psalm/Internal/Codebase/Reflection.php | 1 + .../Reflector/ClassLikeNodeScanner.php | 2 +- src/Psalm/Storage/ClassConstantStorage.php | 3 +- tests/ConstantTest.php | 50 ++++++++++++------- 7 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 339d5079f10..254c87fd4f7 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -30,6 +30,9 @@ use function gettype; use function implode; use function in_array; +use function is_float; +use function is_int; +use function is_string; use function preg_match; use function preg_replace; use function strtolower; @@ -512,6 +515,37 @@ public static function getTypeFromValue($value): Union } } + /** + * Gets the Psalm literal type from a particular value + * + * @param array|scalar|null $value + * + */ + public static function getLiteralTypeFromValue($value): Type\Union + { + if (is_string($value)) { + return Type::getString($value); + } + + if (is_int($value)) { + return Type::getInt(false, $value); + } + + if (is_float($value)) { + return Type::getFloat($value); + } + + if ($value === false) { + return Type::getFalse(); + } + + if ($value === true) { + return Type::getTrue(); + } + + return Type::getNull(); + } + /** * @param string[] $suppressed_issues */ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php index b9dcdee3d66..5ed42002c5a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php @@ -254,7 +254,7 @@ public static function analyze( $class_visibility, $statements_analyzer, [], - $stmt->class->parts[0] === "self" + $stmt->class->parts[0] === "static" ); } catch (InvalidArgumentException $_) { return true; diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 9993cbf4873..ca2bba3ba03 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1633,7 +1633,7 @@ public function getClassConstantType( int $visibility, ?StatementsAnalyzer $statements_analyzer = null, array $visited_constant_ids = [], - bool $static_binding = false + bool $late_static_binding = true ): ?Union { $class_name = strtolower($class_name); @@ -1671,7 +1671,7 @@ public function getClassConstantType( } } - return $static_binding ? $constant_storage->inferred_type : $constant_storage->type; + return $late_static_binding ? $constant_storage->type : ($constant_storage->inferred_type ?? null); } elseif (isset($storage->enum_cases[$constant_name])) { return new Union([new TEnumCase($storage->name, $constant_name)]); } diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index 424c2a1cdeb..e58a98f8402 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -171,6 +171,7 @@ public function registerClass(ReflectionClass $reflected_class): void foreach ($class_constants as $name => $value) { $storage->constants[$name] = new ClassConstantStorage( ClassLikeAnalyzer::getTypeFromValue($value), + ClassLikeAnalyzer::getLiteralTypeFromValue($value), ClassLikeAnalyzer::VISIBILITY_PUBLIC, null ); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 790f201ebb8..35ae6b1fd9c 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1290,6 +1290,7 @@ private function visitClassConstDeclaration( $storage->constants[$const->name->name] = $constant_storage = new ClassConstantStorage( $const_type, + $inferred_type, $stmt->isProtected() ? ClassLikeAnalyzer::VISIBILITY_PROTECTED : ($stmt->isPrivate() @@ -1301,7 +1302,6 @@ private function visitClassConstDeclaration( ) ); - $constant_storage->inferred_type = $inferred_type; $constant_storage->type_location = $type_location; $constant_storage->stmt_location = new CodeLocation( diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php index a3fbc04a5a8..aa7bb8be08a 100644 --- a/src/Psalm/Storage/ClassConstantStorage.php +++ b/src/Psalm/Storage/ClassConstantStorage.php @@ -69,10 +69,11 @@ class ClassConstantStorage /** * @param ClassLikeAnalyzer::VISIBILITY_* $visibility */ - public function __construct(?Union $type, int $visibility, ?CodeLocation $location) + public function __construct(?Union $type, ?Union $inferred_type, int $visibility, ?CodeLocation $location) { $this->visibility = $visibility; $this->location = $location; $this->type = $type; + $this->inferred_type = $inferred_type; } } diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 6889fc183e6..54fb1ff41ff 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -10,6 +10,38 @@ class ConstantTest extends TestCase use InvalidCodeAnalysisTestTrait; use ValidCodeAnalysisTestTrait; + // TODO: Waiting for https://github.com/vimeo/psalm/issues/7125 + // public function testKeyofSelfConstDoesntImplyKeyofStaticConst(): void + // { + // $this->expectException(CodeException::class); + // $this->expectExceptionMessage("PossiblyUndefinedIntArrayOffset"); + + // $this->testConfig->ensure_array_int_offsets_exist = true; + + // $file_path = getcwd() . '/src/somefile.php'; + + // $this->addFile( + // $file_path, + // ' */ + // public const CONST = [1, 2, 3]; + + // /** + // * @param key-of $key + // */ + // public function bar(int $key): int + // { + // return static::CONST[$key]; + // } + // } + // ' + // ); + + // $this->analyzeFile($file_path, new Context()); + // } + /** * @return iterable,ignored_issues?:list, php_version?: string}> */ @@ -1610,24 +1642,6 @@ public function bar(int $key): string ', 'error_message' => 'UnresolvableConstant', ], - 'SKIPPED-keyofSelfConstDoesntImplyKeyofStaticConst' => [ - ' */ - public const CONST = [1, 2, 3]; - - /** - * @param key-of $key - */ - public function bar(int $key): int - { - return static::CONST[$key]; - } - } - ', - 'error_message' => 'MixedArrayAccess', - ], ]; } } From 558208e44e8c31477b1b09b8434be202174b1cd6 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 04/10] Add type checking for class const assignments, fix several other const issues. --- config.xsd | 19 ++++++++ .../issues/InvalidConstantAssignmentValue.md | 12 +++++ src/Psalm/Config.php | 12 +++++ src/Psalm/Config/FileFilter.php | 25 ++++++++++ src/Psalm/Config/IssueHandler.php | 12 +++++ .../Internal/Analyzer/ClassLikeAnalyzer.php | 26 +++++++++- .../Statements/Expression/ArrayAnalyzer.php | 6 +++ .../Fetch/ClassConstFetchAnalyzer.php | 29 +++++++++++ .../Codebase/ConstantTypeResolver.php | 4 +- .../Reflector/ClassLikeNodeScanner.php | 3 ++ src/Psalm/Issue/ClassConstantIssue.php | 21 ++++++++ .../Issue/InvalidConstantAssignmentValue.php | 8 ++++ src/Psalm/Storage/ClassConstantStorage.php | 5 ++ tests/ArrayAssignmentTest.php | 3 +- tests/Config/ConfigTest.php | 13 +++++ tests/ConstantTest.php | 48 +++++++++++++++++++ 16 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 docs/running_psalm/issues/InvalidConstantAssignmentValue.md create mode 100644 src/Psalm/Issue/ClassConstantIssue.php create mode 100644 src/Psalm/Issue/InvalidConstantAssignmentValue.php diff --git a/config.xsd b/config.xsd index 917968bd0f2..cee3627fef1 100644 --- a/config.xsd +++ b/config.xsd @@ -255,6 +255,7 @@ + @@ -603,6 +604,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/running_psalm/issues/InvalidConstantAssignmentValue.md b/docs/running_psalm/issues/InvalidConstantAssignmentValue.md new file mode 100644 index 00000000000..b0bf046ba39 --- /dev/null +++ b/docs/running_psalm/issues/InvalidConstantAssignmentValue.md @@ -0,0 +1,12 @@ +# InvalidConstantAssignmentValue + +Emitted when attempting to assign a value to a class constant that cannot contain that type. + +```php +getReportingLevelForFunction($issue_type, $e->function_id); } elseif ($e instanceof PropertyIssue) { $reporting_level = $this->getReportingLevelForProperty($issue_type, $e->property_id); + } elseif ($e instanceof ClassConstantIssue) { + $reporting_level = $this->getReportingLevelForClassConstant($issue_type, $e->const_id); } elseif ($e instanceof ArgumentIssue && $e->function_id) { $reporting_level = $this->getReportingLevelForArgument($issue_type, $e->function_id); } elseif ($e instanceof VariableIssue) { @@ -1794,6 +1797,15 @@ public function getReportingLevelForProperty(string $issue_type, string $propert return null; } + public function getReportingLevelForClassConstant(string $issue_type, string $constant_id): ?string + { + if (isset($this->issue_handlers[$issue_type])) { + return $this->issue_handlers[$issue_type]->getReportingLevelForClassConstant($constant_id); + } + + return null; + } + public function getReportingLevelForVariable(string $issue_type, string $var_name): ?string { if (isset($this->issue_handlers[$issue_type])) { diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index 2db9bdaaee8..23948b7708e 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -66,6 +66,11 @@ class FileFilter */ protected $property_ids = []; + /** + * @var array + */ + protected $class_constant_ids = []; + /** * @var array */ @@ -326,6 +331,13 @@ public static function loadFromArray( } } + if (isset($config['referencedConstant']) && is_iterable($config['referencedConstant'])) { + /** @var array $referenced_constant */ + foreach ($config['referencedConstant'] as $referenced_constant) { + $filter->class_constant_ids[] = strtolower((string) ($referenced_constant['name'] ?? '')); + } + } + if (isset($config['referencedVariable']) && is_iterable($config['referencedVariable'])) { /** @var array $referenced_variable */ foreach ($config['referencedVariable'] as $referenced_variable) { @@ -400,6 +412,14 @@ public static function loadFromXMLElement( } } + if ($e->referencedConstant) { + $config['referencedConstant'] = []; + /** @var SimpleXMLElement $referenced_constant */ + foreach ($e->referencedConstant as $referenced_constant) { + $config['referencedConstant'][]['name'] = strtolower((string)$referenced_constant['name']); + } + } + if ($e->referencedVariable) { $config['referencedVariable'] = []; @@ -533,6 +553,11 @@ public function allowsProperty(string $property_id): bool return in_array(strtolower($property_id), $this->property_ids, true); } + public function allowsClassConstant(string $constant_id): bool + { + return in_array(strtolower($constant_id), $this->class_constant_ids, true); + } + public function allowsVariable(string $var_name): bool { return in_array(strtolower($var_name), $this->var_names, true); diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php index d3b6a3cdf0a..db68b8af824 100644 --- a/src/Psalm/Config/IssueHandler.php +++ b/src/Psalm/Config/IssueHandler.php @@ -131,6 +131,17 @@ public function getReportingLevelForProperty(string $property_id): ?string return null; } + public function getReportingLevelForClassConstant(string $constant_id): ?string + { + foreach ($this->custom_levels as $custom_level) { + if ($custom_level->allowsClassConstant($constant_id)) { + return $custom_level->getErrorLevel(); + } + } + + return null; + } + public function getReportingLevelForVariable(string $var_name): ?string { foreach ($this->custom_levels as $custom_level) { @@ -155,6 +166,7 @@ public static function getAllIssueTypes(): array fn(string $issue_name): bool => $issue_name !== '' && $issue_name !== 'MethodIssue' && $issue_name !== 'PropertyIssue' + && $issue_name !== 'ClassConstantIssue' && $issue_name !== 'FunctionIssue' && $issue_name !== 'ArgumentIssue' && $issue_name !== 'VariableIssue' diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 254c87fd4f7..66a3dd7ea21 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -22,6 +22,7 @@ use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Type; +use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Union; use UnexpectedValueException; @@ -30,6 +31,7 @@ use function gettype; use function implode; use function in_array; +use function is_array; use function is_float; use function is_int; use function is_string; @@ -519,10 +521,26 @@ public static function getTypeFromValue($value): Union * Gets the Psalm literal type from a particular value * * @param array|scalar|null $value + * @throws InvalidArgumentException * */ - public static function getLiteralTypeFromValue($value): Type\Union + public static function getLiteralTypeFromValue($value, bool $sealed_array = true): Type\Union { + if (is_array($value)) { + if (empty($value)) { + return Type::getEmptyArray(); + } + + $types = []; + /** @var array|scalar|null $val */ + foreach ($value as $key => $val) { + $types[$key] = self::getLiteralTypeFromValue($val, $sealed_array); + } + $type = new TKeyedArray($types); + $type->sealed = $sealed_array; + return new Type\Union([$type]); + } + if (is_string($value)) { return Type::getString($value); } @@ -543,7 +561,11 @@ public static function getLiteralTypeFromValue($value): Type\Union return Type::getTrue(); } - return Type::getNull(); + if ($value === null) { + return Type::getNull(); + } + + throw new InvalidArgumentException(); } /** diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index af55952a1ea..9f5e1ff305b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -117,6 +117,12 @@ public static function analyze( $item_value_type = null; } + if ($item_key_type === null && $item_value_type === null) { + $statements_analyzer->node_data->setType($stmt, Type::getEmptyArray()); + + return true; + } + // if this array looks like an object-like array, let's return that instead if ($item_value_type && $item_key_type diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php index 5ed42002c5a..10ef6434885 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php @@ -15,11 +15,13 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\TraitAnalyzer; use Psalm\Internal\FileManipulation\FileManipulationBuffer; +use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Issue\CircularReference; use Psalm\Issue\DeprecatedClass; use Psalm\Issue\DeprecatedConstant; use Psalm\Issue\InaccessibleClassConstant; use Psalm\Issue\InternalClass; +use Psalm\Issue\InvalidConstantAssignmentValue; use Psalm\Issue\NonStaticSelfCall; use Psalm\Issue\ParentNotFound; use Psalm\Issue\UndefinedConstant; @@ -35,6 +37,7 @@ use Psalm\Type\Union; use ReflectionProperty; +use function assert; use function explode; use function in_array; use function strtolower; @@ -676,6 +679,32 @@ public static function analyzeClassConstAssignment( ): void { foreach ($stmt->consts as $const) { ExpressionAnalyzer::analyze($statements_analyzer, $const->value, $context); + + assert($context->self !== null); + $class_storage = $statements_analyzer->getCodebase()->classlike_storage_provider->get($context->self); + $const_storage = $class_storage->constants[$const->name->name]; + if ($assigned_type = $statements_analyzer->node_data->getType($const->value)) { + if ($const_storage->type !== null + && $const_storage->stmt_location !== null + && $assigned_type !== $const_storage->type + && !UnionTypeComparator::isContainedBy( + $statements_analyzer->getCodebase(), + $assigned_type, + $const_storage->type + ) + ) { + IssueBuffer::maybeAdd( + new InvalidConstantAssignmentValue( + "{$context->self}::{$const->name->name} with declared type {$const_storage->type->getId()} " + . "cannot be assigned type {$assigned_type->getId()}", + $const_storage->stmt_location, + "{$context->self}::{$const->name->name}" + ), + $const_storage->suppressed_issues, + true + ); + } + } } } } diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index dfc1a7f0d4e..20ebf069953 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -137,7 +137,9 @@ public static function resolve( } if ($left instanceof TKeyedArray && $right instanceof TKeyedArray) { - return new TKeyedArray($left->properties + $right->properties); + $type = new TKeyedArray($left->properties + $right->properties); + $type->sealed = true; + return $type; } return new TMixed; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 35ae6b1fd9c..31104071ed6 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1270,8 +1270,10 @@ private function visitClassConstDeclaration( ); $type_location = null; + $suppressed_issues = []; if ($var_comment !== null && $var_comment->type !== null) { $const_type = $var_comment->type; + $suppressed_issues = $var_comment->suppressed_issues; if ($var_comment->type_start !== null && $var_comment->type_end !== null @@ -1301,6 +1303,7 @@ private function visitClassConstDeclaration( $const->name ) ); + $constant_storage->suppressed_issues = $suppressed_issues; $constant_storage->type_location = $type_location; diff --git a/src/Psalm/Issue/ClassConstantIssue.php b/src/Psalm/Issue/ClassConstantIssue.php new file mode 100644 index 00000000000..e747e78ac78 --- /dev/null +++ b/src/Psalm/Issue/ClassConstantIssue.php @@ -0,0 +1,21 @@ +const_id = $const_id; + } +} diff --git a/src/Psalm/Issue/InvalidConstantAssignmentValue.php b/src/Psalm/Issue/InvalidConstantAssignmentValue.php new file mode 100644 index 00000000000..32922b5e37f --- /dev/null +++ b/src/Psalm/Issue/InvalidConstantAssignmentValue.php @@ -0,0 +1,8 @@ + + */ + public $suppressed_issues = []; + /** * @var ?string */ diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index ec0c6831b93..03f4f21f49d 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1514,9 +1514,8 @@ function unpackIterable(Traversable $data): array $y = []; $x = [...$x, ...$y]; - - $x ? 1 : 0; ', + 'assertions' => ['$x' => 'array'] ], 'unpackEmptyKeepsCorrectKeys' => [ 'code' => ' + + + + + ' ) @@ -589,6 +594,14 @@ public function testIssueHandlerWithCustomErrorLevels(): void 'b' ) ); + + $this->assertSame( + 'suppress', + $config->getReportingLevelForClassConstant( + 'InvalidConstantAssignmentValue', + 'Psalm\Bodger::FOO' + ) + ); } public function testIssueHandlerSetDynamically(): void diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 54fb1ff41ff..35e00879dda 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -619,6 +619,8 @@ class B extends A { class Clazz { /** + * @var 0|1 + * * @psalm-suppress RedundantCondition */ const cons2 = (cons1) ? 1 : 0; @@ -1293,6 +1295,30 @@ public function bar(): string } ', ], + 'classConstSuppress' => [ + ' [ + ' $arg */ + function foo(array $arg): void {} + foo([...A::ARR]); + ', + ], ]; } @@ -1642,6 +1668,28 @@ public function bar(int $key): string ', 'error_message' => 'UnresolvableConstant', ], + 'invalidConstantAssignmentType' => [ + ' "InvalidConstantAssignmentValue", + ], + 'invalidConstantAssignmentTypeResolvedLate' => [ + ' "InvalidConstantAssignmentValue", + ], ]; } } From 0fb0632e0d852e6861f128509f8cf2ee847a72b4 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 05/10] Infer type from known ternary condition. --- .../Statements/Expression/TernaryAnalyzer.php | 12 +++++++++--- tests/ConstantTest.php | 2 -- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php index 99ed3eb6c45..39905239d37 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php @@ -274,12 +274,12 @@ function (Clause $c) use ($mixed_var_ids, $cond_id): Clause { ); $lhs_type = null; - + $stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond); if ($stmt->if) { if ($stmt_if_type = $statements_analyzer->node_data->getType($stmt->if)) { $lhs_type = $stmt_if_type; } - } elseif ($stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond)) { + } elseif ($stmt_cond_type) { $if_return_type_reconciled = AssertionReconciler::reconcile( new Truthy(), clone $stmt_cond_type, @@ -295,7 +295,13 @@ function (Clause $c) use ($mixed_var_ids, $cond_id): Clause { } if ($lhs_type && ($stmt_else_type = $statements_analyzer->node_data->getType($stmt->else))) { - $statements_analyzer->node_data->setType($stmt, Type::combineUnionTypes($lhs_type, $stmt_else_type)); + if ($stmt_cond_type !== null && $stmt_cond_type->isAlwaysFalsy()) { + $statements_analyzer->node_data->setType($stmt, $stmt_else_type); + } elseif ($stmt_cond_type !== null && $stmt_cond_type->isAlwaysTruthy()) { + $statements_analyzer->node_data->setType($stmt, $lhs_type); + } else { + $statements_analyzer->node_data->setType($stmt, Type::combineUnionTypes($lhs_type, $stmt_else_type)); + } } else { $statements_analyzer->node_data->setType($stmt, Type::getMixed()); } diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 35e00879dda..8b3c6adb812 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -619,8 +619,6 @@ class B extends A { class Clazz { /** - * @var 0|1 - * * @psalm-suppress RedundantCondition */ const cons2 = (cons1) ? 1 : 0; From c61bde87a0bac4abf1e2d4dd1f81a30b1d34c3d6 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 06/10] Suppress PossiblyUnusedProperty, will be used in the future. --- src/Psalm/Storage/ClassConstantStorage.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php index 3f4c4b092e6..aeef0663831 100644 --- a/src/Psalm/Storage/ClassConstantStorage.php +++ b/src/Psalm/Storage/ClassConstantStorage.php @@ -7,6 +7,9 @@ use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Type\Union; +/** + * @psalm-suppress PossiblyUnusedProperty + */ class ClassConstantStorage { use CustomMetadataTrait; From 1686f28a379a7da070973e6775e36a24c88504f1 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 07/10] Default to using inferred type unless in late static binding context. --- src/Psalm/Internal/Codebase/ClassLikes.php | 2 +- tests/ConstantTest.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index ca2bba3ba03..1d394e2fa37 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1633,7 +1633,7 @@ public function getClassConstantType( int $visibility, ?StatementsAnalyzer $statements_analyzer = null, array $visited_constant_ids = [], - bool $late_static_binding = true + bool $late_static_binding = false ): ?Union { $class_name = strtolower($class_name); diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 8b3c6adb812..2bae45d82e3 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -305,14 +305,17 @@ function foo(array $arg): void {} 'lateConstantResolutionParentStringConcat' => [ 'code' => ' Date: Thu, 20 Jan 2022 16:51:55 -0600 Subject: [PATCH 08/10] Fix cs/psalm errors after rebase. --- docs/running_psalm/issues.md | 2 ++ .../UnresolvableConstantException.php | 1 + src/Psalm/Issue/ClassConstantIssue.php | 1 + .../Issue/InvalidConstantAssignmentValue.php | 1 + src/Psalm/Issue/UnresolvableConstant.php | 1 + src/Psalm/Storage/ClassConstantStorage.php | 1 - tests/ConstantTest.php | 20 +++++++++---------- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 7c3da484357..dca69c9dda2 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -61,6 +61,7 @@ - [InvalidCatch](issues/InvalidCatch.md) - [InvalidClass](issues/InvalidClass.md) - [InvalidClone](issues/InvalidClone.md) + - [InvalidConstantAssignmentValue](issues/InvalidConstantAssignmentValue.md) - [InvalidDocblock](issues/InvalidDocblock.md) - [InvalidDocblockParamName](issues/InvalidDocblockParamName.md) - [InvalidEnumBackingType](issues/InvalidEnumBackingType.md) @@ -266,6 +267,7 @@ - [UnnecessaryVarAnnotation](issues/UnnecessaryVarAnnotation.md) - [UnrecognizedExpression](issues/UnrecognizedExpression.md) - [UnrecognizedStatement](issues/UnrecognizedStatement.md) + - [UnresolvableConstant](issues/UnresolvableConstant.md) - [UnresolvableInclude](issues/UnresolvableInclude.md) - [UnsafeGenericInstantiation](issues/UnsafeGenericInstantiation.md) - [UnsafeInstantiation](issues/UnsafeInstantiation.md) diff --git a/src/Psalm/Exception/UnresolvableConstantException.php b/src/Psalm/Exception/UnresolvableConstantException.php index 3295f663f99..10032182587 100644 --- a/src/Psalm/Exception/UnresolvableConstantException.php +++ b/src/Psalm/Exception/UnresolvableConstantException.php @@ -1,4 +1,5 @@ - * @psalm-suppress PossiblyUnusedProperty */ public $attributes = []; diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 2bae45d82e3..0dcd0630670 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -1266,7 +1266,7 @@ public static function boo( ', ], 'selfConstUsesInferredType' => [ - ' ' [ - ' ' [ - ' ' [ - ' ' '8.1', ], 'returnValueofNonExistantConstant' => [ - ' ' 'UnresolvableConstant', ], 'returnValueofStaticConstant' => [ - ' ' 'UnresolvableConstant', ], 'takeKeyofNonExistantConstant' => [ - ' ' 'UnresolvableConstant', ], 'takeKeyofStaticConstant' => [ - ' ' 'UnresolvableConstant', ], 'invalidConstantAssignmentType' => [ - ' ' "InvalidConstantAssignmentValue", ], 'invalidConstantAssignmentTypeResolvedLate' => [ - ' ' Date: Sat, 22 Jan 2022 17:05:01 -0600 Subject: [PATCH 09/10] Refactor existing method to reduce redundant code. --- .../Internal/Analyzer/ClassLikeAnalyzer.php | 56 ------------------- .../Codebase/ConstantTypeResolver.php | 33 +++++++++-- src/Psalm/Internal/Codebase/Reflection.php | 2 +- 3 files changed, 29 insertions(+), 62 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 66a3dd7ea21..339d5079f10 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -22,7 +22,6 @@ use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Type; -use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Union; use UnexpectedValueException; @@ -31,10 +30,6 @@ use function gettype; use function implode; use function in_array; -use function is_array; -use function is_float; -use function is_int; -use function is_string; use function preg_match; use function preg_replace; use function strtolower; @@ -517,57 +512,6 @@ public static function getTypeFromValue($value): Union } } - /** - * Gets the Psalm literal type from a particular value - * - * @param array|scalar|null $value - * @throws InvalidArgumentException - * - */ - public static function getLiteralTypeFromValue($value, bool $sealed_array = true): Type\Union - { - if (is_array($value)) { - if (empty($value)) { - return Type::getEmptyArray(); - } - - $types = []; - /** @var array|scalar|null $val */ - foreach ($value as $key => $val) { - $types[$key] = self::getLiteralTypeFromValue($val, $sealed_array); - } - $type = new TKeyedArray($types); - $type->sealed = $sealed_array; - return new Type\Union([$type]); - } - - if (is_string($value)) { - return Type::getString($value); - } - - if (is_int($value)) { - return Type::getInt(false, $value); - } - - if (is_float($value)) { - return Type::getFloat($value); - } - - if ($value === false) { - return Type::getFalse(); - } - - if ($value === true) { - return Type::getTrue(); - } - - if ($value === null) { - return Type::getNull(); - } - - throw new InvalidArgumentException(); - } - /** * @param string[] $suppressed_issues */ diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 20ebf069953..88c9e03b189 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Codebase; +use InvalidArgumentException; use Psalm\Exception\CircularReferenceException; use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; @@ -40,6 +41,7 @@ use ReflectionProperty; use function ctype_digit; +use function is_array; use function is_float; use function is_int; use function is_string; @@ -334,10 +336,27 @@ public static function resolve( } /** - * @param string|int|float|bool|null $value + * Note: This takes an array, but any array should only contain other arrays and scalars. + * + * @param array|string|int|float|bool|null $value */ - private static function getLiteralTypeFromScalarValue($value): Atomic + public static function getLiteralTypeFromScalarValue($value, bool $sealed_array = true): Atomic { + if (is_array($value)) { + if (empty($value)) { + return Type::getEmptyArray()->getSingleAtomic(); + } + + $types = []; + /** @var array|scalar|null $val */ + foreach ($value as $key => $val) { + $types[$key] = new Union([self::getLiteralTypeFromScalarValue($val, $sealed_array)]); + } + $type = new TKeyedArray($types); + $type->sealed = $sealed_array; + return $type; + } + if (is_string($value)) { return new TLiteralString($value); } @@ -351,13 +370,17 @@ private static function getLiteralTypeFromScalarValue($value): Atomic } if ($value === false) { - return new TFalse; + return new TFalse(); } if ($value === true) { - return new TTrue; + return new TTrue(); + } + + if ($value === null) { + return new TNull(); } - return new TNull; + throw new InvalidArgumentException('$value must be a scalar.'); } } diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index e58a98f8402..45a12766cec 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -171,7 +171,7 @@ public function registerClass(ReflectionClass $reflected_class): void foreach ($class_constants as $name => $value) { $storage->constants[$name] = new ClassConstantStorage( ClassLikeAnalyzer::getTypeFromValue($value), - ClassLikeAnalyzer::getLiteralTypeFromValue($value), + new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($value)]), ClassLikeAnalyzer::VISIBILITY_PUBLIC, null ); From 1f1f1c53fe5a86b39ddd038d7b64b39747a2b9b7 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Sat, 22 Jan 2022 17:09:50 -0600 Subject: [PATCH 10/10] Fix CS issue due to rebase. --- src/Psalm/Internal/Type/TypeExpander.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index c4b26fab047..230dd5d5592 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -4,8 +4,8 @@ use Psalm\Codebase; use Psalm\Exception\CircularReferenceException; -use Psalm\Storage\Assertion\IsType; use Psalm\Exception\UnresolvableConstantException; +use Psalm\Storage\Assertion\IsType; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable;