Do not report a private method overriding a private trait method as unused - #6191
Conversation
16238a7 to
c718210
Compare
|
do we have a similar problem for private constants ( |
We do. @Jean-Beru Could you do the same for those rules ? |
|
please rebase onto 2.3.x - this PR is hundreds of commits behind |
…nused A class member takes precedence over the member of the same name coming from a used trait, so a private method redeclared in the class is what the trait's own methods call. The rule sees those call sites only when the trait is part of the analysed files: analysing a project whose paths do not include its dependencies leaves ClassMethodsNode::getMethodCalls() with no call at all, and the class method is reported as unused. The canonical case is a Symfony application, where the framework recipe generates a Kernel redeclaring KernelTrait::getAllowedEnvs() while the trait lives in vendor/, outside of the analysed paths. Skip those methods. Nothing is left to distinguish an override that the trait calls from one it does not, so a redeclared private method the trait never calls is no longer reported either. Fixes phpstan/phpstan#12201 Assisted-by: Claude Code:claude-opus-5
…tant as unused PHP merges a class constant with the same-named constant of a used trait into a single slot, so the trait's own methods fetch the class' declaration. The rule sees those fetches only when the trait's body is traversed, which NodeScopeResolver does for analysed files only: analysing a project whose paths do not include its dependencies leaves ClassConstantsNode with no fetch at all, and the class constant is reported as unused. Skip those constants, but only when no ClassConst node was gathered from a trait body for that name. Such a node proves the trait was traversed and its fetches are visible, so a redeclaration whose trait lives inside the analysed paths keeps being judged on what the code actually does. The residual cost is a constant redeclared from a trait outside the analysed paths that the trait never fetches: it is no longer reported, and nothing is left to distinguish it from one the trait does fetch. Assisted-by: Claude Code:claude-opus-5
…erty as unused PHP merges a class property with the same-named property of a used trait into a single slot, so the trait's own methods read and write the class' declaration. The rule sees those usages only when the trait's body is traversed, which NodeScopeResolver does for analysed files only: analysing a project whose paths do not include its dependencies leaves ClassPropertiesNode with no usage at all, and the class property is reported as never read. Skip those properties, but only when no property node declared in a trait was gathered for that name. Such a node proves the trait was traversed and its usages are visible, so a redeclaration whose trait lives inside the analysed paths keeps being judged on what the code actually does. The residual cost is a property redeclared from a trait outside the analysed paths that the trait never reads: it is no longer reported, and nothing is left to distinguish it from one the trait does read. Assisted-by: Claude Code:claude-opus-5
| # only the PHP development headers are needed (php-config --includes); | ||
| # the extension is not built here | ||
| - name: "Install PHP" | ||
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | ||
|
|
||
| - name: "Install PHP" | ||
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 |
|
|
||
| # the differential tests compare the native results against the PHP | ||
| # twins, so the PHP side has to be installed | ||
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 |
| - name: "Compile phpstan_turbo with strict warnings" | ||
| # The training run of `make pgo` (bin/pgo-train.sh) runs bin/phpstan on | ||
| # the checkout, so the dependencies must be in place before the build. | ||
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 |
|
|
||
| - name: "Install PHP" | ||
| if: ${{ !matrix.container }} | ||
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 |
| MATRIX_TS: ${{ matrix.ts }} | ||
| run: bash .github/scripts/install-php86-windows.sh | ||
|
|
||
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 |
| # of ORIGIN_RUN_ID repeated in both). The tests of every binary, reused | ||
| # or compiled, run in the turbo-differential jobs, so the commit job, | ||
| # which needs the binaries, does not wait for them. | ||
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}', matrix.target.artifact, matrix.target.name, matrix.php-version)] && 'ubuntu-latest' || matrix.target.runs-on }} |
| # container via docker exec — native arm64, no QEMU. The steps mirror | ||
| # the musl leg of turbo-compile one for one, including the reuse of a | ||
| # binary compiled in an earlier run, whose leg runs on ubuntu-latest. | ||
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-linux-musl-arm64-php{0}', matrix.php-version)] && 'ubuntu-latest' || 'ubuntu-24.04-arm' }} |
| # PHP 8.6's vs18 development packs need the VS2026 toolchain. | ||
| runs-on: ${{ matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022' }} | ||
| # A leg reusing a binary (see turbo-compile) runs on ubuntu-latest. | ||
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-windows-x86_64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || (matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022') }} |
| if: env.ORIGIN_RUN_ID != '' | ||
| run: | | ||
| echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt | ||
| echo "DLL_PATH=turbo-ext/php_phpstan_turbo.dll" >> "$GITHUB_ENV" |
| if command -v cygpath > /dev/null; then | ||
| EXT="$(cygpath -w "$EXT")" | ||
| fi | ||
| echo "TURBO_DLL=$EXT" >> "$GITHUB_ENV" |
| needs: | ||
| - turbo-compile | ||
| - turbo-compile-musl-arm64 | ||
| runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} |
| path: phpstan-dist | ||
| token: ${{ secrets.PHPSTAN_BOT_TOKEN }} | ||
| ref: 2.2.x | ||
| ref: 2.3.x |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | ||
| with: | ||
| ref: 2.2.x | ||
| ref: 2.3.x |
| turbo-lint: | ||
| name: "Turbo Extension Lint" | ||
|
|
||
| runs-on: "ubuntu-latest" | ||
| timeout-minutes: 30 | ||
|
|
||
| steps: | ||
| - name: Harden the runner (Audit all outbound calls) | ||
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: "Checkout" | ||
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | ||
|
|
||
| # only the PHP development headers are needed (php-config --includes); | ||
| # the extension is not built here | ||
| - name: "Install PHP" | ||
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 | ||
| with: | ||
| coverage: "none" | ||
| php-version: "8.5" | ||
|
|
||
| # The pinned major version is read from the Makefile rather than | ||
| # repeated here, so CI and a local run gate on the same checks — the | ||
| # target refuses to run with any other version. Ubuntu ships an older | ||
| # clang-tidy than the pin, so it comes from apt.llvm.org. | ||
| - name: "Install the pinned clang-tidy" | ||
| run: | | ||
| CLANG_TIDY_VERSION="$(make -s -C turbo-ext print-clang-tidy-version)" | ||
| echo "pinned clang-tidy: $CLANG_TIDY_VERSION" | ||
| # only the codename: /etc/os-release also defines VERSION | ||
| CODENAME="$(. /etc/os-release && echo "$VERSION_CODENAME")" | ||
| wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null | ||
| echo "deb https://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-${CLANG_TIDY_VERSION} main" \ | ||
| | sudo tee "/etc/apt/sources.list.d/llvm-${CLANG_TIDY_VERSION}.list" | ||
| sudo apt-get update | ||
| sudo apt-get install -y "clang-tidy-${CLANG_TIDY_VERSION}" | ||
|
|
||
| - name: "Lint the native sources" | ||
| run: make lint-turbo |
| - name: "Checkout" | ||
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | ||
|
|
||
| # only the PHP development headers are needed (php-config --includes); | ||
| # the extension is not built here |
| turbo-sanitize: | ||
| name: "Turbo Extension Sanitizers" | ||
|
|
||
| runs-on: "ubuntu-latest" | ||
| timeout-minutes: 30 | ||
|
|
||
| steps: | ||
| - name: Harden the runner (Audit all outbound calls) | ||
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: "Checkout" | ||
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | ||
|
|
||
| - name: "Install PHP" | ||
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 | ||
| with: | ||
| coverage: "none" | ||
| php-version: "8.5" | ||
|
|
||
| # the differential tests compare the native results against the PHP | ||
| # twins, so the PHP side has to be installed | ||
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | ||
|
|
||
| - name: "Differential tests under UndefinedBehaviorSanitizer" | ||
| run: make sanitize-turbo |
| - name: "Checkout" | ||
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 |
| # of ORIGIN_RUN_ID repeated in both). The tests of every binary, reused | ||
| # or compiled, run in the turbo-differential jobs, so the commit job, | ||
| # which needs the binaries, does not wait for them. | ||
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}', matrix.target.artifact, matrix.target.name, matrix.php-version)] && 'ubuntu-latest' || matrix.target.runs-on }} |
| env: | ||
| PHP_MINOR: ${{ matrix.php-version }} | ||
| TURBO_ARTIFACT: "${{ matrix.target.artifact }}-${{ matrix.target.name }}-php${{ matrix.php-version }}" | ||
| ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}', matrix.target.artifact, matrix.target.name, matrix.php-version)] }} |
| # container via docker exec — native arm64, no QEMU. The steps mirror | ||
| # the musl leg of turbo-compile one for one, including the reuse of a | ||
| # binary compiled in an earlier run, whose leg runs on ubuntu-latest. | ||
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-linux-musl-arm64-php{0}', matrix.php-version)] && 'ubuntu-latest' || 'ubuntu-24.04-arm' }} |
| env: | ||
| PHP_MINOR: ${{ matrix.php-version }} | ||
| TURBO_ARTIFACT: "phpstan_turbo-linux-musl-arm64-php${{ matrix.php-version }}" | ||
| ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-linux-musl-arm64-php{0}', matrix.php-version)] }} |
| # PHP 8.6's vs18 development packs need the VS2026 toolchain. | ||
| runs-on: ${{ matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022' }} | ||
| # A leg reusing a binary (see turbo-compile) runs on ubuntu-latest. | ||
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-windows-x86_64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || (matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022') }} |
| # uses (ships bison and the unix tools phpize/configure expect). | ||
| PHP_SDK_COMMIT: "c73faaf1cce914e2fc04da5587132f8f425996af" # php-sdk-2.7.1 | ||
| TURBO_ARTIFACT: "phpstan_turbo-windows-x86_64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" | ||
| ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-windows-x86_64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] }} |
d9bc5da to
d171b76
Compare
Good catch. I added two commits to fix constants and properties. I also rebased on Should I have to add a warning somewhere to indicate that baselines has to be updated following this fix ? |
No, it's normal that people have to update the baseline/ignored-error when we fix a bug. |
| @@ -0,0 +1,42 @@ | |||
| <?php declare(strict_types = 1); | |||
|
|
|||
| namespace Bug12201Property; | |||
There was a problem hiding this comment.
please add a comment what this file is supposed to be and why itself is not analyzed
|
|
||
| declare(strict_types = 1); | ||
|
|
||
| namespace Bug12201Constant; |
There was a problem hiding this comment.
please add a comment what this file is supposed to be and why itself is not analyzed
| @@ -0,0 +1,27 @@ | |||
| <?php declare(strict_types = 1); | |||
|
|
|||
| namespace Bug12201; | |||
There was a problem hiding this comment.
please add a comment what this file is supposed to be and why itself is not analyzed
The three *-traits.php fixtures stand for a dependency living outside of the analysed paths, which is the whole point of the reproducers: PHPStan never traverses the trait bodies, so the trait's own calls, fetches and property usages stay invisible. Only the consumer files said so. Assisted-by: Claude Code:claude-opus-5
|
well done - thank you! |
Fixes phpstan/phpstan#12201
Why
A class member takes precedence over the member of the same name coming from a used trait. So a private method redeclared in the class is the one the trait's own methods call.
UnusedPrivateMethodRulesees those call sites only when the trait is part of the analysed files. In a project whosepathsexclude its dependencies,ClassMethodsNode::getMethodCalls()collects no call at all. The class method is then reported as unused.The canonical case is a Symfony application. The recipe generates a
Kernelthat redeclaresKernelTrait::getAllowedEnvs(), and the trait lives invendor/.Every new Symfony 8.1 project hits this from level 4 up.
ignoreErrorsis the only way out.What
A private method is skipped when a used trait declares a private method of the same name.
getTraits()is enough without recursion. PHP flattens trait composition.getAllowedEnvs()comes from the nestedKernelTrait, and the reflection ofMicroKernelTraitalready reports it.