Skip to content

Commit 8c25212

Browse files
authored
Improve the MakeRuleCommand (#323)
* Add more options to the MakeRuleCommand * New template dir * Simplify the make rule command * Refactor the make rule command * Update docs
1 parent 4649b0f commit 8c25212

12 files changed

+528
-49
lines changed

Diff for: README.md

+26
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,32 @@ return RectorConfig::configure()
7272
| [LaravelSetList::LARAVEL_LEGACY_FACTORIES_TO_CLASSES](https://github.com/driftingly/rector-laravel/blob/main/config/sets/laravel-legacy-factories-to-classes.php) | Migrates Eloquent legacy model factories (with closures) into class based factories.<br/>https://laravel.com/docs/8.x/releases#model-factory-classes |
7373
| [LaravelSetList::LARAVEL_STATIC_TO_INJECTION](https://github.com/driftingly/rector-laravel/blob/main/config/sets/laravel-static-to-injection.php) | Replaces Laravel's Facades with Dependency Injection.<br/>https://tomasvotruba.com/blog/2019/03/04/how-to-turn-laravel-from-static-to-dependency-injection-in-one-day/<br/>https://laravel.com/docs/11.x/facades#facades-vs-dependency-injection |
7474

75+
## Creating New Rules
76+
77+
You can create a new rule using the composer script:
78+
79+
```bash
80+
composer make:rule -- YourRuleName
81+
```
82+
83+
This will generate a new rule class in `src/Rector/` along with the corresponding test files.
84+
85+
### Command Options
86+
87+
- `--configurable` or `-c`: Create a configurable rule that implements `ConfigurableRectorInterface`
88+
89+
### Directory Structure
90+
91+
You can specify a subdirectory structure by including slashes in the rule name:
92+
93+
```bash
94+
composer make:rule -- If_/ConvertIfToWhen
95+
```
96+
97+
This will create a rule in the `src/Rector/If_/` directory with the namespace `RectorLaravel\Rector\If_`.
98+
99+
Remember to always add `--` before the arguments when using the composer script. This separator tells Composer that the following arguments should be passed to the script rather than being interpreted as Composer arguments.
100+
75101
## Contributors
76102

77103
Thank you everyone who works so hard on improving this package:

Diff for: commands/MakeRuleCommand.php

+159-36
Original file line numberDiff line numberDiff line change
@@ -6,60 +6,176 @@
66

77
use Nette\Utils\FileSystem;
88
use Rector\Console\Command\CustomRuleCommand;
9-
use Symfony\Component\Finder\Finder;
10-
use Symfony\Component\Finder\SplFileInfo;
119

1210
/**
11+
* Generates scaffolding for a new Rector rule
12+
*
1313
* Modified version of
1414
*
1515
* @see CustomRuleCommand
1616
*/
1717
final class MakeRuleCommand
1818
{
19-
public function execute(string $ruleName): int
19+
private const string TEMPLATE_DIR = __DIR__ . '/../templates';
20+
21+
private bool $configurable = false;
22+
private ?string $directory = null;
23+
private string $rulesNamespace = 'RectorLaravel\\Rector';
24+
private string $testsNamespace = 'RectorLaravel\\Tests\\Rector';
25+
private string $testDir = 'tests/Rector/';
26+
private string $currentDirectory;
27+
28+
public function execute(string $ruleName, bool $configurable = false): int
2029
{
21-
$rectorName = $this->getRuleName($ruleName);
22-
$rulesNamespace = 'RectorLaravel\\Rector';
23-
$testsNamespace = 'RectorLaravel\\Tests\\Rector';
30+
$this->configurable = $configurable;
2431

25-
// find all files in templates directory
26-
$finder = Finder::create()
27-
->files()
28-
->in(__DIR__ . '/../templates/new-rule')
29-
->notName('__NAME__Test.php');
32+
// Validate rule name
33+
if (empty($ruleName)) {
34+
echo PHP_EOL . 'Rule name must not be empty.' . PHP_EOL;
35+
36+
return 1;
37+
}
3038

31-
$finder->append([
32-
new SplFileInfo(
33-
__DIR__ . '/../templates/new-rule/tests/Rector/__NAME__/__NAME__Test.php',
34-
'tests/Rector/__NAME__',
35-
'tests/Rector/__NAME__/__NAME__Test.php',
36-
),
37-
]);
39+
$this->currentDirectory = (string) getcwd();
3840

39-
$currentDirectory = getcwd();
41+
$rectorName = $this->setNamespacesAndDirectories($ruleName);
4042

41-
$generatedFilePaths = [];
43+
$generatedFilePaths = [
44+
$this->createRule($rectorName),
45+
$this->createTest($rectorName),
46+
$this->createTestFixture($rectorName),
47+
$this->createTestConfig($rectorName),
48+
];
4249

43-
$fileInfos = iterator_to_array($finder->getIterator());
50+
echo 'Generated files:' . PHP_EOL . PHP_EOL . "\t";
51+
echo implode(PHP_EOL . "\t", array_filter($generatedFilePaths)) . PHP_EOL;
4452

45-
foreach ($fileInfos as $fileInfo) {
46-
$newContent = $this->replaceNameVariable($rectorName, $fileInfo->getContents());
47-
$newContent = $this->replaceNamespaceVariable($rulesNamespace, $newContent);
48-
$newContent = $this->replaceTestsNamespaceVariable($testsNamespace, $newContent);
49-
$newFilePath = $this->replaceNameVariable($rectorName, $fileInfo->getRelativePathname());
53+
return 0;
54+
}
5055

51-
FileSystem::write($currentDirectory . '/' . $newFilePath, $newContent, null);
56+
private function setNamespacesAndDirectories(string $ruleName): string
57+
{
58+
// Extract directory and rule name if format contains slashes like "Directory/SubDir/More/RuleName"
59+
if (str_contains($ruleName, '/')) {
60+
$parts = explode('/', $ruleName);
61+
// The last part is the rule name
62+
$ruleName = array_pop($parts);
63+
// All other parts form the directory path
64+
if (! empty($parts)) {
65+
$this->directory = implode('/', $parts) . '/';
66+
}
67+
}
5268

53-
$generatedFilePaths[] = $newFilePath;
69+
// Add directory to namespace if provided
70+
if ($this->directory !== null) {
71+
// Clean directory name (ensure it ends with a backslash for paths but not for namespace)
72+
$cleanDir = rtrim($this->directory, '\\/');
73+
$this->directory = $cleanDir . '/';
74+
$namespaceDir = str_replace('/', '\\', $cleanDir);
75+
$this->rulesNamespace .= '\\' . $namespaceDir;
76+
$this->testsNamespace .= '\\' . $namespaceDir;
5477
}
5578

56-
echo PHP_EOL . 'Generated files:' . PHP_EOL . PHP_EOL . "\t";
57-
echo implode(PHP_EOL . "\t", $generatedFilePaths) . PHP_EOL;
79+
$rectorName = $this->formatRuleName($ruleName);
5880

59-
return 0;
81+
$this->testDir .= $this->directory . $rectorName;
82+
83+
return $rectorName;
84+
}
85+
86+
private function createRule(string $rectorName): ?string
87+
{
88+
$ruleFilePath = null;
89+
$ruleTemplateFile = $this->configurable
90+
? self::TEMPLATE_DIR . '/configurable-rule.php.template'
91+
: self::TEMPLATE_DIR . '/non-configurable-rule.php.template';
92+
93+
if (file_exists($ruleTemplateFile)) {
94+
$contents = file_get_contents($ruleTemplateFile);
95+
96+
$newContent = $this->replaceNameVariable($rectorName, $contents);
97+
$newContent = $this->replaceNamespaceVariable($newContent);
98+
$newContent = $this->replaceTestsNamespaceVariable($newContent);
99+
100+
// Create the rule file path
101+
$ruleFilePath = 'src/Rector/';
102+
if ($this->directory !== null) {
103+
$ruleFilePath .= $this->directory;
104+
}
105+
$ruleFilePath .= $rectorName . '.php';
106+
107+
// Ensure directory exists
108+
$this->ensureDirectoryExists($this->currentDirectory . '/' . dirname($ruleFilePath));
109+
FileSystem::write($this->currentDirectory . '/' . $ruleFilePath, $newContent, null);
110+
}
111+
112+
return $ruleFilePath;
113+
}
114+
115+
private function createTest(string $rectorName): ?string
116+
{
117+
$testFilePath = null;
118+
$testTemplateFile = self::TEMPLATE_DIR . '/test.php.template';
119+
120+
if (file_exists($testTemplateFile)) {
121+
$contents = file_get_contents($testTemplateFile);
122+
$newContent = $this->replaceNameVariable($rectorName, $contents);
123+
$newContent = $this->replaceNamespaceVariable($newContent);
124+
$newContent = $this->replaceTestsNamespaceVariable($newContent);
125+
126+
$testFilePath = $this->testDir . '/' . $rectorName . 'Test.php';
127+
$this->ensureDirectoryExists($this->currentDirectory . '/' . dirname($testFilePath));
128+
FileSystem::write($this->currentDirectory . '/' . $testFilePath, $newContent, null);
129+
}
130+
131+
return $testFilePath;
132+
}
133+
134+
private function createTestFixture(string $rectorName): ?string
135+
{
136+
$fixtureFilePath = null;
137+
$fixtureDir = $this->testDir . '/Fixture';
138+
$this->ensureDirectoryExists($this->currentDirectory . '/' . $fixtureDir);
139+
140+
// Create fixture file from template
141+
$fixtureTemplateFile = self::TEMPLATE_DIR . '/fixture.php.inc.template';
142+
143+
if (file_exists($fixtureTemplateFile)) {
144+
$fixtureContents = file_get_contents($fixtureTemplateFile);
145+
$fixtureContents = $this->replaceNameVariable($rectorName, $fixtureContents);
146+
$fixtureContents = $this->replaceTestsNamespaceVariable($fixtureContents);
147+
148+
$fixtureFilePath = $fixtureDir . '/some_class.php.inc';
149+
FileSystem::write($this->currentDirectory . '/' . $fixtureFilePath, $fixtureContents, null);
150+
}
151+
152+
return $fixtureFilePath;
60153
}
61154

62-
private function getRuleName(string $ruleName): string
155+
private function createTestConfig(string $rectorName): ?string
156+
{
157+
$configFilePath = null;
158+
$configDir = $this->testDir . '/config';
159+
$this->ensureDirectoryExists($this->currentDirectory . '/' . $configDir);
160+
161+
// Create config file from template - select based on configurability
162+
$configTemplateFile = $this->configurable
163+
? self::TEMPLATE_DIR . '/configurable-config.php.template'
164+
: self::TEMPLATE_DIR . '/non-configurable-config.php.template';
165+
166+
if (file_exists($configTemplateFile)) {
167+
$configContents = file_get_contents($configTemplateFile);
168+
$configContents = $this->replaceNameVariable($rectorName, $configContents);
169+
$configContents = $this->replaceNamespaceVariable($configContents);
170+
171+
$configFilePath = $configDir . '/configured_rule.php';
172+
FileSystem::write($this->currentDirectory . '/' . $configFilePath, $configContents, null);
173+
}
174+
175+
return $configFilePath;
176+
}
177+
178+
private function formatRuleName(string $ruleName): string
63179
{
64180
if (! str_ends_with($ruleName, 'Rector')) {
65181
$ruleName .= 'Rector';
@@ -73,13 +189,20 @@ private function replaceNameVariable(string $rectorName, string $contents): stri
73189
return str_replace('__NAME__', $rectorName, $contents);
74190
}
75191

76-
private function replaceNamespaceVariable(string $namespace, string $contents): string
192+
private function replaceNamespaceVariable(string $contents): string
77193
{
78-
return str_replace('__NAMESPACE__', $namespace, $contents);
194+
return str_replace('__NAMESPACE__', $this->rulesNamespace, $contents);
79195
}
80196

81-
private function replaceTestsNamespaceVariable(string $testsNamespace, string $contents): string
197+
private function replaceTestsNamespaceVariable(string $contents): string
82198
{
83-
return str_replace('__TESTS_NAMESPACE__', $testsNamespace, $contents);
199+
return str_replace('__TESTS_NAMESPACE__', $this->testsNamespace, $contents);
200+
}
201+
202+
private function ensureDirectoryExists(string $directory): void
203+
{
204+
if (! is_dir($directory)) {
205+
mkdir($directory, 0777, true);
206+
}
84207
}
85208
}

Diff for: commands/make-rule.php

+17-8
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@
33

44
require __DIR__ . '/../vendor/autoload.php';
55

6-
// get the first argument, the rule name
7-
// make sure we have at least one argument
8-
if ($argc < 2) {
9-
echo PHP_EOL . 'Please provide the name of the rule!' . PHP_EOL;
10-
exit(1);
11-
}
6+
// Parse command line arguments
7+
$configurable = false;
8+
$ruleName = '';
9+
10+
// Parse options
11+
foreach ($argv as $i => $arg) {
12+
if ($i === 0) {
13+
continue;
14+
} // Skip script name
1215

13-
$ruleName = $argv[1];
16+
if ($arg === '--configurable' || $arg === '-c') {
17+
$configurable = true;
18+
} elseif (empty($ruleName) && ! str_starts_with($arg, '-')) {
19+
// If it's not an option, and we don't have a rule name yet, it's the rule name
20+
$ruleName = $arg;
21+
}
22+
}
1423

1524
$command = new \RectorLaravel\Commands\MakeRuleCommand;
16-
exit($command->execute($ruleName));
25+
exit($command->execute($ruleName, $configurable));

Diff for: composer.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
"phpunit/phpunit": "^10.5",
2020
"symplify/rule-doc-generator": "^12.2",
2121
"tightenco/duster": "^3.1",
22-
"nette/utils": "^4.0",
23-
"symfony/finder": "^7.2"
22+
"nette/utils": "^4.0"
2423
},
2524
"autoload": {
2625
"psr-4": {

Diff for: docs/rector_rules_overview.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ Change `app()` func calls to facade calls
726726
public function run()
727727
{
728728
- return app('translator')->trans('value');
729-
+ return \Illuminate\Support\Facades\App::get('translator')->trans('value');
729+
+ return \Illuminate\Support\Facades\App::make('translator')->trans('value');
730730
}
731731
}
732732
```

Diff for: templates/configurable-config.php.template

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use __NAMESPACE__\__NAME__;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->ruleWithConfiguration(__NAME__::class, ['option' => 'value']);
10+
};

Diff for: templates/configurable-rule.php.template

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace __NAMESPACE__;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use RectorLaravel\AbstractRector;
10+
use Rector\Contract\Rector\ConfigurableRectorInterface;
11+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
12+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
13+
14+
/**
15+
* @see \__TESTS_NAMESPACE__\__NAME__\__NAME__Test
16+
*/
17+
final class __NAME__ extends AbstractRector implements ConfigurableRectorInterface
18+
{
19+
public function getRuleDefinition(): RuleDefinition
20+
{
21+
return new RuleDefinition(
22+
'Changes something',
23+
[new ConfiguredCodeSample(
24+
<<<'CODE_SAMPLE'
25+
before
26+
CODE_SAMPLE,
27+
<<<'CODE_SAMPLE'
28+
after
29+
CODE_SAMPLE
30+
,
31+
['option' => 'value']
32+
)]
33+
);
34+
}
35+
36+
/**
37+
* @return array<class-string<Node>>
38+
*/
39+
public function getNodeTypes(): array
40+
{
41+
// @todo select node type
42+
return [Class_::class];
43+
}
44+
45+
/**
46+
* @param mixed[] $configuration
47+
*/
48+
public function configure(array $configuration): void
49+
{
50+
// Add configuration logic here
51+
}
52+
53+
/**
54+
* @param Class_ $node
55+
*/
56+
public function refactor(Node $node): ?Node
57+
{
58+
// @todo change the node
59+
60+
return $node;
61+
}
62+
}

0 commit comments

Comments
 (0)