diff --git a/composer.json b/composer.json index 914304006aa..c45f194c036 100644 --- a/composer.json +++ b/composer.json @@ -118,6 +118,7 @@ ], "verify-callmap": "phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php", "psalm": "@php ./psalm", + "psalm-set-baseline": "@php ./psalm --set-baseline=psalm-baseline.xml", "tests": [ "@lint", "@cs", diff --git a/docs/running_psalm/issues/ConstantDeclarationInTrait.md b/docs/running_psalm/issues/ConstantDeclarationInTrait.md new file mode 100644 index 00000000000..088fb2acdd0 --- /dev/null +++ b/docs/running_psalm/issues/ConstantDeclarationInTrait.md @@ -0,0 +1,15 @@ +# ConstantDeclarationInTrait + +Emitted when a trait declares a constant in PHP <8.2.0 + +```php + - + $comment_block->tags['variablesfrom'][0] @@ -537,10 +537,6 @@ replace replace - - TTypeParams|null - TTypeParams|null - $this->type_params[1] diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index c18a2c7c49d..b0e24cfc4f8 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -24,6 +24,7 @@ use UnitEnum; use function array_filter; +use function array_flip; use function array_intersect_key; use function array_keys; use function array_merge; @@ -439,6 +440,7 @@ private function populateDataFromTrait( $this->populateClassLikeStorage($trait_storage, $dependent_classlikes); + $this->inheritConstantsFromParent($storage, $trait_storage); $this->inheritMethodsFromParent($storage, $trait_storage); $this->inheritPropertiesFromParent($storage, $trait_storage); @@ -874,6 +876,41 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file $storage->populated = true; } + private function inheritConstantsFromParent( + ClassLikeStorage $storage, + ClassLikeStorage $parent_storage + ): void { + foreach ($parent_storage->constants as $constant_name => $class_constant_storage) { + if ($parent_storage->is_trait) { + $trait_alias_map_cased = array_flip($storage->trait_alias_map_cased); + if (isset($trait_alias_map_cased[$constant_name])) { + $aliased_constant_name_lc = strtolower($trait_alias_map_cased[$constant_name]); + $aliased_constant_name = $trait_alias_map_cased[$constant_name]; + } else { + $aliased_constant_name_lc = strtolower($constant_name); + $aliased_constant_name = $constant_name; + } + $visibility = $storage->trait_visibility_map[$aliased_constant_name_lc] + ?? $class_constant_storage->visibility; + $final = $storage->trait_final_map[$aliased_constant_name_lc] ?? $class_constant_storage->final; + $storage->constants[$aliased_constant_name] = new ClassConstantStorage( + $class_constant_storage->type, + $class_constant_storage->inferred_type, + $visibility, + $class_constant_storage->location, + $class_constant_storage->type_location, + $class_constant_storage->stmt_location, + $class_constant_storage->deprecated, + $final, + $class_constant_storage->unresolved_node, + $class_constant_storage->attributes, + $class_constant_storage->suppressed_issues, + $class_constant_storage->description, + ); + } + } + } + protected function inheritMethodsFromParent( ClassLikeStorage $storage, ClassLikeStorage $parent_storage diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 52821adfb91..db7188890e3 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -38,6 +38,7 @@ use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias; use Psalm\Internal\Type\TypeParser; use Psalm\Internal\Type\TypeTokenizer; +use Psalm\Issue\ConstantDeclarationInTrait; use Psalm\Issue\DuplicateClass; use Psalm\Issue\DuplicateConstant; use Psalm\Issue\DuplicateEnumCase; @@ -843,10 +844,6 @@ public function handleTraitUse(PhpParser\Node\Stmt\TraitUse $node): void throw new UnexpectedValueException('bad'); } - $method_map = $storage->trait_alias_map ?: []; - $visibility_map = $storage->trait_visibility_map ?: []; - $final_map = $storage->trait_final_map ?: []; - foreach ($node->adaptations as $adaptation) { if ($adaptation instanceof PhpParser\Node\Stmt\TraitUseAdaptation\Alias) { $old_name = strtolower($adaptation->method->name); @@ -856,36 +853,33 @@ public function handleTraitUse(PhpParser\Node\Stmt\TraitUse $node): void $new_name = strtolower($adaptation->newName->name); if ($new_name !== $old_name) { - $method_map[$new_name] = $old_name; + $storage->trait_alias_map[$new_name] = $old_name; + $storage->trait_alias_map_cased[$adaptation->newName->name] = $adaptation->method->name; } } if ($adaptation->newModifier) { switch ($adaptation->newModifier) { case 1: - $visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PUBLIC; + $storage->trait_visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PUBLIC; break; case 2: - $visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PROTECTED; + $storage->trait_visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PROTECTED; break; case 4: - $visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PRIVATE; + $storage->trait_visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PRIVATE; break; case 32: - $final_map[$new_name] = true; + $storage->trait_final_map[$new_name] = true; break; } } } } - $storage->trait_alias_map = $method_map; - $storage->trait_visibility_map = $visibility_map; - $storage->trait_final_map = $final_map; - foreach ($node->traits as $trait) { $trait_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($trait, $this->aliases); $this->codebase->scanner->queueClassLikeForScanning($trait_fqcln, $this->file_scanner->will_analyze); @@ -1210,6 +1204,14 @@ private function visitClassConstDeclaration( ClassLikeStorage $storage, string $fq_classlike_name ): void { + if ($storage->is_trait && $this->codebase->analysis_php_version_id < 8_02_00) { + IssueBuffer::maybeAdd(new ConstantDeclarationInTrait( + 'Traits cannot declare constants until PHP <8.2.0', + new CodeLocation($this->file_scanner, $stmt), + )); + return; + } + $existing_constants = $storage->constants; $comment = $stmt->getDocComment(); diff --git a/src/Psalm/Issue/ConstantDeclarationInTrait.php b/src/Psalm/Issue/ConstantDeclarationInTrait.php new file mode 100644 index 00000000000..112fae6f65c --- /dev/null +++ b/src/Psalm/Issue/ConstantDeclarationInTrait.php @@ -0,0 +1,11 @@ + + */ + public array $trait_alias_map_cased = []; + /** * @var array */ public $trait_final_map = []; /** - * @var array + * @var array */ public $trait_visibility_map = []; diff --git a/tests/TraitTest.php b/tests/TraitTest.php index 20e5c8cae3f..c26870c313e 100644 --- a/tests/TraitTest.php +++ b/tests/TraitTest.php @@ -2,6 +2,7 @@ namespace Psalm\Tests; +use Psalm\Issue\ConstantDeclarationInTrait; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; @@ -1001,6 +1002,40 @@ trait T {} } ', ], + 'constant in trait' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + 'constant in trait with alias' => [ + 'code' => <<<'PHP' + ['$c' => 'string'], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], ]; } @@ -1193,6 +1228,15 @@ class X { }', 'error_message' => 'UndefinedDocblockClass', ], + 'constant declaration in trait, php <8.2.0' => [ + 'code' => <<<'PHP' + ConstantDeclarationInTrait::getIssueType(), + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } }