diff --git a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php deleted file mode 100644 index 61c829218a5..00000000000 --- a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php +++ /dev/null @@ -1,203 +0,0 @@ - $suppressed_issues - * @param 1|2|4|8|16|32 $target - */ - public static function analyze( - SourceAnalyzer $source, - Context $context, - AttributeStorage $attribute, - AttributeGroup $attribute_group, - array $suppressed_issues, - int $target, - ?ClassLikeStorage $classlike_storage = null - ): void { - if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( - $source, - $attribute->fq_class_name, - $attribute->location, - null, - null, - $suppressed_issues, - new ClassLikeNameOptions( - false, - false, - false, - false, - false, - true - ) - ) === false) { - return; - } - - $codebase = $source->getCodebase(); - - if (!$codebase->classlikes->classExists($attribute->fq_class_name)) { - return; - } - - if ($attribute->fq_class_name === 'Attribute' && $classlike_storage) { - if ($classlike_storage->is_trait) { - IssueBuffer::maybeAdd( - new InvalidAttribute( - 'Traits cannot act as attribute classes', - $attribute->name_location - ), - $source->getSuppressedIssues() - ); - } elseif ($classlike_storage->is_interface) { - IssueBuffer::maybeAdd( - new InvalidAttribute( - 'Interfaces cannot act as attribute classes', - $attribute->name_location - ), - $source->getSuppressedIssues() - ); - } elseif ($classlike_storage->abstract) { - IssueBuffer::maybeAdd( - new InvalidAttribute( - 'Abstract classes cannot act as attribute classes', - $attribute->name_location - ), - $source->getSuppressedIssues() - ); - } 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->name_location - ), - $source->getSuppressedIssues() - ); - } elseif ($classlike_storage->is_enum) { - IssueBuffer::maybeAdd( - new InvalidAttribute( - 'Enums cannot act as attribute classes', - $attribute->name_location - ), - $source->getSuppressedIssues() - ); - } - } - - self::checkAttributeTargets($source, $attribute, $target); - - $statements_analyzer = new StatementsAnalyzer( - $source, - new NodeDataProvider() - ); - - $statements_analyzer->analyze(self::attributeGroupToStmts($attribute_group), $context); - } - - /** - * @param 1|2|4|8|16|32 $target - */ - private static function checkAttributeTargets( - SourceAnalyzer $source, - AttributeStorage $attribute, - int $target - ): void { - $codebase = $source->getCodebase(); - - $attribute_class_storage = $codebase->classlike_storage_provider->get($attribute->fq_class_name); - - $has_attribute_attribute = $attribute->fq_class_name === 'Attribute'; - - foreach ($attribute_class_storage->attributes as $attribute_attribute) { - if ($attribute_attribute->fq_class_name === 'Attribute') { - $has_attribute_attribute = true; - - if (!$attribute_attribute->args) { - return; - } - - $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( - $codebase->classlikes, - $first_arg_type, - $source instanceof StatementsAnalyzer ? $source : null - ) - ]); - } - - if (!$first_arg_type->isSingleIntLiteral()) { - return; - } - - $acceptable_mask = $first_arg_type->getSingleIntLiteral()->value; - - if (($acceptable_mask & $target) !== $target) { - $target_map = [ - 1 => 'class', - 2 => 'function', - 4 => 'method', - 8 => 'property', - 16 => 'class constant', - 32 => 'function/method parameter' - ]; - - IssueBuffer::maybeAdd( - new InvalidAttribute( - 'This attribute can not be used on a ' . $target_map[$target], - $attribute->name_location - ), - $source->getSuppressedIssues() - ); - } - } - } - - if (!$has_attribute_attribute) { - IssueBuffer::maybeAdd( - new InvalidAttribute( - 'The class ' . $attribute->fq_class_name . ' doesn’t have the Attribute attribute', - $attribute->name_location - ), - $source->getSuppressedIssues() - ); - } - } - - /** - * @return list - */ - private static function attributeGroupToStmts(AttributeGroup $attribute_group): array - { - $stmts = []; - foreach ($attribute_group->attrs as $attr) { - $stmts[] = new Expression(new New_($attr->name, $attr->args, $attr->getAttributes())); - } - return $stmts; - } -} diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php new file mode 100644 index 00000000000..97a83acc999 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -0,0 +1,377 @@ + 'class', + 2 => 'function', + 4 => 'method', + 8 => 'property', + 16 => 'class constant', + 32 => 'function/method parameter', + 40 => 'promoted property', + ]; + + /** + * @param array $attribute_groups + * @param 1|2|4|8|16|32|40 $target + * @param array $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->fq_class_name, + $attribute_storage->name_location, + $attribute_class_storage, + $suppressed_issues + ); + + self::analyzeAttributeConstruction( + $source, + $context, + $attribute_storage, + $attribute, + $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 $suppressed_issues + */ + private static function analyzeAttributeConstruction( + SourceAnalyzer $source, + Context $context, + AttributeStorage $attribute_storage, + Attribute $attribute, + 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(array_values($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 $suppressed_issues + */ + private static function getAttributeClassFlags( + SourceAnalyzer $source, + string $attribute_name, + CodeLocation $attribute_location, + ?ClassLikeStorage $attribute_class_storage, + array $suppressed_issues + ): int { + if ($attribute_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_name} doesn't have the Attribute attribute", + $attribute_location + ), + $suppressed_issues + ); + + return 63; // Fall back to default if it's invalid + } + + /** + * @param iterable $attribute_groups + * + * @return Generator + */ + private static function iterateAttributeNodes(iterable $attribute_groups): Generator + { + foreach ($attribute_groups as $attribute_group) { + foreach ($attribute_group->attrs as $attribute) { + yield $attribute; + } + } + } + + /** + * Analyze Reflection getAttributes method calls. + + * @param list $args + */ + public static function analyzeGetAttributes( + StatementsAnalyzer $statements_analyzer, + string $method_id, + array $args + ): void { + if (count($args) !== 1) { + // We skip this analysis if $flags is specified on getAttributes, since the only option + // is ReflectionAttribute::IS_INSTANCEOF, which causes getAttributes to return children. + // When returning children we don't want to limit this since a child could add a target. + return; + } + + switch ($method_id) { + case "ReflectionClass::getattributes": + $target = 1; + break; + case "ReflectionFunction::getattributes": + $target = 2; + break; + case "ReflectionMethod::getattributes": + $target = 4; + break; + case "ReflectionProperty::getattributes": + $target = 8; + break; + case "ReflectionClassConstant::getattributes": + $target = 16; + break; + case "ReflectionParameter::getattributes": + $target = 32; + break; + default: + return; + } + + $arg = $args[0]; + if ($arg->name !== null) { + for (; !empty($args) && ($arg->name->name ?? null) !== "name"; $arg = array_shift($args)); + if ($arg->name->name ?? null !== "name") { + // No named argument for "name" parameter + return; + } + } + + $arg_type = $statements_analyzer->getNodeTypeProvider()->getType($arg->value); + if ($arg_type === null || !$arg_type->isSingle() || !$arg_type->hasLiteralString()) { + return; + } + + $class_string = $arg_type->getSingleAtomic(); + assert($class_string instanceof TLiteralString); + + $codebase = $statements_analyzer->getCodebase(); + + if (!$codebase->classExists($class_string->value)) { + return; + } + + $class_storage = $codebase->classlike_storage_provider->get($class_string->value); + $arg_location = new CodeLocation($statements_analyzer, $arg); + $class_attribute_target = self::getAttributeClassFlags( + $statements_analyzer, + $class_string->value, + $arg_location, + $class_storage, + $statements_analyzer->getSuppressedIssues() + ); + + if (($class_attribute_target & $target) === 0) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + "Attribute {$class_string->value} cannot be used on a " + . self::TARGET_DESCRIPTIONS[$target], + $arg_location + ), + $statements_analyzer->getSuppressedIssues() + ); + } + } +} diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index d7bde976e1c..7c8670631c1 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -397,17 +397,14 @@ public function analyze( } } - foreach ($storage->attributes as $i => $attribute) { - AttributeAnalyzer::analyze( - $this, - $class_context, - $attribute, - $class->attrGroups[$i], - $storage->suppressed_issues + $this->getSuppressedIssues(), - 1, - $storage - ); - } + AttributesAnalyzer::analyze( + $this, + $class_context, + $storage, + $class->attrGroups, + 1, + $storage->suppressed_issues + $this->getSuppressedIssues() + ); self::addContextProperties( $this, @@ -553,8 +550,8 @@ public function analyze( } foreach ($class->stmts as $stmt) { - if ($stmt instanceof PhpParser\Node\Stmt\Property && !$storage->is_enum && !isset($stmt->type)) { - $this->checkForMissingPropertyType($this, $stmt, $class_context); + if ($stmt instanceof PhpParser\Node\Stmt\Property) { + $this->analyzeProperty($this, $stmt, $class_context); } elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) { foreach ($stmt->traits as $trait) { $fq_trait_name = self::getFQCLNFromNameObject( @@ -600,7 +597,7 @@ public function analyze( foreach ($trait_node->stmts as $trait_stmt) { if ($trait_stmt instanceof PhpParser\Node\Stmt\Property) { - $this->checkForMissingPropertyType($trait_analyzer, $trait_stmt, $class_context); + $this->analyzeProperty($trait_analyzer, $trait_stmt, $class_context); } } @@ -1494,7 +1491,7 @@ private function analyzeTraitUse( return null; } - private function checkForMissingPropertyType( + private function analyzeProperty( SourceAnalyzer $source, PhpParser\Node\Stmt\Property $stmt, Context $context @@ -1524,16 +1521,14 @@ private function checkForMissingPropertyType( $property_storage = $class_storage->properties[$property_name]; - foreach ($property_storage->attributes as $i => $attribute) { - AttributeAnalyzer::analyze( - $source, - $context, - $attribute, - $stmt->attrGroups[$i], - $this->source->getSuppressedIssues(), - 8 - ); - } + AttributesAnalyzer::analyze( + $source, + $context, + $property_storage, + $stmt->attrGroups, + 8, + $property_storage->suppressed_issues + $this->getSuppressedIssues() + ); if ($class_property_type && ($property_storage->type_location || !$codebase->alter_code)) { return; diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index 61a220f1090..e59a1790789 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -305,7 +305,9 @@ public function populateCheckers(array $stmts): array $leftover_stmts = []; foreach ($stmts as $stmt) { - if ($stmt instanceof PhpParser\Node\Stmt\ClassLike) { + if ($stmt instanceof PhpParser\Node\Stmt\Trait_) { + $leftover_stmts[] = $stmt; + } elseif ($stmt instanceof PhpParser\Node\Stmt\ClassLike) { $this->populateClassLikeAnalyzers($stmt); } elseif ($stmt instanceof PhpParser\Node\Stmt\Namespace_) { $namespace_name = $stmt->name ? implode('\\', $stmt->name->parts) : ''; diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 772ff8bda76..88eb9c6de99 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -819,16 +819,14 @@ public function analyze( ); } - foreach ($storage->attributes as $i => $attribute) { - AttributeAnalyzer::analyze( - $this, - $context, - $attribute, - $this->function->attrGroups[$i], - $storage->suppressed_issues + $this->getSuppressedIssues(), - $storage instanceof MethodStorage ? 4 : 2 - ); - } + AttributesAnalyzer::analyze( + $this, + $context, + $storage, + $this->function->attrGroups, + $storage instanceof MethodStorage ? 4 : 2, + $storage->suppressed_issues + $this->getSuppressedIssues() + ); return null; } @@ -1269,16 +1267,14 @@ private function processParams( $context->hasVariable('$' . $function_param->name); } - foreach ($function_param->attributes as $i => $attribute) { - AttributeAnalyzer::analyze( - $this, - $context, - $attribute, - $param_stmts[$offset]->attrGroups[$i], - $storage->suppressed_issues, - $function_param->promoted_property ? 8 : 32 - ); - } + AttributesAnalyzer::analyze( + $this, + $context, + $function_param, + $param_stmts[$offset]->attrGroups, + $function_param->promoted_property ? 40 : 32, + $storage->suppressed_issues + $this->getSuppressedIssues() + ); } return $check_stmts; diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index 10cb397270d..5bd966765d5 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -97,17 +97,14 @@ public function analyze(): void $class_storage = $codebase->classlike_storage_provider->get($fq_interface_name); $interface_context = new Context($this->getFQCLN()); - foreach ($class_storage->attributes as $i => $attribute) { - AttributeAnalyzer::analyze( - $this, - $interface_context, - $attribute, - $this->class->attrGroups[$i], - $class_storage->suppressed_issues + $this->getSuppressedIssues(), - 1, - $class_storage - ); - } + AttributesAnalyzer::analyze( + $this, + $interface_context, + $class_storage, + $this->class->attrGroups, + 1, + $class_storage->suppressed_issues + $this->getSuppressedIssues() + ); foreach ($this->class->stmts as $stmt) { if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index a05c8a18d57..c3930b74cb8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -6,6 +6,7 @@ use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Context; +use Psalm\Internal\Analyzer\AttributesAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; @@ -54,6 +55,7 @@ use function array_map; use function array_reverse; use function array_slice; +use function array_values; use function count; use function in_array; use function is_string; @@ -260,6 +262,16 @@ public static function analyze( } } + if ($method_id === "ReflectionClass::getattributes" + || $method_id === "ReflectionClassConstant::getattributes" + || $method_id === "ReflectionFunction::getattributes" + || $method_id === "ReflectionMethod::getattributes" + || $method_id === "ReflectionParameter::getattributes" + || $method_id === "ReflectionProperty::getattributes" + ) { + AttributesAnalyzer::analyzeGetAttributes($statements_analyzer, $method_id, array_values($args)); + } + return null; } diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 78a25799a41..3c3e040c1e7 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -593,6 +593,8 @@ private static function analyzeStatement( // disregard this exception, we'll likely see it elsewhere in the form // of an issue } + } elseif ($stmt instanceof PhpParser\Node\Stmt\Trait_) { + TraitAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) { // do nothing } elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) { diff --git a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php index c59fc8c0b9d..129db68300d 100644 --- a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php @@ -2,8 +2,11 @@ namespace Psalm\Internal\Analyzer; -use PhpParser; +use PhpParser\Node\Stmt\Trait_; use Psalm\Aliases; +use Psalm\Context; + +use function assert; /** * @internal @@ -16,7 +19,7 @@ class TraitAnalyzer extends ClassLikeAnalyzer private $aliases; public function __construct( - PhpParser\Node\Stmt\Trait_ $class, + Trait_ $class, SourceAnalyzer $source, string $fq_class_name, Aliases $aliases @@ -56,4 +59,18 @@ public function getAliasedClassesFlippedReplaceable(): array { return []; } + + public static function analyze(StatementsAnalyzer $statements_analyzer, Trait_ $stmt, Context $context): void + { + assert($stmt->name !== null); + $storage = $statements_analyzer->getCodebase()->classlike_storage_provider->get($stmt->name->name); + AttributesAnalyzer::analyze( + $statements_analyzer, + $context, + $storage, + $stmt->attrGroups, + 1, + $storage->suppressed_issues + $statements_analyzer->getSuppressedIssues() + ); + } } diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 78681da1b46..1b33c69acb2 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -11,7 +11,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; -class ClassLikeStorage +class ClassLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -462,4 +462,12 @@ public function __construct(string $name) { $this->name = $name; } + + /** + * @return list + */ + public function getAttributeStorages(): array + { + return $this->attributes; + } } diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 261b203a3c7..70147e8ce00 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -6,7 +6,7 @@ use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Type\Union; -class FunctionLikeParameter +class FunctionLikeParameter implements HasAttributesInterface { use CustomMetadataTrait; @@ -150,4 +150,12 @@ public function __clone() $this->type = clone $this->type; } } + + /** + * @return list + */ + public function getAttributeStorages(): array + { + return $this->attributes; + } } diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index e9928ce565a..aba5fc68b36 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -12,7 +12,7 @@ use function array_map; use function implode; -abstract class FunctionLikeStorage +abstract class FunctionLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -293,4 +293,12 @@ public function addParam(FunctionLikeParameter $param, bool $lookup_value = null $this->params[] = $param; $this->param_lookup[$param->name] = $lookup_value ?? true; } + + /** + * @return list + */ + public function getAttributeStorages(): array + { + return $this->attributes; + } } diff --git a/src/Psalm/Storage/HasAttributesInterface.php b/src/Psalm/Storage/HasAttributesInterface.php new file mode 100644 index 00000000000..2d8bb18e3af --- /dev/null +++ b/src/Psalm/Storage/HasAttributesInterface.php @@ -0,0 +1,15 @@ + + */ + public function getAttributeStorages(): array; +} diff --git a/src/Psalm/Storage/PropertyStorage.php b/src/Psalm/Storage/PropertyStorage.php index 7fdc14f7ad8..cbe8bf6c86c 100644 --- a/src/Psalm/Storage/PropertyStorage.php +++ b/src/Psalm/Storage/PropertyStorage.php @@ -6,7 +6,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Type\Union; -class PropertyStorage +class PropertyStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -124,4 +124,12 @@ public function getInfo(): string return $visibility_text . ' ' . ($this->type ? $this->type->getId() : 'mixed'); } + + /** + * @return list + */ + public function getAttributeStorages(): array + { + return $this->attributes; + } } diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 15df6c68d3b..7f453ba7bc2 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -124,6 +124,14 @@ class ReflectionParameter implements Reflector { public function hasType() : bool {} public function getType() : ?ReflectionType {} + + /** + * @since 8.0 + * @template TClass as object + * @param class-string|null $name + * @return ($name is null ? array> : array>) + */ + public function getAttributes(?string $name = null, int $flags = 0): array {} } /** diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index c6735854541..7d303704203 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -51,9 +51,6 @@ public function __construct( public string $name = "", ) {} }', - [], - [], - '8.0' ], 'functionAttributeExists' => [ ' [ ' [ ' [ ' [ ' [ ' [ + ' [ + ' [ + ' [ + ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', - [], - false, - '8.0' ], 'missingAttributeOnClass' => [ ' 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', - [], - false, - '8.0' + ], + 'missingAttributeOnProperty' => [ + ' 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:27', ], 'missingAttributeOnFunction' => [ ' 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', - [], - false, - '8.0' ], 'missingAttributeOnParam' => [ ' 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:36', - [], - false, - '8.0' ], 'tooFewArgumentsToAttributeConstructor' => [ ' 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23', - [], - false, - '8.0' ], 'invalidArgument' => [ ' 'InvalidScalarArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:27', - [], - false, - '8.0' ], 'classAttributeUsedOnFunction' => [ ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23', - [], - false, - '8.0' ], 'interfaceCannotBeAttributeClass' => [ ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', - [], - false, - '8.0' ], 'traitCannotBeAttributeClass' => [ ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', - [], - false, - '8.0' ], 'abstractClassCannotBeAttributeClass' => [ ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', - [], - false, - '8.0' ], - 'abstractClassCannotHavePrivateConstructor' => [ + 'attributeClassCannotHavePrivateConstructor' => [ ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', - [], - false, - '8.0' + ], + 'SKIPPED-attributeInvalidTargetClassConst' => [ // Will be implemented in Psalm 5 where we have better class const analysis + ' 'InvalidAttribute', + ], + 'attributeInvalidTargetProperty' => [ + ' 'InvalidAttribute', + ], + 'attributeInvalidTargetMethod' => [ + ' 'InvalidAttribute', + ], + 'attributeInvalidTargetFunction' => [ + ' 'InvalidAttribute', + ], + 'attributeInvalidTargetParameter' => [ + ' 'InvalidAttribute', + ], + 'attributeTargetArgCannotBeVariable' => [ + ' 'UndefinedVariable', + ], + 'attributeTargetArgCannotBeSelfConst' => [ + ' 'NonStaticSelfCall', ], 'noParentInAttributeOnClassWithoutParent' => [ ' 'ParentNotFound', ], + 'undefinedConstantInAttribute' => [ + ' 'UndefinedConstant', + ], + 'getAttributesOnClassWithNonClassAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a class', + ], + 'getAttributesOnFunctionWithNonFunctionAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:39 - Attribute Attr cannot be used on a function', + ], + 'getAttributesOnMethodWithNonMethodAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a method', + ], + 'getAttributesOnPropertyWithNonPropertyAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a property', + ], + 'getAttributesOnClassConstantWithNonClassConstantAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a class constant', + ], + 'getAttributesOnParameterWithNonParameterAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a function/method parameter', + ], + 'getAttributesWithNonAttribute' => [ + 'getAttributes(NonAttr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:39 - The class NonAttr doesn\'t have the Attribute attribute', + ], + 'analyzeConstructorForNonexistentAttributes' => [ + ' 'InvalidScalarArgument', + ], + 'multipleAttributesShowErrors' => [ + ' 'InvalidAttribute', + ], + 'repeatNonRepeatableAttribute' => [ + ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:5:28 - Attribute Foo is not repeatable', + ], ]; } } diff --git a/tests/TraitTest.php b/tests/TraitTest.php index 71f402fa137..18f0ebe4caa 100644 --- a/tests/TraitTest.php +++ b/tests/TraitTest.php @@ -13,7 +13,7 @@ class TraitTest extends TestCase use ValidCodeAnalysisTestTrait; /** - * @return iterable,error_levels?:string[]}> + * @return iterable,error_levels?:string[],php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -990,6 +990,12 @@ final class A { use T; }' ], + 'suppressIssueOnTrait' => [ + '