Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ jobs:
../bashunit assert contains 'Caller.php:10:Call to function ResultCacheCollectedDataRules\pure() on a separate line has no effect.' "$OUTPUT"
echo "$OUTPUT" | grep -E 'Result cache reused the errors of ([0-9]+) of \1 rules on collected data\.'
../bashunit assert contains 'Result cache was not rewritten because it is unchanged.' "$OUTPUT"
# With -vvv the diagnose extensions print what the rules filled while they ran, so every rule runs.
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vvv --error-format raw")
echo "$OUTPUT"
../bashunit assert contains 'Caller.php:10:Call to function ResultCacheCollectedDataRules\pure() on a separate line has no effect.' "$OUTPUT"
../bashunit assert not_contains 'Result cache reused the errors' "$OUTPUT"
# src/Other.php is analysed again, but what it collects stays the same.
echo '// changed' >> src/Other.php
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
Expand Down
6 changes: 4 additions & 2 deletions src/Analyser/AnalyserResultFinalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ public function __construct(
* @param Closure(): void|null $postRuleCallback called when the rule's processNode() has returned or thrown, before its errors are processed
* @param array<string, CollectedDataRuleResult> $previousCollectedDataRuleResults the rule results of the
* previous run, from the result cache - a rule whose input did not change reuses its errors.
* Only with the reuseCollectedDataRuleErrors feature toggle, otherwise every rule runs.
* Only with the reuseCollectedDataRuleErrors feature toggle, otherwise every rule runs. A rule that
* does not call get() on the node always runs.
*/
public function finalize(
AnalyserResult $analyserResult,
Expand Down Expand Up @@ -179,7 +180,8 @@ public function finalize(
$errors[] = $this->ruleErrorTransformer->transform($ruleError, $scope, [], $ruleNode);
}

if ($this->reuseCollectedDataRuleErrors) {
// a rule that never asked for collected data works with something else, so it runs every time
if ($this->reuseCollectedDataRuleErrors && $reads->getCollectorTypes() !== []) {
$collectedDataRuleResults[$ruleKey] = $this->createRuleResult($reads, $onlyFiles, $hashCollector, $errors, $ruleValueDependencies);
}

Expand Down
3 changes: 2 additions & 1 deletion src/Analyser/DependencyTracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
* the result cache re-analyses the file when that changes - see ResultCacheValueExtension.
* In a rule for CollectedDataNode, it tracks what the errors of the rule depend on: with the
* reuseCollectedDataRuleErrors feature toggle, the result cache reuses the errors of such a rule
* while its collected data and the tracked values stay the same.
* while its collected data and the tracked values stay the same. A rule that does not call get() on
* the node runs every time.
*
* Extensions that get no Scope because they describe a class - class reflection extensions - use
* DeclarationDependencyTracker instead.
Expand Down
3 changes: 2 additions & 1 deletion src/Command/AnalyseApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ public function analyse(
$debug,
$preRuleCallback,
$postRuleCallback,
$resultCache->getCollectedDataRuleResults(),
// with -vvv the diagnose extensions print, and a rule can fill what they print only while it runs
$errorOutput->isDebug() ? [] : $resultCache->getCollectedDataRuleResults(),
);
if ($finalizerResult->getReusedRulesCount() > 0 && $errorOutput->isVeryVerbose()) {
$errorOutput->writeLineFormatted(sprintf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public function processNode(Node $node, Scope $scope): array
{
$this->runs++;

// a rule that does not ask for collected data runs every time
$node->get(FirstCollector::class);
$contents = file_get_contents($this->file);

return [
Expand Down
60 changes: 60 additions & 0 deletions tests/PHPStan/Analyser/CollectedDataReuse/NoCollectedDataRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\CollectedDataReuse;

use PhpParser\Node;
use PHPStan\Analyser\DependencyTracker;
use PHPStan\Analyser\Scope;
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function file_get_contents;
use function sprintf;

/**
* Asks only whether the analysis is files-only, and reads a file, like a rule that runs a tool over
* the whole project. It tracks the file when told to.
*
* @implements Rule<CollectedDataNode>
*/
final class NoCollectedDataRule implements Rule
{

public int $runs = 0;

public function __construct(private string $file, private bool $track)
{
}

public function getNodeType(): string
{
return CollectedDataNode::class;
}

/**
* @param Scope&DependencyTracker $scope
*/
public function processNode(Node $node, Scope $scope): array
{
$this->runs++;

if ($node->isOnlyFilesAnalysis()) {
return [];
}

if ($this->track) {
$scope->trackFileDependency($this->file);
}

$contents = file_get_contents($this->file);

return [
RuleErrorBuilder::message(sprintf('Tool says %s.', $contents === false ? 'nothing' : $contents))
->file(__FILE__)
->line(1)
->identifier('tests.noCollectedData')
->build(),
];
}

}
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/CollectedDataReuse/OnlyFilesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public function processNode(Node $node, Scope $scope): array
{
$this->runs++;

// a rule that does not ask for collected data runs every time
$node->get(FirstCollector::class);
if ($node->isOnlyFilesAnalysis()) {
return [];
}
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/CollectedDataReuse/TrackingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public function processNode(Node $node, Scope $scope): array
{
$this->runs++;

// a rule that does not ask for collected data runs every time
$node->get(FirstCollector::class);
$scope->trackFileDependency($this->file);
$scope->trackDirectoryDependency($this->directory, '*.txt');
$scope->trackValueDependency(FileResultCacheValueExtension::class, $this->valueFile);
Expand Down
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/CollectedDataRuleReuseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Analyser\CollectedDataReuse\DescribedClassRule;
use PHPStan\Analyser\CollectedDataReuse\EarlyReturnRule;
use PHPStan\Analyser\CollectedDataReuse\FirstCollector;
use PHPStan\Analyser\CollectedDataReuse\NoCollectedDataRule;
use PHPStan\Analyser\CollectedDataReuse\OnlyFilesRule;
use PHPStan\Analyser\CollectedDataReuse\SecondCollector;
use PHPStan\Analyser\CollectedDataReuse\ThrowingRule;
Expand Down Expand Up @@ -118,6 +119,29 @@ public function testOnlyFilesAnalysis(): void
$this->assertSame([], $this->getMessages($result));
}

public function testRuleThatDoesNotAskForCollectedDataRunsEveryTime(): void
{
$file = sys_get_temp_dir() . '/phpstan-collected-data-reuse-' . uniqid() . '.txt';
file_put_contents($file, 'one');
$collectedData = [
self::A => [FirstCollector::class => ['a1']],
];

// tracking a value is not enough, the rule has to ask for collected data
foreach ([false, true] as $track) {
$rule = new NoCollectedDataRule($file, $track);
$result = $this->finalize([$rule], $collectedData, false, []);
$this->assertSame([], $result->getCollectedDataRuleResults());

$result = $this->finalize([$rule], $collectedData, false, $result->getCollectedDataRuleResults());
$this->assertSame(2, $rule->runs);
$this->assertSame(0, $result->getReusedRulesCount());
$this->assertSame(['Tool says one.'], $this->getMessages($result));
}

unlink($file);
}

public function testRuleThatThrowsIsNotReused(): void
{
$rule = new ThrowingRule();
Expand Down
Loading