1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-04 02:27:59 +01:00

Merge branch '4.x' of https://github.com/vimeo/psalm into feature/upgrade-lsp

This commit is contained in:
Andrew Nagy 2022-03-04 18:04:53 +00:00
commit e18f9ccecb
41 changed files with 1192 additions and 466 deletions

View File

@ -109,6 +109,7 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="disableVarParsing" type="xs:boolean" default="false" />
<xs:attribute name="errorLevel" type="xs:integer" default="2" />
<xs:attribute name="reportMixedIssues" type="xs:boolean" default="true" />
<xs:attribute name="useDocblockTypes" type="xs:boolean" default="true" />
@ -127,6 +128,7 @@
<xs:attribute name="limitMethodComplexity" type="xs:boolean" default="false" />
<xs:attribute name="disableSuppressAll" type="xs:boolean" default="false" />
<xs:attribute name="triggerErrorExits" type="TriggerErrorExitsType" default="default" />
<xs:attribute name="threads" type="xs:integer" />
</xs:complexType>
<xs:complexType name="ProjectFilesType">

View File

@ -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'],

View File

@ -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'],

View File

@ -366,7 +366,7 @@ return [
],
'pdoexception' => [
'errorinfo' => 'array',
'code' => 'string',
'code' => 'int|string',
],
'domnode' => [
'nodeName' => 'string',

View File

@ -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
<psalm
disableVarParsing="[bool]"
/>
```
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

View File

@ -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;

View File

@ -1,256 +0,0 @@
<?php
namespace Psalm\Internal\Analyzer;
use Psalm\Context;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Codebase\ConstantTypeResolver;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
use Psalm\Internal\Stubs\Generator\StubsGenerator;
use Psalm\Issue\InvalidAttribute;
use Psalm\IssueBuffer;
use Psalm\Node\Expr\VirtualNew;
use Psalm\Node\Name\VirtualFullyQualified;
use Psalm\Node\Stmt\VirtualExpression;
use Psalm\Node\VirtualArg;
use Psalm\Node\VirtualIdentifier;
use Psalm\Storage\AttributeStorage;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Type\Union;
use function reset;
class AttributeAnalyzer
{
/**
* @param array<string> $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 . ' doesnt have the Attribute attribute',
$attribute->name_location
),
$source->getSuppressedIssues()
);
}
}
}

View File

@ -0,0 +1,377 @@
<?php
namespace Psalm\Internal\Analyzer;
use Generator;
use PhpParser\Node\Arg;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Stmt\Expression;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Codebase\ConstantTypeResolver;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
use Psalm\Issue\InvalidAttribute;
use Psalm\Issue\UndefinedClass;
use Psalm\IssueBuffer;
use Psalm\Storage\AttributeStorage;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\HasAttributesInterface;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Union;
use RuntimeException;
use function array_shift;
use function array_values;
use function assert;
use function count;
use function reset;
class AttributesAnalyzer
{
private const TARGET_DESCRIPTIONS = [
1 => 'class',
2 => 'function',
4 => 'method',
8 => 'property',
16 => 'class constant',
32 => 'function/method parameter',
40 => 'promoted property',
];
/**
* @param array<array-key, AttributeGroup> $attribute_groups
* @param 1|2|4|8|16|32|40 $target
* @param array<array-key, string> $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<array-key, string> $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<array-key, string> $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<AttributeGroup> $attribute_groups
*
* @return Generator<int, Attribute>
*/
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<Arg> $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()
);
}
}
}

View File

@ -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;

View File

@ -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) : '';

View File

@ -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<int, FunctionLikeParameter> $params
* @param list<FunctionLikeParameter> $params
* @param list<Param> $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;

View File

@ -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();

View File

@ -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(

View File

@ -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;
}

View File

@ -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) {

View File

@ -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(

View File

@ -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,

View File

@ -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(

View File

@ -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(

View File

@ -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_) {

View File

@ -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()
);
}
}

View File

@ -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',

View File

@ -361,7 +361,7 @@ class Methods
/**
* @param list<PhpParser\Node\Arg> $args
*
* @return array<int, FunctionLikeParameter>
* @return list<FunctionLikeParameter>
*/
public function getMethodParams(
MethodIdentifier $method_id,

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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<Arg> $call_args
*
* @return ?array<int, FunctionLikeParameter>
* @return ?list<FunctionLikeParameter>
*/
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);
}
}

View File

@ -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,

View File

@ -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<AttributeStorage>
*/
public function getAttributeStorages(): array
{
return $this->attributes;
}
}

View File

@ -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<AttributeStorage>
*/
public function getAttributeStorages(): array
{
return $this->attributes;
}
}

View File

@ -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<AttributeStorage>
*/
public function getAttributeStorages(): array
{
return $this->attributes;
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Psalm\Storage;
interface HasAttributesInterface
{
/**
* Returns a list of AttributeStorages with the same order they appear in the AttributeGroups they come from.
*
* @return list<AttributeStorage>
*/
public function getAttributeStorages(): array;
}

View File

@ -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<AttributeStorage>
*/
public function getAttributeStorages(): array
{
return $this->attributes;
}
}

View File

@ -11,7 +11,7 @@ namespace {
public static function cases(): array;
}
interface BackedEnum
interface BackedEnum extends UnitEnum
{
public readonly int|string $value;

View File

@ -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<TClass>|null $name
* @return ($name is null ? array<ReflectionAttribute<object>> : array<ReflectionAttribute<TClass>>)
*/
public function getAttributes(?string $name = null, int $flags = 0): array {}
}
/**

View File

@ -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' => [
'<?php
@ -64,9 +63,6 @@ class AttributeTest extends TestCase
#[\Deprecated]
function foo() : void {}
}',
[],
[],
'8.0'
],
'paramAttributeExists' => [
'<?php
@ -78,9 +74,6 @@ class AttributeTest extends TestCase
namespace Foo\Bar {
function foo(#[\Deprecated] string $foo) : void {}
}',
[],
[],
'8.0'
],
'testReflectingClass' => [
'<?php
@ -131,18 +124,12 @@ class AttributeTest extends TestCase
#[Route(methods: ["GET"])]
class HealthController
{}',
[],
[],
'8.0'
],
'allowsRepeatableFlag' => [
'<?php
#[Attribute(Attribute::TARGET_ALL|Attribute::IS_REPEATABLE)] // results in int(127)
class A {}
',
[],
[],
'8.0'
],
'allowsClassString' => [
'<?php
@ -160,9 +147,6 @@ class AttributeTest extends TestCase
#[Foo(_className: Baz::class)]
class Baz {}',
[],
[],
'8.0'
],
'allowsClassStringFromDifferentNamespace' => [
'<?php
@ -240,7 +224,149 @@ class AttributeTest extends TestCase
[],
[],
'8.1'
]
],
'createObjectAsAttributeArg' => [
'<?php
#[Attribute]
class B
{
public function __construct(?array $listOfB = null) {}
}
#[Attribute(Attribute::TARGET_CLASS)]
class A
{
/**
* @param B[] $listOfB
*/
public function __construct(?array $listOfB = null) {}
}
#[A([new B])]
class C {}
',
],
'selfInClassAttribute' => [
'<?php
#[Attribute]
class SomeAttr
{
/** @param class-string $class */
public function __construct(string $class) {}
}
#[SomeAttr(self::class)]
class A
{
#[SomeAttr(self::class)]
public const CONST = "const";
#[SomeAttr(self::class)]
public string $foo = "bar";
#[SomeAttr(self::class)]
public function baz(): void {}
}
',
],
'parentInClassAttribute' => [
'<?php
#[Attribute]
class SomeAttr
{
/** @param class-string $class */
public function __construct(string $class) {}
}
class A {}
#[SomeAttr(parent::class)]
class B extends A
{
#[SomeAttr(parent::class)]
public const CONST = "const";
#[SomeAttr(parent::class)]
public string $foo = "bar";
#[SomeAttr(parent::class)]
public function baz(): void {}
}
',
],
'selfInInterfaceAttribute' => [
'<?php
#[Attribute]
class SomeAttr
{
/** @param class-string $class */
public function __construct(string $class) {}
}
#[SomeAttr(self::class)]
interface C
{
#[SomeAttr(self::class)]
public const CONST = "const";
#[SomeAttr(self::class)]
public function baz(): void {}
}
',
],
'allowBothParamAndPropertyAttributesForPromotedProperties' => [
'<?php
#[Attribute(Attribute::TARGET_PARAMETER)]
class Foo {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Bar {}
class Baz
{
public function __construct(#[Foo, Bar] private int $test) {}
}
',
],
'multipleAttributesInMultipleGroups' => [
'<?php
#[Attribute]
class A {}
#[Attribute]
class B {}
#[Attribute]
class C {}
#[Attribute]
class D {}
#[A, B]
#[C, D]
class Foo {}
',
],
'propertyLevelSuppression' => [
'<?php
#[Attribute(Attribute::TARGET_CLASS)]
class ClassAttr {}
class Foo
{
/** @psalm-suppress InvalidAttribute */
#[ClassAttr]
public string $bar = "baz";
}
',
],
'invalidAttributeDoesntCrash' => [
'<?php
/** @psalm-suppress InvalidScalarArgument */
#[Attribute("foobar")]
class Foo {}
#[Foo]
class Bar {}
',
],
];
}
@ -256,10 +382,7 @@ class AttributeTest extends TestCase
#[A]
class B {}',
'error_message' => 'InvalidAttribute',
[],
false,
'8.0'
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23',
],
'missingAttributeOnClass' => [
'<?php
@ -267,10 +390,19 @@ class AttributeTest extends TestCase
#[Pure]
class Video {}',
'error_message' => 'UndefinedAttributeClass',
[],
false,
'8.0'
'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23',
],
'missingAttributeOnProperty' => [
'<?php
use Foo\Bar\Pure;
class Baz
{
#[Pure]
public string $foo = "bar";
}
',
'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:27',
],
'missingAttributeOnFunction' => [
'<?php
@ -278,20 +410,14 @@ class AttributeTest extends TestCase
#[Pure]
function foo() : void {}',
'error_message' => 'UndefinedAttributeClass',
[],
false,
'8.0'
'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:23',
],
'missingAttributeOnParam' => [
'<?php
use Foo\Bar\Pure;
function foo(#[Pure] string $str) : void {}',
'error_message' => 'UndefinedAttributeClass',
[],
false,
'8.0'
'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:36',
],
'tooFewArgumentsToAttributeConstructor' => [
'<?php
@ -304,10 +430,7 @@ class AttributeTest extends TestCase
#[Table()]
class Video {}',
'error_message' => 'TooFewArguments',
[],
false,
'8.0'
'error_message' => 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23',
],
'invalidArgument' => [
'<?php
@ -321,10 +444,7 @@ class AttributeTest extends TestCase
#[Foo("foo")]
class Bar{}',
'error_message' => 'InvalidScalarArgument',
[],
false,
'8.0'
'error_message' => 'InvalidScalarArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:27',
],
'classAttributeUsedOnFunction' => [
'<?php
@ -337,48 +457,248 @@ class AttributeTest extends TestCase
#[Table("videos")]
function foo() : void {}',
'error_message' => 'InvalidAttribute',
[],
false,
'8.0'
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:23',
],
'interfaceCannotBeAttributeClass' => [
'<?php
#[Attribute]
interface Foo {}',
'error_message' => 'InvalidAttribute',
[],
false,
'8.0'
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23',
],
'traitCannotBeAttributeClass' => [
'<?php
#[Attribute]
interface Foo {}',
'error_message' => 'InvalidAttribute',
[],
false,
'8.0'
trait Foo {}',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23',
],
'abstractClassCannotBeAttributeClass' => [
'<?php
#[Attribute]
abstract class Baz {}',
'error_message' => 'InvalidAttribute',
[],
false,
'8.0'
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23',
],
'abstractClassCannotHavePrivateConstructor' => [
'attributeClassCannotHavePrivateConstructor' => [
'<?php
#[Attribute]
class Baz {
private function __construct() {}
}',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:23',
],
'SKIPPED-attributeInvalidTargetClassConst' => [ // Will be implemented in Psalm 5 where we have better class const analysis
'<?php
class Foo {
#[Attribute]
public const BAR = "baz";
}
',
'error_message' => 'InvalidAttribute',
[],
false,
'8.0'
],
'attributeInvalidTargetProperty' => [
'<?php
class Foo {
#[Attribute]
public string $bar = "baz";
}
',
'error_message' => 'InvalidAttribute',
],
'attributeInvalidTargetMethod' => [
'<?php
class Foo {
#[Attribute]
public function bar(): void {}
}
',
'error_message' => 'InvalidAttribute',
],
'attributeInvalidTargetFunction' => [
'<?php
#[Attribute]
function foo(): void {}
',
'error_message' => 'InvalidAttribute',
],
'attributeInvalidTargetParameter' => [
'<?php
function foo(#[Attribute] string $_bar): void {}
',
'error_message' => 'InvalidAttribute',
],
'attributeTargetArgCannotBeVariable' => [
'<?php
$target = 1;
#[Attribute($target)]
class Foo {}
',
'error_message' => 'UndefinedVariable',
],
'attributeTargetArgCannotBeSelfConst' => [
'<?php
#[Attribute(self::BAR)]
class Foo
{
public const BAR = 1;
}
',
'error_message' => 'NonStaticSelfCall',
],
'noParentInAttributeOnClassWithoutParent' => [
'<?php
#[Attribute]
class SomeAttr
{
/** @param class-string $class */
public function __construct(string $class) {}
}
#[SomeAttr(parent::class)]
class A {}
',
'error_message' => 'ParentNotFound',
],
'undefinedConstantInAttribute' => [
'<?php
#[Attribute]
class Foo
{
public function __construct(int $i) {}
}
#[Foo(self::BAR_CONST)]
class Bar {}
',
'error_message' => 'UndefinedConstant',
],
'getAttributesOnClassWithNonClassAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
class Foo {}
$r = new ReflectionClass(Foo::class);
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a class',
],
'getAttributesOnFunctionWithNonFunctionAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
function foo(): void {}
/** @psalm-suppress InvalidArgument */
$r = new ReflectionFunction("foo");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:39 - Attribute Attr cannot be used on a function',
],
'getAttributesOnMethodWithNonMethodAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
class Foo
{
public function bar(): void {}
}
$r = new ReflectionMethod("Foo::bar");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a method',
],
'getAttributesOnPropertyWithNonPropertyAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_CLASS)]
class Attr {}
class Foo
{
public string $bar = "baz";
}
$r = new ReflectionProperty(Foo::class, "bar");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a property',
],
'getAttributesOnClassConstantWithNonClassConstantAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
class Foo
{
public const BAR = "baz";
}
$r = new ReflectionClassConstant(Foo::class, "BAR");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a class constant',
],
'getAttributesOnParameterWithNonParameterAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
function foo(int $bar): void {}
$r = new ReflectionParameter("foo", "bar");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a function/method parameter',
],
'getAttributesWithNonAttribute' => [
'<?php
class NonAttr {}
function foo(int $bar): void {}
$r = new ReflectionParameter("foo", "bar");
$r->getAttributes(NonAttr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:39 - The class NonAttr doesn\'t have the Attribute attribute',
],
'analyzeConstructorForNonexistentAttributes' => [
'<?php
class Foo
{
public function __construct(string $_arg) {}
}
/** @psalm-suppress UndefinedAttributeClass */
#[AttrA(new Foo(1))]
class Bar {}
',
'error_message' => 'InvalidScalarArgument',
],
'multipleAttributesShowErrors' => [
'<?php
#[Attribute(Attribute::TARGET_CLASS)]
class Foo {}
#[Attribute(Attribute::TARGET_PARAMETER)]
class Bar {}
#[Foo, Bar]
class Baz {}
',
'error_message' => 'InvalidAttribute',
],
'repeatNonRepeatableAttribute' => [
'<?php
#[Attribute]
class Foo {}
#[Foo, Foo]
class Baz {}
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:5:28 - Attribute Foo is not repeatable',
],
];
}

View File

@ -773,6 +773,40 @@ class ClosureTest extends TestCase
[],
'8.1',
],
'FirstClassCallable:AssignmentVisitorMap' => [
'<?php
class Test {
/** @var list<\Closure():void> */
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<Closure():void>',
],
'ignored_issues' => [],
'php_version' => '8.1',
],
'arrowFunctionReturnsNeverImplictly' => [
'<?php
$bar = ["foo", "bar"];

View File

@ -377,6 +377,12 @@ class EnumTest extends TestCase
static fn (\UnitEnum $tag): string => $tag->name;
static fn (\BackedEnum $tag): string|int => $tag->value;
interface ExtendedUnitEnum extends \UnitEnum {}
static fn (ExtendedUnitEnum $tag): string => $tag->name;
interface ExtendedBackedEnum extends \BackedEnum {}
static fn (ExtendedBackedEnum $tag): string|int => $tag->value;
',
'assertions' => [],
[],

View File

@ -84,6 +84,74 @@ class ListTest extends TestCase
$a = [1, 1 => 2, 3];
takesList($a);',
],
'simpleTypeInfererNonEmptyList' => [
'<?php
class Foo {
public const VARS = [
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"x",
"y",
];
}
/** @param list<string> $vars */
function foo(array $vars): void {
print_r($vars);
}
foo(Foo::VARS);
',
],
];
}

View File

@ -13,27 +13,33 @@ class ExceptionCodeTest extends TestCase
{
yield 'RuntimeException' => [
'<?php
function f(\RuntimeException $e): int {
return $e->getCode();
}
/** @var \RuntimeException $e */
$code = $e->getCode();
',
[],
['$code' => 'int|string'],
];
yield 'CustomRuntimeException' => [
'<?php
class CustomRuntimeException extends \RuntimeException {}
/** @var CustomRuntimeException $e */
$code = $e->getCode();
',
['$code' => 'int'],
];
yield 'LogicException' => [
'<?php
function f(\LogicException $e): int {
return $e->getCode();
}
/** @var \LogicException $e */
$code = $e->getCode();
',
[],
['$code' => 'int'],
];
yield 'PDOException' => [
'<?php
function f(\PDOException $e): string {
return $e->getCode();
}
/** @var \PDOException $e */
$code = $e->getCode();
',
[],
['$code' => 'int|string'],
];
yield 'CustomThrowable' => [
'<?php

View File

@ -13,7 +13,7 @@ class TraitTest extends TestCase
use ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[],php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
@ -990,6 +990,12 @@ class TraitTest extends TestCase
use T;
}'
],
'suppressIssueOnTrait' => [
'<?php
/** @psalm-suppress InvalidAttribute */
#[Attribute]
trait Foo {}',
],
];
}

View File

@ -55,6 +55,10 @@ trait ValidCodeAnalysisTestTrait
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
$this->markTestSkipped('Test case requires PHP 8.0.');
}
} elseif (strpos($test_name, 'PHP81-') !== false) {
if (version_compare(PHP_VERSION, '8.1.0', '<')) {
$this->markTestSkipped('Test case requires PHP 8.1.');
}
} elseif (strpos($test_name, 'SKIPPED-') !== false) {
$this->markTestSkipped('Skipped due to a bug.');
}