22
33namespace PHPStan \Command ;
44
5+ use Nette \DI \Config \Loader ;
6+ use Nette \FileNotFoundException ;
7+ use Nette \InvalidStateException ;
58use OndraM \CiDetector \CiDetector ;
9+ use PHPStan \Analyser \Ignore \IgnoredError ;
610use PHPStan \Analyser \InternalError ;
711use PHPStan \Command \ErrorFormatter \BaselineNeonErrorFormatter ;
812use PHPStan \Command \ErrorFormatter \BaselinePhpErrorFormatter ;
@@ -102,6 +106,7 @@ protected function configure(): void
102106 new InputOption ('watch ' , null , InputOption::VALUE_NONE , 'Launch PHPStan Pro ' ),
103107 new InputOption ('pro ' , null , InputOption::VALUE_NONE , 'Launch PHPStan Pro ' ),
104108 new InputOption ('fail-without-result-cache ' , null , InputOption::VALUE_NONE , 'Return non-zero exit code when result cache is not used ' ),
109+ new InputOption ('ignore-new-errors ' , null , InputOption::VALUE_NONE , 'Ignore new errors when generating the baseline. ' ),
105110 ]);
106111 }
107112
@@ -136,6 +141,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
136141 $ debugEnabled = (bool ) $ input ->getOption ('debug ' );
137142 $ fix = (bool ) $ input ->getOption ('fix ' ) || (bool ) $ input ->getOption ('watch ' ) || (bool ) $ input ->getOption ('pro ' );
138143 $ failWithoutResultCache = (bool ) $ input ->getOption ('fail-without-result-cache ' );
144+ $ ignoreNewErrors = (bool ) $ input ->getOption ('ignore-new-errors ' );
139145
140146 /** @var string|false|null $generateBaselineFile */
141147 $ generateBaselineFile = $ input ->getOption ('generate-baseline ' );
@@ -182,6 +188,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
182188 return $ inceptionResult ->handleReturn (1 , null , $ this ->analysisStartTime );
183189 }
184190
191+ if ($ generateBaselineFile === null && $ ignoreNewErrors ) {
192+ $ inceptionResult ->getStdOutput ()->getStyle ()->error ('You must pass the --generate-baseline option alongside --ignore-new-errors. ' );
193+ return $ inceptionResult ->handleReturn (1 , null , $ this ->analysisStartTime );
194+ }
195+
185196 $ errorOutput = $ inceptionResult ->getErrorOutput ();
186197 $ errorFormat = $ input ->getOption ('error-format ' );
187198
@@ -411,7 +422,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
411422 return $ inceptionResult ->handleReturn (1 , $ analysisResult ->getPeakMemoryUsageBytes (), $ this ->analysisStartTime );
412423 }
413424
414- return $ this ->generateBaseline ($ generateBaselineFile , $ inceptionResult , $ analysisResult , $ output , $ allowEmptyBaseline , $ baselineExtension , $ failWithoutResultCache );
425+ return $ this ->generateBaseline ($ generateBaselineFile , $ inceptionResult , $ analysisResult , $ output , $ allowEmptyBaseline , $ baselineExtension , $ failWithoutResultCache, $ ignoreNewErrors , $ container );
415426 }
416427
417428 /** @var ErrorFormatter $errorFormatter */
@@ -587,8 +598,13 @@ private function getMessageFromInternalError(FileHelper $fileHelper, InternalErr
587598 return $ message ;
588599 }
589600
590- private function generateBaseline (string $ generateBaselineFile , InceptionResult $ inceptionResult , AnalysisResult $ analysisResult , OutputInterface $ output , bool $ allowEmptyBaseline , string $ baselineExtension , bool $ failWithoutResultCache ): int
601+ private function generateBaseline (string $ generateBaselineFile , InceptionResult $ inceptionResult , AnalysisResult $ analysisResult , OutputInterface $ output , bool $ allowEmptyBaseline , string $ baselineExtension , bool $ failWithoutResultCache, bool $ ignoreNewErrors , Container $ container ): int
591602 {
603+ $ baselineFileDirectory = dirname ($ generateBaselineFile );
604+ $ fileHelper = $ container ->getByType (FileHelper::class);
605+ $ baselinePathHelper = new ParentDirectoryRelativePathHelper ($ baselineFileDirectory );
606+ $ analysisResult = $ this ->processFileSpecificErrorsFromAnalysisResult ($ analysisResult , $ ignoreNewErrors , $ generateBaselineFile , $ inceptionResult , $ fileHelper , $ baselinePathHelper );
607+
592608 if (!$ allowEmptyBaseline && !$ analysisResult ->hasErrors ()) {
593609 $ inceptionResult ->getStdOutput ()->getStyle ()->error ('No errors were found during the analysis. Baseline could not be generated. ' );
594610 $ inceptionResult ->getStdOutput ()->writeLineFormatted ('To allow generating empty baselines, pass <fg=cyan>--allow-empty-baseline</> option. ' );
@@ -599,7 +615,6 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult
599615 $ streamOutput = $ this ->createStreamOutput ();
600616 $ errorConsoleStyle = new ErrorsConsoleStyle (new StringInput ('' ), $ streamOutput );
601617 $ baselineOutput = new SymfonyOutput ($ streamOutput , new SymfonyStyle ($ errorConsoleStyle ));
602- $ baselineFileDirectory = dirname ($ generateBaselineFile );
603618 $ baselinePathHelper = new ParentDirectoryRelativePathHelper ($ baselineFileDirectory );
604619
605620 if ($ baselineExtension === 'php ' ) {
@@ -674,6 +689,63 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult
674689 return $ inceptionResult ->handleReturn ($ exitCode , $ analysisResult ->getPeakMemoryUsageBytes (), $ this ->analysisStartTime );
675690 }
676691
692+ private function processFileSpecificErrorsFromAnalysisResult (AnalysisResult $ analysisResult , bool $ ignoreNewErrors , string $ generateBaselineFile , InceptionResult $ inceptionResult , FileHelper $ fileHelper , RelativePathHelper $ baselinePathHelper ): AnalysisResult
693+ {
694+ $ fileSpecificErrors = $ analysisResult ->getFileSpecificErrors ();
695+ if (!$ ignoreNewErrors ) {
696+ return $ analysisResult ;
697+ }
698+
699+ $ baselineIgnoreErrors = $ this ->getCurrentBaselineIgnoreErrors ($ generateBaselineFile , $ inceptionResult );
700+ $ ignoreErrorsByFile = $ this ->mapIgnoredErrors ($ baselineIgnoreErrors , $ fileHelper );
701+
702+ foreach ($ fileSpecificErrors as $ errorIndex => $ error ) {
703+ $ filePath = $ baselinePathHelper ->getRelativePath ($ error ->getFilePath ());
704+ if (isset ($ ignoreErrorsByFile [$ filePath ])) {
705+ foreach ($ ignoreErrorsByFile [$ filePath ] as $ ignoreError ) {
706+ $ ignore = $ ignoreError ['ignoreError ' ];
707+ $ shouldIgnore = IgnoredError::shouldIgnore ($ fileHelper , $ error , $ ignore ['message ' ] ?? null , $ ignore ['identifier ' ] ?? null , null );
708+ if ($ shouldIgnore ) {
709+ continue 2 ;
710+ }
711+ }
712+ }
713+
714+ $ traitFilePath = $ error ->getTraitFilePath ();
715+ if ($ traitFilePath !== null ) {
716+ $ normalizedTraitFilePath = $ baselinePathHelper ->getRelativePath ($ traitFilePath );
717+ if (isset ($ ignoreErrorsByFile [$ normalizedTraitFilePath ])) {
718+ foreach ($ ignoreErrorsByFile [$ normalizedTraitFilePath ] as $ ignoreError ) {
719+ $ ignore = $ ignoreError ['ignoreError ' ];
720+ $ shouldIgnore = IgnoredError::shouldIgnore ($ fileHelper , $ error , $ ignore ['message ' ] ?? null , $ ignore ['identifier ' ] ?? null , null );
721+ if ($ shouldIgnore ) {
722+ continue 2 ;
723+ }
724+ }
725+ }
726+ }
727+
728+ // the error was not matched in the baseline, making it a new error, new errors should be ignored here
729+ unset($ fileSpecificErrors [$ errorIndex ]);
730+ }
731+
732+ $ fileSpecificErrors = array_values ($ fileSpecificErrors );
733+
734+ return new AnalysisResult (
735+ $ fileSpecificErrors ,
736+ $ analysisResult ->getNotFileSpecificErrors (),
737+ $ analysisResult ->getInternalErrorObjects (),
738+ $ analysisResult ->getWarnings (),
739+ $ analysisResult ->getCollectedData (),
740+ $ analysisResult ->isDefaultLevelUsed (),
741+ $ analysisResult ->getProjectConfigFile (),
742+ $ analysisResult ->isResultCacheSaved (),
743+ $ analysisResult ->getPeakMemoryUsageBytes (),
744+ $ analysisResult ->isResultCacheUsed (),
745+ $ analysisResult ->getChangedProjectExtensionFilesOutsideOfAnalysedPaths (),
746+ );
747+ }
748+
677749 /**
678750 * @param string[] $files
679751 */
@@ -716,4 +788,109 @@ private function runDiagnoseExtensions(Container $container, Output $errorOutput
716788 }
717789 }
718790
791+ private function getCurrentBaselineIgnoreErrors (string $ generateBaselineFile , InceptionResult $ inceptionResult ): mixed
792+ {
793+ $ loader = new Loader ();
794+ try {
795+ $ currentBaselineConfig = $ loader ->load ($ generateBaselineFile );
796+ $ baselineIgnoreErrors = $ currentBaselineConfig ['parameters ' ]['ignoreErrors ' ] ?? [];
797+ } catch (FileNotFoundException ) {
798+ // currently no baseline file -> empty config
799+ $ baselineIgnoreErrors = [];
800+ } catch (InvalidStateException $ invalidStateException ) {
801+ $ inceptionResult ->getErrorOutput ()->writeLineFormatted ($ invalidStateException ->getMessage ());
802+ throw $ invalidStateException ;
803+ }
804+ return $ baselineIgnoreErrors ;
805+ }
806+
807+ /**
808+ * @param (string|mixed[])[] $baselineIgnoreErrors
809+ * @return mixed[][]
810+ * @throws ShouldNotHappenException
811+ */
812+ private function mapIgnoredErrors (array $ baselineIgnoreErrors , FileHelper $ fileHelper ): array
813+ {
814+ $ ignoreErrorsByFile = [];
815+
816+ $ expandedIgnoreErrors = [];
817+ foreach ($ baselineIgnoreErrors as $ ignoreError ) {
818+ if (!is_array ($ ignoreError )) {
819+ throw new ShouldNotHappenException ('Baseline should not have ignore error strings ' );
820+ }
821+
822+ if (!isset ($ ignoreError ['message ' ]) && !isset ($ ignoreError ['messages ' ]) && !isset ($ ignoreError ['identifier ' ])) {
823+ continue ;
824+ }
825+ if (isset ($ ignoreError ['messages ' ])) {
826+ foreach ($ ignoreError ['messages ' ] as $ message ) {
827+ $ expandedIgnoreError = $ ignoreError ;
828+ unset($ expandedIgnoreError ['messages ' ]);
829+ $ expandedIgnoreError ['message ' ] = $ message ;
830+ $ expandedIgnoreErrors [] = $ expandedIgnoreError ;
831+ }
832+ } else {
833+ $ expandedIgnoreErrors [] = $ ignoreError ;
834+ }
835+ }
836+ $ uniquedExpandedIgnoreErrors = [];
837+ foreach ($ expandedIgnoreErrors as $ ignoreError ) {
838+ if (!isset ($ ignoreError ['message ' ]) && !isset ($ ignoreError ['identifier ' ])) {
839+ $ uniquedExpandedIgnoreErrors [] = $ ignoreError ;
840+ continue ;
841+ }
842+ if (!isset ($ ignoreError ['path ' ])) {
843+ $ uniquedExpandedIgnoreErrors [] = $ ignoreError ;
844+ continue ;
845+ }
846+
847+ $ key = $ ignoreError ['path ' ];
848+ if (isset ($ ignoreError ['message ' ])) {
849+ $ key = sprintf ("%s \n%s " , $ key , $ ignoreError ['message ' ]);
850+ }
851+ if (isset ($ ignoreError ['identifier ' ])) {
852+ $ key = sprintf ("%s \n%s " , $ key , $ ignoreError ['identifier ' ]);
853+ }
854+ if ($ key === '' ) {
855+ throw new ShouldNotHappenException ();
856+ }
857+
858+ if (!array_key_exists ($ key , $ uniquedExpandedIgnoreErrors )) {
859+ $ uniquedExpandedIgnoreErrors [$ key ] = $ ignoreError ;
860+ continue ;
861+ }
862+
863+ $ uniquedExpandedIgnoreErrors [$ key ] = [
864+ 'message ' => $ ignoreError ['message ' ] ?? null ,
865+ 'path ' => $ ignoreError ['path ' ],
866+ 'identifier ' => $ ignoreError ['identifier ' ] ?? null ,
867+ 'count ' => ($ uniquedExpandedIgnoreErrors [$ key ]['count ' ] ?? 1 ) + ($ ignoreError ['count ' ] ?? 1 ),
868+ 'reportUnmatched ' => false ,
869+ ];
870+ }
871+ $ expandedIgnoreErrors = array_values ($ uniquedExpandedIgnoreErrors );
872+
873+ foreach ($ expandedIgnoreErrors as $ i => $ ignoreError ) {
874+ $ ignoreErrorEntry = [
875+ 'index ' => $ i ,
876+ 'ignoreError ' => $ ignoreError ,
877+ ];
878+
879+ if (!isset ($ ignoreError ['message ' ]) && !isset ($ ignoreError ['identifier ' ])) {
880+ continue ;
881+ }
882+ if (!isset ($ ignoreError ['path ' ])) {
883+ throw new ShouldNotHappenException ('Baseline should not have ignore errors without path ' );
884+ }
885+
886+ $ normalizedPath = $ fileHelper ->normalizePath ($ ignoreError ['path ' ]);
887+ $ ignoreError ['path ' ] = $ normalizedPath ;
888+ $ ignoreErrorsByFile [$ normalizedPath ][] = $ ignoreErrorEntry ;
889+ $ ignoreError ['realPath ' ] = $ normalizedPath ;
890+ $ expandedIgnoreErrors [$ i ] = $ ignoreError ;
891+ }
892+
893+ return $ ignoreErrorsByFile ;
894+ }
895+
719896}
0 commit comments