diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 9b4e4a7460..53441741a9 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -38,6 +38,8 @@ public function __construct( * @param Closure(string $file): void|null $preFileCallback * @param Closure(int, list=): void|null $postFileCallback * @param string[]|null $allAnalysedFiles + * @param string|null $tmpFile editor mode (--tmp-file): the temp file present in $files whose content is analysed + * @param string|null $insteadOfFile editor mode (--instead-of): the real path $tmpFile should be reported under */ public function analyse( array $files, @@ -45,6 +47,8 @@ public function analyse( ?Closure $postFileCallback = null, bool $debug = false, ?array $allAnalysedFiles = null, + ?string $tmpFile = null, + ?string $insteadOfFile = null, ): AnalyserResult { if ($allAnalysedFiles === null) { @@ -83,12 +87,16 @@ public function analyse( } try { + $reportedFile = $tmpFile !== null && $insteadOfFile !== null && $file === $tmpFile + ? $insteadOfFile + : null; $fileAnalyserResult = $this->fileAnalyser->analyseFile( $file, $allAnalysedFiles, $this->ruleRegistry, $this->collectorRegistry, null, + $reportedFile, ); $errors = array_merge($errors, $fileAnalyserResult->getErrors()); $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); diff --git a/src/Analyser/EditorModeFileHelper.php b/src/Analyser/EditorModeFileHelper.php new file mode 100644 index 0000000000..7ee402fddc --- /dev/null +++ b/src/Analyser/EditorModeFileHelper.php @@ -0,0 +1,47 @@ +singleReflectionFile !== null + && $this->singleReflectionInsteadOfFile !== null + && $reportedFile === $this->singleReflectionInsteadOfFile + ) { + return $this->singleReflectionFile; + } + + return $reportedFile; + } + +} diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index 1870b581a6..9dca317de3 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -71,6 +71,7 @@ public function __construct( /** * @param array $analysedFiles * @param callable(Node $node, Scope $scope): void|null $outerNodeCallback + * @param string|null $reportedFile the path the file should be reported under; differs from $file in editor mode (--tmp-file / --instead-of), where $file is the temp file being read and $reportedFile is the real path */ public function analyseFile( string $file, @@ -78,8 +79,11 @@ public function analyseFile( RuleRegistry $ruleRegistry, CollectorRegistry $collectorRegistry, ?callable $outerNodeCallback, + ?string $reportedFile = null, ): FileAnalyserResult { + $analysisFile = $file; + $reportedFile ??= $file; /** @var list $fileErrors */ $fileErrors = []; @@ -98,14 +102,14 @@ public function analyseFile( $exportedNodes = []; $linesToIgnore = []; $unmatchedLineIgnores = []; - if (is_file($file)) { + if (is_file($analysisFile)) { try { $this->collectErrors($analysedFiles); - $parserNodes = $this->parser->parseFile($file); - $processedFiles[] = $file; + $parserNodes = $this->parser->parseFile($analysisFile); + $processedFiles[] = $reportedFile; $nodeCallback = new FileAnalyserCallback( - $file, + $reportedFile, $analysedFiles, $ruleRegistry, $collectorRegistry, @@ -118,7 +122,7 @@ public function analyseFile( $this->ruleErrorTransformer, $processedFiles, ); - $scope = $this->scopeFactory->create(ScopeContext::create($file), $nodeCallback); + $scope = $this->scopeFactory->create(ScopeContext::create($reportedFile, $analysisFile), $nodeCallback); $nodeCallback(new FileNode($parserNodes), $scope); $this->nodeScopeResolver->processNodes( $parserNodes, @@ -189,27 +193,27 @@ public function analyseFile( $linesToIgnore = $localIgnoresProcessorResult->getLinesToIgnore(); $unmatchedLineIgnores = $localIgnoresProcessorResult->getUnmatchedLineIgnores(); } catch (\PhpParser\Error $e) { - $fileErrors[] = (new Error($e->getRawMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); + $fileErrors[] = (new Error($e->getRawMessage(), $reportedFile, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } catch (ParserErrorsException $e) { foreach ($e->getErrors() as $error) { - $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); + $fileErrors[] = (new Error($error->getMessage(), $e->getParsedFile() === $analysisFile || $e->getParsedFile() === null ? $reportedFile : $e->getParsedFile(), $error->getLine() !== -1 ? $error->getStartLine() : null, $e))->withIdentifier('phpstan.parse'); } } catch (AnalysedCodeException $e) { - $fileErrors[] = (new Error($e->getMessage(), $file, canBeIgnored: $e, tip: $e->getTip())) + $fileErrors[] = (new Error($e->getMessage(), $reportedFile, canBeIgnored: $e, tip: $e->getTip())) ->withIdentifier('phpstan.internal') ->withMetadata([ InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), ]); } catch (IdentifierNotFound $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $file, canBeIgnored: $e, tip: 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) + $fileErrors[] = (new Error(sprintf('Reflection error: %s not found.', $e->getIdentifier()->getName()), $reportedFile, canBeIgnored: $e, tip: 'Learn more at https://phpstan.org/user-guide/discovering-symbols')) ->withIdentifier('phpstan.reflection') ->withMetadata([ InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), InternalError::STACK_TRACE_AS_STRING_METADATA_KEY => $e->getTraceAsString(), ]); } catch (UnableToCompileNode | CircularReference $e) { - $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, canBeIgnored: $e)) + $fileErrors[] = (new Error(sprintf('Reflection error: %s', $e->getMessage()), $reportedFile, canBeIgnored: $e)) ->withIdentifier('phpstan.reflection') ->withMetadata([ InternalError::STACK_TRACE_METADATA_KEY => InternalError::prepareTrace($e), @@ -218,10 +222,10 @@ public function analyseFile( } finally { $this->restoreCollectErrorsHandler(); } - } elseif (is_dir($file)) { - $fileErrors[] = (new Error(sprintf('File %s is a directory.', $file), $file, canBeIgnored: false))->withIdentifier('phpstan.path'); + } elseif (is_dir($analysisFile)) { + $fileErrors[] = (new Error(sprintf('File %s is a directory.', $reportedFile), $reportedFile, canBeIgnored: false))->withIdentifier('phpstan.path'); } else { - $fileErrors[] = (new Error(sprintf('File %s does not exist.', $file), $file, canBeIgnored: false))->withIdentifier('phpstan.path'); + $fileErrors[] = (new Error(sprintf('File %s does not exist.', $reportedFile), $reportedFile, canBeIgnored: false))->withIdentifier('phpstan.path'); } foreach ($linesToIgnore as $fileKey => $lines) { diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8a2e5c78f8..b61af6a434 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -251,6 +251,16 @@ public function getFile(): string return $this->context->getFile(); } + /** + * The file whose content is actually analysed. Equal to getFile() except in + * editor mode (--tmp-file / --instead-of), where getFile() is the real path + * while this is the temp file being read. + */ + public function getAnalysisFile(): string + { + return $this->context->getAnalysisFile(); + } + /** @api */ public function getFileDescription(): string { @@ -919,7 +929,7 @@ public function hasConstant(Name $name): bool private function fileHasCompilerHaltStatementCalls(): bool { - $nodes = $this->parser->parseFile($this->getFile()); + $nodes = $this->parser->parseFile($this->getAnalysisFile()); foreach ($nodes as $node) { if ($node instanceof Node\Stmt\HaltCompiler) { return true; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 72f087b400..95d998ebd8 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2627,14 +2627,14 @@ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $s return null; } - private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection + private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, MutatingScope $scope): ClassReflection { if (!$this->reflectionProvider->hasClass($className)) { return $this->createAstClassReflection($stmt, $className, $scope); } $defaultClassReflection = $this->reflectionProvider->getClass($className); - if ($defaultClassReflection->getFileName() !== $scope->getFile()) { + if ($defaultClassReflection->getFileName() !== $scope->getAnalysisFile()) { return $this->createAstClassReflection($stmt, $className, $scope); } @@ -2646,13 +2646,14 @@ private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $cl return $defaultClassReflection; } - private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection + private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, MutatingScope $scope): ClassReflection { + $analysisFile = $scope->getAnalysisFile(); $nodeToReflection = new NodeToReflection(); $betterReflectionClass = $nodeToReflection->__invoke( $this->reflector, $stmt, - new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()), + new LocatedSource(FileReader::read($analysisFile), $className, $analysisFile), $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null, ); if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) { @@ -2667,7 +2668,7 @@ private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $cla null, null, null, - sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()), + sprintf('%s:%d', $analysisFile, $stmt->getStartLine()), ); } diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 05062d0526..2fd5d207b3 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -851,10 +851,6 @@ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile) { $errorsByFile = $resultCache->getErrors(); foreach ($resultCache->getFilesToAnalyse() as $file) { - if (array_key_exists($file, $this->fileReplacements)) { - unset($errorsByFile[$file]); - $file = $this->fileReplacements[$file]; - } if (!array_key_exists($file, $freshErrorsByFile)) { unset($errorsByFile[$file]); continue; @@ -873,10 +869,6 @@ private function mergeLocallyIgnoredErrors(ResultCache $resultCache, array $fres { $errorsByFile = $resultCache->getLocallyIgnoredErrors(); foreach ($resultCache->getFilesToAnalyse() as $file) { - if (array_key_exists($file, $this->fileReplacements)) { - unset($errorsByFile[$file]); - $file = $this->fileReplacements[$file]; - } if (!array_key_exists($file, $freshLocallyIgnoredErrorsByFile)) { unset($errorsByFile[$file]); continue; @@ -895,10 +887,6 @@ private function mergeCollectedData(ResultCache $resultCache, array $freshCollec { $collectedDataByFile = $resultCache->getCollectedData(); foreach ($resultCache->getFilesToAnalyse() as $file) { - if (array_key_exists($file, $this->fileReplacements)) { - unset($collectedDataByFile[$file]); - $file = $this->fileReplacements[$file]; - } if (!array_key_exists($file, $freshCollectedDataByFile)) { unset($collectedDataByFile[$file]); continue; diff --git a/src/Analyser/ScopeContext.php b/src/Analyser/ScopeContext.php index fd23625611..c97cd8fdd7 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -10,21 +10,25 @@ final class ScopeContext private function __construct( private string $file, + private string $analysisFile, private ?ClassReflection $classReflection, private ?ClassReflection $traitReflection, ) { } - /** @api */ - public static function create(string $file): self + /** + * @api + * @param string|null $analysisFile the file whose content is actually analysed; differs from $file in editor mode (--tmp-file / --instead-of) + */ + public static function create(string $file, ?string $analysisFile = null): self { - return new self($file, classReflection: null, traitReflection: null); + return new self($file, $analysisFile ?? $file, classReflection: null, traitReflection: null); } public function beginFile(): self { - return new self($this->file, classReflection: null, traitReflection: null); + return new self($this->file, $this->analysisFile, classReflection: null, traitReflection: null); } public function enterClass(ClassReflection $classReflection): self @@ -35,7 +39,7 @@ public function enterClass(ClassReflection $classReflection): self if ($classReflection->isTrait()) { throw new ShouldNotHappenException(); } - return new self($this->file, $classReflection, traitReflection: null); + return new self($this->file, $this->analysisFile, $classReflection, traitReflection: null); } public function enterTrait(ClassReflection $traitReflection): self @@ -47,7 +51,7 @@ public function enterTrait(ClassReflection $traitReflection): self throw new ShouldNotHappenException(); } - return new self($this->file, $this->classReflection, $traitReflection); + return new self($this->file, $this->analysisFile, $this->classReflection, $traitReflection); } public function equals(self $otherContext): bool @@ -80,6 +84,16 @@ public function getFile(): string return $this->file; } + /** + * The file whose content is actually analysed. Equal to getFile() except in + * editor mode (--tmp-file / --instead-of), where getFile() is the real path + * while this is the temp file being read. + */ + public function getAnalysisFile(): string + { + return $this->analysisFile; + } + public function getClassReflection(): ?ClassReflection { return $this->classReflection; diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index dac67670a1..1b2e125095 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -103,6 +103,8 @@ public function runAnalyser( $postFileCallback, $debug, $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile), + $tmpFile, + $insteadOfFile, ); } diff --git a/src/Parallel/WorkerRunner.php b/src/Parallel/WorkerRunner.php index c570454e7f..4d158fc763 100644 --- a/src/Parallel/WorkerRunner.php +++ b/src/Parallel/WorkerRunner.php @@ -163,10 +163,12 @@ private function runWorker( $processedFiles = []; foreach ($files as $file) { try { + $reportedFile = null; if ($file === $insteadOfFile) { + $reportedFile = $insteadOfFile; $file = $tmpFile; } - $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); + $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null, $reportedFile); $fileErrors = $fileAnalyserResult->getErrors(); $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 68bee83974..12c9ad80b2 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -5,6 +5,7 @@ use Closure; use Nette\Utils\Strings; use PhpParser\Node; +use PHPStan\Analyser\EditorModeFileHelper; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; @@ -101,6 +102,7 @@ public function __construct( private FileHelper $fileHelper, private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, private AttributeReflectionFactory $attributeReflectionFactory, + private EditorModeFileHelper $editorModeFileHelper, #[AutowiredParameter(ref: '%universalObjectCratesClasses%')] private array $universalObjectCratesClasses, ) @@ -182,11 +184,11 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ } if (!$scope->isInTrait()) { - $scopeFile = $scope->getFile(); + $scopeFile = $this->editorModeFileHelper->getAnalysedFile($scope->getFile()); } else { $scopeFile = $scope->getTraitReflection()->getFileName(); if ($scopeFile === null) { - $scopeFile = $scope->getFile(); + $scopeFile = $this->editorModeFileHelper->getAnalysedFile($scope->getFile()); } } diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 2381ceb823..b258aa768e 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -5,6 +5,7 @@ use Closure; use PhpParser\Comment\Doc; use PhpParser\Node; +use PHPStan\Analyser\EditorModeFileHelper; use PHPStan\Analyser\IntermediaryNameScope; use PHPStan\Analyser\NameScope; use PHPStan\BetterReflection\Util\GetLastDocComment; @@ -82,6 +83,7 @@ public function __construct( private PhpDocNodeResolver $phpDocNodeResolver, private AnonymousClassNameHelper $anonymousClassNameHelper, private FileHelper $fileHelper, + private EditorModeFileHelper $editorModeFileHelper, private Cache $cache, private FileContentHasher $fileContentHasher, #[AutowiredParameter(ref: '%cache.resolvedPhpDocBlockCacheCountMax%')] @@ -110,7 +112,7 @@ public function getResolvedPhpDoc( } if ($fileName !== null) { - $fileName = $this->fileHelper->normalizePath($fileName); + $fileName = $this->editorModeFileHelper->getAnalysedFile($this->fileHelper->normalizePath($fileName)); } $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); @@ -201,6 +203,7 @@ public function getNameScope( ?string $functionName, ): NameScope { + $fileName = $this->editorModeFileHelper->getAnalysedFile($fileName); $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); if (isset($this->inProcess[$nameScopeKey])) { if (isset($this->inProcessNameScopes[$nameScopeKey])) { diff --git a/tests/PHPStan/Analyser/EditorModeGetFileTest.php b/tests/PHPStan/Analyser/EditorModeGetFileTest.php new file mode 100644 index 0000000000..6eaa653ce2 --- /dev/null +++ b/tests/PHPStan/Analyser/EditorModeGetFileTest.php @@ -0,0 +1,127 @@ +getFileHelper(); + $realFile = $fileHelper->normalizePath(__DIR__ . '/data/editor-mode-real.php'); + $tmpFile = $fileHelper->normalizePath(__DIR__ . '/data/editor-mode-tmp.php'); + + $errors = $this->analyse($tmpFile, [$realFile], $realFile); + + $this->assertCount(2, $errors); + // getFile() and getFileDescription() both report the real (--instead-of) path, + // not the temp file that is actually being read. + $this->assertSame($realFile, $errors[0]->getMessage()); + $this->assertSame($realFile, $errors[1]->getMessage()); + $this->assertSame($realFile, $errors[0]->getFilePath()); + } + + public function testScopeGetFileWithoutEditorMode(): void + { + $fileHelper = $this->getFileHelper(); + $realFile = $fileHelper->normalizePath(__DIR__ . '/data/editor-mode-real.php'); + + $errors = $this->analyse($realFile, [$realFile], null); + + $this->assertCount(2, $errors); + $this->assertSame($realFile, $errors[0]->getMessage()); + $this->assertSame($realFile, $errors[0]->getFilePath()); + } + + /** + * @param string[] $analysedFiles + * @return list + */ + private function analyse(string $file, array $analysedFiles, ?string $reportedFile): array + { + $fileAnalyser = $this->createFileAnalyser(); + $rule = new /** @implements Rule */ class implements Rule { + + public function getNodeType(): string + { + return Node\Stmt\Function_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message($scope->getFile()) + ->identifier('tests.editorModeGetFile') + ->build(), + RuleErrorBuilder::message($scope->getFileDescription()) + ->identifier('tests.editorModeGetFileDescription') + ->build(), + ]; + } + + }; + + $result = $fileAnalyser->analyseFile( + $file, + array_fill_keys($analysedFiles, true), + new DirectRuleRegistry([$rule]), + new CollectorRegistry([]), + null, + $reportedFile, + ); + + return $result->getErrors(); + } + + private function createFileAnalyser(): FileAnalyser + { + $reflectionProvider = self::createReflectionProvider(); + $fileHelper = $this->getFileHelper(); + $container = self::getContainer(); + $typeSpecifier = $container->getService('typeSpecifier'); + $fileTypeMapper = $container->getByType(FileTypeMapper::class); + $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); + $nodeScopeResolver->setAnalysedFiles([]); + + $lexer = new Lexer(); + + return new FileAnalyser( + self::createScopeFactory($reflectionProvider, $typeSpecifier), + $nodeScopeResolver, + new RichParser( + new Php7($lexer), + new NameResolver(), + $container, + new IgnoreLexer(), + ), + new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($reflectionProvider, $fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), + new PackageDependencyResolver([], $fileHelper), + new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), + $container->getByType(RuleErrorTransformer::class), + new LocalIgnoresProcessor(), + false, + ); + } + +} diff --git a/tests/PHPStan/Analyser/data/editor-mode-real.php b/tests/PHPStan/Analyser/data/editor-mode-real.php new file mode 100644 index 0000000000..b2faba410b --- /dev/null +++ b/tests/PHPStan/Analyser/data/editor-mode-real.php @@ -0,0 +1,8 @@ +