Skip to content

chore(lint): run PHPStan and Rector isolated from the machine's php.ini - #259

Merged
s2x merged 1 commit into
mainfrom
chore/issue-258-lint-php-n
Oct 3, 2026
Merged

s2x merged 1 commit into
mainfrom
chore/issue-258-lint-php-n

Conversation

@s2x

@s2x s2x commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Closes #258.

What changed

bin/lint.sh started PHPStan and Rector with the normal php, so every php.ini on the
machine was loaded into the process that does the analysis. This repository also builds a
PHP extension (php-ext/, the legacy zvec), and a contributor can have a copy of it
installed and enabled in php.ini — the same reason the test suite needs -n (#188).
What the tools see then depends on the machine instead of on the working tree. AGENTS.md
mentioned php -n for the tests only.

This changes what the lint gate runs. Both tools now run with PHPRC pointing at an
ini that is generated per run and holds nothing, so the machine's php.ini is not read.
No step, level or rule changed:

  • the phpstan and rector steps, and the rector call in the --fix branch, go
    through one php_analysis helper;
  • memory_limit=-1 replaces the built-in 128M, because the replaced php.ini is where
    the real limit usually is;
  • extension_dir is carried into the generated ini: on a Homebrew/pecl setup it is set in
    php.ini while the conf.d entries name their extension without a path, so without it every
    extension installed that way would fail to load (round 3 of the review caught exactly
    that, and the resulting xdebug warning);
  • FFI is added back only where the current PHP has none in that same environment, as
    extension=ffi in the generated ini — every one of those processes loads it, so it is not
    also passed as -d, or PHP would load it twice. phpstan.neon ignores undefined FFI
    methods and properties, not a missing FFI class, so without FFI the analysis would report
    errors on src/;
  • the CI lint job asks setup-php for FFI, so the analysis has it however the runner
    provides it;
  • PHP-CS-Fixer is untouched: it works on the token stream and never resolves the project's
    classes.

Why not php -n

php -n was the first attempt and it does not work here, for two separate reasons.

  1. It drops what the tools need. PHPStan needs the Phar class and Rector needs
    PhpToken from the tokenizer extension, which the ubuntu-24.04 setup-php image
    provides through conf.d inis. The first push of this branch failed lint with
    Class "Phar" not found and Class "PhpToken" not found — see the comment below.
  2. It does not reach the processes PHPStan starts for itself. PHPStan restarts with
    PHP_BINARY -d memory_limit=… -d phpstan.restarted=1 … and starts parallel workers;
    neither carries -n, so those processes loaded the machine's php.ini again. Verified
    with ps -A while php -n vendor/bin/phpstan analyse runs.

PHPRC is an environment variable and is inherited, so it replaces php.ini in the
restart and in the workers too. A probe file calling a method only the loaded extension
could know confirms it: PHPStan resolves the class without the isolation and does not
resolve it with it.

conf.d is still scanned, so an extension installed that way (tokenizer on CI, FFI,
xdebug) stays available — which is why the generated ini carries the machine's
extension_dir over: on a Homebrew/pecl setup that is itself set in php.ini. An extension=zvec.so line in php.ini — the #188 case of a
make install plus a manual line — no longer reaches the tools; one in conf.d still
would. That residual gap is stated in AGENTS.md.

What this does not claim

The extension does not currently shadow src/: php-ext/ registers its 15 classes in the
global namespace (ZVecSchema, ZVecDoc, …) while src/ is namespaced
CrazyGoat\ZVec\, so there is no name collision. The real shadowing in #188 is a runtime
one — src/ZVec.php:9 returns early when extension_loaded('zvec'), so the barrel file
never loads the FFI-only classes — and that is what breaks the test suite. PHPStan and
Rector parse src/ and never run that guard.

So this is not a fix for an observable wrong result today; it removes the machine
dependency that made the #246 finding (a Rector change in CI that the local run did not
report) possible, and it produces the same verdict either way on this machine. That is
also why no regression test comes with it: there is no assertion that would fail before
the change and pass after it.

findings.md records the rest, including that composer.lock is gitignored here, so the
local tool versions float and have to be ruled out before blaming an extension.

Verified

  • bin/lint.sh and bin/lint.sh --fix pass on PHP 8.5.10 with the legacy zvec extension
    loaded, and --fix changes no file.
  • Under the isolation: zvec not loaded and PHPStan does not resolve the extension's
    classes, while Phar, tokenizer, FFI and xdebug are available and memory_limit is
    -1; no startup warning is printed.
  • tests/test_docs_consistency.phpt passes with the CHANGELOG entry.
  • shellcheck and bash -n clean.
  • The full .phpt suite cannot run in a fresh worktree (it needs sdk/ and
    ffi/build/, built by bin/worktree-setup.sh); the change touches no src/, ffi/ or
    tests/ file.

@s2x
s2x force-pushed the chore/issue-258-lint-php-n branch from b5446f9 to 4ffd330 Compare October 3, 2026 08:01
@s2x

s2x commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

First push failed lint, and the log is the reason this PR no longer uses php -n:

==> phpstan
Fatal error: Uncaught Error: Class "PhpToken" not found in .../nikic/php-parser/lib/PhpParser/Internal/TokenPolyfill.php:7
==> rector
Fatal error: Uncaught Error: Class "PhpToken" not found in .../nikic/php-parser/lib/PhpParser/Internal/TokenPolyfill.php:7
Failed: phpstan rector

Both tools use PhpToken from the tokenizer extension, which the setup-php image provides through a conf.d ini, and -n drops every conf.d ini. So -n cannot be used in this repository's lint.

Fixed by replacing the machine's php.ini with PHPRC instead of dropping it: conf.d is still scanned (tokenizer, FFI, xdebug stay available), while an extension=zvec.so line in php.ini — the #188 case — no longer reaches the tools. PHPRC is also the only one of the two that reaches the processes PHPStan starts for itself, since it restarts with PHP_BINARY -d ... and drops -n.

@s2x
s2x force-pushed the chore/issue-258-lint-php-n branch from 4ffd330 to b623581 Compare October 3, 2026 08:11
bin/lint.sh started both tools with the normal `php`, so every php.ini on the
machine was loaded into the process that does the analysis. This repository also
builds a PHP extension (php-ext/, the legacy `zvec`), and a contributor can have a
copy of it installed and enabled in php.ini — the same reason the test suite needs
`php -n` (#188). What the tools see then depends on the machine instead of on the
working tree, which is how a Rector finding in #246 showed up in CI but not
locally. AGENTS.md mentioned `php -n` for the tests only.

The tools now run with `PHPRC` pointing at an ini that is generated per run, which
replaces the machine's php.ini for the process bin/lint.sh starts and for the ones
PHPStan starts for itself: `PHPRC` is an environment variable and is inherited,
while `-n` is not — PHPStan restarts with `PHP_BINARY -d ...` and starts parallel
workers, both of which load the machine's php.ini again (verified with `ps` while
it runs).

`php -n` on its own is not usable here: PHPStan needs the Phar class and Rector
needs `PhpToken` from the tokenizer extension, and on the CI image those come from
conf.d, which `-n` drops — the first push of this branch failed `lint` with
`Class "Phar" not found` and `Class "PhpToken" not found`. conf.d is still scanned
instead, so extensions installed that way stay available.

What the replaced php.ini took with it is put back explicitly: `extension_dir`
(on a Homebrew/pecl setup it is set in php.ini while the conf.d entries name their
extension without a path, so every one of them would otherwise fail to load),
`memory_limit=-1` instead of the built-in 128M, and FFI (`-d extension=ffi` plus
`extension=ffi` in the generated ini) only where the current PHP has none in that
same environment — phpstan.neon ignores undefined FFI methods and properties, not a
missing FFI class, so without FFI the analysis would report errors on src/. The CI
lint job now asks setup-php for FFI, so the analysis has it however the runner
provides it.

No step, level or rule changed, and PHP-CS-Fixer is untouched: it works on the token
stream and never resolves the project's classes. Verified locally with the legacy
zvec extension loaded: bin/lint.sh and bin/lint.sh --fix pass without a warning,
the analysis no longer resolves the extension's classes, and Phar, tokenizer, FFI
and xdebug are all still available. tests/test_docs_consistency.phpt passes with the
CHANGELOG entry. shellcheck clean.
@s2x
s2x force-pushed the chore/issue-258-lint-php-n branch from b623581 to 013d561 Compare October 3, 2026 08:17
@s2x

s2x commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

For the record: the first full run of this branch had ffi - linux-aarch64 fail in tests/test_diskann_index.phpt (a search returned doc2,doc3 instead of doc1,doc2,doc3), which made ci-ok red. That job builds the FFI adapter and runs .github/scripts/run-tests.sh — neither is touched by this diff, and the lint job (the only job this PR changes) was green in the same run.

The same class of failure happened on main on 2026-10-02 (run 37012807599, bug_0057.phpt on ffi - linux-musl-aarch64, also a DiskANN query result), and #257 tracks a one-off aarch64 test failure that passed on a re-run. Re-ran the failed job: green.

@s2x
s2x merged commit b9c79d2 into main Oct 3, 2026
18 of 20 checks passed
@s2x
s2x deleted the chore/issue-258-lint-php-n branch October 3, 2026 08:22
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.

chore(lint): run PHPStan and Rector with php -n so a locally installed zvec extension cannot change the lint result

1 participant