diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 208bb0e5069..54cfaa44f85 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -352,6 +352,13 @@ jobs: composer install ../../bin/phpstan -vvv ../../bin/phpstan -vvv + - script: | + cd e2e/result-cache-constants + ../../bin/phpstan analyse + patch -b src/Constants.php < renameConstant.patch + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") + echo "$OUTPUT" + ../bashunit -a contains 'ClassUsingConstant.php:10:Constant SOME_MODE not found. [identifier=constant.notFound]' "$OUTPUT" - script: | cd e2e/bug-12606 export CONFIGTEST=test diff --git a/e2e/result-cache-constants/phpstan.neon b/e2e/result-cache-constants/phpstan.neon new file mode 100644 index 00000000000..1796e7715c7 --- /dev/null +++ b/e2e/result-cache-constants/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + paths: + - src + tmpDir: tmp diff --git a/e2e/result-cache-constants/renameConstant.patch b/e2e/result-cache-constants/renameConstant.patch new file mode 100644 index 00000000000..5d4706149b6 --- /dev/null +++ b/e2e/result-cache-constants/renameConstant.patch @@ -0,0 +1,8 @@ +--- src/Constants.php ++++ src/Constants.php +@@ -2,4 +2,4 @@ + + namespace ResultCacheE2EConstants; + +-const SOME_MODE = 1; ++const RENAMED_MODE = 1; diff --git a/e2e/result-cache-constants/src/ClassUsingConstant.php b/e2e/result-cache-constants/src/ClassUsingConstant.php new file mode 100644 index 00000000000..cb1bfc8c540 --- /dev/null +++ b/e2e/result-cache-constants/src/ClassUsingConstant.php @@ -0,0 +1,13 @@ +> */ + /** @var array> */ private array $classDependencies = []; public function __construct( @@ -366,6 +367,17 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } } } + } elseif ($node instanceof Node\Expr\ConstFetch) { + $constantName = $node->name; + $lowercasedConstantName = $constantName->toLowerString(); + if ( + $lowercasedConstantName !== 'true' + && $lowercasedConstantName !== 'false' + && $lowercasedConstantName !== 'null' + && $this->reflectionProvider->hasConstant($constantName, $scope) + ) { + $dependenciesReflections[] = $this->reflectionProvider->getConstant($constantName, $scope); + } } elseif ($node instanceof Node\Expr\StaticPropertyFetch) { if ($node->class instanceof Node\Name) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); @@ -521,7 +533,7 @@ private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): } /** - * @param array $dependenciesReflections + * @param array $dependenciesReflections */ private function addClassToDependencies(string $className, array &$dependenciesReflections): void { @@ -533,7 +545,7 @@ private function addClassToDependencies(string $className, array &$dependenciesR } /** - * @return list + * @return list */ private function buildClassDependencies(string $className): array { @@ -687,7 +699,7 @@ private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): Func } /** - * @param array $dependenciesReflections + * @param array $dependenciesReflections */ private function extractFromParametersAcceptor( ExtendedParametersAcceptor $parametersAcceptor, @@ -724,7 +736,7 @@ private function extractFromParametersAcceptor( } /** - * @param array $dependenciesReflections + * @param array $dependenciesReflections */ private function extractThrowType( ?Type $throwType, diff --git a/src/Dependency/ExportedNode/ExportedConstantNode.php b/src/Dependency/ExportedNode/ExportedConstantNode.php new file mode 100644 index 00000000000..fe96901540b --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedConstantNode.php @@ -0,0 +1,70 @@ +name; + } + + public function equals(ExportedNode $node): bool + { + if (!$node instanceof self) { + return false; + } + + return $this->name === $node->name + && $this->value === $node->value; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['name'], + $properties['value'], + ); + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + $data['name'], + $data['value'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + #[Override] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'value' => $this->value, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedConstantsNode.php b/src/Dependency/ExportedNode/ExportedConstantsNode.php new file mode 100644 index 00000000000..2f9c2ee45c4 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedConstantsNode.php @@ -0,0 +1,94 @@ +constants[0]->getName(); + } + + public function equals(ExportedNode $node): bool + { + if (!$node instanceof self) { + return false; + } + + if (count($this->constants) !== count($node->constants)) { + return false; + } + + foreach ($this->constants as $i => $constant) { + if (!$constant->equals($node->constants[$i])) { + return false; + } + } + + return true; + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): self + { + return new self( + $properties['constants'], + ); + } + + /** + * @param mixed[] $data + */ + public static function decode(array $data): self + { + return new self( + array_map(static function (array $constantData): ExportedConstantNode { + if ($constantData['type'] !== ExportedConstantNode::class) { + throw new ShouldNotHappenException(); + } + + return ExportedConstantNode::decode($constantData['data']); + }, $data['constants']), + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + #[Override] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'constants' => $this->constants, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index 678d6e967df..1e230374a0f 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -12,6 +12,8 @@ use PHPStan\Dependency\ExportedNode\ExportedClassConstantNode; use PHPStan\Dependency\ExportedNode\ExportedClassConstantsNode; use PHPStan\Dependency\ExportedNode\ExportedClassNode; +use PHPStan\Dependency\ExportedNode\ExportedConstantNode; +use PHPStan\Dependency\ExportedNode\ExportedConstantsNode; use PHPStan\Dependency\ExportedNode\ExportedEnumCaseNode; use PHPStan\Dependency\ExportedNode\ExportedEnumNode; use PHPStan\Dependency\ExportedNode\ExportedFunctionNode; @@ -226,6 +228,18 @@ public function resolve(string $fileName, Node $node): ?RootExportedNode ); } + if ($node instanceof Node\Stmt\Const_) { + $constants = []; + foreach ($node->consts as $const) { + $constants[] = new ExportedConstantNode( + $const->name->toString(), + $this->exprPrinter->printExpr($const->value), + ); + } + + return new ExportedConstantsNode($constants); + } + return null; } diff --git a/src/Dependency/NodeDependencies.php b/src/Dependency/NodeDependencies.php index 6c944853d08..840928f8660 100644 --- a/src/Dependency/NodeDependencies.php +++ b/src/Dependency/NodeDependencies.php @@ -4,6 +4,7 @@ use PHPStan\File\FileHelper; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\FunctionReflection; use function array_values; @@ -11,7 +12,7 @@ final class NodeDependencies { /** - * @param array $reflections + * @param array $reflections */ public function __construct( private FileHelper $fileHelper, @@ -22,7 +23,7 @@ public function __construct( } /** - * @return array + * @return array */ public function getReflections(): array { diff --git a/src/Dependency/RootExportedNode.php b/src/Dependency/RootExportedNode.php index 9434c913372..7a7f93a328f 100644 --- a/src/Dependency/RootExportedNode.php +++ b/src/Dependency/RootExportedNode.php @@ -15,6 +15,8 @@ interface RootExportedNode extends ExportedNode public const TYPE_FUNCTION = 'function'; + public const TYPE_CONSTANT = 'constant'; + /** @return self::TYPE_* */ public function getType(): string; diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 6f781f28a25..dc148fbb9ad 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -737,6 +737,20 @@ public function testIgnoreErrorExplicitReportUnmatchedEnableMulti(): void $this->assertSame('Ignored error pattern #Fail# was not matched in reported errors.', $result[0]); } + public function testConstantFetchIsTrackedAsDependency(): void + { + $analyser = $this->createAnalyser(); + $defFile = $this->getFileHelper()->normalizePath(__DIR__ . '/data/const-dependency/def.php'); + $userFile = $this->getFileHelper()->normalizePath(__DIR__ . '/data/const-dependency/user.php'); + + $analyserResult = $analyser->analyse([$defFile, $userFile]); + + $dependencies = $analyserResult->getDependencies(); + $this->assertNotNull($dependencies); + $this->assertArrayHasKey($userFile, $dependencies); + $this->assertContains($defFile, $dependencies[$userFile]); + } + /** * @param mixed[] $ignoreErrors * @param string|string[] $filePaths diff --git a/tests/PHPStan/Analyser/data/const-dependency/def.php b/tests/PHPStan/Analyser/data/const-dependency/def.php new file mode 100644 index 00000000000..11532d03c15 --- /dev/null +++ b/tests/PHPStan/Analyser/data/const-dependency/def.php @@ -0,0 +1,5 @@ +