Skip to content

Wrap NameResolver errors into ParserErrorsException so they are attributed to the file they come from - #6455

Merged
ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-g0rsaeq
Sep 17, 2026
Merged

ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-g0rsaeq

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

When a file that is only scanned (or analysed separately) contained a use alias clashing with an earlier one, php-parser's NameResolver threw a raw PhpParser\Error while PHPStan was resolving a symbol declared in that file. The exception escaped the source locators and was finally caught by FileAnalyser::analyseFile(), which assumed it came from parsing the current file — so the error was reported with the wrong file, a line number from the other file, no pointer to the real culprit, and the innocent file's analysis was aborted.

Now such a file is treated exactly like a file with a syntax error: the symbol is simply not found, and the file that declares it reports the error on its own line.

Changes

  • src/Parser/RichParser.php, src/Parser/SimpleParser.php, src/Parser/StubParser.php: the name-resolving NodeTraverser::traverse() call is wrapped in a try/catch (PhpParser\Error) that rethrows as ParserErrorsException. All three parsers declare @throws ParserErrorsException through the PHPStan\Parser\Parser interface, so this restores the documented contract.
  • No change was needed in FileNodesFetcher, ExportedNodeFetcher or FileAnalyser — they already handle ParserErrorsException, and FileAnalyser already attributes it to ParserErrorsException::getParsedFile().

Analogous cases probed and covered by the same fix:

  • The other NameContext error: '\self' is an invalid class name (raised by getResolvedName() for qualified self/parent/static) had exactly the same misattribution; it is fixed and tested.
  • The other use kinds: use function, use const and group uses all go through NameContext::addAlias(); covered by the new parser test cases.
  • The other ways a broken file is reached: besides class instantiation (OptimizedDirectorySourceLocator / OptimizedSingleFileSourceLocator / AutoloadSourceLocator → FileNodesFetcher), trait resolution parses the trait's file too (MutatingScope::parseFile(), TraitUseHandler, FileAnalyserCallback). Both are tested.
  • The other parsers on the same axis: SimpleParser and StubParser share the NameResolver traversal with RichParser and got the same fix, even though only SimpleParser is on the reported code path (the reflection parser is routed through CleaningParser → SimpleParser).
  • ExportedNodeFetcher (result cache / dependency export) has the same catch (ParserErrorsException) shape as FileNodesFetcher and would previously have let the raw error escape; it is fixed by the same change.

Root cause

PHPStan\Parser\Parser::parseFile()/parseString() promise to signal parse failures with ParserErrorsException, which carries the file the failure came from. Syntax errors honoured that promise because ParserRunner::parse() is given a Collecting error handler. Name-resolution errors did not: NameResolver is constructed without an error handler, so NameContext uses ErrorHandler\Throwing and a raw PhpParser\Error — with no file attached — propagated out of parseString().

Every caller that parses another file (FileNodesFetcher, ExportedNodeFetcher, FileTypeMapper, PhpClassReflectionExtension, StubPhpDocProvider, MutatingScope::parseFile(), TraitUseHandler, FileAnalyserCallback) only guards against ParserErrorsException, so the raw error travelled all the way to FileAnalyser's catch (\PhpParser\Error $e) handler, which reports it against the file currently being analysed. Converting the error at the single place it originates fixes all of those call sites at once.

One user-visible consequence: when the offending file is itself analysed, the message now reads Cannot use Ns\Foo2 as Foo because the name is already in use on line 6, i.e. it gains the on line N suffix that PhpParser\Error::getMessage() adds and that syntax errors reported through ParserErrorsException already had. These errors are non-ignorable, so they cannot appear in baselines.

Test

  • AnalyserIntegrationTest::testBug15252() — analyses data/bug-15252.php, which instantiates Bug15252\Helper (declared in data/bug-15252-helper.php with a clashing use alias) and Bug15252\InvalidName (declared in data/bug-15252-invalid-name.php with new \self()). Both now produce Instantiated class ... not found. / Call to method name() on an unknown class ... instead of a foreign parse error.
  • AnalyserIntegrationTest::testBug15252Trait() — data/bug-15252-trait-user.php uses a trait declared in a file with a clashing alias and now gets Class Bug15252\TraitUser uses unknown trait Bug15252\HelperTrait.
  • AnalyserIntegrationTest::testBug15252AnalysedFileWithDuplicateAlias() and testBug15252AnalysedFileWithInvalidClassName() — the offending files, when analysed themselves, still report their own error on their own line.
  • RichParserTest::testNameResolutionErrors() — asserts RichParser, SimpleParser and StubParser all throw ParserErrorsException (with the right raw message and start line) for duplicate class/function/const/group-use aliases and for '\self'.
  • AnalyserIntegrationTest::testBug10358() — updated for the on line 6 message suffix.

All of the above fail without the source change.

Fixes phpstan/phpstan#15252

…ttributed to the file they come from

* `RichParser`, `SimpleParser` and `StubParser` now catch `PhpParser\Error` thrown
  from the name-resolving `NodeTraverser` run and rethrow it as
  `ParserErrorsException`, honouring the `@throws` contract of `PHPStan\Parser\Parser`.
* `parseFile()` then attaches the parsed file name, so `FileNodesFetcher` and
  `ExportedNodeFetcher` degrade gracefully (symbol treated as not found) instead of
  letting the exception escape into the file that merely looks the symbol up.
* Covers both errors `PhpParser\NameContext` can raise: the duplicate `use` alias
  ("Cannot use X as Y because the name is already in use", for `use`, `use function`,
  `use const` and group uses) and "'\self' is an invalid class name".
* Also covers the analogous reach-another-file paths: class instantiation through the
  source locators, and trait resolution (`Class X uses unknown trait Y.`).
* Parse errors of this kind reported for the analysed file itself now include the
  " on line N" suffix, matching how syntax errors collected by php-parser are reported.
@ondrejmirtes
ondrejmirtes merged commit 6354ce7 into phpstan:2.2.x Sep 17, 2026
859 of 890 checks passed
@ondrejmirtes
ondrejmirtes deleted the create-pull-request/patch-g0rsaeq branch September 17, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants