From 5f908adb65a32014e4134d85c265475533cf527a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 13:23:29 +0000 Subject: [PATCH 1/4] Add RuleConventionsTest to enforce rule registration conventions Adds tests/src/RuleConventionsTest.php which parses rules.neon, extension.neon, and bleedingEdge.neon and asserts that every rule is toggleable: registered via conditionalTags with a boolean default under parameters.drupal.rules, opt-in rules enabled in bleedingEdge.neon, and no new rules added directly under rules:. Graduated rules (default true) are tracked via an explicit allowlist. Fixes the pre-existing convention violation surfaced by the test: pluginManagerInspectionRule was opt-in (false default) but missing from bleedingEdge.neon. Adds AGENTS.md documenting the conventions for contributors and agents, and nette/neon as a dev dependency for parsing the neon files. Closes #971 https://claude.ai/code/session_01AAChSrMEaL4S6st2ET2JRF --- AGENTS.md | 80 ++++++++++ bleedingEdge.neon | 1 + composer.json | 1 + tests/src/RuleConventionsTest.php | 237 ++++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 AGENTS.md create mode 100644 tests/src/RuleConventionsTest.php diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..5f677f0c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,80 @@ +# AGENTS.md + +Guidance for AI agents and contributors working on phpstan-drupal. For commands +and architecture see `CLAUDE.md`; this file documents the rule registration +conventions, which are enforced by `tests/src/RuleConventionsTest.php`. + +## Adding a new rule + +Every new rule must be **toggleable**. Never add a new rule directly under +`rules:` in `rules.neon` — that section is frozen for legacy rules only and the +conventions test fails if it grows. + +To add a rule named `fooBarRule`: + +1. **`rules.neon`** — register the rule class under both `services:` and + `conditionalTags:`: + + ```neon + conditionalTags: + mglaman\PHPStanDrupal\Rules\Drupal\FooBarRule: + phpstan.rules.rule: %drupal.rules.fooBarRule% + + services: + - + class: mglaman\PHPStanDrupal\Rules\Drupal\FooBarRule + ``` + +2. **`extension.neon`** — add the parameter under `parameters.drupal.rules` + with a default of `false` (opt-in), and add it to `parametersSchema`: + + ```neon + parameters: + drupal: + rules: + fooBarRule: false + parametersSchema: + drupal: structure([ + rules: structure([ + fooBarRule: boolean() + ]) + ]) + ``` + +3. **`bleedingEdge.neon`** — enable it so bleeding-edge users get it before it + graduates: + + ```neon + parameters: + drupal: + rules: + fooBarRule: true + ``` + +4. Document the rule under "Opt-in rules" in `README.md`. + +## Graduating a rule to the default ruleset + +When an opt-in rule is stable enough to be on by default: + +1. In `extension.neon`, flip its default from `false` to `true`. It stays in + `conditionalTags`, so it remains opt-out (users can still disable it). +2. Remove its entry from `bleedingEdge.neon` — it is on by default now and must + not be listed there. +3. Add its parameter name to `RuleConventionsTest::GRADUATED_RULES`, in the same + change, to record that the `true` default is intentional. + +## Convention summary + +| Location in `rules.neon` | Default in `extension.neon` | Meaning | In `bleedingEdge.neon`? | +|---|---|---|---| +| `conditionalTags` | `false` | Opt-in, not yet default | **Yes** | +| `conditionalTags` | `true` | Graduated, still toggleable (opt-out) | No | +| `rules:` directly | — | Legacy, not toggleable — **no new additions** | — | + +## Enforcement + +`tests/src/RuleConventionsTest.php` parses the three neon files and asserts the +table above. It runs as part of the normal `phpunit` suite, is cheap, and stays +green until a convention is violated. If it fails, the failure message names the +exact rule/parameter and the fix — follow it rather than weakening the test. diff --git a/bleedingEdge.neon b/bleedingEdge.neon index 0bf5b90b..79645e0f 100644 --- a/bleedingEdge.neon +++ b/bleedingEdge.neon @@ -9,6 +9,7 @@ parameters: testClassSuffixNameRule: true dependencySerializationTraitPropertyRule: true accessResultConditionRule: true + pluginManagerInspectionRule: true cacheableDependencyRule: true hookRules: true loggerFromFactoryPropertyAssignmentRule: true diff --git a/composer.json b/composer.json index 926bb508..bf4304ad 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "composer/installers": "^1.9 || ^2", "drupal/core-recommended": "^11", "drush/drush": "^11 || ^12 || ^13", + "nette/neon": "^3.0", "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9 || ^10 || ^11", diff --git a/tests/src/RuleConventionsTest.php b/tests/src/RuleConventionsTest.php new file mode 100644 index 00000000..584d6999 --- /dev/null +++ b/tests/src/RuleConventionsTest.php @@ -0,0 +1,237 @@ + + */ + private const LEGACY_DIRECT_RULES = [ + 'mglaman\PHPStanDrupal\Rules\Drupal\Coder\DiscouragedFunctionsRule', + 'mglaman\PHPStanDrupal\Rules\Drupal\GlobalDrupalDependencyInjectionRule', + 'mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerSetsCacheBackendRule', + 'mglaman\PHPStanDrupal\Rules\Drupal\RenderCallbackRule', + 'mglaman\PHPStanDrupal\Rules\Deprecations\StaticServiceDeprecatedServiceRule', + 'mglaman\PHPStanDrupal\Rules\Deprecations\GetDeprecatedServiceRule', + 'mglaman\PHPStanDrupal\Rules\Drupal\Tests\BrowserTestBaseDefaultThemeRule', + 'mglaman\PHPStanDrupal\Rules\Deprecations\ConfigEntityConfigExportRule', + 'mglaman\PHPStanDrupal\Rules\Deprecations\PluginAnnotationContextDefinitionsRule', + 'mglaman\PHPStanDrupal\Rules\Drupal\ModuleLoadInclude', + 'mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes', + 'mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule', + 'mglaman\PHPStanDrupal\Rules\Drupal\TestClassesProtectedPropertyModulesRule', + ]; + + /** + * Conditional rule parameters that have graduated to the default ruleset. + * + * A graduated rule keeps its `conditionalTags` registration (so it stays + * opt-out) but its `extension.neon` default is `true`. Graduating a rule is + * a deliberate act: add its parameter name here in the same change that + * flips the default to `true`. Graduated rules do not belong in + * `bleedingEdge.neon` because they are already on by default. + * + * @var list + */ + private const GRADUATED_RULES = [ + 'classExtendsInternalClassRule', + ]; + + public function testNewRulesAreNotDirectlyRegistered(): void + { + $rules = $this->decodeFile('rules.neon'); + self::assertArrayHasKey('rules', $rules, 'rules.neon must define a rules: section.'); + self::assertIsArray($rules['rules']); + + $allowlist = array_flip(self::LEGACY_DIRECT_RULES); + foreach ($rules['rules'] as $class) { + $normalized = ltrim((string) $class, '\\'); + self::assertArrayHasKey( + $normalized, + $allowlist, + sprintf( + "%s is registered directly under rules: in rules.neon but is not a known legacy rule.\n" + . 'New rules must be toggleable: register the rule under conditionalTags with a ' + . '%%drupal.rules.%% parameter and add a `false` default under ' + . 'parameters.drupal.rules in extension.neon. See AGENTS.md.', + $normalized + ) + ); + } + } + + public function testConditionalRulesHaveFalseDefaultInExtensionNeon(): void + { + $defaults = $this->extensionRuleDefaults(); + + foreach ($this->conditionalRuleParameters() as $parameter) { + self::assertArrayHasKey( + $parameter, + $defaults, + sprintf( + 'Conditional rule parameter "%s" (from rules.neon conditionalTags) has no default ' + . 'under parameters.drupal.rules in extension.neon. Add it with a `false` default ' + . 'so the rule is opt-in and toggleable. See AGENTS.md.', + $parameter + ) + ); + + $default = $defaults[$parameter]; + self::assertIsBool( + $default, + sprintf('Default for rule parameter "%s" in extension.neon must be a boolean.', $parameter) + ); + + if (in_array($parameter, self::GRADUATED_RULES, true)) { + self::assertTrue( + $default, + sprintf( + 'Rule parameter "%s" is listed as graduated but its extension.neon default is not ' + . '`true`. Either flip the default to `true` or remove it from GRADUATED_RULES.', + $parameter + ) + ); + continue; + } + + self::assertFalse( + $default, + sprintf( + 'Rule parameter "%s" must default to `false` in extension.neon (opt-in). To graduate it ' + . 'to the default ruleset, flip the default to `true` and add "%s" to ' + . 'RuleConventionsTest::GRADUATED_RULES in the same change. See AGENTS.md.', + $parameter, + $parameter + ) + ); + } + } + + public function testOptInRulesAreEnabledInBleedingEdge(): void + { + $defaults = $this->extensionRuleDefaults(); + $bleedingEdge = $this->bleedingEdgeRuleOverrides(); + + foreach ($this->conditionalRuleParameters() as $parameter) { + if (!array_key_exists($parameter, $defaults) || $defaults[$parameter] !== false) { + // Graduated rules (default `true`) are already on by default and + // must not be listed in bleedingEdge.neon. + continue; + } + + self::assertArrayHasKey( + $parameter, + $bleedingEdge, + sprintf( + 'Opt-in rule parameter "%s" (default `false` in extension.neon) is not enabled in ' + . 'bleedingEdge.neon. Add `%s: true` under parameters.drupal.rules in bleedingEdge.neon ' + . 'so the rule runs for bleeding-edge users. See AGENTS.md.', + $parameter, + $parameter + ) + ); + self::assertTrue( + $bleedingEdge[$parameter], + sprintf('Rule parameter "%s" must be set to `true` in bleedingEdge.neon.', $parameter) + ); + } + } + + /** + * Resolves the unique set of `phpstan.rules.rule` parameter names from + * the conditionalTags section of rules.neon. + * + * @return list + */ + private function conditionalRuleParameters(): array + { + $rules = $this->decodeFile('rules.neon'); + self::assertArrayHasKey('conditionalTags', $rules, 'rules.neon must define a conditionalTags: section.'); + self::assertIsArray($rules['conditionalTags']); + + $parameters = []; + foreach ($rules['conditionalTags'] as $tags) { + if (!is_array($tags) || !array_key_exists('phpstan.rules.rule', $tags)) { + continue; + } + $tagValue = (string) $tags['phpstan.rules.rule']; + self::assertSame( + 1, + preg_match('/^%drupal\.rules\.([A-Za-z0-9_]+)%$/', $tagValue, $matches), + sprintf( + 'conditionalTags phpstan.rules.rule value "%s" must be a %%drupal.rules.%% parameter.', + $tagValue + ) + ); + $parameters[$matches[1]] = true; + } + + self::assertNotEmpty($parameters, 'Expected at least one conditional rule in rules.neon.'); + return array_keys($parameters); + } + + /** + * @return array + */ + private function extensionRuleDefaults(): array + { + $extension = $this->decodeFile('extension.neon'); + $rules = $extension['parameters']['drupal']['rules'] ?? null; + self::assertIsArray($rules, 'extension.neon must define parameters.drupal.rules.'); + return $rules; + } + + /** + * @return array + */ + private function bleedingEdgeRuleOverrides(): array + { + $bleedingEdge = $this->decodeFile('bleedingEdge.neon'); + $rules = $bleedingEdge['parameters']['drupal']['rules'] ?? null; + self::assertIsArray($rules, 'bleedingEdge.neon must define parameters.drupal.rules.'); + return $rules; + } + + /** + * @return array + */ + private function decodeFile(string $name): array + { + $path = __DIR__ . '/../../' . $name; + self::assertFileExists($path); + $decoded = Neon::decode((string) file_get_contents($path)); + self::assertIsArray($decoded, sprintf('%s did not decode to an array.', $name)); + return $decoded; + } +} From 6c5d83e429107b38903ded14f219224a83a4676e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 13:40:13 +0000 Subject: [PATCH 2/4] Keep pluginManagerInspectionRule out of bleedingEdge.neon The rule is too unreliable to enable for bleeding-edge users. Instead of adding it to bleedingEdge.neon, record the deliberate exception in RuleConventionsTest::OPT_IN_RULES_EXCLUDED_FROM_BLEEDING_EDGE so the conventions test stays green and the exclusion is explicit and documented. Reverts the earlier bleedingEdge.neon addition and documents the exception mechanism in AGENTS.md. https://claude.ai/code/session_01AAChSrMEaL4S6st2ET2JRF --- AGENTS.md | 12 +++++++++++- bleedingEdge.neon | 1 - tests/src/RuleConventionsTest.php | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5f677f0c..2e1e4ec3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,16 @@ To add a rule named `fooBarRule`: 4. Document the rule under "Opt-in rules" in `README.md`. +### Exception: opt-in rules excluded from bleeding edge + +An opt-in rule that is known to be too noisy or unstable to inflict on +bleeding-edge users may be deliberately kept out of `bleedingEdge.neon`. It then +stays available only through explicit per-rule configuration. Record the +exception by adding its parameter name to +`RuleConventionsTest::OPT_IN_RULES_EXCLUDED_FROM_BLEEDING_EDGE` (with a comment +explaining why) instead of adding it to `bleedingEdge.neon` in step 3. The path +back is to remove it from that list and add it to `bleedingEdge.neon`. + ## Graduating a rule to the default ruleset When an opt-in rule is stable enough to be on by default: @@ -68,7 +78,7 @@ When an opt-in rule is stable enough to be on by default: | Location in `rules.neon` | Default in `extension.neon` | Meaning | In `bleedingEdge.neon`? | |---|---|---|---| -| `conditionalTags` | `false` | Opt-in, not yet default | **Yes** | +| `conditionalTags` | `false` | Opt-in, not yet default | **Yes**, unless excluded (see above) | | `conditionalTags` | `true` | Graduated, still toggleable (opt-out) | No | | `rules:` directly | — | Legacy, not toggleable — **no new additions** | — | diff --git a/bleedingEdge.neon b/bleedingEdge.neon index 79645e0f..0bf5b90b 100644 --- a/bleedingEdge.neon +++ b/bleedingEdge.neon @@ -9,7 +9,6 @@ parameters: testClassSuffixNameRule: true dependencySerializationTraitPropertyRule: true accessResultConditionRule: true - pluginManagerInspectionRule: true cacheableDependencyRule: true hookRules: true loggerFromFactoryPropertyAssignmentRule: true diff --git a/tests/src/RuleConventionsTest.php b/tests/src/RuleConventionsTest.php index 584d6999..13ad6936 100644 --- a/tests/src/RuleConventionsTest.php +++ b/tests/src/RuleConventionsTest.php @@ -68,6 +68,23 @@ final class RuleConventionsTest extends TestCase 'classExtendsInternalClassRule', ]; + /** + * Opt-in rules deliberately kept out of bleedingEdge.neon. + * + * Normally an opt-in rule (default `false`) must also be enabled in + * bleedingEdge.neon. A rule may be excluded here when it is known to be too + * noisy or unstable to inflict on bleeding-edge users; it then stays + * available only through explicit per-rule configuration. The path back is + * to remove it from this list and add it to bleedingEdge.neon. + * + * @var list + */ + private const OPT_IN_RULES_EXCLUDED_FROM_BLEEDING_EDGE = [ + // PluginManagerInspectionRule is currently too unreliable to enable by + // default for bleeding-edge users. + 'pluginManagerInspectionRule', + ]; + public function testNewRulesAreNotDirectlyRegistered(): void { $rules = $this->decodeFile('rules.neon'); @@ -150,6 +167,19 @@ public function testOptInRulesAreEnabledInBleedingEdge(): void continue; } + if (in_array($parameter, self::OPT_IN_RULES_EXCLUDED_FROM_BLEEDING_EDGE, true)) { + self::assertArrayNotHasKey( + $parameter, + $bleedingEdge, + sprintf( + 'Rule "%s" is listed in OPT_IN_RULES_EXCLUDED_FROM_BLEEDING_EDGE but is enabled in ' + . 'bleedingEdge.neon. Remove it from one or the other so the exception stays honest.', + $parameter + ) + ); + continue; + } + self::assertArrayHasKey( $parameter, $bleedingEdge, From 2f8f6ac8cca1d7d0999a7f12fb776d44b9037994 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 13:42:36 +0000 Subject: [PATCH 3/4] Move rule registration conventions into CLAUDE.md Drop the separate AGENTS.md and document the toggleable-rule conventions in the existing CLAUDE.md instead. Update RuleConventionsTest failure messages to point at CLAUDE.md. https://claude.ai/code/session_01AAChSrMEaL4S6st2ET2JRF --- AGENTS.md | 90 ------------------------------- CLAUDE.md | 31 +++++++++++ tests/src/RuleConventionsTest.php | 10 ++-- 3 files changed, 36 insertions(+), 95 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 2e1e4ec3..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,90 +0,0 @@ -# AGENTS.md - -Guidance for AI agents and contributors working on phpstan-drupal. For commands -and architecture see `CLAUDE.md`; this file documents the rule registration -conventions, which are enforced by `tests/src/RuleConventionsTest.php`. - -## Adding a new rule - -Every new rule must be **toggleable**. Never add a new rule directly under -`rules:` in `rules.neon` — that section is frozen for legacy rules only and the -conventions test fails if it grows. - -To add a rule named `fooBarRule`: - -1. **`rules.neon`** — register the rule class under both `services:` and - `conditionalTags:`: - - ```neon - conditionalTags: - mglaman\PHPStanDrupal\Rules\Drupal\FooBarRule: - phpstan.rules.rule: %drupal.rules.fooBarRule% - - services: - - - class: mglaman\PHPStanDrupal\Rules\Drupal\FooBarRule - ``` - -2. **`extension.neon`** — add the parameter under `parameters.drupal.rules` - with a default of `false` (opt-in), and add it to `parametersSchema`: - - ```neon - parameters: - drupal: - rules: - fooBarRule: false - parametersSchema: - drupal: structure([ - rules: structure([ - fooBarRule: boolean() - ]) - ]) - ``` - -3. **`bleedingEdge.neon`** — enable it so bleeding-edge users get it before it - graduates: - - ```neon - parameters: - drupal: - rules: - fooBarRule: true - ``` - -4. Document the rule under "Opt-in rules" in `README.md`. - -### Exception: opt-in rules excluded from bleeding edge - -An opt-in rule that is known to be too noisy or unstable to inflict on -bleeding-edge users may be deliberately kept out of `bleedingEdge.neon`. It then -stays available only through explicit per-rule configuration. Record the -exception by adding its parameter name to -`RuleConventionsTest::OPT_IN_RULES_EXCLUDED_FROM_BLEEDING_EDGE` (with a comment -explaining why) instead of adding it to `bleedingEdge.neon` in step 3. The path -back is to remove it from that list and add it to `bleedingEdge.neon`. - -## Graduating a rule to the default ruleset - -When an opt-in rule is stable enough to be on by default: - -1. In `extension.neon`, flip its default from `false` to `true`. It stays in - `conditionalTags`, so it remains opt-out (users can still disable it). -2. Remove its entry from `bleedingEdge.neon` — it is on by default now and must - not be listed there. -3. Add its parameter name to `RuleConventionsTest::GRADUATED_RULES`, in the same - change, to record that the `true` default is intentional. - -## Convention summary - -| Location in `rules.neon` | Default in `extension.neon` | Meaning | In `bleedingEdge.neon`? | -|---|---|---|---| -| `conditionalTags` | `false` | Opt-in, not yet default | **Yes**, unless excluded (see above) | -| `conditionalTags` | `true` | Graduated, still toggleable (opt-out) | No | -| `rules:` directly | — | Legacy, not toggleable — **no new additions** | — | - -## Enforcement - -`tests/src/RuleConventionsTest.php` parses the three neon files and asserts the -table above. It runs as part of the normal `phpunit` suite, is cheap, and stays -green until a convention is violated. If it fails, the failure message names the -exact rule/parameter and the fix — follow it rather than weakening the test. diff --git a/CLAUDE.md b/CLAUDE.md index bc66715c..a12a1a08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,37 @@ PHPStan extension and rules for Drupal static analysis. - Each error is `[message, line, tip?]` - Many tests use `@dataProvider` with generators yielding `[files, errors]` +## Rule Registration Conventions + +Every rule must be **toggleable**. Never add a new rule directly under `rules:` +in `rules.neon` — that section is frozen for legacy rules and +`tests/src/RuleConventionsTest.php` fails if it grows. + +To add a rule `fooBarRule`: + +- `rules.neon` - register the class under both `services:` and `conditionalTags:` + (`phpstan.rules.rule: %drupal.rules.fooBarRule%`) +- `extension.neon` - add `fooBarRule: false` under `parameters.drupal.rules` and + `fooBarRule: boolean()` under `parametersSchema.drupal...rules` +- `bleedingEdge.neon` - add `fooBarRule: true` so bleeding-edge users get it +- Document it under "Opt-in rules" in `README.md` + +Graduating an opt-in rule to default: flip its `extension.neon` default +`false` → `true`, remove it from `bleedingEdge.neon`, and add its parameter name +to `RuleConventionsTest::GRADUATED_RULES` in the same change. It stays in +`conditionalTags` so it remains opt-out. + +A rule too noisy/unstable for bleeding edge may be kept out of +`bleedingEdge.neon` by listing it in +`RuleConventionsTest::OPT_IN_RULES_EXCLUDED_FROM_BLEEDING_EDGE` (with a reason) +instead of step 3. + +| `rules.neon` location | `extension.neon` default | Meaning | In `bleedingEdge.neon`? | +|---|---|---|---| +| `conditionalTags` | `false` | Opt-in, not yet default | Yes, unless excluded above | +| `conditionalTags` | `true` | Graduated, still opt-out | No | +| `rules:` directly | — | Legacy, not toggleable — no new additions | — | + ## Coding Standards - PSR-2 base with Slevomat coding standard additions diff --git a/tests/src/RuleConventionsTest.php b/tests/src/RuleConventionsTest.php index 13ad6936..7c9389f6 100644 --- a/tests/src/RuleConventionsTest.php +++ b/tests/src/RuleConventionsTest.php @@ -17,7 +17,7 @@ use function sprintf; /** - * Enforces the rule registration conventions documented in AGENTS.md. + * Enforces the rule registration conventions documented in CLAUDE.md. * * Every rule must be toggleable: registered through `conditionalTags` with a * matching boolean parameter under `parameters.drupal.rules` in @@ -101,7 +101,7 @@ public function testNewRulesAreNotDirectlyRegistered(): void "%s is registered directly under rules: in rules.neon but is not a known legacy rule.\n" . 'New rules must be toggleable: register the rule under conditionalTags with a ' . '%%drupal.rules.%% parameter and add a `false` default under ' - . 'parameters.drupal.rules in extension.neon. See AGENTS.md.', + . 'parameters.drupal.rules in extension.neon. See CLAUDE.md.', $normalized ) ); @@ -119,7 +119,7 @@ public function testConditionalRulesHaveFalseDefaultInExtensionNeon(): void sprintf( 'Conditional rule parameter "%s" (from rules.neon conditionalTags) has no default ' . 'under parameters.drupal.rules in extension.neon. Add it with a `false` default ' - . 'so the rule is opt-in and toggleable. See AGENTS.md.', + . 'so the rule is opt-in and toggleable. See CLAUDE.md.', $parameter ) ); @@ -147,7 +147,7 @@ public function testConditionalRulesHaveFalseDefaultInExtensionNeon(): void sprintf( 'Rule parameter "%s" must default to `false` in extension.neon (opt-in). To graduate it ' . 'to the default ruleset, flip the default to `true` and add "%s" to ' - . 'RuleConventionsTest::GRADUATED_RULES in the same change. See AGENTS.md.', + . 'RuleConventionsTest::GRADUATED_RULES in the same change. See CLAUDE.md.', $parameter, $parameter ) @@ -186,7 +186,7 @@ public function testOptInRulesAreEnabledInBleedingEdge(): void sprintf( 'Opt-in rule parameter "%s" (default `false` in extension.neon) is not enabled in ' . 'bleedingEdge.neon. Add `%s: true` under parameters.drupal.rules in bleedingEdge.neon ' - . 'so the rule runs for bleeding-edge users. See AGENTS.md.', + . 'so the rule runs for bleeding-edge users. See CLAUDE.md.', $parameter, $parameter ) From d86ca27096435ea1520c37c9889d3206943dc234 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 13:46:18 +0000 Subject: [PATCH 4/4] Fix PHPStan offsetAccess error in RuleConventionsTest Access the preg_match capture group only inside the `=== 1` branch so PHPStan can prove the offset exists, instead of relying on an assertSame that does not narrow the matches array. https://claude.ai/code/session_01AAChSrMEaL4S6st2ET2JRF --- tests/src/RuleConventionsTest.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/src/RuleConventionsTest.php b/tests/src/RuleConventionsTest.php index 7c9389f6..3a31e741 100644 --- a/tests/src/RuleConventionsTest.php +++ b/tests/src/RuleConventionsTest.php @@ -216,15 +216,14 @@ private function conditionalRuleParameters(): array continue; } $tagValue = (string) $tags['phpstan.rules.rule']; - self::assertSame( - 1, - preg_match('/^%drupal\.rules\.([A-Za-z0-9_]+)%$/', $tagValue, $matches), - sprintf( - 'conditionalTags phpstan.rules.rule value "%s" must be a %%drupal.rules.%% parameter.', - $tagValue - ) - ); - $parameters[$matches[1]] = true; + if (preg_match('/^%drupal\.rules\.([A-Za-z0-9_]+)%$/', $tagValue, $matches) === 1) { + $parameters[$matches[1]] = true; + continue; + } + self::fail(sprintf( + 'conditionalTags phpstan.rules.rule value "%s" must be a %%drupal.rules.%% parameter.', + $tagValue + )); } self::assertNotEmpty($parameters, 'Expected at least one conditional rule in rules.neon.');