Skip to content

Commit

Permalink
Add Reflection getAttributes analysis.
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrolGenhald committed Feb 24, 2022
1 parent 1387f94 commit 43764f0
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 18 deletions.
105 changes: 99 additions & 6 deletions src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
Expand Up @@ -3,10 +3,12 @@
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;
Expand All @@ -18,9 +20,13 @@
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 assert;
use function count;
use function reset;

class AttributesAnalyzer
Expand Down Expand Up @@ -63,7 +69,8 @@ public static function analyze(

$attribute_class_flags = self::getAttributeClassFlags(
$source,
$attribute_storage,
$attribute_storage->fq_class_name,
$attribute_storage->name_location,
$attribute_class_storage,
$suppressed_issues
);
Expand Down Expand Up @@ -114,7 +121,7 @@ public static function analyze(
/**
* @param array<int, string> $suppressed_issues
*/
public static function analyzeAttributeConstruction(
private static function analyzeAttributeConstruction(
SourceAnalyzer $source,
Context $context,
AttributeStorage $attribute_storage,
Expand Down Expand Up @@ -216,11 +223,12 @@ public static function analyzeAttributeConstruction(
*/
private static function getAttributeClassFlags(
SourceAnalyzer $source,
AttributeStorage $attribute,
string $attribute_name,
CodeLocation $attribute_location,
?ClassLikeStorage $attribute_class_storage,
array $suppressed_issues
): int {
if ($attribute->fq_class_name === "Attribute") {
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;
Expand Down Expand Up @@ -260,8 +268,8 @@ private static function getAttributeClassFlags(

IssueBuffer::maybeAdd(
new InvalidAttribute(
"The class {$attribute->fq_class_name} doesn't have the Attribute attribute",
$attribute->name_location
"The class {$attribute_name} doesn't have the Attribute attribute",
$attribute_location
),
$suppressed_issues
);
Expand All @@ -282,4 +290,89 @@ private static function iterateAttributeNodes(array $attribute_groups): Generato
}
}
}

/**
* 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 (; $arg !== null || $arg->name !== null && $arg->name->name !== "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(),
);
}
}
}
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -260,6 +261,16 @@ public static function analyze(
}
}

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, $args);
}

return null;
}

Expand Down
36 changes: 24 additions & 12 deletions tests/AttributeTest.php
Expand Up @@ -571,7 +571,7 @@ class Bar {}
',
'error_message' => 'UndefinedConstant',
],
'SKIPPED-getAttributesOnClassWithNonClassAttribute' => [
'getAttributesOnClassWithNonClassAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
Expand All @@ -581,21 +581,22 @@ class Foo {}
$r = new ReflectionClass(Foo::class);
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a class',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 - Attribute Attr cannot be used on a class',
],
'SKIPPED-getAttributesOnFunctionWithNonFunctionAttribute' => [
'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:8:39 Attr cannot be used on a function',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:39 - Attribute Attr cannot be used on a function',
],
'SKIPPED-getAttributesOnMethodWithNonMethodAttribute' => [
'getAttributesOnMethodWithNonMethodAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
Expand All @@ -608,9 +609,9 @@ public function bar(): void {}
$r = new ReflectionMethod("Foo::bar");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a method',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a method',
],
'SKIPPED-getAttributesOnPropertyWithNonPropertyAttribute' => [
'getAttributesOnPropertyWithNonPropertyAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_CLASS)]
class Attr {}
Expand All @@ -623,9 +624,9 @@ class Foo
$r = new ReflectionProperty(Foo::class, "bar");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a property',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a property',
],
'SKIPPED-getAttributesOnClassConstantWithNonClassConstantAttribute' => [
'getAttributesOnClassConstantWithNonClassConstantAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
Expand All @@ -638,9 +639,9 @@ class Foo
$r = new ReflectionClassConstant(Foo::class, "BAR");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 Attr cannot be used on a class constant',
'error_message' => 'InvalidAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:39 - Attribute Attr cannot be used on a class constant',
],
'SKIPPED-getAttributesOnParameterWithNonParameterAttribute' => [
'getAttributesOnParameterWithNonParameterAttribute' => [
'<?php
#[Attribute(Attribute::TARGET_PROPERTY)]
class Attr {}
Expand All @@ -650,7 +651,18 @@ function foo(int $bar): void {}
$r = new ReflectionParameter("foo", "bar");
$r->getAttributes(Attr::class);
',
'error_message' => 'InvalidAttribute - snc' . DIRECTORY_SEPARATOR . 'somefile.php:8:39 Attr cannot be used on a parameter',
'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
Expand Down

0 comments on commit 43764f0

Please sign in to comment.