Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/bare_run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
php_version: ['7.2', '7.4', '8.0', '8.4']

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

-
uses: shivammathur/setup-php@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check_command_run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
php_version: ['7.2', '7.3', '7.4', '8.0']

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

-
uses: shivammathur/setup-php@v2
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/code_analysis.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Code Analysis

on:
pull_request: null
pull_request:
push:
branches:
- main
Expand All @@ -26,7 +26,7 @@ jobs:

-
name: 'Coding Standard'
run: composer fix-cs --ansi
run: composer check-cs --ansi

-
name: 'Tests'
Expand All @@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
# see https://github.com/shivammathur/setup-php
- uses: shivammathur/setup-php@v2
with:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ composer.lock
rector-local

php-scoper.phar


# gpt
.claude
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CLAUDE.md

Guidance for Claude Code when working in this repository.

## Project

**Symplify EasyCodingStandard (ECS)** — a unified runner for PHP-CS-Fixer and PHP_CodeSniffer. Users configure rules through a fluent PHP API (`ECSConfig::configure()->with...()`) instead of dealing with each tool's native config format.

- PHP `>=8.3`
- Entry binary: `bin/ecs`
- PSR-4: `Symplify\EasyCodingStandard\` → `src/`
- Tests PSR-4: `Symplify\EasyCodingStandard\Tests\` → `tests/`

## Required workflow after every change

Run all three, in this order. Fix anything that fails before reporting work as done.

```bash
composer phpstan # vendor/bin/phpstan analyse (level 8, src + tests + ecs.php + rector.php)
composer rector # vendor/bin/rector process --dry-run
composer check-cs # bin/ecs check
```

Shortcut for all three: `composer lint`.

To auto-apply Rector and ECS fixes: `composer lint.fix` (runs `fix-rector` then `fix-cs`).

Tests: `composer test` (PHPUnit). Run them when behavior may have changed.

Memory limit for PHPStan and Rector is `1G` (already set in composer scripts).

## Architecture

- `src/Config/ECSConfig.php` — Illuminate container subclass, low-level rule registration (`rule()`, `ruleWithConfiguration()`, `sets()`, `import()`). Auto-tags Sniff/FixerInterface/OutputFormatterInterface bindings.
- `src/Configuration/ECSConfigBuilder.php` — fluent user-facing API (`withRules`, `withSets`, `withPreparedSets`, `withPhpCsFixerSets`, `withSpacesLevel`, …). Returned by `ECSConfig::configure()`. `__invoke(ECSConfig)` flushes the builder state into the container.
- `config/set/common/*.php` — prepared rule sets (spaces, arrays, namespaces, docblock, etc.); each returns a closure consumed by `ECSConfig::import()`.
- `src/Config/Level/` — gradual-adoption levels (e.g. `SpacesLevel`). Each level class exposes `RULES` (ordered safest → most invasive) and optionally `RULE_CONFIGURATIONS`.
- `src/Configuration/Levels/LevelRulesResolver.php` — resolves `int $level` to the first N+1 rules from a level's `RULES` array. Clamps to max, throws on negative input or empty rule list.

### Adding a level (mirrors Rector's `withTypeCoverageLevel` pattern)

1. Create `src/Config/Level/<Name>Level.php` with `RULES` (ordered) and `RULE_CONFIGURATIONS` (only for configurable rules).
2. Add `with<Name>Level(int $level): self` to `ECSConfigBuilder`. Use `LevelRulesResolver::resolve()`, then route configurable rules to `$this->rulesWithConfiguration` and the rest to `$this->rules`.

## Conventions

- `declare(strict_types=1);` on every PHP file.
- `final` classes by default.
- PHPStan level 8, `type_coverage.return: 99`. Keep new code fully typed.
- Don't introduce new comments unless they explain a non-obvious why; well-named identifiers should carry meaning.
- Don't add backwards-compat shims, dead re-exports, or features that aren't required by the task.

## Patched dependency

`illuminate/container` is patched via `patches/illuminate-container-container-php.patch` (cweagans/composer-patches). Don't update the package without re-checking the patch.

## Vendor prefixing / release build

`scoper.php` and `prefix-code.sh` / `full_ecs_build.sh` produce the prefixed distribution. Don't edit `scoper.php` casually; it's excluded from PHPStan.

## Don't

- Don't bypass `phpstan`, `rector`, or `check-cs` with skip comments unless the user asks for it.
- Don't run `lint.fix` or `fix-cs` automatically when the user only asked for analysis — the autofix changes files.
- Don't push, force-push, or open PRs unless explicitly asked.
28 changes: 14 additions & 14 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,32 @@
"php": ">=8.3",
"composer/pcre": "^3.3.2",
"composer/xdebug-handler": "^3.0.5",
"friendsofphp/php-cs-fixer": "^3.92.4",
"illuminate/container": "12.39",
"nette/utils": "4.0.2",
"friendsofphp/php-cs-fixer": "^3.92.4|^4.0",
"illuminate/container": "12.39.*",
"nette/utils": "4.0.*",
"sebastian/diff": "^6.0.2",
"squizlabs/php_codesniffer": "^4.0.1",
"symfony/console": "^6.4.24",
"symfony/finder": "^7.3.0",
"symfony/console": "^6.4.24|7.0.*",
"symfony/finder": "^7.4|8.0.*",
"symplify/coding-standard": "^13.0",
"symplify/easy-parallel": "^11.2.2",
"webmozart/assert": "^1.12"
},
"require-dev": {
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.21",
"phpstan/phpstan-phpunit": "^2.0.7",
"phpstan/phpstan": "^2.1.33|^3.0",
"phpstan/phpstan-phpunit": "^2.0.7|^3.0",
"phpstan/phpstan-webmozart-assert": "^2.0",
"phpunit/phpunit": "^11.5.27",
"rector/jack": "^0.2.9",
"rector/rector": "^2.2",
"rector/type-perfect": "^2.1.0",
"rector/jack": "^1.0",
"rector/rector": "^2.3",
"rector/type-perfect": "^2.1.0|^3.0",
"symplify/phpstan-extensions": "^12.0.1",
"symplify/vendor-patches": "^11.5",
"tomasvotruba/class-leak": "^2.0.5",
"tomasvotruba/type-coverage": "^2.0.2",
"tomasvotruba/unused-public": "^2.0.1",
"tracy/tracy": "^2.10.10"
"tomasvotruba/class-leak": "^2.1",
"tomasvotruba/type-coverage": "^2.1",
"tomasvotruba/unused-public": "^2.1|^3.0",
"tracy/tracy": "^2.11|^3.0"
},
"autoload": {
"psr-4": {
Expand Down
75 changes: 8 additions & 67 deletions config/set/common/spaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,13 @@

declare(strict_types=1);

use PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\LanguageConstructSpacingSniff;
use PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\SuperfluousWhitespaceSniff;
use PhpCsFixer\Fixer\CastNotation\CastSpacesFixer;
use PhpCsFixer\Fixer\ClassNotation\ClassAttributesSeparationFixer;
use PhpCsFixer\Fixer\ClassNotation\NoBlankLinesAfterClassOpeningFixer;
use PhpCsFixer\Fixer\ClassNotation\SingleTraitInsertPerStatementFixer;
use PhpCsFixer\Fixer\FunctionNotation\MethodArgumentSpaceFixer;
use PhpCsFixer\Fixer\FunctionNotation\ReturnTypeDeclarationFixer;
use PhpCsFixer\Fixer\NamespaceNotation\NoLeadingNamespaceWhitespaceFixer;
use PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer;
use PhpCsFixer\Fixer\Operator\ConcatSpaceFixer;
use PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer;
use PhpCsFixer\Fixer\Operator\TernaryOperatorSpacesFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocSingleLineVarSpacingFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use PhpCsFixer\Fixer\Semicolon\NoSinglelineWhitespaceBeforeSemicolonsFixer;
use PhpCsFixer\Fixer\Semicolon\SpaceAfterSemicolonFixer;
use PhpCsFixer\Fixer\Whitespace\MethodChainingIndentationFixer;
use PhpCsFixer\Fixer\Whitespace\NoExtraBlankLinesFixer;
use PhpCsFixer\Fixer\Whitespace\NoSpacesAroundOffsetFixer;
use PhpCsFixer\Fixer\Whitespace\NoWhitespaceInBlankLineFixer;
use PhpCsFixer\Fixer\Whitespace\TypeDeclarationSpacesFixer;
use Symplify\CodingStandard\Fixer\Spacing\StandaloneLinePromotedPropertyFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\Config\Level\SpacesLevel;

return ECSConfig::configure()
->withRules([
TypeDeclarationSpacesFixer::class,
StandaloneLinePromotedPropertyFixer::class,
BlankLineAfterOpeningTagFixer::class,
MethodChainingIndentationFixer::class,
NotOperatorWithSuccessorSpaceFixer::class,
CastSpacesFixer::class,
ClassAttributesSeparationFixer::class,
SingleTraitInsertPerStatementFixer::class,
NoBlankLinesAfterClassOpeningFixer::class,
NoSinglelineWhitespaceBeforeSemicolonsFixer::class,
PhpdocSingleLineVarSpacingFixer::class,
NoLeadingNamespaceWhitespaceFixer::class,
NoSpacesAroundOffsetFixer::class,
NoWhitespaceInBlankLineFixer::class,
ReturnTypeDeclarationFixer::class,
SpaceAfterSemicolonFixer::class,
TernaryOperatorSpacesFixer::class,
MethodArgumentSpaceFixer::class,
LanguageConstructSpacingSniff::class,
])
->withConfiguredRule(ClassAttributesSeparationFixer::class, [
'elements' => [
'const' => 'one',
'property' => 'one',
'method' => 'one',
],
])
->withConfiguredRule(NoExtraBlankLinesFixer::class, [
'tokens' => ['extra', 'throw', 'use'],
])
->withConfiguredRule(ConcatSpaceFixer::class, [
'spacing' => 'one',
])
->withConfiguredRule(SuperfluousWhitespaceSniff::class, [
'ignoreBlankLines' => false,
])
->withConfiguredRule(BinaryOperatorSpacesFixer::class, [
'operators' => [
'=>' => 'single_space',
'=' => 'single_space',
],
]);
return static function (ECSConfig $ecsConfig): void {
// the rule order matters, as it's used in withSpacesLevel() method
// place the safest rules first, follow by more complex ones
$ecsConfig->rules(SpacesLevel::RULES);

$ecsConfig->rulesWithConfiguration(SpacesLevel::RULE_CONFIGURATIONS);
};
2 changes: 1 addition & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
naming: true,
earlyReturn: true
)
->withPaths([__DIR__ . '/bin', __DIR__ . '/config', __DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/tests'])
->withPaths([__DIR__ . '/bin', __DIR__ . '/config', __DIR__ . '/src', __DIR__ . '/tests'])
->withRootFiles()
->withImportNames()
->withBootstrapFiles([__DIR__ . '/tests/bootstrap.php'])
Expand Down
112 changes: 112 additions & 0 deletions src/Config/Level/SpacesLevel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Config\Level;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\LanguageConstructSpacingSniff;
use PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\SuperfluousWhitespaceSniff;
use PhpCsFixer\Fixer\CastNotation\CastSpacesFixer;
use PhpCsFixer\Fixer\ClassNotation\ClassAttributesSeparationFixer;
use PhpCsFixer\Fixer\ClassNotation\NoBlankLinesAfterClassOpeningFixer;
use PhpCsFixer\Fixer\ClassNotation\SingleTraitInsertPerStatementFixer;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\FunctionNotation\MethodArgumentSpaceFixer;
use PhpCsFixer\Fixer\FunctionNotation\ReturnTypeDeclarationFixer;
use PhpCsFixer\Fixer\NamespaceNotation\NoLeadingNamespaceWhitespaceFixer;
use PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer;
use PhpCsFixer\Fixer\Operator\ConcatSpaceFixer;
use PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer;
use PhpCsFixer\Fixer\Operator\TernaryOperatorSpacesFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocSingleLineVarSpacingFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use PhpCsFixer\Fixer\Semicolon\NoSinglelineWhitespaceBeforeSemicolonsFixer;
use PhpCsFixer\Fixer\Semicolon\SpaceAfterSemicolonFixer;
use PhpCsFixer\Fixer\Whitespace\MethodChainingIndentationFixer;
use PhpCsFixer\Fixer\Whitespace\NoExtraBlankLinesFixer;
use PhpCsFixer\Fixer\Whitespace\NoSpacesAroundOffsetFixer;
use PhpCsFixer\Fixer\Whitespace\NoWhitespaceInBlankLineFixer;
use PhpCsFixer\Fixer\Whitespace\TypeDeclarationSpacesFixer;
use Symplify\CodingStandard\Fixer\Spacing\StandaloneLinePromotedPropertyFixer;

/**
* Key 0 = level 0
* Key 22 = level 22
*
* Start at 0, go slowly higher, one level per PR, and improve your spacing coverage.
*
* From the safest rules to more changing ones.
*
* This list can change in time, based on community feedback,
* what rules are safer than other. The safest rules will be always in the top.
*/
final class SpacesLevel
{
/**
* @var array<class-string<Sniff|FixerInterface>>
*/
public const array RULES = [
// easy picks - pure whitespace cleanup with no formatting opinion
NoLeadingNamespaceWhitespaceFixer::class,
NoSinglelineWhitespaceBeforeSemicolonsFixer::class,
NoWhitespaceInBlankLineFixer::class,
NoSpacesAroundOffsetFixer::class,
SpaceAfterSemicolonFixer::class,
NoBlankLinesAfterClassOpeningFixer::class,
BlankLineAfterOpeningTagFixer::class,
SingleTraitInsertPerStatementFixer::class,
PhpdocSingleLineVarSpacingFixer::class,
LanguageConstructSpacingSniff::class,

// operator and type spacing
CastSpacesFixer::class,
NotOperatorWithSuccessorSpaceFixer::class,
TernaryOperatorSpacesFixer::class,
ReturnTypeDeclarationFixer::class,
TypeDeclarationSpacesFixer::class,
SuperfluousWhitespaceSniff::class,

// configurable, more impactful
ConcatSpaceFixer::class,
BinaryOperatorSpacesFixer::class,

// most invasive structural changes
MethodChainingIndentationFixer::class,
StandaloneLinePromotedPropertyFixer::class,
MethodArgumentSpaceFixer::class,
ClassAttributesSeparationFixer::class,
NoExtraBlankLinesFixer::class,
];

/**
* Configurations matching the spaces set, applied when a configurable rule
* is enabled via withSpacesLevel(). Rules absent from this map use defaults.
*
* @var array<class-string<Sniff|FixerInterface>, mixed[]>
*/
public const array RULE_CONFIGURATIONS = [
ClassAttributesSeparationFixer::class => [
'elements' => [
'const' => 'one',
'property' => 'one',
'method' => 'one',
],
],
NoExtraBlankLinesFixer::class => [
'tokens' => ['extra', 'throw', 'use'],
],
ConcatSpaceFixer::class => [
'spacing' => 'one',
],
SuperfluousWhitespaceSniff::class => [
'ignoreBlankLines' => false,
],
BinaryOperatorSpacesFixer::class => [
'operators' => [
'=>' => 'single_space',
'=' => 'single_space',
],
],
];
}
Loading
Loading