Skip to content

Commit

Permalink
Support constants in traits
Browse files Browse the repository at this point in the history
  • Loading branch information
jack-worman committed Jan 17, 2023
1 parent 8b9cd5f commit a1152b2
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 19 deletions.
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions docs/running_psalm/issues/ConstantDeclarationInTrait.md
@@ -0,0 +1,15 @@
# ConstantDeclarationInTrait

Emitted when a trait declares a constant in PHP <8.2.0

```php
<?php

trait A {
const B = 0;
}
```

## Why this is bad

A fatal error will be thrown.
6 changes: 1 addition & 5 deletions psalm-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@dbcfe62c5224603912c94c1eab5d7c31841ada82">
<files psalm-version="dev-master@8b9cd5fb333866c1e84ca9564394816a7ff5ae6f">
<file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset>
<code>$comment_block-&gt;tags['variablesfrom'][0]</code>
Expand Down Expand Up @@ -537,10 +537,6 @@
<code>replace</code>
<code>replace</code>
</ImpureMethodCall>
<InvalidReturnType>
<code>TTypeParams|null</code>
<code>TTypeParams|null</code>
</InvalidReturnType>
<PossiblyUndefinedIntArrayOffset>
<code>$this-&gt;type_params[1]</code>
</PossiblyUndefinedIntArrayOffset>
Expand Down
37 changes: 37 additions & 0 deletions src/Psalm/Internal/Codebase/Populator.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
28 changes: 15 additions & 13 deletions src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions src/Psalm/Issue/ConstantDeclarationInTrait.php
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psalm\Issue;

final class ConstantDeclarationInTrait extends CodeIssue
{
public const ERROR_LEVEL = -1;
public const SHORTCODE = 315;
}
8 changes: 7 additions & 1 deletion src/Psalm/Storage/ClassLikeStorage.php
Expand Up @@ -4,6 +4,7 @@

use Psalm\Aliases;
use Psalm\CodeLocation;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TypeAlias\ClassTypeAlias;
use Psalm\Issue\CodeIssue;
Expand Down Expand Up @@ -183,13 +184,18 @@ final class ClassLikeStorage implements HasAttributesInterface
*/
public $trait_alias_map = [];

/**
* @var array<string, string>
*/
public array $trait_alias_map_cased = [];

/**
* @var array<lowercase-string, bool>
*/
public $trait_final_map = [];

/**
* @var array<string, int>
* @var array<string, ClassLikeAnalyzer::VISIBILITY_*>
*/
public $trait_visibility_map = [];

Expand Down
44 changes: 44 additions & 0 deletions tests/TraitTest.php
Expand Up @@ -2,6 +2,7 @@

namespace Psalm\Tests;

use Psalm\Issue\ConstantDeclarationInTrait;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;

Expand Down Expand Up @@ -1001,6 +1002,40 @@ trait T {}
}
',
],
'constant in trait' => [
'code' => <<<'PHP'
<?php
trait TraitA {
public const PUBLIC_CONST = 'PUBLIC_CONST';
protected const PROTECTED_CONST = 'PROTECTED_CONST';
private const PRIVATE_CONST = 'PRIVATE_CONST';
}
class ClassB {
use TraitA;
public static function getPublicConst(): string { return self::PUBLIC_CONST; }
public static function getProtectedConst(): string { return self::PROTECTED_CONST; }
public static function getPrivateConst(): string { return self::PRIVATE_CONST; }
}
class ClassC extends ClassB {
public static function getPublicConst(): string { return self::PUBLIC_CONST; }
public static function getProtectedConst(): string { return self::PROTECTED_CONST; }
}
PHP,
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.2',
],
'constant in trait with alias' => [
'code' => <<<'PHP'
<?php
trait TraitA { private const PRIVATE_CONST = 'PRIVATE_CONST'; }
class ClassB { use TraitA { PRIVATE_CONST as public PUBLIC_CONST; } }
$c = ClassB::PUBLIC_CONST;
PHP,
'assertions' => ['$c' => 'string'],
'ignored_issues' => [],
'php_version' => '8.2',
],
];
}

Expand Down Expand Up @@ -1193,6 +1228,15 @@ class X {
}',
'error_message' => 'UndefinedDocblockClass',
],
'constant declaration in trait, php <8.2.0' => [
'code' => <<<'PHP'
<?php
trait A { const B = 0; }
PHP,
'error_message' => ConstantDeclarationInTrait::getIssueType(),
'ignored_issues' => [],
'php_version' => '8.1',
],
];
}
}

0 comments on commit a1152b2

Please sign in to comment.