2020namespace FastForward \DevTools \Console \Command ;
2121
2222use Composer \Command \BaseCommand ;
23+ use FastForward \DevTools \Filesystem \FilesystemInterface ;
2324use FastForward \DevTools \Process \ProcessBuilderInterface ;
2425use FastForward \DevTools \Process \ProcessQueueInterface ;
2526use 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