Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support constants in traits #9126

Merged
merged 1 commit into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions config.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@
<xs:element name="ComplexMethod" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ConfigIssue" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ConflictingReferenceConstraint" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ConstantDeclarationInTrait" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ConstructorSignatureMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ContinueOutsideLoop" type="IssueHandlerType" minOccurs="0" />
<xs:element name="DeprecatedClass" type="ClassIssueHandlerType" minOccurs="0" />
Expand Down
1 change: 1 addition & 0 deletions docs/running_psalm/issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [ComplexMethod](issues/ComplexMethod.md)
- [ConfigIssue](issues/ConfigIssue.md)
- [ConflictingReferenceConstraint](issues/ConflictingReferenceConstraint.md)
- [ConstantDeclarationInTrait](issues/ConstantDeclarationInTrait.md)
- [ConstructorSignatureMismatch](issues/ConstructorSignatureMismatch.md)
- [ContinueOutsideLoop](issues/ContinueOutsideLoop.md)
- [DeprecatedClass](issues/DeprecatedClass.md)
Expand Down
15 changes: 15 additions & 0 deletions docs/running_psalm/issues/ConstantDeclarationInTrait.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions src/Psalm/Internal/Codebase/Populator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Psalm\Internal\Codebase;

use BackedEnum;
use Exception;
use InvalidArgumentException;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\MethodIdentifier;
Expand All @@ -24,6 +25,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 +441,7 @@ private function populateDataFromTrait(

$this->populateClassLikeStorage($trait_storage, $dependent_classlikes);

$this->inheritConstantsFromTrait($storage, $trait_storage);
$this->inheritMethodsFromParent($storage, $trait_storage);
$this->inheritPropertiesFromParent($storage, $trait_storage);

Expand Down Expand Up @@ -874,6 +877,42 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
$storage->populated = true;
}

private function inheritConstantsFromTrait(
ClassLikeStorage $storage,
ClassLikeStorage $trait_storage
): void {
if (!$trait_storage->is_trait) {
throw new Exception('Class like storage is not for a trait.');
}
foreach ($trait_storage->constants as $constant_name => $class_constant_storage) {
$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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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',
],
];
}
}