mirror of
https://github.com/danog/psalm.git
synced 2024-12-04 18:48:03 +01:00
Merge branch '4.x' of https://github.com/vimeo/psalm into feature/upgrade-lsp
This commit is contained in:
commit
e18f9ccecb
@ -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">
|
||||
|
@ -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'],
|
||||
|
@ -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'],
|
||||
|
@ -366,7 +366,7 @@ return [
|
||||
],
|
||||
'pdoexception' => [
|
||||
'errorinfo' => 'array',
|
||||
'code' => 'string',
|
||||
'code' => 'int|string',
|
||||
],
|
||||
'domnode' => [
|
||||
'nodeName' => 'string',
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 . ' doesn’t have the Attribute attribute',
|
||||
$attribute->name_location
|
||||
),
|
||||
$source->getSuppressedIssues()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
377
src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
Normal file
377
src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -397,15 +397,14 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($storage->attributes as $attribute) {
|
||||
AttributeAnalyzer::analyze(
|
||||
AttributesAnalyzer::analyze(
|
||||
$this,
|
||||
$attribute,
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
$class_context,
|
||||
$storage,
|
||||
$class->attrGroups,
|
||||
1,
|
||||
$storage
|
||||
$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(
|
||||
AttributesAnalyzer::analyze(
|
||||
$source,
|
||||
$attribute,
|
||||
$this->source->getSuppressedIssues(),
|
||||
8
|
||||
$context,
|
||||
$property_storage,
|
||||
$stmt->attrGroups,
|
||||
8,
|
||||
$property_storage->suppressed_issues + $this->getSuppressedIssues()
|
||||
);
|
||||
}
|
||||
|
||||
if ($class_property_type && ($property_storage->type_location || !$codebase->alter_code)) {
|
||||
return;
|
||||
|
@ -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) : '';
|
||||
|
@ -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(
|
||||
AttributesAnalyzer::analyze(
|
||||
$this,
|
||||
$attribute,
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
$storage instanceof MethodStorage ? 4 : 2
|
||||
$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,15 +1267,15 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
$context->hasVariable('$' . $function_param->name);
|
||||
}
|
||||
|
||||
foreach ($function_param->attributes as $attribute) {
|
||||
AttributeAnalyzer::analyze(
|
||||
AttributesAnalyzer::analyze(
|
||||
$this,
|
||||
$attribute,
|
||||
$storage->suppressed_issues,
|
||||
$function_param->promoted_property ? 8 : 32
|
||||
$context,
|
||||
$function_param,
|
||||
$param_stmts[$offset]->attrGroups,
|
||||
$function_param->promoted_property ? 40 : 32,
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $check_stmts;
|
||||
}
|
||||
|
@ -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(
|
||||
AttributesAnalyzer::analyze(
|
||||
$this,
|
||||
$attribute,
|
||||
$class_storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
$interface_context,
|
||||
$class_storage,
|
||||
$this->class->attrGroups,
|
||||
1,
|
||||
$class_storage
|
||||
$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();
|
||||
|
||||
|
@ -155,7 +155,9 @@ class AssignmentAnalyzer
|
||||
$template_type_map = $statements_analyzer->getTemplateTypeMap();
|
||||
|
||||
try {
|
||||
$var_comments = CommentAnalyzer::getTypeFromComment(
|
||||
$var_comments = $codebase->config->disable_var_parsing
|
||||
? []
|
||||
: CommentAnalyzer::getTypeFromComment(
|
||||
$doc_comment,
|
||||
$statements_analyzer->getSource(),
|
||||
$statements_analyzer->getAliases(),
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,17 +98,15 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
if ($declaring_method_id && $declaring_method_id !== $method_id) {
|
||||
$declaring_fq_class_name = $declaring_method_id->fq_class_name;
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -74,7 +74,9 @@ class ReturnAnalyzer
|
||||
$file_storage = $file_storage_provider->get($statements_analyzer->getFilePath());
|
||||
|
||||
try {
|
||||
$var_comments = CommentAnalyzer::arrayToDocblocks(
|
||||
$var_comments = $codebase->config->disable_var_parsing
|
||||
? []
|
||||
: CommentAnalyzer::arrayToDocblocks(
|
||||
$doc_comment,
|
||||
$parsed_docblock,
|
||||
$statements_analyzer->getSource(),
|
||||
|
@ -57,7 +57,9 @@ class StaticAnalyzer
|
||||
$var_comments = [];
|
||||
|
||||
try {
|
||||
$var_comments = CommentAnalyzer::arrayToDocblocks(
|
||||
$var_comments = $codebase->config->disable_var_parsing
|
||||
? []
|
||||
: CommentAnalyzer::arrayToDocblocks(
|
||||
$doc_comment,
|
||||
$parsed_docblock,
|
||||
$statements_analyzer->getSource(),
|
||||
|
@ -442,7 +442,9 @@ class StatementsAnalyzer extends SourceAnalyzer
|
||||
$var_comments = [];
|
||||
|
||||
try {
|
||||
$var_comments = CommentAnalyzer::arrayToDocblocks(
|
||||
$var_comments = $codebase->config->disable_var_parsing
|
||||
? []
|
||||
: CommentAnalyzer::arrayToDocblocks(
|
||||
$docblock,
|
||||
$statements_analyzer->parsed_docblock,
|
||||
$statements_analyzer->getSource(),
|
||||
@ -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_) {
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -77,6 +77,7 @@ class AssignmentMapVisitor extends PhpParser\NodeVisitorAbstract
|
||||
|| $node instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $node instanceof PhpParser\Node\Expr\StaticCall
|
||||
) {
|
||||
if (!$node->isFirstClassCallable()) {
|
||||
foreach ($node->getArgs() as $arg) {
|
||||
$arg_var_id = ExpressionIdentifier::getRootVarId($arg->value, $this->this_class_name);
|
||||
|
||||
@ -84,6 +85,7 @@ class AssignmentMapVisitor extends PhpParser\NodeVisitorAbstract
|
||||
$this->assignment_map[$arg_var_id][$arg_var_id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof PhpParser\Node\Expr\MethodCall) {
|
||||
$var_id = ExpressionIdentifier::getRootVarId($node->var, $this->this_class_name);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ class AttributeArg
|
||||
{
|
||||
/**
|
||||
* @var ?string
|
||||
* @psalm-suppress PossiblyUnusedProperty It's part of the public API for now
|
||||
*/
|
||||
public $name;
|
||||
|
||||
@ -20,6 +21,7 @@ class AttributeArg
|
||||
|
||||
/**
|
||||
* @var CodeLocation
|
||||
* @psalm-suppress PossiblyUnusedProperty It's part of the public API for now
|
||||
*/
|
||||
public $location;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
15
src/Psalm/Storage/HasAttributesInterface.php
Normal file
15
src/Psalm/Storage/HasAttributesInterface.php
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ namespace {
|
||||
public static function cases(): array;
|
||||
}
|
||||
|
||||
interface BackedEnum
|
||||
interface BackedEnum extends UnitEnum
|
||||
{
|
||||
public readonly int|string $value;
|
||||
|
||||
|
@ -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 {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -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"];
|
||||
|
@ -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' => [],
|
||||
[],
|
||||
|
@ -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);
|
||||
',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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.');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user