22
33namespace ShipMonk \PHPStan \Baseline ;
44
5+ use ArrayIterator ;
56use ShipMonk \PHPStan \Baseline \Exception \ErrorException ;
7+ use ShipMonk \PHPStan \Baseline \Handler \BaselineHandler ;
68use ShipMonk \PHPStan \Baseline \Handler \HandlerFactory ;
79use SplFileInfo ;
810use function array_reduce ;
9- use function assert ;
1011use function dirname ;
1112use function file_put_contents ;
12- use function is_array ;
13- use function is_int ;
14- use function is_string ;
13+ use function is_file ;
1514use function ksort ;
1615use function str_replace ;
1716
@@ -49,28 +48,21 @@ public function split(string $loaderFilePath): array
4948 $ extension = $ splFile ->getExtension ();
5049
5150 $ handler = HandlerFactory::create ($ extension );
52- $ data = $ handler ->decodeBaseline ($ realPath );
53-
54- $ ignoredErrors = $ data ['parameters ' ]['ignoreErrors ' ] ?? null ; // @phpstan-ignore offsetAccess.nonOffsetAccessible
55-
56- if (!is_array ($ ignoredErrors )) {
57- throw new ErrorException (
58- "Invalid argument, expected $ extension file with 'parameters.ignoreErrors' key in ' $ loaderFilePath'. " .
59- "\n - Did you run native baseline generation first? " .
60- "\n - You can so via vendor/bin/phpstan --generate-baseline= $ loaderFilePath " ,
61- );
62- }
63-
51+ $ ignoredErrors = $ handler ->decodeBaseline ($ realPath );
6452 $ groupedErrors = $ this ->groupErrorsByIdentifier ($ ignoredErrors , $ folder );
6553
6654 $ outputInfo = [];
6755 $ baselineFiles = [];
6856 $ totalErrorCount = 0 ;
6957
70- foreach ($ groupedErrors as $ identifier => $ errors ) {
58+ foreach ($ groupedErrors as $ identifier => $ newErrors ) {
7159 $ fileName = $ identifier . '. ' . $ extension ;
7260 $ filePath = $ folder . '/ ' . $ fileName ;
73- $ errorsCount = array_reduce ($ errors , static fn (int $ carry , array $ item ): int => $ carry + $ item ['count ' ], 0 );
61+
62+ $ oldErrors = $ this ->readExistingErrors ($ filePath , $ handler ) ?? [];
63+ $ sortedErrors = $ this ->sortErrors ($ oldErrors , $ newErrors );
64+
65+ $ errorsCount = array_reduce ($ sortedErrors , static fn (int $ carry , array $ item ): int => $ carry + $ item ['count ' ], 0 );
7466 $ totalErrorCount += $ errorsCount ;
7567
7668 $ outputInfo [$ filePath ] = $ errorsCount ;
@@ -79,7 +71,7 @@ public function split(string $loaderFilePath): array
7971 $ plural = $ errorsCount === 1 ? '' : 's ' ;
8072 $ prefix = $ this ->includeCount ? "total $ errorsCount error $ plural " : null ;
8173
82- $ encodedData = $ handler ->encodeBaseline ($ prefix , $ errors , $ this ->indent );
74+ $ encodedData = $ handler ->encodeBaseline ($ prefix , $ sortedErrors , $ this ->indent );
8375 $ this ->writeFile ($ filePath , $ encodedData );
8476 }
8577
@@ -94,7 +86,7 @@ public function split(string $loaderFilePath): array
9486 }
9587
9688 /**
97- * @param array<mixed > $errors
89+ * @param list<array{message: string, count: int, path: string, identifier: string|null}|array{rawMessage: string, count: int, path: string, identifier: string|null} > $errors
9890 * @return array<string, list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}>>
9991 *
10092 * @throws ErrorException
@@ -106,53 +98,27 @@ private function groupErrorsByIdentifier(
10698 {
10799 $ groupedErrors = [];
108100
109- foreach ($ errors as $ index => $ error ) {
110- if (!is_array ($ error )) {
111- throw new ErrorException ("Ignored error # $ index is not an array " );
112- }
113-
101+ foreach ($ errors as $ error ) {
114102 $ identifier = $ error ['identifier ' ] ?? 'missing-identifier ' ;
103+ $ normalizedPath = str_replace ($ folder . '/ ' , '' , $ error ['path ' ]);
115104
116105 if (isset ($ error ['rawMessage ' ])) {
117- $ message = $ error ['rawMessage ' ];
118- $ rawMessage = true ;
119- } elseif (isset ($ error ['message ' ])) {
120- $ message = $ error ['message ' ];
121- $ rawMessage = false ;
122- } else {
123- throw new ErrorException ("Ignored error # $ index is missing 'message' or 'rawMessage' " );
124- }
106+ $ groupedErrors [$ identifier ][] = [
107+ 'rawMessage ' => $ error ['rawMessage ' ],
108+ 'count ' => $ error ['count ' ],
109+ 'path ' => $ normalizedPath ,
110+ ];
125111
126- if (!isset ($ error ['count ' ])) {
127- throw new ErrorException ("Ignored error # $ index is missing 'count' " );
128- }
129-
130- $ count = $ error ['count ' ];
112+ } elseif (isset ($ error ['message ' ])) {
113+ $ groupedErrors [$ identifier ][] = [
114+ 'message ' => $ error ['message ' ],
115+ 'count ' => $ error ['count ' ],
116+ 'path ' => $ normalizedPath ,
117+ ];
131118
132- if (! isset ( $ error [ ' path ' ])) {
133- throw new ErrorException (" Ignored error # $ index is missing 'path' " );
119+ } else {
120+ throw new ErrorException (' Error is missing message or rawMessage ' );
134121 }
135-
136- $ path = $ error ['path ' ];
137-
138- assert (is_string ($ identifier ));
139- assert (is_string ($ message ));
140- assert (is_int ($ count ));
141- assert (is_string ($ path ));
142-
143- $ normalizedPath = str_replace ($ folder . '/ ' , '' , $ path );
144-
145- unset($ error ['identifier ' ]);
146-
147- $ groupedErrors [$ identifier ][] = $ rawMessage ? [
148- 'rawMessage ' => $ message ,
149- 'count ' => $ count ,
150- 'path ' => $ normalizedPath ,
151- ] : [
152- 'message ' => $ message ,
153- 'count ' => $ count ,
154- 'path ' => $ normalizedPath ,
155- ];
156122 }
157123
158124 ksort ($ groupedErrors );
@@ -175,4 +141,83 @@ private function writeFile(
175141 }
176142 }
177143
144+ /**
145+ * @param array{message?: string, rawMessage?: string, count: int, path: string} $error
146+ */
147+ private function getErrorKey (array $ error ): string
148+ {
149+ return $ error ['path ' ] . "\x00" . ($ error ['rawMessage ' ] ?? $ error ['message ' ] ?? '' );
150+ }
151+
152+ /**
153+ * @return list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}>|null
154+ */
155+ private function readExistingErrors (
156+ string $ filePath ,
157+ BaselineHandler $ handler
158+ ): ?array
159+ {
160+ if (!is_file ($ filePath )) {
161+ return null ;
162+ }
163+
164+ try {
165+ return $ handler ->decodeBaseline ($ filePath );
166+
167+ } catch (ErrorException $ e ) {
168+ return null ;
169+ }
170+ }
171+
172+ /**
173+ * @param list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}> $oldErrors
174+ * @param list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}> $newErrors
175+ * @return list<array{message: string, count: int, path: string}|array{rawMessage: string, count: int, path: string}>
176+ */
177+ private function sortErrors (
178+ array $ oldErrors ,
179+ array $ newErrors
180+ ): array
181+ {
182+ $ newErrorsByKey = [];
183+
184+ foreach ($ newErrors as $ newError ) {
185+ $ key = $ this ->getErrorKey ($ newError );
186+ $ newErrorsByKey [$ key ] = $ newError ;
187+ }
188+
189+ // collect errors that existed before
190+ $ existingByKey = [];
191+
192+ foreach ($ oldErrors as $ oldError ) {
193+ $ key = $ this ->getErrorKey ($ oldError );
194+
195+ if (isset ($ newErrorsByKey [$ key ])) {
196+ $ existingByKey [$ key ] = $ newErrorsByKey [$ key ];
197+ unset($ newErrorsByKey [$ key ]);
198+ }
199+ }
200+
201+ // insert new errors at their sorted positions among existing errors
202+ ksort ($ newErrorsByKey );
203+ $ newErrorsIterator = new ArrayIterator ($ newErrorsByKey );
204+ $ result = [];
205+
206+ foreach ($ existingByKey as $ existingKey => $ existingError ) {
207+ while ($ newErrorsIterator ->valid () && $ newErrorsIterator ->key () < $ existingKey ) {
208+ $ result [] = $ newErrorsIterator ->current ();
209+ $ newErrorsIterator ->next ();
210+ }
211+
212+ $ result [] = $ existingError ;
213+ }
214+
215+ while ($ newErrorsIterator ->valid ()) {
216+ $ result [] = $ newErrorsIterator ->current ();
217+ $ newErrorsIterator ->next ();
218+ }
219+
220+ return $ result ;
221+ }
222+
178223}
0 commit comments