Skip to content

Look up line ignores under the analysed file for trait errors reported in the context of a class - #6174

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-uy33ir4
Open

Look up line ignores under the analysed file for trait errors reported in the context of a class#6174
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-uy33ir4

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

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

  • src/Analyser/AnalyserResultFinalizer.php
    • Replaced $file = $tempCollectorError->getTraitFilePath() ?? $tempCollectorError->getFilePath()
      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.
  • tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php — five new tests plus a
    runAnalyseAndFinalize() helper (the existing runAnalyse() skips the finalizer, where the
    deferred errors are produced).
  • tests/PHPStan/Analyser/traits/ignore/* — new fixtures.
  • e2e/bug-14993/ + .github/workflows/e2e-tests.yml — end-to-end coverage including the
    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:

  • two classes using the trait in the same file and in separate files;
  • nested traits (trait A { use B; });
  • anonymous classes using the trait;
  • aliased trait methods (use T { check as checkSecond; });
  • every inline ignore form: @phpstan-ignore <identifier> (own line and trailing),
    @phpstan-ignore-line, @phpstan-ignore-next-line;
  • both rules that defer trait errors — FunctionCallConstantConditionRule
    (function.alreadyNarrowedType, function.impossibleType, and therefore the whole
    ImpossibleCheckType*/FunctionCallConstantConditionHelper family) and
    ConstantConditionInTraitRule (identical.alwaysFalse and the rest of the
    ConstantConditionInTraitHelper family);
  • empty and primed result cache.

Probed and found already correct (no change needed):

  • a trait used by a single class, where the errors deduplicate into the trait file via
    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;
  • baseline generation for in-context trait errors (entries are keyed by the using-class files);
  • collector errors outside traits (getTraitFilePath() === null), whose lookup is unchanged.

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:

array<analysedFilePath, array<fileDescription, array<line, identifiers|null>>>

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:

  • testIgnoreErrorsReportedInContextOfEachClassUsingTheTrait — the reported reproducer, extended
    to cover function.alreadyNarrowedType, function.impossibleType, identical.alwaysFalse, all
    four inline ignore syntaxes and an aliased trait method;
  • testIgnoreErrorsReportedInContextOfClassesLivingInSeparateFiles;
  • testIgnoreErrorsReportedInContextOfClassUsingNestedTrait;
  • testIgnoreErrorsReportedInContextOfAnonymousClassUsingTrait;
  • testIgnoreErrorsReportedDirectlyInTraitUsedByASingleClass (regression guard for the Fix file path of trait errors reported directly in the trait #5780
    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

…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.
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.

Traits: can't phpstan-ignore alreadyNarrowedType

1 participant