Project-specific conventions for rector/rector-src. See CONTRIBUTING.md for the human-facing version.
- PHP
^8.4required. Do not use syntax that breaks on 8.4. - The package is
rector/rector-src; itreplacesrector/rector. - Sibling extension packages (
rector-doctrine,rector-symfony,rector-phpunit,rector-downgrade-php) are pulled in asdev-main.
src/— core engine (Rector\namespace).rules/— built-in Rector rules, also underRector\namespace (PSR-4 maps bothsrc/andrules/toRector\).rules-tests/— tests forrules/, namespaceRector\Tests\.tests/— tests forsrc/, sameRector\Tests\namespace.utils/— internal dev tooling (Rector\Utils\).config/— config sets/presets (kept as plain class-string literals; do not let Rector rewrite them).build/target-repository/docs— documentation lives here, not in repo root.
declare(strict_types=1);at the top of every PHP file.- Classes are
finalby default;abstractonly when explicitly intended for extension. - Constructor property promotion with
private readonlyfor dependencies. - Run
composer fix-cs(ECS) before committing; the ruleset is symplify + common + psr12. - Do not add
@author,@since, or change/@vartags that ECS would strip. - No emojis in source.
Match what composer complete-check runs:
composer check-cs # ECS, read-only
composer phpstan # PHPStan level 8, 512M
vendor/bin/phpunitPHPStan extras enabled in phpstan.neon:
type-perfect:no_mixed,null_over_false,narrow_param,narrow_return— return/param types must be narrow; prefernulloverfalsefor "no result".unused-public: public methods/properties/constants must be used somewhere. If you add a new public API, expect to use it or mark it accordingly.symplify/phpstan-rules+rector-rules: forbidsvar_dump,dd,property_exists,class_exists,@error suppression, dynamic names, etc., outside the narrowly listed exceptions inphpstan.neon. Do not add new exceptions casually — fix the code.
Rector applies to its own source: composer rector runs the config in rector.php.
Required shape (see rules/Php85/Rector/FuncCall/OrdSingleByteRector.php as a canonical example):
- Namespace mirrors the path:
Rector\<Category>\Rector\<NodeType>\<RuleName>. final classextendsRector\Rector\AbstractRector.- Implement
MinPhpVersionInterfacewhen the rule targets a specific PHP version; return aPhpVersionFeature::*constant fromprovideMinPhpVersion(). - Implement three methods:
getRuleDefinition(): RuleDefinition— one-line description + at least oneCodeSample(before/after).getNodeTypes(): array— list ofPhpParser\Node\...classes to subscribe to.refactor(Node $node): ?Node— return the new node,nullfor no change, orNodeVisitor::REMOVE_NODEto delete. Do not return integer values exceptREMOVE_NODE(seerector.noIntegerRefactorReturn).
- Add a
@seePHPDoc pointing to the test class:@see \Rector\Tests\<...>\<RuleName>Test. - Inject services via constructor promotion (
ValueResolver, etc.); reuse whatAbstractRectoralready exposes ($this->nodeNameResolver,$this->nodeTypeResolver,$this->nodeFactory,$this->nodeComparator). - Bail out early: check
isFirstClassCallable(), name match, arg presence, type, then transform.
Mirror the rule path under rules-tests/:
rules-tests/<Category>/Rector/<NodeType>/<RuleName>/
├── <RuleName>Test.php
├── Fixture/
│ ├── some_case.php.inc
│ └── skip_some_case.php.inc
└── config/
└── configured_rule.php
- Test class extends
Rector\Testing\PHPUnit\AbstractRectorTestCase, uses#[DataProvider('provideData')], and returnsself::yieldFilesFromDirectory(__DIR__ . '/Fixture'). provideConfigFilePath()returns the config file that registers the rule and pinsphpVersion(PhpVersion::PHP_XX)when version-bound.- Fixtures use the
.php.incextension. Before/after are separated by a line containing exactly-----. A fixture with no-----separator asserts the file is unchanged; name thoseskip_*.php.inc. - The fixture's
namespacemust match its directory. Fixture/,Source/,Expected/directories are auto-skipped by ECS, PHPStan, and Rector — don't try to make them conformant.
- Don't introduce new abstractions, traits, or helpers beyond what the task needs — the existing
AbstractRectoralready exposes most node helpers. - Don't modify
phpstan.neonignore lists orecs.phpskip lists to silence a new warning; fix the underlying code instead. - Don't add
class_exists/property_exists/function_existsruntime checks — useReflectionProvider(errors fromsymplify/phpstan-ruleswill reject the PR). - Don't bypass
instanceofrules by adding a new ignore; only existingSkipper/internal paths are allowed. - Don't touch files under
config/with code-style rewrites — class strings there are intentional. - Don't push docs into repo root — they belong under
build/target-repository/docs.
.github/workflows/code_analysis.yaml, tests.yaml, rector.yaml, e2e*.yaml, and phpstan_printer_test.yaml mirror the local composer complete-check. If it passes locally with composer complete-check && composer rector, CI usually agrees.