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

Issue:
Add support enum dynamic constant
PHP: 8.3.1
When i use dynamic constants for enum. 
For example: 
```php
<?php declare(strict_types = 1);

enum Foo
{
	case Bar;
}

$variable = 'Bar';
$const = Foo::{$variable};
```
https://phpstan.org/r/fc74146a-7438-4cd8-ae4c-56c8db97dad5

<details>
<summary>Screenshot</summary>

![Screenshot 2024-01-21 at 4 04 15 PM](https://github.com/spaze/phpstan-disallowed-calls/assets/34651075/d73de7d4-73a8-4082-8362-8c0a14c04878)
</details>


I catch error 
```
Uncaught PHPStan\ShouldNotHappenException: $node->name should be PhpParser\Node\Identifier but is PhpParser\Node\Expr\PropertyFetch in /api/vendor/spaze/phpstan-disallowed-calls/src/Usages/ClassConstantUsages.php: 84
#0 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/FileAnalyser.php(107): Spaze\PHPStan\Rules\Disallowed\Usages\ClassConstantUsages->processNode(Object(PhpParser\Node\Expr\ClassConstFetch), Object(PHPStan\Analyser\MutatingScope))
#1 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Node/ClassStatementsGatherer.php(108): PHPStan\Analyser\FileAnalyser->PHPStan\Analyser\{closure}(Object(PhpParser\Node\Expr\ClassConstFetch), Object(PHPStan\Analyser\MutatingScope))
#2 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(567): PHPStan\Node\ClassStatementsGatherer-> __ invoke(Object(PhpParser\Node\Expr\ClassConstFetch), Object(PHPStan\Analyser\MutatingScope)
#3 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(2653): PHPStan\Analyser\NodeScopeResolver: : PHPStan\Analyser\{closure}(Object(PhpParser\Node\Expr\ClassConstFetch), Object(PHPStan\Analyser\MutatingScope))
#4 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(1592): PHPStan\Analyser\NodeScopeResolver->callNodeCallbackWithExpression(Object(Closure), Object(PhpParser\Node\Expr\ClassConstFetch), Object(PHPStan\Analyser\MutatingScope), ObjectPHPStan\Analyser\ExpressionContext))
#5 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(605): PHPStan\Analyser\NodeScopeResolver->processExprNode(Object(PhpParser\Node\Stmt\Return_), Object(PhpParser\Node\Expr\ClassConstFetch), Object(PHPStan\Analyser\MutatingScope), Object(Closure), Object(PHPStan\Analyser\ExpressionContext))
#6 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(401): PHPStan\Analyser\NodeScopeResolver->processStmtNode(Object(PhpParser\Node\Stmt\Return_), Object(PHPStan\Analyser\MutatingScope), Object(Closure), Object(PHPStan\Analyser\StatementContext))
#7 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(566): PHPStan\Analyser\NodeScopeResolver->processStmtNodes(Object(PhpParser\Node\Stmt\ClassMethod), Array, Object(PHPStan\Analyser\MutatingScope), Object(Closure), Object(PHPStan\Analyser\StatementContext))
#8 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(401): PHPStan\Analyser\NodeScopeResolver->processStmtNode(Object(PhpParser\Node\Stmt\ClassMethod), Object(PHPStan\Analyser\MutatingScope), Object(PHPStan\Node\ClassStatementsGatherer), Object(PHPStan\Analyser\StatementContext))
#9 phar:///api/vendor/phpstan/phpstan/phpstan.phar/src/Analyser/NodeScopeResolver.php(668): PHPStan\Analyser\NodeScopeResolver->processStmtNodes(Object(PhpParser\Node\Stmt\Class_), Array, Object(PHPStan\Analyser\MutatingScope), Object(PHPStan\Node\ClassStatementsGatherer), Object(PHPStan\Analyser\StatementContext))
```

<details>
<summary>Screenshot</summary>

![Screenshot 2024-01-21 at 4 02 54 PM](https://github.com/spaze/phpstan-disallowed-calls/assets/34651075/6211a05c-870c-4cb5-83fc-7f4ae7dec17b)
</details>


Pull request description:
Support dynamic class constant fetch available in PHP 8.3
https://wiki.php.net/rfc/dynamic_class_constant_fetch

Fix #241

This should have been done in #217 already.

Patch:
diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index 89e5ebb..01a37aa 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -31,6 +31,8 @@ jobs:
             run: composer lint
           - php-version: "8.1"
             run: composer lint
+          - php-version: "8.2"
+            run: composer lint
         include:
           - php-version: "7.2"
             run: composer lint-7.x
@@ -42,6 +44,8 @@ jobs:
             run: composer lint-8.0
           - php-version: "8.1"
             run: composer lint-8.1
+          - php-version: "8.2"
+            run: composer lint-8.2
 
     steps:
     - uses: actions/checkout@v4
diff --git a/composer.json b/composer.json
index d65da08..341a027 100644
--- a/composer.json
+++ b/composer.json
@@ -40,9 +40,10 @@
 	},
 	"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/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-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 --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php",
+		"lint-8.0": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/TypesEverywhere.php --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php",
+		"lint-8.1": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/AttributesEverywhere.php --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.php",
+		"lint-8.2": "vendor/bin/parallel-lint --colors src/ tests/ --exclude tests/src/disallowed/constantDynamicUsages.php --exclude tests/src/disallowed-allow/constantDynamicUsages.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/ClassConstantUsages.php b/src/Usages/ClassConstantUsages.php
index 7ed49ee..52e5a56 100644
--- a/src/Usages/ClassConstantUsages.php
+++ b/src/Usages/ClassConstantUsages.php
@@ -5,6 +5,7 @@ namespace Spaze\PHPStan\Rules\Disallowed\Usages;
 
 use PhpParser\Node;
 use PhpParser\Node\Expr\ClassConstFetch;
+use PhpParser\Node\Expr\Variable;
 use PhpParser\Node\Identifier;
 use PHPStan\Analyser\Scope;
 use PHPStan\Rules\Rule;
@@ -12,6 +13,7 @@ use PHPStan\Rules\RuleError;
 use PHPStan\Rules\RuleErrorBuilder;
 use PHPStan\ShouldNotHappenException;
 use PHPStan\Type\Constant\ConstantStringType;
+use PHPStan\Type\Type;
 use PHPStan\Type\VerbosityLevel;
 use Spaze\PHPStan\Rules\Disallowed\DisallowedConstant;
 use Spaze\PHPStan\Rules\Disallowed\DisallowedConstantFactory;
@@ -80,17 +82,38 @@ class ClassConstantUsages implements Rule
 		if (!($node instanceof ClassConstFetch)) {
 			throw new ShouldNotHappenException(sprintf('$node should be %s but is %s', ClassConstFetch::class, get_class($node)));
 		}
-		if (!($node->name instanceof Identifier)) {
-			throw new ShouldNotHappenException(sprintf('$node->name should be %s but is %s', Identifier::class, get_class($node->name)));
+		if ($node->name instanceof Identifier) {
+			return $this->getConstantRuleErrors($scope, (string)$node->name, $this->typeResolver->getType($node->class, $scope));
 		}
-		$constant = (string)$node->name;
-		$type = $this->typeResolver->getType($node->class, $scope);
-		$usedOnType = $type->getObjectTypeOrClassStringObjectType();
+		if ($node->name instanceof Variable) {
+			$type = $scope->getType($node->name);
+			$errors = [];
+			foreach ($type->getConstantStrings() as $constantString) {
+				$errors = array_merge(
+					$errors,
+					$this->getConstantRuleErrors($scope, $constantString->getValue(), $this->typeResolver->getType($node->class, $scope))
+				);
+			}
+			return $errors;
+		}
+		throw new ShouldNotHappenException(sprintf('$node->name should be %s but is %s', Identifier::class, get_class($node->name)));
+	}
 
+
+	/**
+	 * @param Scope $scope
+	 * @param string $constant
+	 * @param Type $type
+	 * @return list<RuleError>
+	 * @throws ShouldNotHappenException
+	 */
+	private function getConstantRuleErrors(Scope $scope, string $constant, Type $type): array
+	{
 		if (strtolower($constant) === 'class') {
 			return [];
 		}
 
+		$usedOnType = $type->getObjectTypeOrClassStringObjectType();
 		$displayName = $usedOnType->getObjectClassNames() ? $this->getFullyQualified($usedOnType->getObjectClassNames(), $constant) : null;
 		if ($usedOnType->getConstantStrings()) {
 			$classNames = array_map(


Test patch:
diff --git a/tests/Usages/ClassConstantUsagesTest.php b/tests/Usages/ClassConstantUsagesTest.php
index 34d3752..f487ddf 100644
--- a/tests/Usages/ClassConstantUsagesTest.php
+++ b/tests/Usages/ClassConstantUsagesTest.php
@@ -141,6 +141,27 @@ class ClassConstantUsagesTest extends RuleTestCase
 	}
 
 
+	public function testRuleDynamicClassConstantFetch(): void
+	{
+		// Based on the configuration above, in this file:
+		$this->analyse([__DIR__ . '/../src/disallowed/constantDynamicUsages.php'], [
+			[
+				'Using Waldo\Quux\Blade::RUNNER is forbidden, not a replicant.',
+				8,
+			],
+			[
+				'Using Waldo\Quux\Blade::DECKARD is forbidden, maybe a replicant.',
+				10,
+			],
+			[
+				'Using Waldo\Quux\Blade::RUNNER is forbidden, not a replicant.',
+				10,
+			],
+		]);
+		$this->analyse([__DIR__ . '/../src/disallowed-allow/constantDynamicUsages.php'], []);
+	}
+
+
 	public static function getAdditionalConfigFiles(): array
 	{
 		return [
diff --git a/tests/src/Blade.php b/tests/src/Blade.php
index e2c3c31..05faded 100644
--- a/tests/src/Blade.php
+++ b/tests/src/Blade.php
@@ -12,6 +12,8 @@ class Blade
 
 	public const WESLEY = 'Snipes';
 
+	public const MOVIE = Blade::WESLEY;
+
 
 	public function runner(int $everything = 0, bool $yes = false, string $roland = '303'): void
 	{
diff --git a/tests/src/disallowed-allow/constantDynamicUsages.php b/tests/src/disallowed-allow/constantDynamicUsages.php
new file mode 100644
index 0000000..cc8ba5b
--- /dev/null
+++ b/tests/src/disallowed-allow/constantDynamicUsages.php
@@ -0,0 +1,10 @@
+<?php
+declare(strict_types = 1);
+
+use Waldo\Quux\Blade;
+
+// allowed by path
+$kind = 'RUNNER';
+echo Blade::{$kind};
+/** @var 'DECKARD'|'MOVIE'|'RUNNER' $kind2 */
+echo Blade::{$kind2};
diff --git a/tests/src/disallowed/constantDynamicUsages.php b/tests/src/disallowed/constantDynamicUsages.php
new file mode 100644
index 0000000..45c5f4d
--- /dev/null
+++ b/tests/src/disallowed/constantDynamicUsages.php
@@ -0,0 +1,10 @@
+<?php
+declare(strict_types = 1);
+
+use Waldo\Quux\Blade;
+
+// disallowed
+$kind = 'RUNNER';
+echo Blade::{$kind};
+/** @var 'DECKARD'|'MOVIE'|'RUNNER' $kind2 */
+echo Blade::{$kind2};