mirror of
https://github.com/danog/psalm.git
synced 2024-12-11 16:59:45 +01:00
286 lines
10 KiB
PHP
286 lines
10 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Psalm\Internal\Analyzer;
|
||
|
|
||
|
use Generator;
|
||
|
use PhpParser\Node\Attribute;
|
||
|
use PhpParser\Node\AttributeGroup;
|
||
|
use PhpParser\Node\Expr\New_;
|
||
|
use PhpParser\Node\Stmt\Expression;
|
||
|
use Psalm\Context;
|
||
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||
|
use Psalm\Internal\Codebase\ConstantTypeResolver;
|
||
|
use Psalm\Internal\Provider\NodeDataProvider;
|
||
|
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
|
||
|
use Psalm\Issue\InvalidAttribute;
|
||
|
use Psalm\Issue\UndefinedClass;
|
||
|
use Psalm\IssueBuffer;
|
||
|
use Psalm\Storage\AttributeStorage;
|
||
|
use Psalm\Storage\ClassLikeStorage;
|
||
|
use Psalm\Storage\HasAttributesInterface;
|
||
|
use Psalm\Type\Union;
|
||
|
use RuntimeException;
|
||
|
|
||
|
use function reset;
|
||
|
|
||
|
class AttributesAnalyzer
|
||
|
{
|
||
|
private const TARGET_DESCRIPTIONS = [
|
||
|
1 => 'class',
|
||
|
2 => 'function',
|
||
|
4 => 'method',
|
||
|
8 => 'property',
|
||
|
16 => 'class constant',
|
||
|
32 => 'function/method parameter',
|
||
|
40 => 'promoted property',
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* @param list<AttributeGroup> $attribute_groups
|
||
|
* @param 1|2|4|8|16|32 $target
|
||
|
* @param array<int, string> $suppressed_issues
|
||
|
*/
|
||
|
public static function analyze(
|
||
|
SourceAnalyzer $source,
|
||
|
Context $context,
|
||
|
HasAttributesInterface $storage,
|
||
|
array $attribute_groups,
|
||
|
int $target,
|
||
|
array $suppressed_issues
|
||
|
): void {
|
||
|
$codebase = $source->getCodebase();
|
||
|
$appearing_non_repeatable_attributes = [];
|
||
|
$attribute_iterator = self::iterateAttributeNodes($attribute_groups);
|
||
|
foreach ($storage->getAttributeStorages() as $attribute_storage) {
|
||
|
if (!$attribute_iterator->valid()) {
|
||
|
throw new RuntimeException("Expected attribute count to match attribute storage count");
|
||
|
}
|
||
|
$attribute = $attribute_iterator->current();
|
||
|
|
||
|
$attribute_class_storage = $codebase->classlikes->classExists($attribute_storage->fq_class_name)
|
||
|
? $codebase->classlike_storage_provider->get($attribute_storage->fq_class_name)
|
||
|
: null;
|
||
|
|
||
|
$attribute_class_flags = self::getAttributeClassFlags(
|
||
|
$source,
|
||
|
$attribute_storage,
|
||
|
$attribute_class_storage,
|
||
|
$suppressed_issues
|
||
|
);
|
||
|
|
||
|
self::analyzeAttributeConstruction(
|
||
|
$source,
|
||
|
$context,
|
||
|
$attribute_storage,
|
||
|
$attribute,
|
||
|
$attribute_class_storage,
|
||
|
$suppressed_issues,
|
||
|
$storage instanceof ClassLikeStorage ? $storage : null
|
||
|
);
|
||
|
|
||
|
if (($attribute_class_flags & 64) === 0) {
|
||
|
// Not IS_REPEATABLE
|
||
|
if (isset($appearing_non_repeatable_attributes[$attribute_storage->fq_class_name])) {
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
"Attribute {$attribute_storage->fq_class_name} is not repeatable",
|
||
|
$attribute_storage->location,
|
||
|
),
|
||
|
$suppressed_issues,
|
||
|
);
|
||
|
}
|
||
|
$appearing_non_repeatable_attributes[$attribute_storage->fq_class_name] = true;
|
||
|
}
|
||
|
|
||
|
if (($attribute_class_flags & $target) === 0) {
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
"Attribute {$attribute_storage->fq_class_name} cannot be used on a "
|
||
|
. self::TARGET_DESCRIPTIONS[$target],
|
||
|
$attribute_storage->name_location
|
||
|
),
|
||
|
$suppressed_issues,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$attribute_iterator->next();
|
||
|
}
|
||
|
|
||
|
if ($attribute_iterator->valid()) {
|
||
|
throw new RuntimeException("Expected attribute count to match attribute storage count");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array<int, string> $suppressed_issues
|
||
|
*/
|
||
|
public static function analyzeAttributeConstruction(
|
||
|
SourceAnalyzer $source,
|
||
|
Context $context,
|
||
|
AttributeStorage $attribute_storage,
|
||
|
Attribute $attribute,
|
||
|
?ClassLikeStorage $attribute_class_storage,
|
||
|
array $suppressed_issues,
|
||
|
?ClassLikeStorage $classlike_storage = null
|
||
|
): void {
|
||
|
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
||
|
$source,
|
||
|
$attribute_storage->fq_class_name,
|
||
|
$attribute_storage->location,
|
||
|
null,
|
||
|
null,
|
||
|
$suppressed_issues,
|
||
|
new ClassLikeNameOptions(
|
||
|
false,
|
||
|
false,
|
||
|
false,
|
||
|
false,
|
||
|
false,
|
||
|
true
|
||
|
)
|
||
|
) === false) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ($attribute_storage->fq_class_name === 'Attribute' && $classlike_storage) {
|
||
|
if ($classlike_storage->is_trait) {
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
'Traits cannot act as attribute classes',
|
||
|
$attribute_storage->name_location
|
||
|
),
|
||
|
$suppressed_issues,
|
||
|
);
|
||
|
} elseif ($classlike_storage->is_interface) {
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
'Interfaces cannot act as attribute classes',
|
||
|
$attribute_storage->name_location
|
||
|
),
|
||
|
$suppressed_issues,
|
||
|
);
|
||
|
} elseif ($classlike_storage->abstract) {
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
'Abstract classes cannot act as attribute classes',
|
||
|
$attribute_storage->name_location
|
||
|
),
|
||
|
$suppressed_issues,
|
||
|
);
|
||
|
} elseif (isset($classlike_storage->methods['__construct'])
|
||
|
&& $classlike_storage->methods['__construct']->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||
|
) {
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
'Classes with protected/private constructors cannot act as attribute classes',
|
||
|
$attribute_storage->name_location
|
||
|
),
|
||
|
$suppressed_issues,
|
||
|
);
|
||
|
} elseif ($classlike_storage->is_enum) {
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
'Enums cannot act as attribute classes',
|
||
|
$attribute_storage->name_location
|
||
|
),
|
||
|
$suppressed_issues,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$statements_analyzer = new StatementsAnalyzer(
|
||
|
$source,
|
||
|
new NodeDataProvider()
|
||
|
);
|
||
|
$statements_analyzer->addSuppressedIssues($suppressed_issues);
|
||
|
|
||
|
IssueBuffer::startRecording();
|
||
|
$statements_analyzer->analyze(
|
||
|
[new Expression(new New_($attribute->name, $attribute->args, $attribute->getAttributes()))],
|
||
|
// Use a new Context for the Attribute attribute so that it can't access `self`
|
||
|
$attribute_storage->fq_class_name === "Attribute" ? new Context() : $context
|
||
|
);
|
||
|
$issues = IssueBuffer::clearRecordingLevel();
|
||
|
IssueBuffer::stopRecording();
|
||
|
foreach ($issues as $issue) {
|
||
|
if ($issue instanceof UndefinedClass && $issue->fq_classlike_name === $attribute_storage->fq_class_name) {
|
||
|
// Remove UndefinedClass for the attribute, since we already added UndefinedAttribute
|
||
|
continue;
|
||
|
}
|
||
|
IssueBuffer::bubbleUp($issue);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array<int, string> $suppressed_issues
|
||
|
*/
|
||
|
private static function getAttributeClassFlags(
|
||
|
SourceAnalyzer $source,
|
||
|
AttributeStorage $attribute,
|
||
|
?ClassLikeStorage $attribute_class_storage,
|
||
|
array $suppressed_issues
|
||
|
): int {
|
||
|
if ($attribute->fq_class_name === "Attribute") {
|
||
|
// We override this here because we still want to analyze attributes
|
||
|
// for PHP 7.4 when the Attribute class doesn't yet exist.
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
if ($attribute_class_storage === null) {
|
||
|
return 63; // Defaults to TARGET_ALL
|
||
|
}
|
||
|
|
||
|
foreach ($attribute_class_storage->attributes as $attribute_attribute) {
|
||
|
if ($attribute_attribute->fq_class_name === 'Attribute') {
|
||
|
if (!$attribute_attribute->args) {
|
||
|
return 63; // Defaults to TARGET_ALL
|
||
|
}
|
||
|
|
||
|
$first_arg = reset($attribute_attribute->args);
|
||
|
|
||
|
$first_arg_type = $first_arg->type;
|
||
|
|
||
|
if ($first_arg_type instanceof UnresolvedConstantComponent) {
|
||
|
$first_arg_type = new Union([
|
||
|
ConstantTypeResolver::resolve(
|
||
|
$source->getCodebase()->classlikes,
|
||
|
$first_arg_type,
|
||
|
$source instanceof StatementsAnalyzer ? $source : null
|
||
|
)
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
if (!$first_arg_type->isSingleIntLiteral()) {
|
||
|
return 63; // Fall back to default if it's invalid
|
||
|
}
|
||
|
|
||
|
return $first_arg_type->getSingleIntLiteral()->value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
IssueBuffer::maybeAdd(
|
||
|
new InvalidAttribute(
|
||
|
"The class {$attribute->fq_class_name} doesn't have the Attribute attribute",
|
||
|
$attribute->name_location
|
||
|
),
|
||
|
$suppressed_issues
|
||
|
);
|
||
|
|
||
|
return 63; // Fall back to default if it's invalid
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param list<AttributeGroup> $attribute_groups
|
||
|
*
|
||
|
* @return iterator<int, Attribute>
|
||
|
*/
|
||
|
private static function iterateAttributeNodes(array $attribute_groups): Generator
|
||
|
{
|
||
|
foreach ($attribute_groups as $attribute_group) {
|
||
|
foreach ($attribute_group->attrs as $attribute) {
|
||
|
yield $attribute;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|