diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c128594..1416397 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -66,15 +66,15 @@ jobs: - uses: shivammathur/setup-php@v2 with: - php-version: '8.4' + php-version: '8.5' coverage: none - name: Cache Composer downloads uses: actions/cache@v4 with: path: ~/.cache/composer/files - key: composer-${{ runner.os }}-php-8.4-${{ hashFiles('composer.json') }} - restore-keys: composer-${{ runner.os }}-php-8.4- + key: composer-${{ runner.os }}-php-8.5-${{ hashFiles('composer.json') }} + restore-keys: composer-${{ runner.os }}-php-8.5- - name: Install dependencies run: composer update --no-interaction --prefer-dist --prefer-stable diff --git a/README.md b/README.md index e324cb2..58e7de2 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ [![Packagist downloads](https://img.shields.io/packagist/dt/cakephp/mago-rules?logo=packagist&label=downloads)](https://packagist.org/packages/cakephp/mago-rules) [![Mago compatibility](https://img.shields.io/badge/Mago-%5E1.47-f97316)](https://mago.carthage.software/) -A Mago-first coding-standard preset for CakePHP applications and plugins. Mago -owns generic formatting, linting, code-quality checks, and fixes; this package -adds CakePHP formatter preferences and the few framework conventions that Mago -cannot express natively. +A coding-standard preset for CakePHP applications and plugins, combining +[Mago](https://mago.carthage.software/)'s formatter and linter with CakePHP conventions. + +Requires PHP `^8.4` and Mago `^1.47`. Designed for CakePHP 6.x applications and plugins. ## Installation @@ -22,110 +22,73 @@ Import the preset from the project's `mago.toml`: extends = "vendor/cakephp/mago-rules/cakephp.mago.toml" ``` -The imported configuration starts the package-owned extension worker. No -copied bootstrap or manual rule registration is needed. - ## Usage -Use Mago's normal commands: +Check formatting and lint your project: ```sh -mago format --check -mago lint +vendor/bin/mago format --check +vendor/bin/mago lint ``` Apply formatting and safe lint fixes with: ```sh -mago format -mago lint --fix --format-after-fix --fail-on-remaining +vendor/bin/mago format +vendor/bin/mago lint --fix --format-after-fix --fail-on-remaining ``` -Adopting the preset is intentionally a code-quality migration. Existing -CakePHP repositories are expected to have formatter changes and native Mago -findings on their first run. Review the formatting as a dedicated change, then -either address lint findings incrementally or establish a baseline: +PHPUnit assertion-style fixes require the additional `--potentially-unsafe` flag. +Review those changes before keeping them. -```sh -mago format --dry-run -mago lint --stats -mago lint --baseline mago-baseline.toml --generate-baseline -mago lint --baseline mago-baseline.toml -mago lint --baseline mago-baseline.toml --remove-outdated-baseline-entries -``` +## What the preset includes + +The preset starts from Mago's default rules and applies CakePHP formatting and +style choices, including short arrays, imported class references, camelCase +function names, and `$this->assert…` in PHPUnit tests. It also checks for negated +ternary conditions and parameter shadowing. See [cakephp.mago.toml](cakephp.mago.toml) +for the complete configuration. + +CakePHP-specific rules cover: + +| Rule | Convention | +| --- | --- | +| `mago-cakephp/trait-suffix` | Trait names end in `Trait` | +| `mago-cakephp/public-method-underscore` | No underscore-prefixed methods, except magic methods and Entity accessors/mutators | +| `mago-cakephp/elseif` | Use `elseif` rather than `else if` | +| `mago-cakephp/function-docblock` | Document functions and methods outside tests | +| `mago-cakephp/docblock-tag-spacing` | One space after PHPDoc tags | +| `mago-cakephp/throws-tag` | Include an exception type in `@throws` | +| `mago-cakephp/inherit-doc` | Use CakePHP's `@inheritDoc` spelling and placement | -Commit the baseline and remove outdated entries as findings are fixed. New -findings remain visible, so the baseline is a migration tool rather than a -weaker permanent ruleset. +Method-count checks exclude conventional `src/Controller`, `src/Model/Table`, and +`src/View/Helper` paths in applications and plugins. Property-count checks exclude +`src/Model/Entity`. Both checks remain active elsewhere; custom layouts may need +additional exclusions. -The preset starts from Mago's default rule catalogue, then disables language -preferences that conflict with established CakePHP code. Projects can override -native or CakePHP rules after the `extends` declaration: +## Configuration + +Override individual rules in your project's `mago.toml` after the `extends` declaration: ```toml [linter.rules] -cyclomatic-complexity = { enabled = false } +no-parameter-shadowing = { level = "error" } "mago-cakephp/function-docblock" = { enabled = false } ``` -Class references must use imports rather than fully qualified names in executable -code. The native `no-fully-qualified-global-class-like` rule is enabled at error -severity; `mago lint --fix` adds imports and shortens references. PHPDoc names -are unaffected. +For an intentional local exception, use `@mago-expect lint:`. +PHPCS suppression comments are not recognized. -## Responsibility map +## Adopting in an existing project -| Concern | Owner | -| --- | --- | -| PSR-12 layout, braces, imports, quotes, commas, casts and return spacing | Mago formatter with CakePHP settings | -| Imported class references | Native `no-fully-qualified-global-class-like` rule | -| Short arrays, braced blocks, short tags, silenced errors, assignments in conditions and redundant syntax | Native Mago rules | -| Conflicting `empty()`/`isset()`/comparison/named-argument preferences | Disabled by the CakePHP preset | -| Trait `Trait` suffix | `mago-cakephp/trait-suffix` | -| Method underscore prefixes, with Entity accessor/mutator exceptions | `mago-cakephp/public-method-underscore` | -| `elseif` spelling | `mago-cakephp/elseif` | -| Function and method docblocks outside tests | `mago-cakephp/function-docblock` | -| One space after PHPDoc tags | `mago-cakephp/docblock-tag-spacing` | -| Exception type on `@throws` | `mago-cakephp/throws-tag` | -| CakePHP `@inheritDoc` spelling and placement | `mago-cakephp/inherit-doc` | - -Use `@mago-expect lint:` for an intentional local exception, or a -Mago baseline when migrating an existing project. PHPCS suppression comments -are not interpreted by this package. - -## Relationship to CakePHP CodeSniffer - -This is not a PHPCS emulator or a promise of sniff-for-sniff parity. Generic -PHPCS and Slevomat concerns belong to Mago's formatter and native linter, even -when diagnostics or fixes differ. Mago may report additional quality or -security findings because its defaults remain enabled, and formatter output is -not expected to be byte-for-byte identical to PHPCBF. - -Rules are added here only when they encode CakePHP-specific policy that native -Mago cannot express. A difference from CakePHP CodeSniffer alone is not a reason -to recreate a sniff. Conversely, a clean CakePHP CodeSniffer run does not imply -a clean Mago run: Mago's maintainability, correctness, and security diagnostics -are part of the value of adopting this preset. - -Mago internally understands PHPDoc, but version 1.47's PHP extension SDK exposes -docblocks as source trivia rather than a parsed PHPDoc tree. The extension -therefore implements only the small line-oriented PHPDoc policies listed above. - -## Compatibility and development - -| Package requirement | Supported version | -| --- | --- | -| PHP | `^8.4` | -| Mago | `^1.47` | -| Primary target | CakePHP 6.x applications and plugins | - -Run `composer run cs-check`, `composer test`, `composer run lint-corpus`, -`composer run test-fixes`, `composer run test-formatter`, and -`composer run test-consumer`. To check the CakePHP-specific extension rules for -false positives against CakePHP itself, pass an existing shallow checkout with -`scripts/check-cakephp-6.sh --workspace /path/to/cakephp`; without an argument -the script creates a temporary shallow clone of CakePHP 6.x. Add -`--migration-audit` to run the complete native Mago preset and see the -refactoring backlog. That audit is expected to exit unsuccessfully until the -target codebase has completed its migration. A bare positional path remains -supported as shorthand. +Preview formatting changes with `vendor/bin/mago format --dry-run`. Mago may +report additional findings compared with CakePHP CodeSniffer. To fix existing +lint findings gradually, generate a baseline: + +```sh +vendor/bin/mago lint --baseline mago-baseline.toml --generate-baseline +vendor/bin/mago lint --baseline mago-baseline.toml +``` + +Commit the baseline and use it in subsequent lint runs. Remove resolved entries +with `vendor/bin/mago lint --baseline mago-baseline.toml --remove-outdated-baseline-entries`. diff --git a/cakephp.mago.toml b/cakephp.mago.toml index ad0b715..a676d0b 100644 --- a/cakephp.mago.toml +++ b/cakephp.mago.toml @@ -43,6 +43,22 @@ integrations = ["phpunit"] # preferences that conflict with established CakePHP practices. # Consuming projects remain free to override any rule. [linter.rules] +assertion-style = { enabled = true, level = "error", style = "this" } + +# Shared naming and readability choices retain Mago's native severity levels. +function-name = { enabled = true, camel = true } +no-negated-ternary = { enabled = true } +no-parameter-shadowing = { enabled = true } + +# Runtime INI settings and message-free native assertions are CakePHP idioms. +no-ini-set = { enabled = false } +assert-description = { enabled = false } + +# Framework-facing classes expose actions, finders, helpers, and persisted fields. +# Keep native size checks active outside these conventional CakePHP paths. +too-many-methods = { exclude = ["**/src/Controller/**", "**/src/Model/Table/**", "**/src/View/Helper/**"] } +too-many-properties = { exclude = ["**/src/Model/Entity/**"] } + array-style = { enabled = true, level = "error", style = "short" } block-statement = { enabled = true, level = "error" } no-short-opening-tag = { enabled = true, level = "error" } diff --git a/scripts/check-consumer.sh b/scripts/check-consumer.sh index 0111534..57b69da 100755 --- a/scripts/check-consumer.sh +++ b/scripts/check-consumer.sh @@ -14,6 +14,7 @@ mkdir -p "$consumer_dir/vendor/cakephp" ln -s "$repo_root" "$consumer_dir/vendor/cakephp/mago-rules" mago="$repo_root/vendor/bin/mago" +"$mago" --workspace "$repo_root" format --check tests/policy "$mago" --workspace "$consumer_dir" extension validate "$mago" --workspace "$consumer_dir" format --check "$mago" --workspace "$consumer_dir" lint @@ -33,3 +34,79 @@ fi "$mago" --workspace "$consumer_dir" lint --fix --format-after-fix --fail-on-remaining diff -u "$repo_root/tests/fixer/class-imports.expected.php" "$consumer_dir/src/ClassImports.php" "$mago" --workspace "$consumer_dir" lint + +# Check that the installed preset rejects static assertions at error severity. +cp "$repo_root/tests/policy/assertion-style.php" "$consumer_dir/tests/AssertionStyleTest.php" +if "$mago" --workspace "$consumer_dir" lint --reporting-format short > "$consumer_dir/assertion-style.log" 2>&1; then + echo "Expected the installed preset to reject static assertions." >&2 + exit 1 +fi +if ! grep -q 'error\[assertion-style\]' "$consumer_dir/assertion-style.log"; then + cat "$consumer_dir/assertion-style.log" >&2 + exit 1 +fi +rm "$consumer_dir/tests/AssertionStyleTest.php" + +# Exercise native convention choices through the installed preset. Include help +# diagnostics in the failure threshold so default advisory levels are checked. +policy_codes=( + --retain-code function-name + --retain-code no-negated-ternary + --retain-code no-parameter-shadowing + --retain-code no-ini-set + --retain-code assert-description +) +cp "$repo_root/tests/policy/native-conventions.valid.php" "$consumer_dir/src/NativeConventions.php" +if ! "$mago" --workspace "$consumer_dir" lint --minimum-fail-level help \ + "${policy_codes[@]}" src/NativeConventions.php > "$consumer_dir/native-conventions.log" 2>&1; then + cat "$consumer_dir/native-conventions.log" >&2 + exit 1 +fi +cp "$repo_root/tests/policy/native-conventions.invalid.php" "$consumer_dir/src/NativeConventions.php" +if "$mago" --workspace "$consumer_dir" lint --minimum-fail-level help --reporting-format short \ + "${policy_codes[@]}" src/NativeConventions.php > "$consumer_dir/native-conventions.log" 2>&1; then + echo "Expected the installed preset to report native convention violations." >&2 + exit 1 +fi +for diagnostic in 'help[function-name]' 'help[no-negated-ternary]' 'warning[no-parameter-shadowing]'; do + if ! grep -Fq "$diagnostic" "$consumer_dir/native-conventions.log"; then + cat "$consumer_dir/native-conventions.log" >&2 + exit 1 + fi +done +rm "$consumer_dir/src/NativeConventions.php" + +# Native count rules remain active for services, while app and plugin framework +# paths are exempt. Do not use --only: it bypasses the preset's configuration. +check_count_policy() { + local rule="$1" + local fixture="$2" + local prefix location target + shift 2 + for prefix in "" "plugins/Example/"; do + for location in "$@"; do + target="$prefix$location/Count.php" + mkdir -p "$consumer_dir/$(dirname "$target")" + cp "$repo_root/tests/policy/$fixture" "$consumer_dir/$target" + if ! "$mago" --workspace "$consumer_dir" lint --retain-code "$rule" "$target" > "$consumer_dir/count.log" 2>&1; then + cat "$consumer_dir/count.log" >&2 + exit 1 + fi + rm "$consumer_dir/$target" + done + target="${prefix}src/Service/Count.php" + mkdir -p "$consumer_dir/$(dirname "$target")" + cp "$repo_root/tests/policy/$fixture" "$consumer_dir/$target" + if "$mago" --workspace "$consumer_dir" lint --retain-code "$rule" --reporting-format short "$target" > "$consumer_dir/count.log" 2>&1; then + echo "Expected $rule to remain active for $target." >&2 + exit 1 + fi + if ! grep -Fq "error[$rule]" "$consumer_dir/count.log"; then + cat "$consumer_dir/count.log" >&2 + exit 1 + fi + rm "$consumer_dir/$target" + done +} +check_count_policy too-many-methods method-count.php src/Controller src/Model/Table src/View/Helper +check_count_policy too-many-properties property-count.php src/Model/Entity diff --git a/tests/Unit/CakePhpExtensionTest.php b/tests/Unit/CakePhpExtensionTest.php index d379bc3..0607abe 100644 --- a/tests/Unit/CakePhpExtensionTest.php +++ b/tests/Unit/CakePhpExtensionTest.php @@ -13,8 +13,8 @@ public function testRegistersTheCakePhpCompatibilityRules(): void { $extension = CakePhpExtension::create(); - self::assertSame('mago-cakephp/rules', $extension->identifier); - self::assertSame( + $this->assertSame('mago-cakephp/rules', $extension->identifier); + $this->assertSame( [ 'mago-cakephp/trait-suffix', 'mago-cakephp/public-method-underscore', diff --git a/tests/Unit/PathMatcherTest.php b/tests/Unit/PathMatcherTest.php index e0d2bd3..6a38059 100644 --- a/tests/Unit/PathMatcherTest.php +++ b/tests/Unit/PathMatcherTest.php @@ -24,6 +24,6 @@ public static function paths(): iterable #[DataProvider('paths')] public function testMatchesTestPaths(string $path, bool $expected): void { - self::assertSame($expected, PathMatcher::matches($path, ['tests/**', '**/tests/**'])); + $this->assertSame($expected, PathMatcher::matches($path, ['tests/**', '**/tests/**'])); } } diff --git a/tests/Unit/RuleIntegrationTest.php b/tests/Unit/RuleIntegrationTest.php index ef2cc26..00c7e3f 100644 --- a/tests/Unit/RuleIntegrationTest.php +++ b/tests/Unit/RuleIntegrationTest.php @@ -54,11 +54,11 @@ public function testRuleFixtures(string $rule, array $fixtures): void $root, ); - self::assertIsResource($process); + $this->assertIsResource($process); $output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); - self::assertSame(0, proc_close($process), $output); + $this->assertSame(0, proc_close($process), $output); } } diff --git a/tests/cakephp6/mago.toml b/tests/cakephp6/mago.toml index a2e9811..a285814 100644 --- a/tests/cakephp6/mago.toml +++ b/tests/cakephp6/mago.toml @@ -1,5 +1,5 @@ version = "1.47" -php-version = "8.4" +php-version = "8.5" [source] paths = ["config", "src"] diff --git a/tests/policy/assertion-style.php b/tests/policy/assertion-style.php new file mode 100644 index 0000000..bc318a5 --- /dev/null +++ b/tests/policy/assertion-style.php @@ -0,0 +1,16 @@ +assertFalse(false); + } +} diff --git a/tests/policy/method-count.php b/tests/policy/method-count.php new file mode 100644 index 0000000..4a348dd --- /dev/null +++ b/tests/policy/method-count.php @@ -0,0 +1,73 @@ + $items + * @return list + */ +function normalizeItems(array $items): array +{ + $result = []; + foreach ($items as $items) { + $result[] = normalized_name($items); + } + + return $result; +} diff --git a/tests/policy/native-conventions.valid.php b/tests/policy/native-conventions.valid.php new file mode 100644 index 0000000..5eaaab0 --- /dev/null +++ b/tests/policy/native-conventions.valid.php @@ -0,0 +1,29 @@ + $items + * @return list + */ +function normalizeItems(array $items): array +{ + $result = []; + foreach ($items as $item) { + $result[] = normalizedName($item); + } + + return $result; +} diff --git a/tests/policy/property-count.php b/tests/policy/property-count.php new file mode 100644 index 0000000..63ebded --- /dev/null +++ b/tests/policy/property-count.php @@ -0,0 +1,19 @@ +