2020-10-24 06:10:22 +02:00
|
|
|
|
<?php
|
|
|
|
|
namespace Psalm\Internal\Analyzer;
|
|
|
|
|
|
2021-02-15 22:18:41 +01:00
|
|
|
|
use Psalm\Node\Expr\VirtualNew;
|
|
|
|
|
use Psalm\Node\Name\VirtualFullyQualified;
|
|
|
|
|
use Psalm\Node\Stmt\VirtualExpression;
|
|
|
|
|
use Psalm\Node\VirtualArg;
|
|
|
|
|
use Psalm\Node\VirtualIdentifier;
|
2020-10-24 06:10:22 +02:00
|
|
|
|
use Psalm\Storage\AttributeStorage;
|
2020-11-22 07:15:52 +01:00
|
|
|
|
use Psalm\Storage\ClassLikeStorage;
|
2020-10-24 06:10:22 +02:00
|
|
|
|
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
|
2020-10-30 18:28:14 +01:00
|
|
|
|
use Psalm\Issue\InvalidAttribute;
|
2020-10-24 06:10:22 +02:00
|
|
|
|
use Psalm\Type\Union;
|
2020-10-30 18:28:14 +01:00
|
|
|
|
use function reset;
|
2020-10-24 06:10:22 +02:00
|
|
|
|
|
|
|
|
|
class AttributeAnalyzer
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string> $suppressed_issues
|
2020-10-30 18:28:14 +01:00
|
|
|
|
* @param 1|2|4|8|16|32 $target
|
2020-10-24 06:10:22 +02:00
|
|
|
|
*/
|
|
|
|
|
public static function analyze(
|
|
|
|
|
SourceAnalyzer $source,
|
|
|
|
|
AttributeStorage $attribute,
|
2020-10-30 18:28:14 +01:00
|
|
|
|
array $suppressed_issues,
|
2020-11-22 07:15:52 +01:00
|
|
|
|
int $target,
|
|
|
|
|
?ClassLikeStorage $classlike_storage = null
|
2020-10-24 06:10:22 +02:00
|
|
|
|
) : void {
|
|
|
|
|
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
|
|
|
|
$source,
|
|
|
|
|
$attribute->fq_class_name,
|
|
|
|
|
$attribute->location,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
$suppressed_issues,
|
2021-04-30 21:01:33 +02:00
|
|
|
|
new ClassLikeNameOptions(
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
true
|
|
|
|
|
)
|
2020-10-24 06:10:22 +02:00
|
|
|
|
) === false) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$codebase = $source->getCodebase();
|
|
|
|
|
|
|
|
|
|
if (!$codebase->classlikes->classExists($attribute->fq_class_name)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-22 07:15:52 +01:00
|
|
|
|
if ($attribute->fq_class_name === 'Attribute' && $classlike_storage) {
|
|
|
|
|
if ($classlike_storage->is_trait) {
|
|
|
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
|
|
|
new InvalidAttribute(
|
|
|
|
|
'Traits cannot act a attribute classes',
|
|
|
|
|
$attribute->name_location
|
|
|
|
|
),
|
|
|
|
|
$source->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
} elseif ($classlike_storage->is_interface) {
|
|
|
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
|
|
|
new InvalidAttribute(
|
|
|
|
|
'Interfaces cannot act a attribute classes',
|
|
|
|
|
$attribute->name_location
|
|
|
|
|
),
|
|
|
|
|
$source->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
} elseif ($classlike_storage->abstract) {
|
|
|
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
|
|
|
new InvalidAttribute(
|
|
|
|
|
'Abstract classes cannot act a attribute classes',
|
|
|
|
|
$attribute->name_location
|
|
|
|
|
),
|
|
|
|
|
$source->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
} elseif (isset($classlike_storage->methods['__construct'])
|
|
|
|
|
&& $classlike_storage->methods['__construct']->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
|
|
|
|
) {
|
|
|
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
|
|
|
new InvalidAttribute(
|
|
|
|
|
'Classes with protected/private constructors cannot act a attribute classes',
|
|
|
|
|
$attribute->name_location
|
|
|
|
|
),
|
|
|
|
|
$source->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-30 18:28:14 +01:00
|
|
|
|
self::checkAttributeTargets($source, $attribute, $target);
|
|
|
|
|
|
2020-10-24 06:10:22 +02:00
|
|
|
|
$node_args = [];
|
|
|
|
|
|
|
|
|
|
foreach ($attribute->args as $storage_arg) {
|
|
|
|
|
$type = $storage_arg->type;
|
|
|
|
|
|
|
|
|
|
if ($type instanceof UnresolvedConstantComponent) {
|
|
|
|
|
$type = new Union([
|
|
|
|
|
\Psalm\Internal\Codebase\ConstantTypeResolver::resolve(
|
|
|
|
|
$codebase->classlikes,
|
|
|
|
|
$type,
|
|
|
|
|
$source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer ? $source : null
|
|
|
|
|
)
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($type->isMixed()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-22 06:44:44 +01:00
|
|
|
|
$type_expr = \Psalm\Internal\Stubs\Generator\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);
|
|
|
|
|
|
2021-02-15 22:18:41 +01:00
|
|
|
|
$node_args[] = new VirtualArg(
|
2020-11-22 06:44:44 +01:00
|
|
|
|
$type_expr,
|
2020-10-24 06:10:22 +02:00
|
|
|
|
false,
|
|
|
|
|
false,
|
2020-11-22 06:44:44 +01:00
|
|
|
|
$arg_attributes,
|
2020-10-24 06:10:22 +02:00
|
|
|
|
$storage_arg->name
|
2021-02-15 22:18:41 +01:00
|
|
|
|
? new VirtualIdentifier(
|
2020-10-24 06:10:22 +02:00
|
|
|
|
$storage_arg->name,
|
2020-11-22 06:44:44 +01:00
|
|
|
|
$arg_attributes
|
2020-10-24 06:10:22 +02:00
|
|
|
|
)
|
|
|
|
|
: null
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-15 22:18:41 +01:00
|
|
|
|
$new_stmt = new VirtualNew(
|
|
|
|
|
new VirtualFullyQualified(
|
2020-10-24 06:10:22 +02:00
|
|
|
|
$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 \Psalm\Internal\Provider\NodeDataProvider()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$statements_analyzer->analyze(
|
2021-02-15 22:18:41 +01:00
|
|
|
|
[new VirtualExpression($new_stmt)],
|
2020-10-24 06:10:22 +02:00
|
|
|
|
new \Psalm\Context()
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-10-30 18:28:14 +01:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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);
|
|
|
|
|
|
2020-11-22 06:52:56 +01:00
|
|
|
|
$has_attribute_attribute = $attribute->fq_class_name === 'Attribute';
|
2020-10-30 18:28:14 +01:00
|
|
|
|
|
2020-11-22 06:52:56 +01:00
|
|
|
|
foreach ($attribute_class_storage->attributes as $attribute_attribute) {
|
|
|
|
|
if ($attribute_attribute->fq_class_name === 'Attribute') {
|
|
|
|
|
$has_attribute_attribute = true;
|
2020-10-30 18:28:14 +01:00
|
|
|
|
|
2020-11-22 06:52:56 +01:00
|
|
|
|
if (!$attribute_attribute->args) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-10-30 18:28:14 +01:00
|
|
|
|
|
2020-11-22 06:52:56 +01:00
|
|
|
|
$first_arg = reset($attribute_attribute->args);
|
2020-10-30 18:28:14 +01:00
|
|
|
|
|
2020-11-22 06:52:56 +01:00
|
|
|
|
$first_arg_type = $first_arg->type;
|
|
|
|
|
|
|
|
|
|
if ($first_arg_type instanceof UnresolvedConstantComponent) {
|
|
|
|
|
$first_arg_type = new Union([
|
|
|
|
|
\Psalm\Internal\Codebase\ConstantTypeResolver::resolve(
|
|
|
|
|
$codebase->classlikes,
|
|
|
|
|
$first_arg_type,
|
|
|
|
|
$source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer ? $source : null
|
|
|
|
|
)
|
|
|
|
|
]);
|
|
|
|
|
}
|
2020-10-30 18:28:14 +01:00
|
|
|
|
|
2020-11-22 06:52:56 +01:00
|
|
|
|
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'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
|
|
|
new InvalidAttribute(
|
|
|
|
|
'This attribute can not be used on a ' . $target_map[$target],
|
|
|
|
|
$attribute->name_location
|
|
|
|
|
),
|
|
|
|
|
$source->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
2020-10-30 18:28:14 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-11-22 06:52:56 +01:00
|
|
|
|
|
|
|
|
|
if (!$has_attribute_attribute) {
|
|
|
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
|
|
|
new InvalidAttribute(
|
|
|
|
|
'The class ' . $attribute->fq_class_name . ' doesn’t have the Attribute attribute',
|
|
|
|
|
$attribute->name_location
|
|
|
|
|
),
|
|
|
|
|
$source->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-10-30 18:28:14 +01:00
|
|
|
|
}
|
2020-10-24 06:10:22 +02:00
|
|
|
|
}
|