Repository: spaze/phpstan-disallowed-calls (php)

Issue:
Supporting attributes restrictions on more targets, i.e. properties
Currently `disallowedAttributes` support only on attributes that target classes/interfaces/... or functions/methods (by [`AttributeUsages::processNode`](https://github.com/spaze/phpstan-disallowed-calls/blob/main/src/Usages/AttributeUsages.php#L68))

But attributes can have [other targets](https://www.php.net/manual/en/language.attributes.classes.php), I discovered this limitation while trying to restrict an attributes that is used on class properties.

I think it should be possible for `AttributeUsages` to support nodes for the other targets, but I'd like to know where @spaze stands on this:

1. lets add support for properties only, and see if someone else needs other targets in the future
2. lets support all possible attribute targets, by detecting the specific php parser nodes
3. lets support all possible attribute targets, by duck typing on attrGroups
4. not adding support for other attribute targets

I'd go for 2, let me know what you thing and if you'd welcome a PR (probably in a few weeks) or if you'd like to handle it directly

Pull request:
Support more attribute targets: properties, class constants, params
Close #224

Patch:
diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index 95a384b..89e5ebb 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -29,6 +29,8 @@ jobs:
             run: composer lint
           - php-version: "8.0"
             run: composer lint
+          - php-version: "8.1"
+            run: composer lint
         include:
           - php-version: "7.2"
             run: composer lint-7.x
@@ -38,6 +40,8 @@ jobs:
             run: composer lint-7.x
           - php-version: "8.0"
             run: composer lint-8.0
+          - php-version: "8.1"
+            run: composer lint-8.1
 
     steps:
     - uses: actions/checkout@v4
diff --git a/composer.json b/composer.json
index 1d08229..56a8f6d 100644
--- a/composer.json
+++ b/composer.json
@@ -41,8 +41,9 @@
 	},
 	"scripts": {
 		"lint": "vendor/bin/parallel-lint --colors src/ tests/",
-		"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php",
-		"lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php",
+		"lint-7.x": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/functionCallsNamedParams.php --exclude tests/src/disallowed-allow/functionCallsNamedParams.php --exclude tests/src/disallowed/attributeUsages.php --exclude tests/src/disallowed-allow/attributeUsages.php",
+		"lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php",
+		"lint-8.1": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/AttributesEverywhere.php",
 		"lint-neon": "vendor/bin/neon-lint .",
 		"phpcs": "vendor/bin/phpcs src/ tests/",
 		"cs-fix": "vendor/bin/phpcbf src/ tests/",
diff --git a/src/Usages/AttributeUsages.php b/src/Usages/AttributeUsages.php
index 4515c60..7a594e0 100644
--- a/src/Usages/AttributeUsages.php
+++ b/src/Usages/AttributeUsages.php
@@ -7,7 +7,11 @@ use PhpParser\Node;
 use PhpParser\Node\Attribute;
 use PhpParser\Node\AttributeGroup;
 use PhpParser\Node\FunctionLike;
+use PhpParser\Node\Param;
+use PhpParser\Node\Stmt\ClassConst;
 use PhpParser\Node\Stmt\ClassLike;
+use PhpParser\Node\Stmt\EnumCase;
+use PhpParser\Node\Stmt\Property;
 use PHPStan\Analyser\Scope;
 use PHPStan\Rules\Rule;
 use Spaze\PHPStan\Rules\Disallowed\DisallowedAttribute;
@@ -72,6 +76,14 @@ class AttributeUsages implements Rule
 			$this->addAttrs(array_values($node->attrGroups));
 		} elseif ($node instanceof FunctionLike) {
 			$this->addAttrs(array_values($node->getAttrGroups()));
+		} elseif ($node instanceof Property) {
+			$this->addAttrs(array_values($node->attrGroups));
+		} elseif ($node instanceof ClassConst) {
+			$this->addAttrs(array_values($node->attrGroups));
+		} elseif ($node instanceof Param) {
+			$this->addAttrs(array_values($node->attrGroups));
+		} elseif ($node instanceof EnumCase) {
+			$this->addAttrs(array_values($node->attrGroups));
 		} else {
 			return [];
 		}


Test patch:
diff --git a/tests/Usages/AttributeUsagesAllowParamsMultipleTest.php b/tests/Usages/AttributeUsagesAllowParamsMultipleTest.php
index 7a4a4f1..0539bca 100644
--- a/tests/Usages/AttributeUsagesAllowParamsMultipleTest.php
+++ b/tests/Usages/AttributeUsagesAllowParamsMultipleTest.php
@@ -71,9 +71,25 @@ class AttributeUsagesAllowParamsMultipleTest extends RuleTestCase
 				// on this line:
 				8,
 			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				12,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				15,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				18,
+			],
 			[
 				'Attribute Attributes\AttributeClass is forbidden.',
-				30,
+				40,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				42,
 			],
 		]);
 		$this->analyse([__DIR__ . '/../src/disallowed-allow/ClassWithAttributesAllow.php'], [
@@ -85,10 +101,26 @@ class AttributeUsagesAllowParamsMultipleTest extends RuleTestCase
 				'Attribute Attributes\AttributeEntity is forbidden.',
 				12,
 			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				15,
+			],
 			[
 				'Attribute Attributes\AttributeEntity is forbidden.',
 				18,
 			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				22,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				28,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				42,
+			],
 		]);
 	}
 
diff --git a/tests/Usages/AttributeUsagesTest.php b/tests/Usages/AttributeUsagesTest.php
index af25218..55769c9 100644
--- a/tests/Usages/AttributeUsagesTest.php
+++ b/tests/Usages/AttributeUsagesTest.php
@@ -63,12 +63,99 @@ class AttributeUsagesTest extends RuleTestCase
 				// on this line:
 				8,
 			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				12,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				15,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				18,
+			],
 			[
 				'Attribute Attributes\AttributeClass is forbidden.',
-				30,
+				40,
+			],
+			[
+				'Attribute Attributes\AttributeEntity is forbidden.',
+				42,
 			],
 		]);
 		$this->analyse([__DIR__ . '/../src/disallowed-allow/ClassWithAttributesAllow.php'], []);
+
+		$this->analyse([__DIR__ . '/../src/AttributesEverywhere.php'], [
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				6,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				10,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				13,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				19,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				23,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				26,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				30,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				32,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				48,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				52,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				54,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				61,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				63,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				69,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				70,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				76,
+			],
+			[
+				'Attribute Attributes\AttributeClass is forbidden.',
+				77,
+			],
+		]);
 	}
 
 }
diff --git a/tests/src/AttributesEverywhere.php b/tests/src/AttributesEverywhere.php
new file mode 100644
index 0000000..efa52ee
--- /dev/null
+++ b/tests/src/AttributesEverywhere.php
@@ -0,0 +1,79 @@
+<?php
+declare(strict_types = 1);
+
+namespace Attributes;
+
+#[AttributeClass]
+enum EnumWithAttributes
+{
+
+	#[AttributeClass]
+	public const ENUM_CONST = true;
+
+	#[AttributeClass]
+	case Foo;
+
+}
+
+
+#[AttributeClass]
+trait TraitWithAttributes
+{
+
+	#[AttributeClass]
+	private const TRAIT_CONST = true;
+
+	#[AttributeClass]
+	private $bar;
+
+
+	#[AttributeClass]
+	public function traitMethod(
+		#[AttributeClass]
+		bool $param
+	): void {
+	}
+
+}
+
+// https://phpstan.org/blog/how-phpstan-analyses-traits
+class ClassWithTraitWithAttributes
+{
+
+	use TraitWithAttributes;
+
+}
+
+
+#[AttributeClass]
+interface InterfaceWithAttributes
+{
+
+	#[AttributeClass]
+	public function interfaceMethod(
+		#[AttributeClass]
+		bool $param
+	): void;
+
+}
+
+
+#[AttributeClass]
+function functionWithAttributes(
+	#[AttributeClass]
+	int $param
+): void {
+}
+
+
+$anonymousFunction = #[AttributeClass] function (
+	#[AttributeClass]
+	int $param
+): void {
+};
+
+
+$arrowFunction = #[AttributeClass] fn(
+	#[AttributeClass]
+	int $param
+) => 1;
diff --git a/tests/src/disallowed-allow/ClassWithAttributesAllow.php b/tests/src/disallowed-allow/ClassWithAttributesAllow.php
index 3b3384a..dedde20 100644
--- a/tests/src/disallowed-allow/ClassWithAttributesAllow.php
+++ b/tests/src/disallowed-allow/ClassWithAttributesAllow.php
@@ -9,6 +9,16 @@ use Waldo\Quux\Blade;
 class ClassWithAttributesAllow
 {
 
+	#[AttributeEntity] // allowed by path in all tests
+	private const MAYO = true;
+
+	#[AttributeEntity] // allowed by path in all tests
+	public $cheddar = 'plz';
+
+	#[AttributeEntity] // disallowed
+	public static $pepper = 'ofc';
+
+
 	#[\Attributes\AttributeEntity(repositoryClass: \Attributes\UserRepository::class, readOnly: false)] // allowed by path in AttributeUsagesTest, disallowed in AttributeUsagesAllowParamsMultipleTest because $repositoryClass has other value
 	public function hasAvocado(): bool
 	{
@@ -28,8 +38,10 @@ class ClassWithAttributesAllow
 
 
 	#[AttributeClass()] // allowed by path in all tests
-	public function hasPineapple(): bool
-	{
+	public function hasPineapple(
+		#[AttributeEntity] // allowed by path in all tests
+		bool $really
+	): bool {
 	}
 
 }
diff --git a/tests/src/disallowed/ClassWithAttributes.php b/tests/src/disallowed/ClassWithAttributes.php
index 6abc5de..0b87416 100644
--- a/tests/src/disallowed/ClassWithAttributes.php
+++ b/tests/src/disallowed/ClassWithAttributes.php
@@ -9,6 +9,16 @@ use Waldo\Quux\Blade;
 class ClassWithAttributes
 {
 
+	#[AttributeEntity] // disallowed
+	private const MAYO = true;
+
+	#[AttributeEntity] // disallowed
+	public $cheddar = 'plz';
+
+	#[AttributeEntity] // disallowed
+	public static $pepper = 'ofc';
+
+
 	#[AttributeEntity(repositoryClass: UserRepository::class, readOnly: false)] // disallowed, $repositoryClass present with any value
 	public function hasAvocado(): bool
 	{
@@ -28,8 +38,10 @@ class ClassWithAttributes
 
 
 	#[AttributeClass()] // disallowed
-	public function hasPineapple(): bool
-	{
+	public function hasPineapple(
+		#[AttributeEntity] // disallowed
+		bool $really
+	): bool {
 	}
 
 }