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
12 changes: 12 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ jobs:
echo "$OUTPUT"
../bashunit -a contains 'Result cache might not behave correctly' "$OUTPUT"
../bashunit -a contains 'ResultCache8E2E\CustomRule' "$OUTPUT"
- script: |
cd e2e/result-cache-symlink/app
# https://github.com/phpstan/phpstan/issues/14924
# A custom rule provided by a package symlinked into vendor (composer path
# repository) and added to the analysed paths must NOT be reported as being
# outside of analysed paths, even though reflection reports its realpath.
composer install
../../../bin/phpstan
echo -en '\n' >> ../ext/src/CustomRule.php
OUTPUT=$(../../../bin/phpstan analyze 2>&1 || true)
echo "$OUTPUT"
../../bashunit -a not_contains 'Result cache might not behave correctly' "$OUTPUT"
- script: |
cd e2e/env-int-key
env 1=1 ../../bin/phpstan analyse test.php
Expand Down
1 change: 1 addition & 0 deletions e2e/result-cache-symlink/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vendor
19 changes: 19 additions & 0 deletions e2e/result-cache-symlink/app/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"require-dev": {
"phpstan/phpstan": "^1.10"
},
"require": {
"resultcachesymlink/ext": "*"
},
"repositories": [
{
"type": "path",
"url": "../ext"
}
],
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"classmap": ["src"]
}
}
8 changes: 8 additions & 0 deletions e2e/result-cache-symlink/app/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
parameters:
paths:
- src
- vendor/resultcachesymlink/ext/src
level: 8

rules:
- ResultCacheSymlinkE2E\CustomRule
8 changes: 8 additions & 0 deletions e2e/result-cache-symlink/app/src/Foo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace ResultCacheSymlinkE2E;

class Foo
{

}
6 changes: 6 additions & 0 deletions e2e/result-cache-symlink/ext/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "resultcachesymlink/ext",
"autoload": {
"classmap": ["src"]
}
}
24 changes: 24 additions & 0 deletions e2e/result-cache-symlink/ext/src/CustomRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace ResultCacheSymlinkE2E;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;

/**
* @implements Rule<Node>
*/
class CustomRule implements Rule
{
public function getNodeType(): string
{
return Node::class;
}

public function processNode(Node $node, Scope $scope): array
{
return [];
}

}
44 changes: 44 additions & 0 deletions src/Analyser/ResultCache/ResultCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use function is_file;
use function ksort;
use function microtime;
use function realpath;
use function sort;
use function sprintf;
use function str_starts_with;
Expand All @@ -75,6 +76,9 @@ final class ResultCacheManager
/** @var array<string, true> */
private array $alreadyProcessed = [];

/** @var array<string, string>|null */
private ?array $analysedFileNamesByRealpath = null;

/**
* @param string[] $analysedPaths
* @param string[] $analysedPathsFromConfig
Expand Down Expand Up @@ -1251,6 +1255,7 @@ private function streamArrayVarExportToHandle($handle, string $file, array $valu
private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array
{
$this->alreadyProcessed = [];
$this->analysedFileNamesByRealpath = null;
$projectExtensionFiles = [];
if ($projectConfig !== null) {
$vendorDirs = [];
Expand Down Expand Up @@ -1289,6 +1294,17 @@ private function getProjectExtensionFiles(?array $projectConfig, array $dependen
continue 2;
}
}

// Reflection reports the realpath of the class file, but analysed paths
// may reach it through a symlink (e.g. a package symlinked into vendor).
// Match the analysed file by realpath before treating it as not analysed.
$analysedFileName = $this->findAnalysedFileNameByRealpath($fileName, $dependencies);
if ($analysedFileName !== null) {
$allServiceFiles = $this->getAllDependencies($analysedFileName, $dependencies);
}
}

if (count($allServiceFiles) === 0) {
$projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class];
continue;
}
Expand Down Expand Up @@ -1335,6 +1351,34 @@ private function getAllDependencies(string $fileName, array $dependencies): arra
return $files;
}

/**
* Resolves a class file name (a realpath reported by reflection) to the analysed
* file it was reached through, matching by realpath so symlinked paths still line up.
*
* @param array<string, array<int, string>> $dependencies
*/
private function findAnalysedFileNameByRealpath(string $fileName, array $dependencies): ?string
{
$realFileName = realpath($fileName);
if ($realFileName === false) {
return null;
}
$realFileName = $this->fileHelper->normalizePath($realFileName);

if ($this->analysedFileNamesByRealpath === null) {
$this->analysedFileNamesByRealpath = [];
foreach (array_keys($dependencies) as $dependencyFile) {
$realDependencyFile = realpath($dependencyFile);
if ($realDependencyFile === false) {
continue;
}
$this->analysedFileNamesByRealpath[$this->fileHelper->normalizePath($realDependencyFile)] = $dependencyFile;
}
}

return $this->analysedFileNamesByRealpath[$realFileName] ?? null;
}

/**
* @param string[] $allAnalysedFiles
* @param mixed[]|null $projectConfigArray
Expand Down
Loading