diff --git a/config.xsd b/config.xsd index 9805ba361..2d295436a 100644 --- a/config.xsd +++ b/config.xsd @@ -109,6 +109,7 @@ + @@ -127,6 +128,7 @@ + diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 3f67d8fb0..d3f197460 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 206ea13ec..393c6b677 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 e5d326244..8c874598b 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/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..33035789e 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', @@ -1246,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; diff --git a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php deleted file mode 100644 index 3e18d03c1..000000000 --- a/src/Psalm/Internal/Analyzer/AttributeAnalyzer.php +++ /dev/null @@ -1,256 +0,0 @@ - $suppressed_issues - * @param 1|2|4|8|16|32 $target - */ - public static function analyze( - SourceAnalyzer $source, - AttributeStorage $attribute, - 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); - - $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() - ); - } - - /** - * @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() - ); - } - } -} diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php new file mode 100644 index 000000000..97a83acc9 --- /dev/null +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -0,0 +1,377 @@ + 'class', + 2 => 'function', + 4 => 'method', + 8 => 'property', + 16 => 'class constant', + 32 => 'function/method parameter', + 40 => 'promoted property', + ]; + + /** + * @param array $attribute_groups + * @param 1|2|4|8|16|32|40 $target + * @param array $suppressed_issues + */ + public static function analyze( + SourceAnalyzer $source, + Context $context, + HasAttributesInterface $storage, + array $attribute_groups, + int $target, + array $suppressed_issues + ): void { + $codebase = $source->getCodebase(); + $appearing_non_repeatable_attributes = []; + $attribute_iterator = self::iterateAttributeNodes($attribute_groups); + foreach ($storage->getAttributeStorages() as $attribute_storage) { + if (!$attribute_iterator->valid()) { + throw new RuntimeException("Expected attribute count to match attribute storage count"); + } + $attribute = $attribute_iterator->current(); + + $attribute_class_storage = $codebase->classlikes->classExists($attribute_storage->fq_class_name) + ? $codebase->classlike_storage_provider->get($attribute_storage->fq_class_name) + : null; + + $attribute_class_flags = self::getAttributeClassFlags( + $source, + $attribute_storage->fq_class_name, + $attribute_storage->name_location, + $attribute_class_storage, + $suppressed_issues + ); + + self::analyzeAttributeConstruction( + $source, + $context, + $attribute_storage, + $attribute, + $suppressed_issues, + $storage instanceof ClassLikeStorage ? $storage : null + ); + + if (($attribute_class_flags & 64) === 0) { + // Not IS_REPEATABLE + if (isset($appearing_non_repeatable_attributes[$attribute_storage->fq_class_name])) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + "Attribute {$attribute_storage->fq_class_name} is not repeatable", + $attribute_storage->location + ), + $suppressed_issues + ); + } + $appearing_non_repeatable_attributes[$attribute_storage->fq_class_name] = true; + } + + if (($attribute_class_flags & $target) === 0) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + "Attribute {$attribute_storage->fq_class_name} cannot be used on a " + . self::TARGET_DESCRIPTIONS[$target], + $attribute_storage->name_location + ), + $suppressed_issues + ); + } + + $attribute_iterator->next(); + } + + if ($attribute_iterator->valid()) { + throw new RuntimeException("Expected attribute count to match attribute storage count"); + } + } + + /** + * @param array $suppressed_issues + */ + private static function analyzeAttributeConstruction( + SourceAnalyzer $source, + Context $context, + AttributeStorage $attribute_storage, + Attribute $attribute, + array $suppressed_issues, + ?ClassLikeStorage $classlike_storage = null + ): void { + if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName( + $source, + $attribute_storage->fq_class_name, + $attribute_storage->location, + null, + null, + $suppressed_issues, + new ClassLikeNameOptions( + false, + false, + false, + false, + false, + true + ) + ) === false) { + return; + } + + if ($attribute_storage->fq_class_name === 'Attribute' && $classlike_storage) { + if ($classlike_storage->is_trait) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + 'Traits cannot act as attribute classes', + $attribute_storage->name_location + ), + $suppressed_issues + ); + } elseif ($classlike_storage->is_interface) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + 'Interfaces cannot act as attribute classes', + $attribute_storage->name_location + ), + $suppressed_issues + ); + } elseif ($classlike_storage->abstract) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + 'Abstract classes cannot act as attribute classes', + $attribute_storage->name_location + ), + $suppressed_issues + ); + } elseif (isset($classlike_storage->methods['__construct']) + && $classlike_storage->methods['__construct']->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC + ) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + 'Classes with protected/private constructors cannot act as attribute classes', + $attribute_storage->name_location + ), + $suppressed_issues + ); + } elseif ($classlike_storage->is_enum) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + 'Enums cannot act as attribute classes', + $attribute_storage->name_location + ), + $suppressed_issues + ); + } + } + + $statements_analyzer = new StatementsAnalyzer( + $source, + new NodeDataProvider() + ); + $statements_analyzer->addSuppressedIssues(array_values($suppressed_issues)); + + IssueBuffer::startRecording(); + $statements_analyzer->analyze( + [new Expression(new New_($attribute->name, $attribute->args, $attribute->getAttributes()))], + // Use a new Context for the Attribute attribute so that it can't access `self` + $attribute_storage->fq_class_name === "Attribute" ? new Context() : $context + ); + $issues = IssueBuffer::clearRecordingLevel(); + IssueBuffer::stopRecording(); + foreach ($issues as $issue) { + if ($issue instanceof UndefinedClass && $issue->fq_classlike_name === $attribute_storage->fq_class_name) { + // Remove UndefinedClass for the attribute, since we already added UndefinedAttribute + continue; + } + IssueBuffer::bubbleUp($issue); + } + } + + /** + * @param array $suppressed_issues + */ + private static function getAttributeClassFlags( + SourceAnalyzer $source, + string $attribute_name, + CodeLocation $attribute_location, + ?ClassLikeStorage $attribute_class_storage, + array $suppressed_issues + ): int { + if ($attribute_name === "Attribute") { + // We override this here because we still want to analyze attributes + // for PHP 7.4 when the Attribute class doesn't yet exist. + return 1; + } + + if ($attribute_class_storage === null) { + return 63; // Defaults to TARGET_ALL + } + + foreach ($attribute_class_storage->attributes as $attribute_attribute) { + if ($attribute_attribute->fq_class_name === 'Attribute') { + if (!$attribute_attribute->args) { + return 63; // Defaults to TARGET_ALL + } + + $first_arg = reset($attribute_attribute->args); + + $first_arg_type = $first_arg->type; + + if ($first_arg_type instanceof UnresolvedConstantComponent) { + $first_arg_type = new Union([ + ConstantTypeResolver::resolve( + $source->getCodebase()->classlikes, + $first_arg_type, + $source instanceof StatementsAnalyzer ? $source : null + ) + ]); + } + + if (!$first_arg_type->isSingleIntLiteral()) { + return 63; // Fall back to default if it's invalid + } + + return $first_arg_type->getSingleIntLiteral()->value; + } + } + + IssueBuffer::maybeAdd( + new InvalidAttribute( + "The class {$attribute_name} doesn't have the Attribute attribute", + $attribute_location + ), + $suppressed_issues + ); + + return 63; // Fall back to default if it's invalid + } + + /** + * @param iterable $attribute_groups + * + * @return Generator + */ + private static function iterateAttributeNodes(iterable $attribute_groups): Generator + { + foreach ($attribute_groups as $attribute_group) { + foreach ($attribute_group->attrs as $attribute) { + yield $attribute; + } + } + } + + /** + * Analyze Reflection getAttributes method calls. + + * @param list $args + */ + public static function analyzeGetAttributes( + StatementsAnalyzer $statements_analyzer, + string $method_id, + array $args + ): void { + if (count($args) !== 1) { + // We skip this analysis if $flags is specified on getAttributes, since the only option + // is ReflectionAttribute::IS_INSTANCEOF, which causes getAttributes to return children. + // When returning children we don't want to limit this since a child could add a target. + return; + } + + switch ($method_id) { + case "ReflectionClass::getattributes": + $target = 1; + break; + case "ReflectionFunction::getattributes": + $target = 2; + break; + case "ReflectionMethod::getattributes": + $target = 4; + break; + case "ReflectionProperty::getattributes": + $target = 8; + break; + case "ReflectionClassConstant::getattributes": + $target = 16; + break; + case "ReflectionParameter::getattributes": + $target = 32; + break; + default: + return; + } + + $arg = $args[0]; + if ($arg->name !== null) { + for (; !empty($args) && ($arg->name->name ?? null) !== "name"; $arg = array_shift($args)); + if ($arg->name->name ?? null !== "name") { + // No named argument for "name" parameter + return; + } + } + + $arg_type = $statements_analyzer->getNodeTypeProvider()->getType($arg->value); + if ($arg_type === null || !$arg_type->isSingle() || !$arg_type->hasLiteralString()) { + return; + } + + $class_string = $arg_type->getSingleAtomic(); + assert($class_string instanceof TLiteralString); + + $codebase = $statements_analyzer->getCodebase(); + + if (!$codebase->classExists($class_string->value)) { + return; + } + + $class_storage = $codebase->classlike_storage_provider->get($class_string->value); + $arg_location = new CodeLocation($statements_analyzer, $arg); + $class_attribute_target = self::getAttributeClassFlags( + $statements_analyzer, + $class_string->value, + $arg_location, + $class_storage, + $statements_analyzer->getSuppressedIssues() + ); + + if (($class_attribute_target & $target) === 0) { + IssueBuffer::maybeAdd( + new InvalidAttribute( + "Attribute {$class_string->value} cannot be used on a " + . self::TARGET_DESCRIPTIONS[$target], + $arg_location + ), + $statements_analyzer->getSuppressedIssues() + ); + } + } +} diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index e878baf77..7c8670631 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -397,15 +397,14 @@ class ClassAnalyzer extends ClassLikeAnalyzer } } - foreach ($storage->attributes as $attribute) { - AttributeAnalyzer::analyze( - $this, - $attribute, - $storage->suppressed_issues + $this->getSuppressedIssues(), - 1, - $storage - ); - } + AttributesAnalyzer::analyze( + $this, + $class_context, + $storage, + $class->attrGroups, + 1, + $storage->suppressed_issues + $this->getSuppressedIssues() + ); self::addContextProperties( $this, @@ -551,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( @@ -598,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); } } @@ -1492,7 +1491,7 @@ class ClassAnalyzer extends ClassLikeAnalyzer return null; } - private function checkForMissingPropertyType( + private function analyzeProperty( SourceAnalyzer $source, PhpParser\Node\Stmt\Property $stmt, Context $context @@ -1522,14 +1521,14 @@ class ClassAnalyzer extends ClassLikeAnalyzer $property_storage = $class_storage->properties[$property_name]; - foreach ($property_storage->attributes as $attribute) { - AttributeAnalyzer::analyze( - $source, - $attribute, - $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 5e7eae3ba..88eb9c6de 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,14 +819,14 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer ); } - foreach ($storage->attributes as $attribute) { - AttributeAnalyzer::analyze( - $this, - $attribute, - $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; } @@ -968,13 +971,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,14 +1267,14 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $context->hasVariable('$' . $function_param->name); } - foreach ($function_param->attributes as $attribute) { - AttributeAnalyzer::analyze( - $this, - $attribute, - $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 0e9b11e4a..5bd966765 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -95,16 +95,16 @@ 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 $attribute) { - AttributeAnalyzer::analyze( - $this, - $attribute, - $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) { @@ -112,7 +112,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/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/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index a05c8a18d..c3930b74c 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; @@ -54,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; @@ -260,6 +262,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, array_values($args)); + } + return null; } 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/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index b0ffb87d2..0011896ea 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,35 +186,19 @@ 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) { - $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 + 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($case_values) + new Union([ + new TString(), + new TInt() + ]) ); + } elseif ($prop_name === 'value' && $class_storage->enum_type !== null && $class_storage->enum_cases) { + 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, @@ -908,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, @@ -1009,7 +1036,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/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/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..3c3e040c1 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( @@ -591,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/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', 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/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/src/Psalm/Internal/PluginManager/ConfigFile.php b/src/Psalm/Internal/PluginManager/ConfigFile.php index 4fd025425..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; @@ -143,6 +144,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)); + } } } 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/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 a108513ba..517b22494 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -13,7 +13,7 @@ use function array_map; use function count; use function implode; -abstract class FunctionLikeStorage +abstract class FunctionLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; @@ -325,4 +325,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/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/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 25a8b1bcd..7d3037042 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; @@ -49,9 +51,6 @@ class AttributeTest extends TestCase public string $name = "", ) {} }', - [], - [], - '8.0' ], 'functionAttributeExists' => [ ' [ ' [ ' [ ' [ ' [ ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' [ + ' 'InvalidAttribute', - [], - false, - '8.0' + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', ], 'missingAttributeOnClass' => [ ' 'UndefinedAttributeClass', - [], - false, - '8.0' + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', + ], + 'missingAttributeOnProperty' => [ + ' 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:27', ], 'missingAttributeOnFunction' => [ ' 'UndefinedAttributeClass', - [], - false, - '8.0' + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23', ], 'missingAttributeOnParam' => [ ' 'UndefinedAttributeClass', - [], - false, - '8.0' + 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:36', ], 'tooFewArgumentsToAttributeConstructor' => [ ' 'TooFewArguments', - [], - false, - '8.0' + 'error_message' => 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23', ], 'invalidArgument' => [ ' 'InvalidScalarArgument', - [], - false, - '8.0' + 'error_message' => 'InvalidScalarArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:27', ], 'classAttributeUsedOnFunction' => [ ' 'InvalidAttribute', - [], - false, - '8.0' + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23', ], 'interfaceCannotBeAttributeClass' => [ ' 'InvalidAttribute', - [], - false, - '8.0' + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', ], 'traitCannotBeAttributeClass' => [ ' 'InvalidAttribute', - [], - false, - '8.0' + trait Foo {}', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', ], 'abstractClassCannotBeAttributeClass' => [ ' 'InvalidAttribute', - [], - false, - '8.0' + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', ], - 'abstractClassCannotHavePrivateConstructor' => [ + 'attributeClassCannotHavePrivateConstructor' => [ ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23', + ], + 'SKIPPED-attributeInvalidTargetClassConst' => [ // Will be implemented in Psalm 5 where we have better class const analysis + ' 'InvalidAttribute', - [], - false, - '8.0' + ], + 'attributeInvalidTargetProperty' => [ + ' 'InvalidAttribute', + ], + 'attributeInvalidTargetMethod' => [ + ' 'InvalidAttribute', + ], + 'attributeInvalidTargetFunction' => [ + ' 'InvalidAttribute', + ], + 'attributeInvalidTargetParameter' => [ + ' 'InvalidAttribute', + ], + 'attributeTargetArgCannotBeVariable' => [ + ' 'UndefinedVariable', + ], + 'attributeTargetArgCannotBeSelfConst' => [ + ' 'NonStaticSelfCall', + ], + 'noParentInAttributeOnClassWithoutParent' => [ + ' 'ParentNotFound', + ], + 'undefinedConstantInAttribute' => [ + ' 'UndefinedConstant', + ], + 'getAttributesOnClassWithNonClassAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a class', + ], + 'getAttributesOnFunctionWithNonFunctionAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:39 - Attribute Attr cannot be used on a function', + ], + 'getAttributesOnMethodWithNonMethodAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a method', + ], + 'getAttributesOnPropertyWithNonPropertyAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a property', + ], + 'getAttributesOnClassConstantWithNonClassConstantAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a class constant', + ], + 'getAttributesOnParameterWithNonParameterAttribute' => [ + 'getAttributes(Attr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a function/method parameter', + ], + 'getAttributesWithNonAttribute' => [ + 'getAttributes(NonAttr::class); + ', + 'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:39 - The class NonAttr doesn\'t have the Attribute attribute', + ], + 'analyzeConstructorForNonexistentAttributes' => [ + ' 'InvalidScalarArgument', + ], + 'multipleAttributesShowErrors' => [ + ' 'InvalidAttribute', + ], + 'repeatNonRepeatableAttribute' => [ + ' 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:5:28 - Attribute Foo is not repeatable', ], ]; } diff --git a/tests/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' => [ ' $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' => [], [], 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); + ', + ], ]; } 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' => [ ',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' => [ + '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.'); }