22
33namespace PHPStan \Command ;
44
5+ use PHPStan \Command \ErrorFormatter \BaselineNeonErrorFormatter ;
56use PHPStan \Command \ErrorFormatter \ErrorFormatter ;
7+ use PHPStan \Command \Symfony \SymfonyOutput ;
8+ use PHPStan \Command \Symfony \SymfonyStyle ;
9+ use PHPStan \File \ParentDirectoryRelativePathHelper ;
610use Symfony \Component \Console \Input \InputArgument ;
711use Symfony \Component \Console \Input \InputInterface ;
812use Symfony \Component \Console \Input \InputOption ;
13+ use Symfony \Component \Console \Input \StringInput ;
914use Symfony \Component \Console \Output \OutputInterface ;
15+ use Symfony \Component \Console \Output \StreamOutput ;
16+ use function file_put_contents ;
17+ use function stream_get_contents ;
1018
1119class AnalyseCommand extends \Symfony \Component \Console \Command \Command
1220{
@@ -44,6 +52,7 @@ protected function configure(): void
4452 new InputOption ('debug ' , null , InputOption::VALUE_NONE , 'Show debug information - which file is analysed, do not catch internal errors ' ),
4553 new InputOption ('autoload-file ' , 'a ' , InputOption::VALUE_REQUIRED , 'Project \'s additional autoload file path ' ),
4654 new InputOption ('error-format ' , null , InputOption::VALUE_REQUIRED , 'Format in which to print the result of the analysis ' , 'table ' ),
55+ new InputOption ('generate-baseline ' , null , InputOption::VALUE_OPTIONAL , 'Path to a file where the baseline should be saved ' , false ),
4756 new InputOption ('memory-limit ' , null , InputOption::VALUE_REQUIRED , 'Memory limit for analysis ' ),
4857 new InputOption ('xdebug ' , null , InputOption::VALUE_NONE , 'Allow running with XDebug for debugging purposes ' ),
4958 ]);
@@ -79,6 +88,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7988 $ pathsFile = $ input ->getOption ('paths-file ' );
8089 $ allowXdebug = $ input ->getOption ('xdebug ' );
8190
91+ /** @var string|false|null $generateBaselineFile */
92+ $ generateBaselineFile = $ input ->getOption ('generate-baseline ' );
93+ if ($ generateBaselineFile === false ) {
94+ $ generateBaselineFile = null ;
95+ } elseif ($ generateBaselineFile === null ) {
96+ $ generateBaselineFile = 'phpstan-baseline.neon ' ;
97+ }
98+
8299 if (
83100 !is_array ($ paths )
84101 || (!is_string ($ memoryLimit ) && $ memoryLimit !== null )
@@ -101,6 +118,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
101118 $ autoloadFile ,
102119 $ this ->composerAutoloaderProjectPaths ,
103120 $ configuration ,
121+ $ generateBaselineFile ,
104122 $ level ,
105123 $ allowXdebug ,
106124 true
@@ -129,6 +147,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
129147 return 1 ;
130148 }
131149
150+ if ($ errorFormat === 'baselineNeon ' ) {
151+ $ errorOutput = $ inceptionResult ->getErrorOutput ();
152+ $ errorOutput ->writeLineFormatted ('⚠️ You \'re using an obsolete option <fg=cyan>--error-format baselineNeon</>. ⚠️️ ' );
153+ $ errorOutput ->writeLineFormatted ('' );
154+ $ errorOutput ->writeLineFormatted (' There \'s a new and much better option <fg=cyan>--generate-baseline</>. Here are the advantages: ' );
155+ $ errorOutput ->writeLineFormatted (' 1) The current baseline file does not have to be commented-out ' );
156+ $ errorOutput ->writeLineFormatted (' nor emptied when generating the new baseline. It \'s excluded automatically. ' );
157+ $ errorOutput ->writeLineFormatted (' 2) Output no longer has to be redirected to a file, PHPStan saves the baseline ' );
158+ $ errorOutput ->writeLineFormatted (' to a specified path (defaults to <fg=cyan>phpstan-baseline.neon</>). ' );
159+ $ errorOutput ->writeLineFormatted (' 3) Baseline contains correct relative paths if saved to a subdirectory. ' );
160+ $ errorOutput ->writeLineFormatted ('' );
161+ }
162+
132163 /** @var ErrorFormatter $errorFormatter */
133164 $ errorFormatter = $ container ->getService ($ errorFormatterServiceName );
134165
@@ -140,6 +171,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
140171 throw new \PHPStan \ShouldNotHappenException ();
141172 }
142173
174+ $ generateBaselineFile = $ inceptionResult ->getGenerateBaselineFile ();
175+ if ($ generateBaselineFile !== null ) {
176+ $ baselineExtension = pathinfo ($ generateBaselineFile , PATHINFO_EXTENSION );
177+ if ($ baselineExtension === '' ) {
178+ $ inceptionResult ->getStdOutput ()->getStyle ()->error (sprintf ('Baseline filename must have an extension, %s provided instead. ' , pathinfo ($ generateBaselineFile , PATHINFO_BASENAME )));
179+ return $ inceptionResult ->handleReturn (1 );
180+ }
181+
182+ if ($ baselineExtension !== 'neon ' ) {
183+ $ inceptionResult ->getStdOutput ()->getStyle ()->error (sprintf ('Baseline filename extension must be .neon, .%s was used instead. ' , $ baselineExtension ));
184+
185+ return $ inceptionResult ->handleReturn (1 );
186+ }
187+ }
188+
143189 $ analysisResult = $ application ->analyse (
144190 $ inceptionResult ->getFiles (),
145191 $ inceptionResult ->isOnlyFiles (),
@@ -151,9 +197,81 @@ protected function execute(InputInterface $input, OutputInterface $output): int
151197 $ input
152198 );
153199
200+ if ($ generateBaselineFile !== null ) {
201+ if (!$ analysisResult ->hasErrors ()) {
202+ $ inceptionResult ->getStdOutput ()->getStyle ()->error ('No errors were found during the analysis. Baseline could not be generated. ' );
203+
204+ return $ inceptionResult ->handleReturn (1 );
205+ }
206+
207+ $ baselineFileDirectory = dirname ($ generateBaselineFile );
208+ $ baselineErrorFormatter = new BaselineNeonErrorFormatter (new ParentDirectoryRelativePathHelper ($ baselineFileDirectory ));
209+
210+ $ streamOutput = $ this ->createStreamOutput ();
211+ $ errorConsoleStyle = new ErrorsConsoleStyle (new StringInput ('' ), $ streamOutput );
212+ $ output = new SymfonyOutput ($ streamOutput , new SymfonyStyle ($ errorConsoleStyle ));
213+ $ baselineErrorFormatter ->formatErrors ($ analysisResult , $ output );
214+
215+ $ stream = $ streamOutput ->getStream ();
216+ rewind ($ stream );
217+ $ baselineContents = stream_get_contents ($ stream );
218+ if ($ baselineContents === false ) {
219+ throw new \PHPStan \ShouldNotHappenException ();
220+ }
221+
222+ if (!is_dir ($ baselineFileDirectory )) {
223+ $ mkdirResult = @mkdir ($ baselineFileDirectory , 0644 , true );
224+ if ($ mkdirResult === false ) {
225+ $ inceptionResult ->getStdOutput ()->writeLineFormatted (sprintf ('Failed to create directory "%s". ' , $ baselineFileDirectory ));
226+
227+ return $ inceptionResult ->handleReturn (1 );
228+ }
229+ }
230+
231+ $ writeResult = @file_put_contents ($ generateBaselineFile , $ baselineContents );
232+ if ($ writeResult === false ) {
233+ $ inceptionResult ->getStdOutput ()->writeLineFormatted (sprintf ('Failed to write the baseline to file "%s". ' , $ generateBaselineFile ));
234+
235+ return $ inceptionResult ->handleReturn (1 );
236+ }
237+
238+ $ errorsCount = 0 ;
239+ $ unignorableCount = 0 ;
240+ foreach ($ analysisResult ->getFileSpecificErrors () as $ fileSpecificError ) {
241+ if (!$ fileSpecificError ->canBeIgnored ()) {
242+ $ unignorableCount ++;
243+ continue ;
244+ }
245+
246+ $ errorsCount ++;
247+ }
248+
249+ $ message = sprintf ('Baseline generated with %d %s. ' , $ errorsCount , $ errorsCount === 1 ? 'error ' : 'errors ' );
250+
251+ if (
252+ $ unignorableCount === 0
253+ && count ($ analysisResult ->getNotFileSpecificErrors ()) === 0
254+ ) {
255+ $ inceptionResult ->getStdOutput ()->getStyle ()->success ($ message );
256+ } else {
257+ $ inceptionResult ->getStdOutput ()->getStyle ()->warning ($ message . "\nSome errors could not be put into baseline. Re-run PHPStan and fix them. " );
258+ }
259+
260+ return $ inceptionResult ->handleReturn (0 );
261+ }
262+
154263 return $ inceptionResult ->handleReturn (
155264 $ errorFormatter ->formatErrors ($ analysisResult , $ inceptionResult ->getStdOutput ())
156265 );
157266 }
158267
268+ private function createStreamOutput (): StreamOutput
269+ {
270+ $ resource = fopen ('php://memory ' , 'w ' , false );
271+ if ($ resource === false ) {
272+ throw new \PHPStan \ShouldNotHappenException ();
273+ }
274+ return new StreamOutput ($ resource );
275+ }
276+
159277}
0 commit comments