diff --git a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php index 3e18d03c1e5..01b860c6c69 100644 --- a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php @@ -2,19 +2,17 @@ namespace Psalm\Internal\Analyzer; +use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Expr\New_; +use PhpParser\Node\Stmt; +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\Internal\Stubs\Generator\StubsGenerator; use Psalm\Issue\InvalidAttribute; use Psalm\IssueBuffer; -use Psalm\Node\Expr\VirtualNew; -use Psalm\Node\Name\VirtualFullyQualified; -use Psalm\Node\Stmt\VirtualExpression; -use Psalm\Node\VirtualArg; -use Psalm\Node\VirtualIdentifier; use Psalm\Storage\AttributeStorage; use Psalm\Storage\ClassLikeStorage; use Psalm\Type\Union; @@ -30,6 +28,7 @@ class AttributeAnalyzer public static function analyze( SourceAnalyzer $source, AttributeStorage $attribute, + AttributeGroup $attribute_group, array $suppressed_issues, int $target, ?ClassLikeStorage $classlike_storage = null @@ -107,77 +106,12 @@ public static function analyze( self::checkAttributeTargets($source, $attribute, $target); - $node_args = []; - - foreach ($attribute->args as $storage_arg) { - $type = $storage_arg->type; - - if ($type instanceof UnresolvedConstantComponent) { - $type = new Union([ - ConstantTypeResolver::resolve( - $codebase->classlikes, - $type, - $source instanceof StatementsAnalyzer ? $source : null - ) - ]); - } - - if ($type->isMixed()) { - return; - } - - $type_expr = StubsGenerator::getExpressionFromType( - $type - ); - - $arg_attributes = [ - 'startFilePos' => $storage_arg->location->raw_file_start, - 'endFilePos' => $storage_arg->location->raw_file_end, - 'startLine' => $storage_arg->location->raw_line_number - ]; - - $type_expr->setAttributes($arg_attributes); - - $node_args[] = new VirtualArg( - $type_expr, - false, - false, - $arg_attributes, - $storage_arg->name - ? new VirtualIdentifier( - $storage_arg->name, - $arg_attributes - ) - : null - ); - } - - $new_stmt = new VirtualNew( - new VirtualFullyQualified( - $attribute->fq_class_name, - [ - 'startFilePos' => $attribute->name_location->raw_file_start, - 'endFilePos' => $attribute->name_location->raw_file_end, - 'startLine' => $attribute->name_location->raw_line_number - ] - ), - $node_args, - [ - 'startFilePos' => $attribute->location->raw_file_start, - 'endFilePos' => $attribute->location->raw_file_end, - 'startLine' => $attribute->location->raw_line_number - ] - ); - $statements_analyzer = new StatementsAnalyzer( $source, new NodeDataProvider() ); - $statements_analyzer->analyze( - [new VirtualExpression($new_stmt)], - new Context() - ); + $statements_analyzer->analyze(self::attributeGroupToStmts($attribute_group), new Context()); } /** @@ -253,4 +187,16 @@ private static function checkAttributeTargets( ); } } + + /** + * @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/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index e878baf77f3..6fcfb427aea 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -397,10 +397,11 @@ public function analyze( } } - foreach ($storage->attributes as $attribute) { + foreach ($storage->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $this, $attribute, + $class->attrGroups[$i], $storage->suppressed_issues + $this->getSuppressedIssues(), 1, $storage @@ -1522,10 +1523,11 @@ private function checkForMissingPropertyType( $property_storage = $class_storage->properties[$property_name]; - foreach ($property_storage->attributes as $attribute) { + foreach ($property_storage->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $source, $attribute, + $stmt->attrGroups[$i], $this->source->getSuppressedIssues(), 8 ); diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 5e7eae3ba7a..2102ddd006e 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -5,6 +5,7 @@ use PhpParser; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Param; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use Psalm\CodeLocation; @@ -63,6 +64,7 @@ use function array_keys; use function array_merge; use function array_search; +use function array_values; use function count; use function end; use function in_array; @@ -351,6 +353,7 @@ public function analyze( $storage, $cased_method_id, $params, + array_values($this->function->params), $context, (bool) $template_types ); @@ -816,10 +819,11 @@ public function analyze( ); } - foreach ($storage->attributes as $attribute) { + foreach ($storage->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $this, $attribute, + $this->function->attrGroups[$i], $storage->suppressed_issues + $this->getSuppressedIssues(), $storage instanceof MethodStorage ? 4 : 2 ); @@ -968,13 +972,15 @@ private function checkParamReferences( } /** - * @param array $params + * @param list $params + * @param list $param_stmts */ private function processParams( StatementsAnalyzer $statements_analyzer, FunctionLikeStorage $storage, ?string $cased_method_id, array $params, + array $param_stmts, Context $context, bool $has_template_types ): bool { @@ -1262,10 +1268,11 @@ private function processParams( $context->hasVariable('$' . $function_param->name); } - foreach ($function_param->attributes as $attribute) { + foreach ($function_param->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $this, $attribute, + $param_stmts[$offset]->attrGroups[$i], $storage->suppressed_issues, $function_param->promoted_property ? 8 : 32 ); diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index 0e9b11e4abb..5759c1c6b2e 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -96,10 +96,11 @@ public function analyze(): void $class_storage = $codebase->classlike_storage_provider->get($fq_interface_name); - foreach ($class_storage->attributes as $attribute) { + foreach ($class_storage->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $this, $attribute, + $this->class->attrGroups[$i], $class_storage->suppressed_issues + $this->getSuppressedIssues(), 1, $class_storage diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 8dadb1929ed..6324b775674 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -361,7 +361,7 @@ public function methodExists( /** * @param list $args * - * @return array + * @return list */ public function getMethodParams( MethodIdentifier $method_id, diff --git a/src/Psalm/Internal/Provider/MethodParamsProvider.php b/src/Psalm/Internal/Provider/MethodParamsProvider.php index 7cf39e78f75..0033a1ee9ad 100644 --- a/src/Psalm/Internal/Provider/MethodParamsProvider.php +++ b/src/Psalm/Internal/Provider/MethodParamsProvider.php @@ -13,6 +13,7 @@ use Psalm\StatementsSource; use Psalm\Storage\FunctionLikeParameter; +use function array_values; use function is_subclass_of; use function strtolower; @@ -101,7 +102,7 @@ public function has(string $fq_classlike_name): bool /** * @param ?list $call_args * - * @return ?array + * @return ?list */ public function getMethodParams( string $fq_classlike_name, @@ -122,7 +123,7 @@ public function getMethodParams( ); if ($result !== null) { - return $result; + return array_values($result); } } @@ -138,7 +139,7 @@ public function getMethodParams( $result = $class_handler($event); if ($result !== null) { - return $result; + return array_values($result); } } diff --git a/src/Psalm/Storage/AttributeArg.php b/src/Psalm/Storage/AttributeArg.php index 510fe06c7e7..c51fa723e84 100644 --- a/src/Psalm/Storage/AttributeArg.php +++ b/src/Psalm/Storage/AttributeArg.php @@ -10,6 +10,7 @@ class AttributeArg { /** * @var ?string + * @psalm-suppress PossiblyUnusedProperty It's part of the public API for now */ public $name; @@ -20,11 +21,12 @@ class AttributeArg /** * @var CodeLocation + * @psalm-suppress PossiblyUnusedProperty It's part of the public API for now */ public $location; /** - * @param Union|UnresolvedConstantComponent $type + * @param Union|UnresolvedConstantComponent $type */ public function __construct( ?string $name, diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 25a8b1bcddd..06ebe3f6ce7 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -5,6 +5,8 @@ use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; +use const DIRECTORY_SEPARATOR; + class AttributeTest extends TestCase { use InvalidCodeAnalysisTestTrait; @@ -240,7 +242,28 @@ public function getIterator() [], [], '8.1' - ] + ], + 'createObjectAsAttributeArg' => [ + ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', [], false, '8.0' @@ -267,7 +290,7 @@ class B {}', #[Pure] class Video {}', - 'error_message' => 'UndefinedAttributeClass', + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', [], false, '8.0' @@ -278,7 +301,7 @@ class Video {}', #[Pure] function foo() : void {}', - 'error_message' => 'UndefinedAttributeClass', + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', [], false, '8.0' @@ -288,7 +311,7 @@ function foo() : void {}', use Foo\Bar\Pure; function foo(#[Pure] string $str) : void {}', - 'error_message' => 'UndefinedAttributeClass', + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:36', [], false, '8.0' @@ -304,7 +327,7 @@ public function __construct(public string $name) {} #[Table()] class Video {}', - 'error_message' => 'TooFewArguments', + 'error_message' => 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23', [], false, '8.0' @@ -321,7 +344,7 @@ public function __construct(int $i) #[Foo("foo")] class Bar{}', - 'error_message' => 'InvalidScalarArgument', + 'error_message' => 'InvalidScalarArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:27', [], false, '8.0' @@ -337,7 +360,7 @@ public function __construct(public string $name) {} #[Table("videos")] function foo() : void {}', - 'error_message' => 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23', [], false, '8.0' @@ -346,7 +369,7 @@ function foo() : void {}', ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0' @@ -355,7 +378,7 @@ interface Foo {}', ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0' @@ -364,7 +387,7 @@ interface Foo {}', ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0' @@ -375,7 +398,7 @@ abstract class Baz {}', class Baz { private function __construct() {} }', - 'error_message' => 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0'