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.');
}