From 694157b2e0afbf5869d93fd91f2cecec273ee1fb Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 14 Feb 2022 20:54:26 +0100 Subject: [PATCH 01/21] PDOException extends RuntimeException and can use int code errors --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- dictionaries/PropertyMap.php | 2 +- .../Method/MethodCallReturnTypeFetcher.php | 9 +++--- .../ReturnTypeProvider/ExceptionCodeTest.php | 30 +++++++++++-------- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 5c8237d38..7fff40940 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -9892,7 +9892,7 @@ return [ 'PDO::sqliteCreateCollation' => ['bool', 'name'=>'string', 'callback'=>'callable'], 'PDO::sqliteCreateFunction' => ['bool', 'function_name'=>'string', 'callback'=>'callable', 'num_args='=>'int'], 'pdo_drivers' => ['array'], -'PDOException::getCode' => ['string'], +'PDOException::getCode' => ['int|string'], 'PDOException::getFile' => ['string'], 'PDOException::getLine' => ['int'], 'PDOException::getMessage' => ['string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 8ca16b883..eca9d13fb 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -4890,7 +4890,7 @@ return [ 'PDO::sqliteCreateAggregate' => ['bool', 'function_name'=>'string', 'step_func'=>'callable', 'finalize_func'=>'callable', 'num_args='=>'int'], 'PDO::sqliteCreateCollation' => ['bool', 'name'=>'string', 'callback'=>'callable'], 'PDO::sqliteCreateFunction' => ['bool', 'function_name'=>'string', 'callback'=>'callable', 'num_args='=>'int'], - 'PDOException::getCode' => ['string'], + 'PDOException::getCode' => ['int|string'], 'PDOException::getFile' => ['string'], 'PDOException::getLine' => ['int'], 'PDOException::getMessage' => ['string'], diff --git a/dictionaries/PropertyMap.php b/dictionaries/PropertyMap.php index e0f91665d..1b2ffd01f 100644 --- a/dictionaries/PropertyMap.php +++ b/dictionaries/PropertyMap.php @@ -366,7 +366,7 @@ return [ ], 'pdoexception' => [ 'errorinfo' => 'array', - 'code' => 'string', + 'code' => 'int|string', ], 'domnode' => [ 'nodeName' => 'string', diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index a5dc24d17..76d3dfa8e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -29,6 +29,7 @@ use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; +use RuntimeException; use Throwable; use UnexpectedValueException; @@ -97,16 +98,14 @@ class MethodCallReturnTypeFetcher if ($premixin_method_id->method_name === 'getcode' && $premixin_method_id->fq_class_name !== Exception::class + && $premixin_method_id->fq_class_name !== RuntimeException::class + && $premixin_method_id->fq_class_name !== PDOException::class && ( $codebase->classImplements($premixin_method_id->fq_class_name, Throwable::class) || $codebase->interfaceExtends($premixin_method_id->fq_class_name, Throwable::class) ) ) { - if ($premixin_method_id->fq_class_name === PDOException::class) { - return Type::getString(); - } else { - return Type::getInt(true); // TODO: Remove the flag in Psalm 5 - } + return Type::getInt(true); // TODO: Remove the flag in Psalm 5 } if ($declaring_method_id && $declaring_method_id !== $method_id) { diff --git a/tests/ReturnTypeProvider/ExceptionCodeTest.php b/tests/ReturnTypeProvider/ExceptionCodeTest.php index 84e8acb28..a6aa3b8ee 100644 --- a/tests/ReturnTypeProvider/ExceptionCodeTest.php +++ b/tests/ReturnTypeProvider/ExceptionCodeTest.php @@ -13,27 +13,33 @@ class ExceptionCodeTest extends TestCase { yield 'RuntimeException' => [ 'getCode(); - } + /** @var \RuntimeException $e */ + $code = $e->getCode(); ', - [], + ['$code' => 'int|string'], + ]; + yield 'CustomRuntimeException' => [ + 'getCode(); + ', + ['$code' => 'int'], ]; yield 'LogicException' => [ 'getCode(); - } + /** @var \LogicException $e */ + $code = $e->getCode(); ', - [], + ['$code' => 'int'], ]; yield 'PDOException' => [ 'getCode(); - } + /** @var \PDOException $e */ + $code = $e->getCode(); ', - [], + ['$code' => 'int|string'], ]; yield 'CustomThrowable' => [ ' Date: Wed, 19 Jan 2022 14:20:13 +0100 Subject: [PATCH 02/21] Add option to disable @var parsing everywhere except for properties. --- config.xsd | 1 + docs/running_psalm/configuration.md | 10 ++++++++++ src/Psalm/Config.php | 6 ++++++ .../Expression/AssignmentAnalyzer.php | 16 +++++++++------- .../Analyzer/Statements/ReturnAnalyzer.php | 18 ++++++++++-------- .../Analyzer/Statements/StaticAnalyzer.php | 16 +++++++++------- .../Internal/Analyzer/StatementsAnalyzer.php | 18 ++++++++++-------- 7 files changed, 55 insertions(+), 30 deletions(-) diff --git a/config.xsd b/config.xsd index 9805ba361..e2301ea78 100644 --- a/config.xsd +++ b/config.xsd @@ -109,6 +109,7 @@ + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index f8735e390..8b1fdf5c5 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -116,6 +116,16 @@ The PHPDoc `@method` annotation normally only applies to classes with a `__call` ``` The PHPDoc `@property`, `@property-read` and `@property-write` annotations normally only apply to classes with `__get`/`__set` methods. Setting this to `true` allows you to use the `@property`, `@property-read` and `@property-write` annotations to override property existence checks and resulting property types. Defaults to `false`. +#### disableVarParsing + +```xml + +``` + +Disables parsing of `@var` PHPDocs everywhere except for properties. Setting this to `true` can remove many false positives due to outdated `@var` annotations, used before integrations of Psalm generics and proper typing, enforcing Single Source Of Truth principles. Defaults to `false`. + #### strictBinaryOperands ```xml diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index ac4355b55..2ca59125d 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -373,6 +373,11 @@ class Config */ public $add_param_default_to_docblock_type = false; + /** + * @var bool + */ + public $disable_var_parsing = false; + /** * @var bool */ @@ -920,6 +925,7 @@ class Config 'allowFileIncludes' => 'allow_includes', 'strictBinaryOperands' => 'strict_binary_operands', 'rememberPropertyAssignmentsAfterCall' => 'remember_property_assignments_after_call', + 'disableVarParsing' => 'disable_var_parsing', 'allowPhpStormGenerics' => 'allow_phpstorm_generics', 'allowStringToStandInForClass' => 'allow_string_standin_for_class', 'disableSuppressAll' => 'disable_suppress_all', diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 50ccf068d..51e919192 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -155,13 +155,15 @@ class AssignmentAnalyzer $template_type_map = $statements_analyzer->getTemplateTypeMap(); try { - $var_comments = CommentAnalyzer::getTypeFromComment( - $doc_comment, - $statements_analyzer->getSource(), - $statements_analyzer->getAliases(), - $template_type_map, - $file_storage->type_aliases - ); + $var_comments = $codebase->config->disable_var_parsing + ? [] + : CommentAnalyzer::getTypeFromComment( + $doc_comment, + $statements_analyzer->getSource(), + $statements_analyzer->getAliases(), + $template_type_map, + $file_storage->type_aliases + ); } catch (IncorrectDocblockException $e) { IssueBuffer::maybeAdd( new MissingDocblockType( diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 6936fc33e..79832cc9d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -74,14 +74,16 @@ class ReturnAnalyzer $file_storage = $file_storage_provider->get($statements_analyzer->getFilePath()); try { - $var_comments = CommentAnalyzer::arrayToDocblocks( - $doc_comment, - $parsed_docblock, - $statements_analyzer->getSource(), - $statements_analyzer->getAliases(), - $statements_analyzer->getTemplateTypeMap(), - $file_storage->type_aliases - ); + $var_comments = $codebase->config->disable_var_parsing + ? [] + : CommentAnalyzer::arrayToDocblocks( + $doc_comment, + $parsed_docblock, + $statements_analyzer->getSource(), + $statements_analyzer->getAliases(), + $statements_analyzer->getTemplateTypeMap(), + $file_storage->type_aliases + ); } catch (DocblockParseException $e) { IssueBuffer::maybeAdd( new InvalidDocblock( diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php index 6133afd45..12d068d52 100644 --- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php @@ -57,13 +57,15 @@ class StaticAnalyzer $var_comments = []; try { - $var_comments = CommentAnalyzer::arrayToDocblocks( - $doc_comment, - $parsed_docblock, - $statements_analyzer->getSource(), - $statements_analyzer->getSource()->getAliases(), - $statements_analyzer->getSource()->getTemplateTypeMap() - ); + $var_comments = $codebase->config->disable_var_parsing + ? [] + : CommentAnalyzer::arrayToDocblocks( + $doc_comment, + $parsed_docblock, + $statements_analyzer->getSource(), + $statements_analyzer->getSource()->getAliases(), + $statements_analyzer->getSource()->getTemplateTypeMap() + ); } catch (IncorrectDocblockException $e) { IssueBuffer::maybeAdd( new MissingDocblockType( diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 10ea73806..78a25799a 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -442,14 +442,16 @@ class StatementsAnalyzer extends SourceAnalyzer $var_comments = []; try { - $var_comments = CommentAnalyzer::arrayToDocblocks( - $docblock, - $statements_analyzer->parsed_docblock, - $statements_analyzer->getSource(), - $statements_analyzer->getAliases(), - $template_type_map, - $file_storage->type_aliases - ); + $var_comments = $codebase->config->disable_var_parsing + ? [] + : CommentAnalyzer::arrayToDocblocks( + $docblock, + $statements_analyzer->parsed_docblock, + $statements_analyzer->getSource(), + $statements_analyzer->getAliases(), + $template_type_map, + $file_storage->type_aliases + ); } catch (IncorrectDocblockException $e) { IssueBuffer::maybeAdd( new MissingDocblockType( From dc8764153e27f9034ee0f9f55767d92fda41b54e Mon Sep 17 00:00:00 2001 From: "a.dmitryuk" Date: Fri, 18 Feb 2022 10:05:23 +0700 Subject: [PATCH 03/21] Throw exception if file_put_contents failed --- src/Psalm/Internal/PluginManager/ConfigFile.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PluginManager/ConfigFile.php b/src/Psalm/Internal/PluginManager/ConfigFile.php index 4fd025425..db3a3242b 100644 --- a/src/Psalm/Internal/PluginManager/ConfigFile.php +++ b/src/Psalm/Internal/PluginManager/ConfigFile.php @@ -143,6 +143,9 @@ class ConfigFile } } - file_put_contents($this->path, $new_file_contents); + $result = file_put_contents($this->path, $new_file_contents); + if ($result === false) { + throw new RuntimeException(sprintf('Unable to save xml to %s', $this->path)); + } } } From 8e1e0d1e5ed12d00754cf5e547f1c90db5e0ee1d Mon Sep 17 00:00:00 2001 From: "a.dmitryuk" Date: Fri, 18 Feb 2022 10:33:35 +0700 Subject: [PATCH 04/21] style-ci --- src/Psalm/Internal/PluginManager/ConfigFile.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/PluginManager/ConfigFile.php b/src/Psalm/Internal/PluginManager/ConfigFile.php index db3a3242b..a945dd249 100644 --- a/src/Psalm/Internal/PluginManager/ConfigFile.php +++ b/src/Psalm/Internal/PluginManager/ConfigFile.php @@ -10,6 +10,7 @@ use RuntimeException; use function assert; use function file_get_contents; use function file_put_contents; +use function sprintf; use function strpos; use function substr; From ea2f452c25a1d690a7fdb2641f3fb6ad30de73f7 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 17 Feb 2022 23:15:58 -0600 Subject: [PATCH 05/21] Analyze attribute statements instead of constructing virtual statements. --- .../Internal/Analyzer/AttributeAnalyzer.php | 92 ++++--------------- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 8 +- .../Analyzer/FunctionLikeAnalyzer.php | 13 ++- .../Internal/Analyzer/InterfaceAnalyzer.php | 3 +- src/Psalm/Internal/Codebase/Methods.php | 2 +- .../Provider/MethodParamsProvider.php | 7 +- src/Psalm/Storage/AttributeArg.php | 4 +- tests/AttributeTest.php | 47 +++++++--- 8 files changed, 79 insertions(+), 97 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php index 3e18d03c1..7effa08e8 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,9 +28,10 @@ class AttributeAnalyzer public static function analyze( SourceAnalyzer $source, AttributeStorage $attribute, + AttributeGroup $attribute_group, array $suppressed_issues, int $target, - ?ClassLikeStorage $classlike_storage = null + ?ClassLikeStorage $classlike_storage = null, ): void { if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( $source, @@ -107,77 +106,12 @@ class AttributeAnalyzer 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 @@ class AttributeAnalyzer ); } } + + /** + * @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 e878baf77..92510287e 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -397,13 +397,14 @@ class ClassAnalyzer extends ClassLikeAnalyzer } } - 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 + $storage, ); } @@ -1522,10 +1523,11 @@ class ClassAnalyzer extends ClassLikeAnalyzer $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 5e7eae3ba..2102ddd00 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -5,6 +5,7 @@ namespace Psalm\Internal\Analyzer; 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_key_exists; 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 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $storage, $cased_method_id, $params, + array_values($this->function->params), $context, (bool) $template_types ); @@ -816,10 +819,11 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer ); } - 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 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer } /** - * @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 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $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 0e9b11e4a..5759c1c6b 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -96,10 +96,11 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer $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 8dadb1929..6324b7756 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -361,7 +361,7 @@ class Methods /** * @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 7cf39e78f..0033a1ee9 100644 --- a/src/Psalm/Internal/Provider/MethodParamsProvider.php +++ b/src/Psalm/Internal/Provider/MethodParamsProvider.php @@ -13,6 +13,7 @@ use Psalm\Plugin\Hook\MethodParamsProviderInterface as LegacyMethodParamsProvide use Psalm\StatementsSource; use Psalm\Storage\FunctionLikeParameter; +use function array_values; use function is_subclass_of; use function strtolower; @@ -101,7 +102,7 @@ class MethodParamsProvider /** * @param ?list $call_args * - * @return ?array + * @return ?list */ public function getMethodParams( string $fq_classlike_name, @@ -122,7 +123,7 @@ class MethodParamsProvider ); if ($result !== null) { - return $result; + return array_values($result); } } @@ -138,7 +139,7 @@ class MethodParamsProvider $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 510fe06c7..c51fa723e 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 25a8b1bcd..06ebe3f6c 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -5,6 +5,8 @@ namespace Psalm\Tests; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; +use const DIRECTORY_SEPARATOR; + class AttributeTest extends TestCase { use InvalidCodeAnalysisTestTrait; @@ -240,7 +242,28 @@ class AttributeTest extends TestCase [], [], '8.1' - ] + ], + 'createObjectAsAttributeArg' => [ + ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', [], false, '8.0' @@ -267,7 +290,7 @@ class AttributeTest extends TestCase #[Pure] class Video {}', - 'error_message' => 'UndefinedAttributeClass', + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', [], false, '8.0' @@ -278,7 +301,7 @@ class AttributeTest extends TestCase #[Pure] function foo() : void {}', - 'error_message' => 'UndefinedAttributeClass', + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', [], false, '8.0' @@ -288,7 +311,7 @@ class AttributeTest extends TestCase 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 @@ class AttributeTest extends TestCase #[Table()] class Video {}', - 'error_message' => 'TooFewArguments', + 'error_message' => 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23', [], false, '8.0' @@ -321,7 +344,7 @@ class AttributeTest extends TestCase #[Foo("foo")] class Bar{}', - 'error_message' => 'InvalidScalarArgument', + 'error_message' => 'InvalidScalarArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:27', [], false, '8.0' @@ -337,7 +360,7 @@ class AttributeTest extends TestCase #[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 @@ class AttributeTest extends TestCase ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0' @@ -355,7 +378,7 @@ class AttributeTest extends TestCase ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0' @@ -364,7 +387,7 @@ class AttributeTest extends TestCase ' 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0' @@ -375,7 +398,7 @@ class AttributeTest extends TestCase class Baz { private function __construct() {} }', - 'error_message' => 'InvalidAttribute', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', [], false, '8.0' From 0476ca78446f3dcce0f8c73c0e62b892094a0b66 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Fri, 18 Feb 2022 08:43:10 -0600 Subject: [PATCH 06/21] Fix trailing commas for PHP < 7.3. --- src/Psalm/Internal/Analyzer/AttributeAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php index 7effa08e8..01b860c6c 100644 --- a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php @@ -31,7 +31,7 @@ class AttributeAnalyzer AttributeGroup $attribute_group, array $suppressed_issues, int $target, - ?ClassLikeStorage $classlike_storage = null, + ?ClassLikeStorage $classlike_storage = null ): void { if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( $source, diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 92510287e..6fcfb427a 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -404,7 +404,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer $class->attrGroups[$i], $storage->suppressed_issues + $this->getSuppressedIssues(), 1, - $storage, + $storage ); } From 9d78c3e22ad57178ddab12feadbd7e23967853a8 Mon Sep 17 00:00:00 2001 From: m1ke Date: Mon, 21 Feb 2022 10:26:34 +0000 Subject: [PATCH 07/21] Add threads config to xsd --- config.xsd | 1 + 1 file changed, 1 insertion(+) diff --git a/config.xsd b/config.xsd index e2301ea78..2d295436a 100644 --- a/config.xsd +++ b/config.xsd @@ -128,6 +128,7 @@ + From 628bf584c25427caff243352003e69c724927fad Mon Sep 17 00:00:00 2001 From: m1ke Date: Mon, 21 Feb 2022 11:07:21 +0000 Subject: [PATCH 08/21] Alter config file to actually load threads param --- src/Psalm/Config.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 2ca59125d..33035789e 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1252,8 +1252,8 @@ class Config } } - if (isset($config_xml->threads)) { - $config->threads = (int)$config_xml->threads; + if (isset($config_xml['threads'])) { + $config->threads = (int)$config_xml['threads']; } return $config; From 04c0db5affe61218a75524d2c39241ab50a2388c Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Mon, 21 Feb 2022 10:38:50 -0600 Subject: [PATCH 09/21] Use current context when analyzing attributes (fixes #7710). --- .../Internal/Analyzer/AttributeAnalyzer.php | 3 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 ++ .../Analyzer/FunctionLikeAnalyzer.php | 2 ++ .../Internal/Analyzer/InterfaceAnalyzer.php | 4 ++- tests/AttributeTest.php | 33 +++++++++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php index 01b860c6c..61c829218 100644 --- a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php @@ -27,6 +27,7 @@ class AttributeAnalyzer */ public static function analyze( SourceAnalyzer $source, + Context $context, AttributeStorage $attribute, AttributeGroup $attribute_group, array $suppressed_issues, @@ -111,7 +112,7 @@ class AttributeAnalyzer new NodeDataProvider() ); - $statements_analyzer->analyze(self::attributeGroupToStmts($attribute_group), new Context()); + $statements_analyzer->analyze(self::attributeGroupToStmts($attribute_group), $context); } /** diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 6fcfb427a..d7bde976e 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -400,6 +400,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer foreach ($storage->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $this, + $class_context, $attribute, $class->attrGroups[$i], $storage->suppressed_issues + $this->getSuppressedIssues(), @@ -1526,6 +1527,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer foreach ($property_storage->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $source, + $context, $attribute, $stmt->attrGroups[$i], $this->source->getSuppressedIssues(), diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 2102ddd00..772ff8bda 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -822,6 +822,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer foreach ($storage->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $this, + $context, $attribute, $this->function->attrGroups[$i], $storage->suppressed_issues + $this->getSuppressedIssues(), @@ -1271,6 +1272,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer foreach ($function_param->attributes as $i => $attribute) { AttributeAnalyzer::analyze( $this, + $context, $attribute, $param_stmts[$offset]->attrGroups[$i], $storage->suppressed_issues, diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index 5759c1c6b..10cb39727 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -95,10 +95,12 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer } $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(), @@ -113,7 +115,7 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer $type_provider = new NodeDataProvider(); - $method_analyzer->analyze(new Context($this->getFQCLN()), $type_provider); + $method_analyzer->analyze($interface_context, $type_provider); $actual_method_id = $method_analyzer->getMethodId(); diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 06ebe3f6c..a3472780b 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -264,6 +264,25 @@ class AttributeTest extends TestCase class C {} ', ], + 'attributeUsesClassContext' => [ + ' [ + ' 'ParentNotFound', + ], ]; } } From 103ec628b0a9a2dd094a68efcd230126d0690a3d Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Mon, 21 Feb 2022 10:44:59 -0600 Subject: [PATCH 10/21] Improve tests. --- tests/AttributeTest.php | 55 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index a3472780b..c67358545 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -264,7 +264,7 @@ class AttributeTest extends TestCase class C {} ', ], - 'attributeUsesClassContext' => [ + 'selfInClassAttribute' => [ ' [ + ' [ + ' Date: Mon, 21 Feb 2022 18:37:20 -0600 Subject: [PATCH 11/21] Fix first-class callable in loop --- .../PhpVisitor/AssignmentMapVisitor.php | 10 +++--- tests/ClosureTest.php | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php index 89119466d..a3fd78e28 100644 --- a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php @@ -77,11 +77,13 @@ class AssignmentMapVisitor extends PhpParser\NodeVisitorAbstract || $node instanceof PhpParser\Node\Expr\MethodCall || $node instanceof PhpParser\Node\Expr\StaticCall ) { - foreach ($node->getArgs() as $arg) { - $arg_var_id = ExpressionIdentifier::getRootVarId($arg->value, $this->this_class_name); + if (!$node->isFirstClassCallable()) { + foreach ($node->getArgs() as $arg) { + $arg_var_id = ExpressionIdentifier::getRootVarId($arg->value, $this->this_class_name); - if ($arg_var_id) { - $this->assignment_map[$arg_var_id][$arg_var_id] = true; + if ($arg_var_id) { + $this->assignment_map[$arg_var_id][$arg_var_id] = true; + } } } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index e5f6c9551..def99f995 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -773,6 +773,40 @@ class ClosureTest extends TestCase [], '8.1', ], + 'FirstClassCallable:AssignmentVisitorMap' => [ + ' */ + public array $handlers = []; + + public function register(): void { + foreach ([1, 2, 3] as $index) { + $this->push($this->handler(...)); + } + } + + /** + * @param Closure():void $closure + * @return void + */ + private function push(\Closure $closure): void { + $this->handlers[] = $closure; + } + + private function handler(): void { + } + } + + $test = new Test(); + $test->register(); + $handlers = $test->handlers; + ', + 'assertions' => [ + '$handlers' => 'list', + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'arrowFunctionReturnsNeverImplictly' => [ ' Date: Tue, 22 Feb 2022 16:04:56 +0200 Subject: [PATCH 12/21] Support interfaces extending enums --- .../Fetch/AtomicPropertyFetchAnalyzer.php | 17 ++++++++++++++--- tests/EnumTest.php | 6 ++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index b0ffb87d2..c18cde6f3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -49,6 +49,7 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TEnumCase; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TGenericObject; +use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; @@ -56,6 +57,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; +use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -184,8 +186,16 @@ class AtomicPropertyFetchAnalyzer $property_id = $fq_class_name . '::$' . $prop_name; - if ($class_storage->is_enum) { - if ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) { + if ($class_storage->is_enum || in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name))) { + if ($prop_name === 'value' && !$class_storage->is_enum) { + $statements_analyzer->node_data->setType( + $stmt, + new Union([ + new TString(), + new TInt() + ]) + ); + } elseif ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) { $case_values = []; foreach ($class_storage->enum_cases as $enum_case) { @@ -1009,7 +1019,8 @@ class AtomicPropertyFetchAnalyzer if (!$class_exists && //interfaces can't have properties. Except when they do... In PHP Core, they can - !in_array($fq_class_name, ['UnitEnum', 'BackedEnum'], true) + !in_array($fq_class_name, ['UnitEnum', 'BackedEnum'], true) && + !in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name)) ) { if (IssueBuffer::accepts( new NoInterfaceProperties( diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 9cfe0c938..7b4aa17f3 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -377,6 +377,12 @@ class EnumTest extends TestCase static fn (\UnitEnum $tag): string => $tag->name; static fn (\BackedEnum $tag): string|int => $tag->value; + + interface ExtendedUnitEnum extends \UnitEnum {} + static fn (ExtendedUnitEnum $tag): string => $tag->name; + + interface ExtendedBackedEnum extends \BackedEnum {} + static fn (ExtendedBackedEnum $tag): string|int => $tag->value; ', 'assertions' => [], [], From c9666bbeb5d2cef64f4a44db76939bfbeae1de90 Mon Sep 17 00:00:00 2001 From: Bei Xiao Date: Tue, 22 Feb 2022 20:50:43 +0200 Subject: [PATCH 13/21] Reduce method complexity --- .../Fetch/AtomicPropertyFetchAnalyzer.php | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index c18cde6f3..0011896ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -196,33 +196,9 @@ class AtomicPropertyFetchAnalyzer ]) ); } elseif ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) { - $case_values = []; - - foreach ($class_storage->enum_cases as $enum_case) { - if (is_string($enum_case->value)) { - $case_values[] = new TLiteralString($enum_case->value); - } elseif (is_int($enum_case->value)) { - $case_values[] = new TLiteralInt($enum_case->value); - } else { - // this should never happen - $case_values[] = new TMixed(); - } - } - - // todo: this is suboptimal when we reference enum directly, e.g. Status::Open->value - $statements_analyzer->node_data->setType( - $stmt, - new Union($case_values) - ); + self::handleEnumValue($statements_analyzer, $stmt, $class_storage); } elseif ($prop_name === 'name') { - if ($lhs_type_part instanceof TEnumCase) { - $statements_analyzer->node_data->setType( - $stmt, - new Union([new TLiteralString($lhs_type_part->case_name)]) - ); - } else { - $statements_analyzer->node_data->setType($stmt, Type::getNonEmptyString()); - } + self::handleEnumName($statements_analyzer, $stmt, $lhs_type_part); } else { self::handleNonExistentProperty( $statements_analyzer, @@ -918,6 +894,47 @@ class AtomicPropertyFetchAnalyzer } } + private static function handleEnumName( + StatementsAnalyzer $statements_analyzer, + PropertyFetch $stmt, + Atomic $lhs_type_part + ): void { + if ($lhs_type_part instanceof TEnumCase) { + $statements_analyzer->node_data->setType( + $stmt, + new Union([new TLiteralString($lhs_type_part->case_name)]) + ); + } else { + $statements_analyzer->node_data->setType($stmt, Type::getNonEmptyString()); + } + } + + private static function handleEnumValue( + StatementsAnalyzer $statements_analyzer, + PropertyFetch $stmt, + ClassLikeStorage $class_storage + ): void { + $case_values = []; + + foreach ($class_storage->enum_cases as $enum_case) { + if (is_string($enum_case->value)) { + $case_values[] = new TLiteralString($enum_case->value); + } elseif (is_int($enum_case->value)) { + $case_values[] = new TLiteralInt($enum_case->value); + } else { + // this should never happen + $case_values[] = new TMixed(); + } + } + + // todo: this is suboptimal when we reference enum directly, e.g. Status::Open->value + /** @psalm-suppress ArgumentTypeCoercion */ + $statements_analyzer->node_data->setType( + $stmt, + new Union($case_values) + ); + } + private static function handleUndefinedProperty( Context $context, StatementsAnalyzer $statements_analyzer, From 0b24b0742317e3caac9e14bf8ffa3992603c8d9c Mon Sep 17 00:00:00 2001 From: Bei Xiao Date: Tue, 22 Feb 2022 23:41:53 +0200 Subject: [PATCH 14/21] Specify required php version for test --- tests/EnumTest.php | 2 +- tests/Traits/ValidCodeAnalysisTestTrait.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 7b4aa17f3..080e4ca50 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -371,7 +371,7 @@ class EnumTest extends TestCase [], '8.1', ], - 'InterfacesWithProperties' => [ + 'PHP81-InterfacesWithProperties' => [ ' $tag->name; diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index 46a57ac4a..222b184c5 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -55,6 +55,10 @@ trait ValidCodeAnalysisTestTrait if (version_compare(PHP_VERSION, '8.0.0', '<')) { $this->markTestSkipped('Test case requires PHP 8.0.'); } + } elseif (strpos($test_name, 'PHP81-') !== false) { + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + $this->markTestSkipped('Test case requires PHP 8.1.'); + } } elseif (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } From 40cc346991eb31596013de2987f3a893a3790e48 Mon Sep 17 00:00:00 2001 From: Bei Xiao Date: Wed, 23 Feb 2022 00:52:53 +0200 Subject: [PATCH 15/21] Update stub --- stubs/Php81.phpstub | 2 +- tests/EnumTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stubs/Php81.phpstub b/stubs/Php81.phpstub index d3398388d..694e4e9ce 100644 --- a/stubs/Php81.phpstub +++ b/stubs/Php81.phpstub @@ -11,7 +11,7 @@ namespace { public static function cases(): array; } - interface BackedEnum + interface BackedEnum extends UnitEnum { public readonly int|string $value; diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 080e4ca50..7b4aa17f3 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -371,7 +371,7 @@ class EnumTest extends TestCase [], '8.1', ], - 'PHP81-InterfacesWithProperties' => [ + 'InterfacesWithProperties' => [ ' $tag->name; From 1387f94324f72e154256bbfda12184228b3ef392 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 23 Feb 2022 18:50:05 -0600 Subject: [PATCH 16/21] Attribute analysis improvements. --- .../Internal/Analyzer/AttributeAnalyzer.php | 203 ------------ .../Internal/Analyzer/AttributesAnalyzer.php | 285 ++++++++++++++++ src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 45 ++- src/Psalm/Internal/Analyzer/FileAnalyzer.php | 4 +- .../Analyzer/FunctionLikeAnalyzer.php | 36 +- .../Internal/Analyzer/InterfaceAnalyzer.php | 19 +- .../Internal/Analyzer/StatementsAnalyzer.php | 2 + src/Psalm/Internal/Analyzer/TraitAnalyzer.php | 21 +- src/Psalm/Storage/ClassLikeStorage.php | 10 +- src/Psalm/Storage/FunctionLikeParameter.php | 10 +- src/Psalm/Storage/FunctionLikeStorage.php | 10 +- src/Psalm/Storage/HasAttributesInterface.php | 15 + src/Psalm/Storage/PropertyStorage.php | 10 +- stubs/Reflection.phpstub | 8 + tests/AttributeTest.php | 309 +++++++++++++++--- tests/TraitTest.php | 8 +- 16 files changed, 675 insertions(+), 320 deletions(-) delete mode 100644 src/Psalm/Internal/Analyzer/AttributeAnalyzer.php create mode 100644 src/Psalm/Internal/Analyzer/AttributesAnalyzer.php create mode 100644 src/Psalm/Storage/HasAttributesInterface.php diff --git a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php deleted file mode 100644 index 61c829218..000000000 --- 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 000000000..2164f286f --- /dev/null +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -0,0 +1,285 @@ + 'class', + 2 => 'function', + 4 => 'method', + 8 => 'property', + 16 => 'class constant', + 32 => 'function/method parameter', + 40 => 'promoted property', + ]; + + /** + * @param list $attribute_groups + * @param 1|2|4|8|16|32 $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, + $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 $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 $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 $attribute_groups + * + * @return iterator + */ + private static function iterateAttributeNodes(array $attribute_groups): Generator + { + foreach ($attribute_groups as $attribute_group) { + foreach ($attribute_group->attrs as $attribute) { + yield $attribute; + } + } + } +} diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index d7bde976e..7c8670631 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -397,17 +397,14 @@ class ClassAnalyzer extends ClassLikeAnalyzer } } - 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 @@ class ClassAnalyzer extends ClassLikeAnalyzer } 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 @@ class ClassAnalyzer extends ClassLikeAnalyzer 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 @@ class ClassAnalyzer extends ClassLikeAnalyzer return null; } - private function checkForMissingPropertyType( + private function analyzeProperty( SourceAnalyzer $source, PhpParser\Node\Stmt\Property $stmt, Context $context @@ -1524,16 +1521,14 @@ class ClassAnalyzer extends ClassLikeAnalyzer $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 61a220f10..e59a17907 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -305,7 +305,9 @@ class FileAnalyzer extends SourceAnalyzer $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 772ff8bda..88eb9c6de 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -819,16 +819,14 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer ); } - 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 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $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 10cb39727..5bd966765 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -97,17 +97,14 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer $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/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 78a25799a..3c3e040c1 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -593,6 +593,8 @@ class StatementsAnalyzer extends SourceAnalyzer // 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 c59fc8c0b..129db6830 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 @@ class TraitAnalyzer extends ClassLikeAnalyzer { 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 78681da1b..1b33c69ac 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -11,7 +11,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; -class ClassLikeStorage +class ClassLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -462,4 +462,12 @@ class ClassLikeStorage { $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 261b203a3..70147e8ce 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -6,7 +6,7 @@ use Psalm\CodeLocation; use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Type\Union; -class FunctionLikeParameter +class FunctionLikeParameter implements HasAttributesInterface { use CustomMetadataTrait; @@ -150,4 +150,12 @@ class FunctionLikeParameter $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 e9928ce56..aba5fc68b 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -12,7 +12,7 @@ use function array_fill_keys; use function array_map; use function implode; -abstract class FunctionLikeStorage +abstract class FunctionLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -293,4 +293,12 @@ abstract class FunctionLikeStorage $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 000000000..2d8bb18e3 --- /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 7fdc14f7a..cbe8bf6c8 100644 --- a/src/Psalm/Storage/PropertyStorage.php +++ b/src/Psalm/Storage/PropertyStorage.php @@ -6,7 +6,7 @@ use Psalm\CodeLocation; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Type\Union; -class PropertyStorage +class PropertyStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -124,4 +124,12 @@ class PropertyStorage 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 15df6c68d..7f453ba7b 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 c67358545..ab7a0ea74 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -51,9 +51,6 @@ class AttributeTest extends TestCase 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', + ], + 'SKIPPED-getAttributesOnClassWithNonClassAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a class', + ], + 'SKIPPED-getAttributesOnFunctionWithNonFunctionAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a function', + ], + 'SKIPPED-getAttributesOnMethodWithNonMethodAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a method', + ], + 'SKIPPED-getAttributesOnPropertyWithNonPropertyAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a property', + ], + 'SKIPPED-getAttributesOnClassConstantWithNonClassConstantAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a class constant', + ], + 'SKIPPED-getAttributesOnParameterWithNonParameterAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - snc' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a parameter', + ], + '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 71f402fa1..18f0ebe4c 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 @@ class TraitTest extends TestCase use T; }' ], + 'suppressIssueOnTrait' => [ + ' Date: Wed, 23 Feb 2022 19:54:17 -0600 Subject: [PATCH 17/21] Add Reflection getAttributes analysis. --- .../Internal/Analyzer/AttributesAnalyzer.php | 105 +++++++++++++++++- .../Expression/Call/ArgumentsAnalyzer.php | 11 ++ tests/AttributeTest.php | 36 ++++-- 3 files changed, 134 insertions(+), 18 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php index 2164f286f..3fa20363f 100644 --- a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -3,10 +3,12 @@ namespace Psalm\Internal\Analyzer; use Generator; +use PhpParser\Node\Arg; use PhpParser\Node\Attribute; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\New_; use PhpParser\Node\Stmt\Expression; +use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\ConstantTypeResolver; @@ -18,9 +20,13 @@ use Psalm\IssueBuffer; use Psalm\Storage\AttributeStorage; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\HasAttributesInterface; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Union; use RuntimeException; +use function array_shift; +use function assert; +use function count; use function reset; class AttributesAnalyzer @@ -63,7 +69,8 @@ class AttributesAnalyzer $attribute_class_flags = self::getAttributeClassFlags( $source, - $attribute_storage, + $attribute_storage->fq_class_name, + $attribute_storage->name_location, $attribute_class_storage, $suppressed_issues ); @@ -114,7 +121,7 @@ class AttributesAnalyzer /** * @param array $suppressed_issues */ - public static function analyzeAttributeConstruction( + private static function analyzeAttributeConstruction( SourceAnalyzer $source, Context $context, AttributeStorage $attribute_storage, @@ -216,11 +223,12 @@ class AttributesAnalyzer */ private static function getAttributeClassFlags( SourceAnalyzer $source, - AttributeStorage $attribute, + string $attribute_name, + CodeLocation $attribute_location, ?ClassLikeStorage $attribute_class_storage, array $suppressed_issues ): int { - if ($attribute->fq_class_name === "Attribute") { + 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; @@ -260,8 +268,8 @@ class AttributesAnalyzer IssueBuffer::maybeAdd( new InvalidAttribute( - "The class {$attribute->fq_class_name} doesn't have the Attribute attribute", - $attribute->name_location + "The class {$attribute_name} doesn't have the Attribute attribute", + $attribute_location ), $suppressed_issues ); @@ -282,4 +290,89 @@ class AttributesAnalyzer } } } + + /** + * 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 (; $arg !== null || $arg->name !== null && $arg->name->name !== "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/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index a05c8a18d..aea81f3fe 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 PhpParser; 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; @@ -260,6 +261,16 @@ class ArgumentsAnalyzer } } + 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, $args); + } + return null; } diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index ab7a0ea74..7d3037042 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -571,7 +571,7 @@ class AttributeTest extends TestCase ', 'error_message' => 'UndefinedConstant', ], - 'SKIPPED-getAttributesOnClassWithNonClassAttribute' => [ + 'getAttributesOnClassWithNonClassAttribute' => [ 'getAttributes(Attr::class); ', - 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a class', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a class', ], - 'SKIPPED-getAttributesOnFunctionWithNonFunctionAttribute' => [ + 'getAttributesOnFunctionWithNonFunctionAttribute' => [ 'getAttributes(Attr::class); ', - 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a function', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:39 - Attribute Attr cannot be used on a function', ], - 'SKIPPED-getAttributesOnMethodWithNonMethodAttribute' => [ + 'getAttributesOnMethodWithNonMethodAttribute' => [ 'getAttributes(Attr::class); ', - 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a method', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a method', ], - 'SKIPPED-getAttributesOnPropertyWithNonPropertyAttribute' => [ + 'getAttributesOnPropertyWithNonPropertyAttribute' => [ 'getAttributes(Attr::class); ', - 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a property', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a property', ], - 'SKIPPED-getAttributesOnClassConstantWithNonClassConstantAttribute' => [ + 'getAttributesOnClassConstantWithNonClassConstantAttribute' => [ 'getAttributes(Attr::class); ', - 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a class constant', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a class constant', ], - 'SKIPPED-getAttributesOnParameterWithNonParameterAttribute' => [ + 'getAttributesOnParameterWithNonParameterAttribute' => [ 'getAttributes(Attr::class); ', - 'error_message' => 'InvalidAttribute - snc' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a parameter', + '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' => [ ' Date: Wed, 23 Feb 2022 22:05:24 -0600 Subject: [PATCH 18/21] Fix types. --- .../Internal/Analyzer/AttributesAnalyzer.php | 23 +++++++++---------- .../Expression/Call/ArgumentsAnalyzer.php | 3 ++- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php index 3fa20363f..38f163c1a 100644 --- a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -25,6 +25,7 @@ use Psalm\Type\Union; use RuntimeException; use function array_shift; +use function array_values; use function assert; use function count; use function reset; @@ -42,9 +43,9 @@ class AttributesAnalyzer ]; /** - * @param list $attribute_groups - * @param 1|2|4|8|16|32 $target - * @param array $suppressed_issues + * @param array $attribute_groups + * @param 1|2|4|8|16|32|40 $target + * @param array $suppressed_issues */ public static function analyze( SourceAnalyzer $source, @@ -80,7 +81,6 @@ class AttributesAnalyzer $context, $attribute_storage, $attribute, - $attribute_class_storage, $suppressed_issues, $storage instanceof ClassLikeStorage ? $storage : null ); @@ -119,14 +119,13 @@ class AttributesAnalyzer } /** - * @param array $suppressed_issues + * @param array $suppressed_issues */ private static function analyzeAttributeConstruction( SourceAnalyzer $source, Context $context, AttributeStorage $attribute_storage, Attribute $attribute, - ?ClassLikeStorage $attribute_class_storage, array $suppressed_issues, ?ClassLikeStorage $classlike_storage = null ): void { @@ -199,7 +198,7 @@ class AttributesAnalyzer $source, new NodeDataProvider() ); - $statements_analyzer->addSuppressedIssues($suppressed_issues); + $statements_analyzer->addSuppressedIssues(array_values($suppressed_issues)); IssueBuffer::startRecording(); $statements_analyzer->analyze( @@ -219,7 +218,7 @@ class AttributesAnalyzer } /** - * @param array $suppressed_issues + * @param array $suppressed_issues */ private static function getAttributeClassFlags( SourceAnalyzer $source, @@ -278,11 +277,11 @@ class AttributesAnalyzer } /** - * @param list $attribute_groups + * @param iterable $attribute_groups * - * @return iterator + * @return Generator */ - private static function iterateAttributeNodes(array $attribute_groups): Generator + private static function iterateAttributeNodes(iterable $attribute_groups): Generator { foreach ($attribute_groups as $attribute_group) { foreach ($attribute_group->attrs as $attribute) { @@ -333,7 +332,7 @@ class AttributesAnalyzer $arg = $args[0]; if ($arg->name !== null) { - for (; $arg !== null || $arg->name !== null && $arg->name->name !== "name"; $arg = array_shift($args)); + 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; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index aea81f3fe..c3930b74c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -55,6 +55,7 @@ use UnexpectedValueException; 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; @@ -268,7 +269,7 @@ class ArgumentsAnalyzer || $method_id === "ReflectionParameter::getattributes" || $method_id === "ReflectionProperty::getattributes" ) { - AttributesAnalyzer::analyzeGetAttributes($statements_analyzer, $method_id, $args); + AttributesAnalyzer::analyzeGetAttributes($statements_analyzer, $method_id, array_values($args)); } return null; From c82abe3017c09ee0c548feaccad8982c9c513b3f Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 23 Feb 2022 22:28:27 -0600 Subject: [PATCH 19/21] Fix trailing commas for PHP 7. --- .../Internal/Analyzer/AttributesAnalyzer.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php index 38f163c1a..97a83acc9 100644 --- a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -91,9 +91,9 @@ class AttributesAnalyzer IssueBuffer::maybeAdd( new InvalidAttribute( "Attribute {$attribute_storage->fq_class_name} is not repeatable", - $attribute_storage->location, + $attribute_storage->location ), - $suppressed_issues, + $suppressed_issues ); } $appearing_non_repeatable_attributes[$attribute_storage->fq_class_name] = true; @@ -106,7 +106,7 @@ class AttributesAnalyzer . self::TARGET_DESCRIPTIONS[$target], $attribute_storage->name_location ), - $suppressed_issues, + $suppressed_issues ); } @@ -155,7 +155,7 @@ class AttributesAnalyzer 'Traits cannot act as attribute classes', $attribute_storage->name_location ), - $suppressed_issues, + $suppressed_issues ); } elseif ($classlike_storage->is_interface) { IssueBuffer::maybeAdd( @@ -163,7 +163,7 @@ class AttributesAnalyzer 'Interfaces cannot act as attribute classes', $attribute_storage->name_location ), - $suppressed_issues, + $suppressed_issues ); } elseif ($classlike_storage->abstract) { IssueBuffer::maybeAdd( @@ -171,7 +171,7 @@ class AttributesAnalyzer 'Abstract classes cannot act as attribute classes', $attribute_storage->name_location ), - $suppressed_issues, + $suppressed_issues ); } elseif (isset($classlike_storage->methods['__construct']) && $classlike_storage->methods['__construct']->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC @@ -181,7 +181,7 @@ class AttributesAnalyzer 'Classes with protected/private constructors cannot act as attribute classes', $attribute_storage->name_location ), - $suppressed_issues, + $suppressed_issues ); } elseif ($classlike_storage->is_enum) { IssueBuffer::maybeAdd( @@ -189,7 +189,7 @@ class AttributesAnalyzer 'Enums cannot act as attribute classes', $attribute_storage->name_location ), - $suppressed_issues, + $suppressed_issues ); } } @@ -360,7 +360,7 @@ class AttributesAnalyzer $class_string->value, $arg_location, $class_storage, - $statements_analyzer->getSuppressedIssues(), + $statements_analyzer->getSuppressedIssues() ); if (($class_attribute_target & $target) === 0) { @@ -368,9 +368,9 @@ class AttributesAnalyzer new InvalidAttribute( "Attribute {$class_string->value} cannot be used on a " . self::TARGET_DESCRIPTIONS[$target], - $arg_location, + $arg_location ), - $statements_analyzer->getSuppressedIssues(), + $statements_analyzer->getSuppressedIssues() ); } } From 26bfc95b139c207e19348f3c3271879e839c2db4 Mon Sep 17 00:00:00 2001 From: orklah Date: Thu, 24 Feb 2022 20:57:29 +0100 Subject: [PATCH 20/21] allow SimpleTypeInferer to infer non empty lists --- .../Expression/SimpleTypeInferer.php | 7 ++ tests/ListTest.php | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 6462c6d1c..206b30c2c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -26,6 +26,7 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TString; use Psalm\Type\Union; @@ -525,6 +526,12 @@ class SimpleTypeInferer return null; } + if ($array_creation_info->all_list) { + return new Union([ + new TNonEmptyList($item_value_type), + ]); + } + return new Union([ new TNonEmptyArray([ $item_key_type, diff --git a/tests/ListTest.php b/tests/ListTest.php index a9928658d..aa32fb353 100644 --- a/tests/ListTest.php +++ b/tests/ListTest.php @@ -84,6 +84,74 @@ class ListTest extends TestCase $a = [1, 1 => 2, 3]; takesList($a);', ], + 'simpleTypeInfererNonEmptyList' => [ + ' $vars */ + function foo(array $vars): void { + print_r($vars); + } + + foo(Foo::VARS); + ', + ], ]; } From 9666b90e41823ba4586511e436da40406af96f4c Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 2 Mar 2022 22:59:12 +0700 Subject: [PATCH 21/21] Register openssl_sign function to impure functions openssl_sign has $signature parameter that by reference that can re-used --- src/Psalm/Internal/Codebase/Functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index a31bebd69..f8f5b16fa 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -454,7 +454,7 @@ class Functions // well-known functions 'libxml_use_internal_errors', 'libxml_disable_entity_loader', 'curl_exec', - 'mt_srand', 'openssl_pkcs7_sign', + 'mt_srand', 'openssl_pkcs7_sign', 'openssl_sign', 'mt_rand', 'rand', 'random_int', 'random_bytes', 'wincache_ucache_delete', 'wincache_ucache_set', 'wincache_ucache_inc', 'class_alias',