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:
parent
4ea87b9054
commit
579327a470
@ -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" />
|
||||
|
16
docs/running_psalm/issues/InvalidAttribute.md
Normal file
16
docs/running_psalm/issues/InvalidAttribute.md
Normal 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 {}
|
||||
```
|
@ -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';
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
8
src/Psalm/Issue/InvalidAttribute.php
Normal file
8
src/Psalm/Issue/InvalidAttribute.php
Normal 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
67
stubs/Php80.php
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
interface Stringable
|
||||
{
|
||||
/** @return string */
|
||||
function __toString();
|
||||
}
|
@ -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'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user