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/config.xsd b/config.xsd
index c36262ad15a..046547eeeed 100644
--- a/config.xsd
+++ b/config.xsd
@@ -206,6 +206,7 @@
+
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..1495cdcb211 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',
+ ],
];
}
}