1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Detect when targets are incorrectly targeted

This commit is contained in:
Matt Brown 2020-10-30 13:28:14 -04:00
parent 4ea87b9054
commit 579327a470
10 changed files with 204 additions and 23 deletions

View File

@ -230,6 +230,7 @@
<xs:element name="InvalidArrayAccess" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidArrayAssignment" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidArrayOffset" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidAttribute" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidCast" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidCatch" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidClass" type="ClassIssueHandlerType" minOccurs="0" />

View File

@ -0,0 +1,16 @@
# InvalidAttribute
Emitted when using an attribute on an element that doesn't match the attribute's target
```php
<?php
namespace Foo;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Table {
public function __construct(public string $name) {}
}
#[Table("videos")]
function foo() : void {}
```

View File

@ -1724,10 +1724,10 @@ class Config
$core_generic_files = [];
if (\PHP_VERSION_ID < 80000 && $codebase->php_major_version >= 8) {
$stringable_path = dirname(__DIR__, 2) . '/stubs/Stringable.php';
$stringable_path = dirname(__DIR__, 2) . '/stubs/Php80.php';
if (!file_exists($stringable_path)) {
throw new \UnexpectedValueException('Cannot locate core generic classes');
throw new \UnexpectedValueException('Cannot locate PHP 8.0 classes');
}
$core_generic_files[] = $stringable_path;
@ -1796,6 +1796,16 @@ class Config
$core_generic_files = [$generic_stubs_path, $generic_classes_path, $immutable_classes_path];
if (\PHP_VERSION_ID >= 80000 && $codebase->php_major_version >= 8) {
$stringable_path = dirname(__DIR__, 2) . '/stubs/Php80.php';
if (!file_exists($stringable_path)) {
throw new \UnexpectedValueException('Cannot locate PHP 8.0 classes');
}
$core_generic_files[] = $stringable_path;
}
if (\extension_loaded('ds')) {
$ext_ds_path = dirname(__DIR__, 2) . '/stubs/ext-ds.php';

View File

@ -5,22 +5,22 @@ use PhpParser;
use Psalm\Internal\Analyzer\SourceAnalyzer;
use Psalm\Storage\AttributeStorage;
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
use Psalm\Issue\InvalidAttribute;
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
array $suppressed_issues,
int $target
) : void {
if ($attribute->fq_class_name === 'Attribute') {
return;
}
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$source,
$attribute->fq_class_name,
@ -43,6 +43,8 @@ class AttributeAnalyzer
return;
}
self::checkAttributeTargets($source, $attribute, $target);
$node_args = [];
foreach ($attribute->args as $storage_arg) {
@ -113,4 +115,68 @@ class AttributeAnalyzer
new \Psalm\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);
if ($attribute_class_storage->attributes) {
foreach ($attribute_class_storage->attributes as $attribute_attribute) {
if ($attribute_attribute->fq_class_name === 'Attribute') {
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([
\Psalm\Internal\Codebase\ConstantTypeResolver::resolve(
$codebase->classlikes,
$first_arg_type,
$source instanceof \Psalm\Internal\Analyzer\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'
];
if (\Psalm\IssueBuffer::accepts(
new InvalidAttribute(
'This attribute can not be used on a ' . $target_map[$target],
$attribute->name_location
),
$source->getSuppressedIssues()
)) {
// fall through
}
}
}
}
}
}
}

View File

@ -722,7 +722,8 @@ class ClassAnalyzer extends ClassLikeAnalyzer
AttributeAnalyzer::analyze(
$this,
$attribute,
$storage->suppressed_issues + $this->getSuppressedIssues()
$storage->suppressed_issues + $this->getSuppressedIssues(),
1
);
}
@ -1687,7 +1688,8 @@ class ClassAnalyzer extends ClassLikeAnalyzer
AttributeAnalyzer::analyze(
$source,
$attribute,
$this->source->getSuppressedIssues()
$this->source->getSuppressedIssues(),
8
);
}

View File

@ -951,7 +951,8 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
AttributeAnalyzer::analyze(
$this,
$attribute,
$storage->suppressed_issues + $this->getSuppressedIssues()
$storage->suppressed_issues + $this->getSuppressedIssues(),
$storage instanceof MethodStorage ? 4 : 2
);
}
@ -1418,7 +1419,8 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
AttributeAnalyzer::analyze(
$this,
$attribute,
$storage->suppressed_issues
$storage->suppressed_issues,
$function_param->promoted_property ? 8 : 32
);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Psalm\Issue;
class InvalidAttribute extends CodeIssue
{
public const ERROR_LEVEL = -1;
public const SHORTCODE = 242;
}

67
stubs/Php80.php Normal file
View File

@ -0,0 +1,67 @@
<?php
interface Stringable
{
/** @return string */
function __toString();
}
/**
* @template TClass as object
*/
class ReflectionAttribute
{
const IS_INSTANCEOF = 2;
private function __construct()
{
}
public function getName() : string
{
}
public function getTarget() : int
{
}
public function isRepeated() : bool
{
}
public function getArguments() : array
{
}
/**
* @return TClass
*/
public function newInstance() : object
{
}
/**
* @return never-return
*/
private function __clone()
{
}
}
class Attribute
{
public const TARGET_CLASS = 1;
public const TARGET_FUNCTION = 2;
public const TARGET_METHOD = 4;
public const TARGET_PROPERTY = 8;
public const TARGET_CLASS_CONSTANT = 16;
public const TARGET_PARAMETER = 32;
public const TARGET_ALL = 63;
/**
* @param 1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63 $flags
*/
public function __construct(int $flags = self::TARGET_ALL)
{
}
}

View File

@ -1,7 +0,0 @@
<?php
interface Stringable
{
/** @return string */
function __toString();
}

View File

@ -20,12 +20,12 @@ class AttributeTest extends TestCase
'<?php
namespace Foo;
#[\Attribute]
#[\Attribute(\Attribute::TARGET_CLASS)]
class Table {
public function __construct(public string $name) {}
}
#[\Attribute]
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Column {
public function __construct(public string $name) {}
}
@ -56,7 +56,7 @@ class AttributeTest extends TestCase
'functionAttributeExists' => [
'<?php
namespace {
#[Attribute]
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION | Attribute::TARGET_PARAMETER)]
class Deprecated {}
}
@ -71,7 +71,7 @@ class AttributeTest extends TestCase
'paramAttributeExists' => [
'<?php
namespace {
#[Attribute]
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION | Attribute::TARGET_PARAMETER)]
class Deprecated {}
}
@ -127,7 +127,7 @@ class AttributeTest extends TestCase
'<?php
namespace Foo;
#[\Attribute]
#[\Attribute(\Attribute::TARGET_CLASS)]
class Table {
public function __construct(public string $name) {}
}
@ -139,6 +139,22 @@ class AttributeTest extends TestCase
false,
'8.0'
],
'classAttributeUsedOnFunction' => [
'<?php
namespace Foo;
#[\Attribute(\Attribute::TARGET_CLASS)]
class Table {
public function __construct(public string $name) {}
}
#[Table("videos")]
function foo() : void {}',
'error_message' => 'InvalidAttribute',
[],
false,
'8.0'
],
];
}
}