…d in the context of a class
- `AnalyserResultFinalizer` matched deferred (`CollectedDataNode`) errors against
`$allLinesToIgnore[$error->getTraitFilePath() ?? $error->getFilePath()]`. That outer key is
the *analysed* file, so for an error reported as `Trait.php (in context of class Foo)` the
ignores of the class file were never consulted and `@phpstan-ignore` had no effect.
- New `AnalyserResultFinalizer::resolveAnalysedFileWithLineIgnores()` picks the entry that
actually contains the error's file description: the using-class file for in-context errors,
the trait file when the context was removed by `Error::removeTraitContext()`.
- Fixes all identifiers deferred through `FunctionCallConstantConditionRule` and
`ConstantConditionInTraitRule` (`function.alreadyNarrowedType`, `function.impossibleType`,
`identical.alwaysFalse`, ...) and all inline ignore forms (`@phpstan-ignore <identifier>`,
`@phpstan-ignore-line`, `@phpstan-ignore-next-line`).
- Probed and also fixed by the same change: classes in separate files, nested traits,
anonymous classes, aliased trait methods (`use T { m as n; }`), primed result cache.
- Probed and found already correct: a trait used by a single class (`removeTraitContext()`,
the case fixed by phpstan#5780 - kept as a regression test), baseline generation for in-context
trait errors, and non-trait collector errors.
Summary
An inline @phpstan-ignore inside a trait had no effect on errors that PHPStan reports once per
using class (Trait.php (in context of class Foo)) when the trait and the classes live in
separate files. The reported case was function.alreadyNarrowedType from
is_subclass_of(self::class, ...) in a trait used by two classes; the same snippet in the
playground (single file) was correctly suppressed, which is why it looked like it should work.
The fix makes AnalyserResultFinalizer look up the line ignores under the file whose analysis
actually recorded them.
Changes
with a new private resolveAnalysedFileWithLineIgnores() helper that picks the outer key whose
entry actually contains the error's file description, trying the analysed (using-class) file
first and the trait file second.
runAnalyseAndFinalize() helper (the existing runAnalyse() skips the finalizer, where the
deferred errors are produced).
result cache and baseline generation, mirroring the existing e2e/bug-14718 block.
Analogous cases probed, all covered by the same fix and by a test each:
@phpstan-ignore-line, @phpstan-ignore-next-line;
(function.alreadyNarrowedType, function.impossibleType, and therefore the whole
ImpossibleCheckType*/FunctionCallConstantConditionHelper family) and
ConstantConditionInTraitRule (identical.alwaysFalse and the rest of the
ConstantConditionInTraitHelper family);
Probed and found already correct (no change needed):
Error::removeTraitContext() — this is the case fixed by Fix file path of trait errors reported directly in the trait #5780 on the very same line; kept as
testIgnoreErrorsReportedDirectlyInTraitUsedByASingleClass so the fallback stays covered;
Probed, broken, but not fixed here (different root cause, noted for a follow-up): the
*WithoutImpurePoints dead-code rules (CallToFunctionStatementWithoutImpurePointsRule and its
method/static-method/constructor siblings) build their errors from the collected-data key with
RuleErrorBuilder::file(), so a call inside a trait is reported at the using class file with
the trait's line number, and once per using class. Those errors never carry trait context at
all, so they cannot be reached by a lookup fix — repairing them requires the collectors to keep
the trait context and the rules to deduplicate per trait the way
ConstantConditionInTraitRule does.
Root cause
AnalyserResult::getLinesToIgnore() is a two-level map:
The outer key is the file that was passed to FileAnalyser (Analyser::analyse() /
WorkerRunner store it as $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore()).
The inner key is what Error::getFile() returns — the plain path for normal errors and
"Trait.php (in context of class Foo)" for errors found while analysing trait statements in a
class context, which FileAnalyserCallback records when it sees an InTraitNode.
When a class uses a trait from another file, the trait's ignore lines are therefore stored under
the class file's entry, keyed by the trait's file description. The finalizer, however, keyed
the lookup by the error's trait file path, landing on the entry produced by analysing the trait
file on its own — which only ever contains the trait file's plain path as an inner key. The
identifier lookup in LocalIgnoresProcessor then never found the ignore.
That getTraitFilePath() ?? getFilePath() expression was introduced by #5780 to fix the opposite
half of the problem: errors that deduplicate into the trait file via removeTraitContext(), whose
getFilePath() is the class file but whose getFile() is the trait path. Instead of choosing one
of the two keys up front, the finalizer now picks the one that actually holds the error's file
description, so both halves work.
This only affects errors deferred to CollectedDataNode rules; errors reported directly from a
rule are locally ignored inside FileAnalyser, which passes the complete per-file map and was
never affected.
Test
AnalyserTraitsIntegrationTest, using the new runAnalyseAndFinalize() helper:
to cover function.alreadyNarrowedType, function.impossibleType, identical.alwaysFalse, all
four inline ignore syntaxes and an aliased trait method;
behaviour that the fix must keep).
Four of the five fail before the fix (the single-class one passes both before and after, as
intended) and all five pass after it. e2e/bug-14993 additionally checks the empty and primed
result cache, the unignored output, and baseline generation.
make tests, make phpstan and make cs-fix are green.
Fixes phpstan/phpstan#14993