Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions e2e/result-cache-constants/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
parameters:
level: 8
paths:
- src
tmpDir: tmp
8 changes: 8 additions & 0 deletions e2e/result-cache-constants/renameConstant.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--- src/Constants.php
+++ src/Constants.php
@@ -2,4 +2,4 @@

namespace ResultCacheE2EConstants;

-const SOME_MODE = 1;
+const RENAMED_MODE = 1;
13 changes: 13 additions & 0 deletions e2e/result-cache-constants/src/ClassUsingConstant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace ResultCacheE2EConstants;

class ClassUsingConstant
{

public function getMode(): int
{
return SOME_MODE;
}

}
5 changes: 5 additions & 0 deletions e2e/result-cache-constants/src/Constants.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace ResultCacheE2EConstants;

const SOME_MODE = 1;
2 changes: 2 additions & 0 deletions e2e/result-cache-constants/tmp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.*
22 changes: 17 additions & 5 deletions src/Dependency/DependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PHPStan\Node\MethodCallableNode;
use PHPStan\Node\StaticMethodCallableNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ConstantReflection;
use PHPStan\Reflection\ExtendedParameterReflection;
use PHPStan\Reflection\ExtendedParametersAcceptor;
use PHPStan\Reflection\FunctionReflection;
Expand All @@ -38,7 +39,7 @@
final class DependencyResolver
{

/** @var array<string, list<ClassReflection|FunctionReflection>> */
/** @var array<string, list<ClassReflection|FunctionReflection|ConstantReflection>> */
private array $classDependencies = [];

public function __construct(
Expand Down Expand Up @@ -366,6 +367,17 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
}
}
}
} elseif ($node instanceof Node\Expr\ConstFetch) {

@staabm staabm Jul 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes in DependencyResolver are usually covered with a e2e tests, see e.g. e5db864

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the e2e test actually surfaced a gap, working on it!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch. I added e2e/result-cache-constants modelled on result-cache-traits: it analyses, renames a global constant via a patch, and asserts the file reading it re-analyses and reports the constant as undefined.

Writing the e2e surfaced that the dependency edge alone wasn't enough: global constant declarations weren't exported nodes either, so a constant change never invalidated the reading files (the result cache re-analyses a changed file's dependents only when its exported nodes change). I pushed a second commit exporting global constants (name + value), like class constants already are. With both, the e2e fails before the change and passes after.

$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);
Expand Down Expand Up @@ -521,7 +533,7 @@ private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode):
}

/**
* @param array<int, ClassReflection|FunctionReflection> $dependenciesReflections
* @param array<int, ClassReflection|FunctionReflection|ConstantReflection> $dependenciesReflections
*/
private function addClassToDependencies(string $className, array &$dependenciesReflections): void
{
Expand All @@ -533,7 +545,7 @@ private function addClassToDependencies(string $className, array &$dependenciesR
}

/**
* @return list<ClassReflection|FunctionReflection>
* @return list<ClassReflection|FunctionReflection|ConstantReflection>
*/
private function buildClassDependencies(string $className): array
{
Expand Down Expand Up @@ -687,7 +699,7 @@ private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): Func
}

/**
* @param array<ClassReflection|FunctionReflection> $dependenciesReflections
* @param array<ClassReflection|FunctionReflection|ConstantReflection> $dependenciesReflections
*/
private function extractFromParametersAcceptor(
ExtendedParametersAcceptor $parametersAcceptor,
Expand Down Expand Up @@ -724,7 +736,7 @@ private function extractFromParametersAcceptor(
}

/**
* @param array<ClassReflection|FunctionReflection> $dependenciesReflections
* @param array<ClassReflection|FunctionReflection|ConstantReflection> $dependenciesReflections
*/
private function extractThrowType(
?Type $throwType,
Expand Down
70 changes: 70 additions & 0 deletions src/Dependency/ExportedNode/ExportedConstantNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);

namespace PHPStan\Dependency\ExportedNode;

use JsonSerializable;
use Override;
use PHPStan\Dependency\ExportedNode;
use ReturnTypeWillChange;

final class ExportedConstantNode implements ExportedNode, JsonSerializable
{

public function __construct(private string $name, private string $value)
{
}

public function getName(): string
{
return $this->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,
],
];
}

}
94 changes: 94 additions & 0 deletions src/Dependency/ExportedNode/ExportedConstantsNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);

namespace PHPStan\Dependency\ExportedNode;

use JsonSerializable;
use Override;
use PHPStan\Dependency\ExportedNode;
use PHPStan\Dependency\RootExportedNode;
use PHPStan\ShouldNotHappenException;
use ReturnTypeWillChange;
use function array_map;
use function count;

final class ExportedConstantsNode implements RootExportedNode, JsonSerializable
{

/**
* @param ExportedConstantNode[] $constants
*/
public function __construct(private array $constants)
{
}

public function getType(): string
{
return self::TYPE_CONSTANT;
}

public function getName(): string
{
return $this->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,
],
];
}

}
14 changes: 14 additions & 0 deletions src/Dependency/ExportedNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
5 changes: 3 additions & 2 deletions src/Dependency/NodeDependencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

use PHPStan\File\FileHelper;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ConstantReflection;
use PHPStan\Reflection\FunctionReflection;
use function array_values;

final class NodeDependencies
{

/**
* @param array<int, ClassReflection|FunctionReflection> $reflections
* @param array<int, ClassReflection|FunctionReflection|ConstantReflection> $reflections
*/
public function __construct(
private FileHelper $fileHelper,
Expand All @@ -22,7 +23,7 @@ public function __construct(
}

/**
* @return array<int, ClassReflection|FunctionReflection>
* @return array<int, ClassReflection|FunctionReflection|ConstantReflection>
*/
public function getReflections(): array
{
Expand Down
2 changes: 2 additions & 0 deletions src/Dependency/RootExportedNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 14 additions & 0 deletions tests/PHPStan/Analyser/AnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Analyser/data/const-dependency/def.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php declare(strict_types = 1);

namespace ConstDependencyTest;

const SOME_MODE = 1;
8 changes: 8 additions & 0 deletions tests/PHPStan/Analyser/data/const-dependency/user.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace ConstDependencyTest;

function useMode(): int
{
return SOME_MODE;
}
Loading