stolt / coding-standard-migrator
A CLI tool and library which migrates a PHP coding-standard configuration to a Mago based one.
Package info
github.com/raphaelstolt/coding-standard-migrator
pkg:composer/stolt/coding-standard-migrator
Fund package maintenance!
Requires
- php: ^8.3
- ext-json: *
- ext-libxml: *
- ext-simplexml: *
- symfony/console: ^6.4 || ^7.0
Requires (Dev)
- carthage-software/mago: ^1.48.1
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A CLI tool and library which migrates a PHP coding-standard configuration to another coding standard. Mago is the migration target, the supported sources are PHP-CS-Fixer, PHP_CodeSniffer, and Pint.
| Migration path | Source configuration |
|---|---|
php-cs-fixer → mago |
.php-cs-fixer.php, .php-cs-fixer.dist.php, .php_cs, .php_cs.dist |
phpcs → mago |
phpcs.xml, phpcs.xml.dist, .phpcs.xml, .phpcs.xml.dist, ruleset.xml |
pint → mago |
pint.json, .pint.json |
Since the coding standards do not overlap one to one, a migration is a starting point and not a finished configuration. Every migration therefore comes with a report which states what was translated, what the target standard does implicitly, and which rules need a manual decision.
Why Coding Standard Migrator?
Moving from PHP-CS-Fixer, PHP_CodeSniffer, or Pint to Mago is not just a configuration-file rename.
Different tools expose different rule sets and semantics.
Coding Standard Migrator makes that migration explicit:
- analyze what can be migrated
- identify gaps
- generate a Mago configuration
- review the migration report
- verify the result with Mago
What the Coding Standard Migrator does not do
The Coding Standard Migrator does not guarantee behavioural equivalence.
It translates configuration where a known Mago equivalent exists and explicitly reports mappings that require a manual review.
Installation
composer require --dev stolt/coding-standard-migrator
Usage
See how much of the configuration of the current directory a migration would carry over before running one:
vendor/bin/cs-migrator analyze
PHP-CS-Fixer configuration ────────────────────────── Rules: 10 Mappable: 6 Equivalent: 5 Partial: 1 Unsupported: 2 No mapping known: 1 Disabled in source: 0 Linter candidates: 3 Redundant with Mago: 1 Migration confidence: 65%
Mappable is the sum of Equivalent and Partial, the rules which end up in the
migrated configuration. The other three rule counts each name a reason why a rule does
not: Unsupported and No mapping known need a manual decision, Disabled in source
never asked for anything. Linter candidates and Redundant with Mago are cross
sections of the same rules, the ones which need a [linter.rules] entry, and the ones
the Mago formatter applies anyway.
The migration confidence is the share of the source configuration which survives, with
a partial mapping counting half and a rule the target standard covers anyway or which
was disabled to begin with counting fully. Add --verbose to list the rules which need
a manual decision, or --fail-under 80 to turn the confidence into a CI gate.
Migrate the PHP-CS-Fixer configuration of the current directory to a mago.toml:
vendor/bin/cs-migrator migrate
Migrate a PHP_CodeSniffer ruleset or a Pint configuration instead:
vendor/bin/cs-migrator migrate --from phpcs vendor/bin/cs-migrator migrate --from pint
Preview the migration without writing anything:
vendor/bin/cs-migrator migrate --dry-run
Mapped rules ------------ ------------------------ ---------------------- ---------------------------------- Rule Outcome Mago ------------------------ ---------------------- ---------------------------------- Config::setIndent() formatter option use-tabs = false, tab-width = 4 @PSR12 formatter option preset = "psr-12" array_syntax linter rule array-style no_trailing_whitespace covered by formatter - header_comment unsupported - ------------------------ ---------------------- ----------------------------------
Options of the migrate command
| Option | Description |
|---|---|
--from |
The coding standard to migrate from, defaults to php-cs-fixer |
--to |
The coding standard to migrate to, defaults to mago |
--working-dir, -d |
The directory to run in, defaults to the current one |
--config, -c |
The source configuration file, autodetected when omitted |
--target, -t |
The file to write to, defaults to the target standard's default |
--php-version |
The PHP version to pin in the migrated configuration |
--path, -p |
A path the coding standard applies to, repeatable |
--dry-run |
Shows the migrated configuration instead of writing it |
--force, -f |
Overwrites an existing target configuration |
--fail-on-unmapped |
Exits non-zero when a rule could not be migrated, made for CI |
Options of the analyze command
| Option | Description |
|---|---|
--from |
The coding standard to analyze, defaults to php-cs-fixer |
--to |
The coding standard to analyze the migration to, defaults to mago |
--working-dir, -d |
The directory to run in, defaults to the current one |
--config, -c |
The configuration file to analyze, autodetected when omitted |
--fail-under |
Exits non-zero below this migration confidence, made for CI |
The command is also reachable as analyse. The supported standards and migration paths
are listed by the standards command.
Usage via cpx
If you don't want to add coding-standard-migrator as a development dependency, you can run it directly with cpx.
Install cpx globally:
composer global require cpx/cpx
Then run the migrator directly from your project:
cpx stolt/coding-standard-migrator analyze
To migrate your PHP-CS-Fixer configuration to Mago:
cpx stolt/coding-standard-migrator migrate
You can pass the same options as with the locally installed binary:
cpx stolt/coding-standard-migrator migrate --from pint cpx stolt/coding-standard-migrator migrate --from phpcs cpx stolt/coding-standard-migrator migrate --dry-run cpx stolt/coding-standard-migrator analyze --fail-under 80
cpx keeps the package isolated from your project's Composer dependencies, so you can use coding-standard-migrator
without adding it to composer.json. It also caches the package between runs, making later invocations faster.
Tip: If
coding-standard-migratoris already installed in your project, cpx can use the project's local binary first. This makescpx stolt/coding-standard-migrator ...convenient both for projects that have the tool installed and for one-off migrations.
Usage as a library
use Stolt\CodingStandardMigrator\Migration\MigrationEngine; use Stolt\CodingStandardMigrator\Migration\MigrationRequest; use Stolt\CodingStandardMigrator\Standard\Standard; $engine = new MigrationEngine(); $request = new MigrationRequest( from: Standard::PhpCsFixer, to: Standard::Mago, sourceFile: $engine->locateSourceConfiguration(Standard::PhpCsFixer, \getcwd()), phpVersion: '8.3', ); echo $engine->analyze($request)->confidence() . '%' . \PHP_EOL; $result = $engine->migrate($request); foreach ($result->report as $mapping) { echo $mapping->sourceRule . ': ' . $mapping->outcome->label() . \PHP_EOL; } if ($result->isComplete()) { \file_put_contents($result->targetFileName, $result->configuration); }
How a migration works
ConfigurationReader -> Ruleset -> RuleMapper -> MappingReport -> ConfigurationWriter
-
A
ConfigurationReaderturns the configuration file of the source standard into a tool-agnosticSourceConfiguration, holding aRulesetplus the settings which live outside of the rules, such as the indent and the line ending. -
A
RuleMappertranslates everyRuleinto aMappingand collects them in aMappingReport. Each mapping carries one of these outcomes:Outcome Meaning linter ruleThe rule became one or more rules of the target linter formatter optionThe rule became one or more options of the target formatter covered by formatterThe target formatter does this anyway, no configuration needed skippedThe rule was disabled in the source and needed no translation unsupportedThe target standard knowingly has no equivalent unknownNo mapping is known, the rule needs a manual decision A translated mapping additionally carries a
MappingFidelity, eitherequivalentorpartial. Mapping tables state their caveats as notes, which makes a note the default signal for a partial mapping, overridable per mapping. -
A
ConfigurationWriterrenders the report as the configuration file of the target standard. Rules which need attention are added as comments to the generated file.MigrationAnalysisinstead reduces the report to the counts theanalyzecommand shows, which is why analyzing never touches a writer.
Mappings are only added to a
RuleTable once they are verified against the
Mago formatter reference
and the Mago linter rules.
Everything else stays unknown, so a migration never invents a setting which does not
exist.
Rule sets are shared ground between the standards: a @PSR12 rule of PHP-CS-Fixer, a
<rule ref="PSR12"/> of PHP_CodeSniffer, and a "preset": "psr12" of Pint all end up
as the same rule-set entry, which the tables map onto a Mago formatter preset.
There are two rule vocabularies and therefore two tables:
PhpCsFixerToMago\MappingTable, which is shared by the PHP-CS-Fixer and the Pint migration, as Pint is a wrapper around PHP-CS-Fixer and configures its rules;PhpCodeSnifferToMago\MappingTable, which maps sniffs by theirStandard.Category.Sniffname and ignores the error code of aStandard.Category.Sniff.ErrorCodereference.
Supporting another coding standard
Adding one means adding implementations, not changing the engine:
- as a migration source: implement
Reader\ConfigurationReaderplus aMapping\RuleMapperper target standard. For Mago as the target, extendMapping\Mago\AbstractRuleMapper, which brings the pipeline, and point it at aMapping\Mago\RuleTable; - as a migration target: implement
Writer\ConfigurationWriterplus aMapping\RuleMapperper source standard.
Register them in Standard\StandardRegistry and the migrate command picks up the new
migration path.
Known limitations
- Rule sets without a Mago preset, such as
@Symfonyor theSquizstandard, are not expanded into their rules. They are reported so they can be migrated rule by rule, for instance by way ofphp-cs-fixer describeorphpcs --explain. - For PHP-CS-Fixer and Pint the paths of the target configuration are guessed from the
directories next to the source configuration, as PHP-CS-Fixer keeps them in a
Symfony\Component\Finder\Finderinstance which does not expose them, and Pint defaults to the whole project. Pass--pathto be explicit. PHP_CodeSniffer rulesets declare their paths, so they are read from the<file>elements. - The mapping tables cover the commonly used rules and sniffs, not all of the several hundred PHP-CS-Fixer rules or the sniffs of every third party standard.
- Exclusions are not migrated yet, neither the
exclude,notName, andnotPathkeys of a Pint configuration nor the<exclude-pattern>elements of a PHP_CodeSniffer ruleset end up in Mago'ssource.excludes.
Contributing
If you're considering contributing to this library, have a look at this repository's CONTRIBUTING.md for more advice.
License
This package is licensed under the MIT license, see LICENSE.md.
