Skip to content

Commit 9940f3e

Browse files
committed
[refactor] Integrate Type Perfect into the refactor workflow (#9)
1 parent 1890a0d commit 9940f3e

7 files changed

Lines changed: 315 additions & 3 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ composer dev-tools code-style
6464

6565
# Refactor code using Rector
6666
composer dev-tools refactor
67+
composer dev-tools refactor -- --type-perfect
68+
composer dev-tools refactor -- --type-perfect-groups=null_over_false,no_mixed
6769

6870
# Check and fix PHPDoc comments
6971
composer dev-tools phpdoc
@@ -113,6 +115,14 @@ The `metrics` command ships with `phpmetrics/phpmetrics` as a direct
113115
dependency of `fast-forward/dev-tools`, so consumer repositories can generate
114116
metrics reports without extra setup.
115117

118+
Type Perfect support is opt-in on top of the `refactor` command. To enable the
119+
Fast Forward integration path in a consumer repository, install the companion
120+
packages first:
121+
122+
```bash
123+
composer require --dev rector/type-perfect phpstan/extension-installer
124+
```
125+
116126
The `skills` command keeps `.agents/skills` aligned with the packaged Fast
117127
Forward skill set. It creates missing links, repairs broken links, and
118128
preserves existing non-symlink directories. The `dev-tools:sync` command calls

docs/advanced/rector-and-phpdoc.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ The default ``rector.php`` also loads shared Rector sets, imports names,
4242
removes unused imports, skips generated directories, and enables Safe migration
4343
rules when ``thecodingmachine/safe`` is installed.
4444

45+
Type Perfect in the Refactor Workflow
46+
-------------------------------------
47+
48+
The ``refactor`` command can optionally run Type Perfect immediately after the
49+
Rector pass:
50+
51+
.. code-block:: bash
52+
53+
composer dev-tools refactor -- --type-perfect
54+
55+
The Fast Forward integration path is intentionally opt-in. It expects
56+
``rector/type-perfect`` and ``phpstan/extension-installer`` to be installed in
57+
the consumer project. When the consumer already has ``phpstan.neon`` or
58+
``phpstan.neon.dist``, DevTools includes that file automatically in the
59+
generated ``tmp/cache/phpstan/type-perfect.neon`` and then enables the selected
60+
Type Perfect groups there.
61+
4562
Why ``.docheader`` Appears Automatically
4663
----------------------------------------
4764

docs/commands/refactor.rst

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ Description
77
-----------
88

99
The ``refactor`` command (alias: ``rector``) runs Rector to automatically
10-
refactor PHP code. Without ``--fix``, it runs in dry-run mode.
10+
refactor PHP code. Without ``--fix``, it runs in dry-run mode. It can also run
11+
`Type Perfect <https://getrector.com/blog/introducing-type-perfect-for-extra-safety>`_
12+
as a follow-up PHPStan safety pass when the companion packages are installed.
1113

1214
Usage
1315
-----
@@ -29,6 +31,16 @@ Options
2931
``--config, -c`` (optional)
3032
Path to the Rector configuration file. Default: ``rector.php``.
3133

34+
``--type-perfect``
35+
Runs Type Perfect after Rector using a generated PHPStan config in
36+
``tmp/cache/phpstan/type-perfect.neon``.
37+
38+
``--type-perfect-groups=<groups>`` (optional)
39+
Comma-separated Type Perfect groups to enable. Supported groups:
40+
``null_over_false``, ``no_mixed``, and ``narrow_param``.
41+
42+
Default: ``null_over_false,no_mixed,narrow_param``.
43+
3244
Examples
3345
--------
3446

@@ -44,6 +56,18 @@ Apply fixes automatically:
4456
4557
composer refactor --fix
4658
59+
Run Rector and Type Perfect together:
60+
61+
.. code-block:: bash
62+
63+
composer dev-tools refactor -- --type-perfect
64+
65+
Limit Type Perfect to selected groups:
66+
67+
.. code-block:: bash
68+
69+
composer dev-tools refactor -- --type-perfect --type-perfect-groups=null_over_false,no_mixed
70+
4771
Exit Codes
4872
---------
4973

@@ -63,3 +87,8 @@ Behavior
6387
- Local ``rector.php`` is preferred when present.
6488
- Packaged default includes Fast Forward custom Rector rules plus shared Rector sets.
6589
- Uses ``--dry-run`` mode unless ``--fix`` is specified.
90+
- ``--type-perfect`` requires ``rector/type-perfect`` and
91+
``phpstan/extension-installer`` in the consumer project.
92+
- When the consumer already has ``phpstan.neon`` or ``phpstan.neon.dist``,
93+
the generated Type Perfect config includes it automatically before enabling
94+
the requested Type Perfect groups.

docs/faq.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,24 @@ Use the ``RectorConfig`` class to extend instead of replace:
128128
129129
This approach automatically receives upstream updates while allowing additive customization.
130130

131+
How do I enable Type Perfect together with ``refactor``?
132+
--------------------------------------------------------
133+
134+
Install the companion packages in the consumer project:
135+
136+
.. code-block:: bash
137+
138+
composer require --dev rector/type-perfect phpstan/extension-installer
139+
140+
Then run:
141+
142+
.. code-block:: bash
143+
144+
composer dev-tools refactor -- --type-perfect
145+
146+
You can narrow the rollout by selecting groups with
147+
``--type-perfect-groups=null_over_false,no_mixed``.
148+
131149
Can I generate coverage without running the full ``standards`` pipeline?
132150
------------------------------------------------------------------------
133151

docs/running/specialized-commands.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,18 @@ Runs Rector against the current project.
8989
.. code-block:: bash
9090
9191
composer refactor --fix
92+
composer dev-tools refactor -- --type-perfect
9293
9394
Important details:
9495

9596
- without ``--fix``, Rector runs in dry-run mode;
9697
- local ``rector.php`` is preferred when present;
9798
- the packaged default includes Fast Forward custom Rector rules plus shared
98-
Rector sets.
99+
Rector sets;
100+
- ``--type-perfect`` adds a PHPStan Type Perfect pass after Rector using a
101+
generated config in ``tmp/cache/phpstan/type-perfect.neon``;
102+
- the Fast Forward Type Perfect path expects ``rector/type-perfect`` and
103+
``phpstan/extension-installer`` to be installed in the consumer project.
99104

100105
``phpdoc``
101106
----------

src/Console/Command/RefactorCommand.php

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
namespace FastForward\DevTools\Console\Command;
2121

2222
use Composer\Command\BaseCommand;
23+
use FastForward\DevTools\Filesystem\FilesystemInterface;
2324
use FastForward\DevTools\Process\ProcessBuilderInterface;
2425
use FastForward\DevTools\Process\ProcessQueueInterface;
2526
use Symfony\Component\Config\FileLocatorInterface;
@@ -45,15 +46,31 @@ final class RefactorCommand extends BaseCommand
4546
*/
4647
public const string CONFIG = 'rector.php';
4748

49+
/**
50+
* @var string the generated PHPStan config used to run Type Perfect checks
51+
*/
52+
private const string TYPE_PERFECT_CONFIG = 'tmp/cache/phpstan/type-perfect.neon';
53+
54+
/**
55+
* @var list<string> the supported Type Perfect rule groups
56+
*/
57+
private const array TYPE_PERFECT_GROUPS = [
58+
'null_over_false',
59+
'no_mixed',
60+
'narrow_param',
61+
];
62+
4863
/**
4964
* Creates a new RefactorCommand instance.
5065
*
5166
* @param FileLocatorInterface $fileLocator the file locator
67+
* @param FilesystemInterface $filesystem the filesystem used for Type Perfect configuration generation
5268
* @param ProcessBuilderInterface $processBuilder the process builder
5369
* @param ProcessQueueInterface $processQueue the process queue
5470
*/
5571
public function __construct(
5672
private readonly FileLocatorInterface $fileLocator,
73+
private readonly FilesystemInterface $filesystem,
5774
private readonly ProcessBuilderInterface $processBuilder,
5875
private readonly ProcessQueueInterface $processQueue,
5976
) {
@@ -83,6 +100,17 @@ protected function configure(): void
83100
mode: InputOption::VALUE_OPTIONAL,
84101
description: 'The path to the Rector configuration file.',
85102
default: self::CONFIG
103+
)
104+
->addOption(
105+
name: 'type-perfect',
106+
mode: InputOption::VALUE_NONE,
107+
description: 'Run PHPStan Type Perfect checks after Rector using the supported Fast Forward preset.'
108+
)
109+
->addOption(
110+
name: 'type-perfect-groups',
111+
mode: InputOption::VALUE_OPTIONAL,
112+
description: 'Comma-separated Type Perfect groups to enable.',
113+
default: implode(',', self::TYPE_PERFECT_GROUPS)
86114
);
87115
}
88116

@@ -101,17 +129,124 @@ protected function execute(InputInterface $input, OutputInterface $output): int
101129
{
102130
$output->writeln('<info>Running Rector for code refactoring...</info>');
103131

132+
$config = (string) $input->getOption('config');
104133
$processBuilder = $this->processBuilder
105134
->withArgument('process')
106135
->withArgument('--config')
107-
->withArgument($this->fileLocator->locate(self::CONFIG));
136+
->withArgument($this->fileLocator->locate($config));
108137

109138
if (! $input->getOption('fix')) {
110139
$processBuilder = $processBuilder->withArgument('--dry-run');
111140
}
112141

113142
$this->processQueue->add($processBuilder->build('vendor/bin/rector'));
114143

144+
if ($input->getOption('type-perfect')) {
145+
$typePerfectGroups = $this->resolveTypePerfectGroups((string) $input->getOption('type-perfect-groups'));
146+
147+
if ([] === $typePerfectGroups) {
148+
$output->writeln(
149+
'<error>No valid Type Perfect groups were provided. Supported groups: '
150+
. implode(', ', self::TYPE_PERFECT_GROUPS)
151+
. '.</error>'
152+
);
153+
154+
return self::FAILURE;
155+
}
156+
157+
if (! $this->filesystem->exists('vendor/rector/type-perfect')) {
158+
$output->writeln(
159+
'<error>Type Perfect support requires rector/type-perfect. Install it with '
160+
. '"composer require rector/type-perfect --dev" before using --type-perfect.</error>'
161+
);
162+
163+
return self::FAILURE;
164+
}
165+
166+
if (! $this->filesystem->exists('vendor/phpstan/extension-installer')) {
167+
$output->writeln(
168+
'<error>Type Perfect support requires phpstan/extension-installer for the Fast Forward integration path. '
169+
. 'Install it with "composer require phpstan/extension-installer --dev" before using --type-perfect.</error>'
170+
);
171+
172+
return self::FAILURE;
173+
}
174+
175+
$output->writeln('<info>Running Type Perfect safety checks...</info>');
176+
177+
$typePerfectConfig = $this->writeTypePerfectConfig($typePerfectGroups);
178+
$typePerfect = $this->processBuilder
179+
->withArgument('analyse')
180+
->withArgument('--configuration', $typePerfectConfig)
181+
->build('vendor/bin/phpstan');
182+
183+
$this->processQueue->add($typePerfect);
184+
}
185+
115186
return $this->processQueue->run($output);
116187
}
188+
189+
/**
190+
* Filters the requested Type Perfect groups down to the supported subset.
191+
*
192+
* @param string $groups the raw comma-separated option value
193+
*
194+
* @return list<string> the valid requested groups in declaration order
195+
*/
196+
private function resolveTypePerfectGroups(string $groups): array
197+
{
198+
$requestedGroups = array_map('trim', explode(',', $groups));
199+
$requestedGroups = array_filter($requestedGroups, static fn(string $group): bool => '' !== $group);
200+
201+
return array_values(array_intersect(self::TYPE_PERFECT_GROUPS, $requestedGroups));
202+
}
203+
204+
/**
205+
* Writes the temporary PHPStan config used to run Type Perfect.
206+
*
207+
* @param list<string> $groups the enabled Type Perfect groups
208+
*
209+
* @return string the generated config path
210+
*/
211+
private function writeTypePerfectConfig(array $groups): string
212+
{
213+
$configPath = (string) $this->filesystem->getAbsolutePath(self::TYPE_PERFECT_CONFIG);
214+
$this->filesystem->mkdir($this->filesystem->dirname($configPath));
215+
216+
$lines = [];
217+
$projectPhpStanConfig = $this->resolveProjectPhpStanConfig();
218+
219+
if (null !== $projectPhpStanConfig) {
220+
$lines[] = 'includes:';
221+
$lines[] = sprintf(" - '%s'", str_replace("'", "''", $projectPhpStanConfig));
222+
$lines[] = '';
223+
}
224+
225+
$lines[] = 'parameters:';
226+
$lines[] = ' type_perfect:';
227+
228+
foreach ($groups as $group) {
229+
$lines[] = sprintf(' %s: true', $group);
230+
}
231+
232+
$this->filesystem->dumpFile($configPath, implode("\n", $lines) . "\n");
233+
234+
return $configPath;
235+
}
236+
237+
/**
238+
* Resolves the consumer PHPStan config to include in the generated Type Perfect file.
239+
*
240+
* @return string|null the absolute PHPStan config path, or null when the consumer has no PHPStan config yet
241+
*/
242+
private function resolveProjectPhpStanConfig(): ?string
243+
{
244+
foreach (['phpstan.neon', 'phpstan.neon.dist'] as $candidate) {
245+
if ($this->filesystem->exists($candidate)) {
246+
return (string) $this->filesystem->getAbsolutePath($candidate);
247+
}
248+
}
249+
250+
return null;
251+
}
117252
}

0 commit comments

Comments
 (0)