From c1c14d73a2288770d12463cf61f2aee3b884b3a6 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 13 May 2026 13:13:08 +0200 Subject: [PATCH 1/9] Drop loose-comparison rule; add PHPStan + Psalm fixture guardrail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loose-comparison fixture exercised a rule both PHPStan (strict-rules) and Psalm already catch. Removing the duplication: SlevomatCodingStandard .Operators.DisallowEqualOperators is pinned to severity=0 and strict_comparison is set to false, so a future preset switch can't silently bring it back. PHPStan (level max + strict-rules + phpunit) and Psalm (errorLevel 1 + strictBinaryOperands) now run on tests/fixtures/ in CI. If a fixture demonstrates a pattern either tool already flags, the build fails — which is the signal that the rule belongs in static analysis, not in the coding standard. Configs live in tests/ because today only fixtures are analyzed; if sniff/fixer sources are ever analyzed, separate root configs can be added. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/checks.yml | 8 +++++ Eventjet/ruleset.xml | 8 +++-- composer.json | 6 +++- php-cs-fixer-rules.php | 5 ++- tests/fixtures/invalid/loose-comparison.php | 7 ---- tests/fixtures/valid/mixed-assignment.php | 2 +- tests/fixtures/valid/no-array-typehint.php | 3 ++ tests/fixtures/valid/psr-12-import-order.php | 4 +-- tests/phpstan.neon | 24 ++++++++++++++ tests/psalm.xml | 34 ++++++++++++++++++++ 10 files changed, 87 insertions(+), 14 deletions(-) delete mode 100644 tests/fixtures/invalid/loose-comparison.php create mode 100644 tests/phpstan.neon create mode 100644 tests/psalm.xml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ed27975..3dd278d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -42,3 +42,11 @@ jobs: - name: Tests run: | vendor/bin/phpunit + + - name: PHPStan (fixtures) + run: | + vendor/bin/phpstan analyse --configuration tests/phpstan.neon --no-progress + + - name: Psalm (fixtures) + run: | + vendor/bin/psalm --config tests/psalm.xml --no-progress diff --git a/Eventjet/ruleset.xml b/Eventjet/ruleset.xml index d8607a3..920d2f9 100644 --- a/Eventjet/ruleset.xml +++ b/Eventjet/ruleset.xml @@ -297,6 +297,10 @@ - - + + + 0 + diff --git a/composer.json b/composer.json index 499277d..e57d3ec 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,11 @@ "webimpress/coding-standard": "^1.1" }, "require-dev": { - "phpunit/phpunit": "^10.2" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^6.16" }, "autoload": { "psr-4": { diff --git a/php-cs-fixer-rules.php b/php-cs-fixer-rules.php index 9bb9dc2..f89b42b 100644 --- a/php-cs-fixer-rules.php +++ b/php-cs-fixer-rules.php @@ -77,7 +77,10 @@ 'php_unit_data_provider_static' => true, 'single_quote' => true, 'single_space_around_construct' => true, - 'strict_comparison' => true, + // Loose comparison is lint territory — PHPStan and Psalm both flag it. Kept + // explicitly off so a broader preset (e.g. @PhpCsFixer, @Symfony) can't + // re-enable it silently. + 'strict_comparison' => false, 'trailing_comma_in_multiline' => [ 'elements' => [ 'arrays', diff --git a/tests/fixtures/invalid/loose-comparison.php b/tests/fixtures/invalid/loose-comparison.php deleted file mode 100644 index f2ca38d..0000000 --- a/tests/fixtures/invalid/loose-comparison.php +++ /dev/null @@ -1,7 +0,0 @@ - $items + */ function foo(array $items): void { foreach ($items as $item) { diff --git a/tests/fixtures/valid/psr-12-import-order.php b/tests/fixtures/valid/psr-12-import-order.php index 5f96381..5370545 100644 --- a/tests/fixtures/valid/psr-12-import-order.php +++ b/tests/fixtures/valid/psr-12-import-order.php @@ -14,5 +14,5 @@ use const E_USER_DEPRECATED; -new stdClass(); -trigger_error('Test', E_USER_DEPRECATED); +$obj = new stdClass(); +trigger_error('Test ' . $obj::class, E_USER_DEPRECATED); diff --git a/tests/phpstan.neon b/tests/phpstan.neon new file mode 100644 index 0000000..f52aae4 --- /dev/null +++ b/tests/phpstan.neon @@ -0,0 +1,24 @@ +# PHPStan config for the test fixtures. +# +# This project is a coding-standard package, not an application. We don't lint +# our sniff/fixer sources with PHPStan (yet). PHPStan runs over the fixtures so +# that if someone adds a fixture demonstrating something PHPStan already catches, +# CI fails — that's the signal "this is lint territory, don't add a CS rule for it". +# +# If we ever want PHPStan to check the production sources too, add a separate +# config at the repository root. + +parameters: + level: max + paths: + - fixtures + # Fixtures are isolated code snippets that routinely declare consts purely + # to exercise a syntactic pattern, with no consumer. classConstant.unused + # fires on that shape and is not a lint bug we want to catch in fixtures. + ignoreErrors: + - identifier: classConstant.unused + +includes: + - ../vendor/phpstan/phpstan-phpunit/extension.neon + - ../vendor/phpstan/phpstan-phpunit/rules.neon + - ../vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/tests/psalm.xml b/tests/psalm.xml new file mode 100644 index 0000000..4b347bd --- /dev/null +++ b/tests/psalm.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + From 2a6c0dd61d94dd53b66bf45d003a73aa785bace6 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 13 May 2026 13:28:34 +0200 Subject: [PATCH 2/9] Enforce SA overlap via per-fixture AND intersection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the two separate PHPStan and Psalm CI steps (which gave OR semantics — a fixture failed CI when either tool flagged it) with a single gate that fails only when both tools flag the same file. The previous setup would have wrongly dropped fixtures that only one SA catches. AND is the conservative policy: keep a CS rule unless both major analyzers already catch the pattern. A canary fixture (tests/sa-canary/loose-comparison.php) must always be in the intersection. If it isn't, the SA configs drifted, a tool crashed, or the JSON schema changed; CI fails loudly rather than silently passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/checks.yml | 8 +--- tests/check-sa-overlap.sh | 59 ++++++++++++++++++++++++++++ tests/phpstan.neon | 3 ++ tests/psalm.xml | 3 ++ tests/sa-canary/loose-comparison.php | 11 ++++++ 5 files changed, 78 insertions(+), 6 deletions(-) create mode 100755 tests/check-sa-overlap.sh create mode 100644 tests/sa-canary/loose-comparison.php diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3dd278d..fca249b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -43,10 +43,6 @@ jobs: run: | vendor/bin/phpunit - - name: PHPStan (fixtures) + - name: SA-overlap gate (fixtures flagged by both PHPStan and Psalm) run: | - vendor/bin/phpstan analyse --configuration tests/phpstan.neon --no-progress - - - name: Psalm (fixtures) - run: | - vendor/bin/psalm --config tests/psalm.xml --no-progress + tests/check-sa-overlap.sh diff --git a/tests/check-sa-overlap.sh b/tests/check-sa-overlap.sh new file mode 100755 index 0000000..481ce41 --- /dev/null +++ b/tests/check-sa-overlap.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# CI gate: fail if any fixture is flagged by BOTH PHPStan and Psalm. +# +# Fixtures flagged by both major static analyzers are "lint territory" — +# patterns the analyzers already catch — so we must not add a CS rule for +# that pattern. This script implements that AND policy at the file level by +# intersecting the two tools' JSON output. +# +# The canary fixture (tests/sa-canary/loose-comparison.php) MUST always be +# in the intersection. If it isn't, the SA configs drifted, a tool crashed, +# or the JSON schema changed; fail loudly rather than silently passing every +# fixture. +set -uo pipefail + +cd "$(dirname "$0")/.." + +phpstan_json="$(vendor/bin/phpstan analyse \ + --configuration tests/phpstan.neon \ + --error-format=json \ + --no-progress 2>/dev/null || true)" + +psalm_json="$(vendor/bin/psalm \ + --config tests/psalm.xml \ + --output-format=json \ + --no-progress 2>/dev/null || true)" + +phpstan_files="$(printf '%s' "$phpstan_json" | jq -r '.files? | keys[]?' | sort -u)" +psalm_files="$(printf '%s' "$psalm_json" | jq -r '.[]?.file_path // empty' | sort -u)" + +intersection="$(comm -12 <(printf '%s\n' "$phpstan_files") <(printf '%s\n' "$psalm_files") | sed '/^$/d')" + +canary="$(realpath tests/sa-canary/loose-comparison.php)" + +if ! printf '%s\n' "$intersection" | grep -qxF "$canary"; then + echo "ERROR: canary fixture not in PHPStan ∩ Psalm." + echo "Expected: $canary" + echo + echo "PHPStan flagged:" + printf '%s\n' "${phpstan_files:-(nothing)}" + echo + echo "Psalm flagged:" + printf '%s\n' "${psalm_files:-(nothing)}" + echo + echo "SA configs may have drifted too lax, a tool may have crashed, or the JSON schema changed." + exit 1 +fi + +other="$(printf '%s\n' "$intersection" | grep -vxF "$canary" || true)" +if [ -n "$other" ]; then + echo "ERROR: the following fixtures are flagged by BOTH PHPStan and Psalm:" + printf '%s\n' "$other" | sed 's/^/ - /' + echo + echo "Both major static analyzers already catch these patterns. A CS rule for" + echo "them duplicates lint work users already have. Drop the fixture (and any" + echo "corresponding CS rule), or rewrite it so only one SA catches it." + exit 1 +fi + +echo "OK: canary fired; no other fixtures flagged by both PHPStan and Psalm." diff --git a/tests/phpstan.neon b/tests/phpstan.neon index f52aae4..8e5eb60 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -12,6 +12,9 @@ parameters: level: max paths: - fixtures + # The canary fixture must always be flagged by both PHPStan and Psalm — + # see tests/check-sa-overlap.sh. + - sa-canary # Fixtures are isolated code snippets that routinely declare consts purely # to exercise a syntactic pattern, with no consumer. classConstant.unused # fires on that shape and is not a lint bug we want to catch in fixtures. diff --git a/tests/psalm.xml b/tests/psalm.xml index 4b347bd..301328b 100644 --- a/tests/psalm.xml +++ b/tests/psalm.xml @@ -22,6 +22,9 @@ > + + diff --git a/tests/sa-canary/loose-comparison.php b/tests/sa-canary/loose-comparison.php new file mode 100644 index 0000000..d65c7b3 --- /dev/null +++ b/tests/sa-canary/loose-comparison.php @@ -0,0 +1,11 @@ + Date: Wed, 13 May 2026 13:32:27 +0200 Subject: [PATCH 3/9] Make canary fixtures dual-purpose (CS + SA verification) Move tests/sa-canary/loose-comparison.php into tests/fixtures/valid/ so RulesTest::valid asserts the CS side: phpcs and php-cs-fixer must not flag the file. If a preset bump silently re-enables strict_comparison or DisallowEqualOperators, that test fails. Previously we only checked the SA side (both lint tools flag it). Drop the now-unneeded tests/sa-canary/ directory from the SA configs and generalize the gate script's hardcoded canary path into an EXPECTED_REL array, so future rule drops follow the same one-fixture-per-rule pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/check-sa-overlap.sh | 47 +++++++++++++++-------- tests/fixtures/valid/loose-comparison.php | 11 ++++++ tests/phpstan.neon | 3 -- tests/psalm.xml | 3 -- tests/sa-canary/loose-comparison.php | 11 ------ 5 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 tests/fixtures/valid/loose-comparison.php delete mode 100644 tests/sa-canary/loose-comparison.php diff --git a/tests/check-sa-overlap.sh b/tests/check-sa-overlap.sh index 481ce41..73002cf 100755 --- a/tests/check-sa-overlap.sh +++ b/tests/check-sa-overlap.sh @@ -1,19 +1,29 @@ #!/usr/bin/env bash -# CI gate: fail if any fixture is flagged by BOTH PHPStan and Psalm. +# CI gate: assert that PHPStan ∩ Psalm (over fixtures) equals exactly the set +# of fixtures registered below as "dropped-rule canaries". # -# Fixtures flagged by both major static analyzers are "lint territory" — -# patterns the analyzers already catch — so we must not add a CS rule for -# that pattern. This script implements that AND policy at the file level by -# intersecting the two tools' JSON output. +# Each entry in EXPECTED_REL is a fixture demonstrating a pattern whose CS +# rule we dropped because lint already catches it. Two invariants: +# 1. Each canary MUST be flagged by both PHPStan and Psalm. Missing entries +# mean SA configs drifted, a tool crashed, or the JSON schema changed — +# fail loudly rather than silently letting fixtures through. +# 2. NO other fixture may appear in the intersection. A new fixture flagged +# by both SAs is "lint territory" — drop or rewrite it instead of adding +# a CS rule. # -# The canary fixture (tests/sa-canary/loose-comparison.php) MUST always be -# in the intersection. If it isn't, the SA configs drifted, a tool crashed, -# or the JSON schema changed; fail loudly rather than silently passing every -# fixture. +# Each canary also lives in tests/fixtures/valid/ so RulesTest::valid asserts +# the CS side: phpcs and php-cs-fixer must NOT flag it (= the CS rule really +# is disabled). set -uo pipefail cd "$(dirname "$0")/.." +EXPECTED_REL=( + tests/fixtures/valid/loose-comparison.php +) + +expected="$(for f in "${EXPECTED_REL[@]}"; do realpath "$f"; done | sort -u)" + phpstan_json="$(vendor/bin/phpstan analyse \ --configuration tests/phpstan.neon \ --error-format=json \ @@ -29,11 +39,12 @@ psalm_files="$(printf '%s' "$psalm_json" | jq -r '.[]?.file_path // empty' | sor intersection="$(comm -12 <(printf '%s\n' "$phpstan_files") <(printf '%s\n' "$psalm_files") | sed '/^$/d')" -canary="$(realpath tests/sa-canary/loose-comparison.php)" +missing="$(comm -23 <(printf '%s\n' "$expected") <(printf '%s\n' "$intersection"))" +extra="$(comm -13 <(printf '%s\n' "$expected") <(printf '%s\n' "$intersection"))" -if ! printf '%s\n' "$intersection" | grep -qxF "$canary"; then - echo "ERROR: canary fixture not in PHPStan ∩ Psalm." - echo "Expected: $canary" +if [ -n "$missing" ]; then + echo "ERROR: expected canary fixtures missing from PHPStan ∩ Psalm:" + printf '%s\n' "$missing" | sed 's/^/ - /' echo echo "PHPStan flagged:" printf '%s\n' "${phpstan_files:-(nothing)}" @@ -45,15 +56,17 @@ if ! printf '%s\n' "$intersection" | grep -qxF "$canary"; then exit 1 fi -other="$(printf '%s\n' "$intersection" | grep -vxF "$canary" || true)" -if [ -n "$other" ]; then +if [ -n "$extra" ]; then echo "ERROR: the following fixtures are flagged by BOTH PHPStan and Psalm:" - printf '%s\n' "$other" | sed 's/^/ - /' + printf '%s\n' "$extra" | sed 's/^/ - /' echo echo "Both major static analyzers already catch these patterns. A CS rule for" echo "them duplicates lint work users already have. Drop the fixture (and any" echo "corresponding CS rule), or rewrite it so only one SA catches it." + echo + echo "If this is a deliberate rule drop, also register the fixture in" + echo "EXPECTED_REL in this script." exit 1 fi -echo "OK: canary fired; no other fixtures flagged by both PHPStan and Psalm." +echo "OK: PHPStan ∩ Psalm matches the registered canary set exactly." diff --git a/tests/fixtures/valid/loose-comparison.php b/tests/fixtures/valid/loose-comparison.php new file mode 100644 index 0000000..aab67db --- /dev/null +++ b/tests/fixtures/valid/loose-comparison.php @@ -0,0 +1,11 @@ + - - diff --git a/tests/sa-canary/loose-comparison.php b/tests/sa-canary/loose-comparison.php deleted file mode 100644 index d65c7b3..0000000 --- a/tests/sa-canary/loose-comparison.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Wed, 13 May 2026 13:43:24 +0200 Subject: [PATCH 4/9] Capture SA tool stderr for canary-missing diagnostics CI failed on PHP 8.1 / --prefer-lowest with Psalm flagging nothing while PHPStan flagged the canary correctly. Stderr was suppressed, so we had no signal on why Psalm produced no output. Capture both tools' stderr and include it in the canary-missing error message so the next run tells us. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/check-sa-overlap.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/check-sa-overlap.sh b/tests/check-sa-overlap.sh index 73002cf..a671fcb 100755 --- a/tests/check-sa-overlap.sh +++ b/tests/check-sa-overlap.sh @@ -24,15 +24,19 @@ EXPECTED_REL=( expected="$(for f in "${EXPECTED_REL[@]}"; do realpath "$f"; done | sort -u)" +phpstan_stderr="$(mktemp)" +psalm_stderr="$(mktemp)" +trap 'rm -f "$phpstan_stderr" "$psalm_stderr"' EXIT + phpstan_json="$(vendor/bin/phpstan analyse \ --configuration tests/phpstan.neon \ --error-format=json \ - --no-progress 2>/dev/null || true)" + --no-progress 2>"$phpstan_stderr" || true)" psalm_json="$(vendor/bin/psalm \ --config tests/psalm.xml \ --output-format=json \ - --no-progress 2>/dev/null || true)" + --no-progress 2>"$psalm_stderr" || true)" phpstan_files="$(printf '%s' "$phpstan_json" | jq -r '.files? | keys[]?' | sort -u)" psalm_files="$(printf '%s' "$psalm_json" | jq -r '.[]?.file_path // empty' | sort -u)" @@ -52,6 +56,12 @@ if [ -n "$missing" ]; then echo "Psalm flagged:" printf '%s\n' "${psalm_files:-(nothing)}" echo + echo "PHPStan stderr:" + cat "$phpstan_stderr" 2>/dev/null || echo "(empty)" + echo + echo "Psalm stderr:" + cat "$psalm_stderr" 2>/dev/null || echo "(empty)" + echo echo "SA configs may have drifted too lax, a tool may have crashed, or the JSON schema changed." exit 1 fi From 45e73832847725a27c462864f2d0ab7b1fd5f872 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 13 May 2026 13:56:56 +0200 Subject: [PATCH 5/9] Restrict SA-overlap gate to one matrix entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SA-overlap gate asks whether PHPStan ∩ Psalm flags the canary fixture. The answer is matrix-independent, so running it on all 8 (php × composer) combinations adds no signal — and on PHP 8.4 + --prefer-lowest the oldest Psalm transitive deps (Symfony Console, Amp DNS, …) emit a flood of "Implicitly marking parameter as nullable is deprecated" warnings that corrupt the JSON output, breaking the gate. Run it once on the canonical config instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/checks.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index fca249b..e1abeef 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -43,6 +43,12 @@ jobs: run: | vendor/bin/phpunit + # The SA-overlap gate is matrix-independent — it asks whether PHPStan and + # Psalm both flag a fixture, which doesn't change across PHP versions or + # composer flags. Run it once on the canonical (latest stable + current + # deps) config. On PHP 8.4 + --prefer-lowest, ancient Psalm transitive + # deps emit deprecation warnings that corrupt the JSON output anyway. - name: SA-overlap gate (fixtures flagged by both PHPStan and Psalm) + if: matrix.php == '8.4' && matrix.composer_flags == '' run: | tests/check-sa-overlap.sh From 9d3c2aaf23b4d615f424fd813030343ee21be3fd Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 13 May 2026 14:08:03 +0200 Subject: [PATCH 6/9] Fail fast in SA-overlap gate when a canary fixture is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a path in EXPECTED_REL doesn't exist, realpath exits non-zero with no stdout. The script lacked set -e, so `expected` could become empty and both missing/extra checks would silently succeed — a renamed or deleted canary would be "passed" by an effectively no-op gate. Validate every EXPECTED_REL entry with -f before building `expected`, and exit 1 with a clear message if any is missing. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/check-sa-overlap.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/check-sa-overlap.sh b/tests/check-sa-overlap.sh index a671fcb..23eb5cc 100755 --- a/tests/check-sa-overlap.sh +++ b/tests/check-sa-overlap.sh @@ -22,6 +22,14 @@ EXPECTED_REL=( tests/fixtures/valid/loose-comparison.php ) +for f in "${EXPECTED_REL[@]}"; do + if [ ! -f "$f" ]; then + echo "ERROR: registered canary fixture does not exist: $f" >&2 + echo "Update EXPECTED_REL in $(basename "$0") if the fixture was renamed or removed." >&2 + exit 1 + fi +done + expected="$(for f in "${EXPECTED_REL[@]}"; do realpath "$f"; done | sort -u)" phpstan_stderr="$(mktemp)" From d3f4298c0787dacfeabf6d5d93420f6c205a4af2 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 13 May 2026 14:24:24 +0200 Subject: [PATCH 7/9] Use explicit mktemp template for BSD/macOS portability Bare `mktemp` works on GNU coreutils (Linux/CI) but fails on BSD mktemp (macOS) which requires a template argument. Pass an explicit template under ${TMPDIR:-/tmp} so the gate is runnable locally on either OS. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/check-sa-overlap.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/check-sa-overlap.sh b/tests/check-sa-overlap.sh index 23eb5cc..0cce050 100755 --- a/tests/check-sa-overlap.sh +++ b/tests/check-sa-overlap.sh @@ -32,8 +32,8 @@ done expected="$(for f in "${EXPECTED_REL[@]}"; do realpath "$f"; done | sort -u)" -phpstan_stderr="$(mktemp)" -psalm_stderr="$(mktemp)" +phpstan_stderr="$(mktemp "${TMPDIR:-/tmp}/check-sa-overlap.phpstan.XXXXXX")" +psalm_stderr="$(mktemp "${TMPDIR:-/tmp}/check-sa-overlap.psalm.XXXXXX")" trap 'rm -f "$phpstan_stderr" "$psalm_stderr"' EXIT phpstan_json="$(vendor/bin/phpstan analyse \ From d5c5a818aa0d9f6b47c30504068d9042001efed5 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 13 May 2026 14:30:15 +0200 Subject: [PATCH 8/9] Surface jq failures in SA-overlap gate output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes so a jq problem doesn't masquerade as a missing canary: 1. Check up front that jq is in PATH and fail with an explicit message if it isn't, instead of letting empty parse output trigger the "missing canary" error. 2. Append jq's stderr to the per-tool stderr capture files so any JSON-parse errors get printed alongside PHPStan/Psalm stderr in the missing-canary error path — making the real cause visible there. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/check-sa-overlap.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/check-sa-overlap.sh b/tests/check-sa-overlap.sh index 0cce050..dd31eb0 100755 --- a/tests/check-sa-overlap.sh +++ b/tests/check-sa-overlap.sh @@ -16,6 +16,12 @@ # is disabled). set -uo pipefail +if ! command -v jq >/dev/null 2>&1; then + echo "ERROR: jq is required but was not found in PATH." >&2 + echo "Install jq (e.g., 'apt-get install jq' or 'brew install jq')." >&2 + exit 1 +fi + cd "$(dirname "$0")/.." EXPECTED_REL=( @@ -46,8 +52,8 @@ psalm_json="$(vendor/bin/psalm \ --output-format=json \ --no-progress 2>"$psalm_stderr" || true)" -phpstan_files="$(printf '%s' "$phpstan_json" | jq -r '.files? | keys[]?' | sort -u)" -psalm_files="$(printf '%s' "$psalm_json" | jq -r '.[]?.file_path // empty' | sort -u)" +phpstan_files="$(printf '%s' "$phpstan_json" | jq -r '.files? | keys[]?' 2>>"$phpstan_stderr" | sort -u)" +psalm_files="$(printf '%s' "$psalm_json" | jq -r '.[]?.file_path // empty' 2>>"$psalm_stderr" | sort -u)" intersection="$(comm -12 <(printf '%s\n' "$phpstan_files") <(printf '%s\n' "$psalm_files") | sed '/^$/d')" From f880d447896749675e8b9cda1f9d68c253f09cf9 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Wed, 13 May 2026 16:10:48 +0200 Subject: [PATCH 9/9] Document policy on not duplicating linter rules in README Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index e73a978..e38ffd3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # Eventjet Coding Standard +## Scope: formatting, not linting + +This package historically included rules that overlap with what static +analyzers (PHPStan, Psalm) already catch. Going forward we will not add such +rules, and we will progressively disable existing ones — if PHPStan or Psalm +flag a pattern, a CS rule for it is duplicate work. + +Treat this package as a formatter/style standard only. To get the full +benefit, run a static analyzer alongside it; PHP-CS-Fixer and PHPCS are not a +substitute for PHPStan or Psalm. + ## PHP-CS-Fixer ### Basic Usage: Add the following `.php-cs-fixer.dist.php` file to your project's root: