@@ -40,17 +40,25 @@ class XCResultGenerator {
4040 List <XCResultIssueDiscarder > issueDiscarders = const < XCResultIssueDiscarder > [],
4141 }) async {
4242 final Version ? xcodeVersion = xcode.currentVersion;
43- final RunResult result = await processUtils.run (< String > [
44- ...xcode.xcrunCommand (),
45- 'xcresulttool' ,
46- 'get' ,
47- // See https://github.com/flutter/flutter/issues/151502
48- if (xcodeVersion != null && xcodeVersion >= Version (16 , 0 , 0 )) '--legacy' ,
49- '--path' ,
50- resultPath,
51- '--format' ,
52- 'json' ,
53- ]);
43+ final bool useNewCommand = xcodeVersion != null && xcodeVersion >= Version (16 , 0 , 0 );
44+
45+ final baseCommand = < String > [...xcode.xcrunCommand (), 'xcresulttool' ];
46+
47+ if (useNewCommand) {
48+ baseCommand.addAll (< String > [
49+ 'get' ,
50+ 'build-results' ,
51+ '--path' ,
52+ resultPath,
53+ '--format' ,
54+ 'json' ,
55+ ]);
56+ } else {
57+ baseCommand.addAll (< String > ['get' , '--path' , resultPath, '--format' , 'json' ]);
58+ }
59+
60+ final RunResult result = await processUtils.run (baseCommand);
61+
5462 if (result.exitCode != 0 ) {
5563 return XCResult .failed (errorMessage: result.stderr);
5664 }
@@ -59,9 +67,6 @@ class XCResultGenerator {
5967 }
6068 final Object ? resultJson = json.decode (result.stdout);
6169 if (resultJson == null || resultJson is ! Map <String , Object ?>) {
62- // If json parsing failed, indicate such error.
63- // This also includes the top level json object is an array, which indicates
64- // the structure of the json is changed and this parser class possibly needs to update for this change.
6570 return XCResult .failed (errorMessage: 'xcresult parser: Unrecognized top level json format.' );
6671 }
6772 return XCResult (resultJson: resultJson, issueDiscarders: issueDiscarders);
@@ -80,40 +85,71 @@ class XCResult {
8085 }) {
8186 final issues = < XCResultIssue > [];
8287
83- final Object ? issuesMap = resultJson['issues' ];
84- if (issuesMap == null || issuesMap is ! Map <String , Object ?>) {
85- return XCResult .failed (errorMessage: 'xcresult parser: Failed to parse the issues map.' );
86- }
88+ /// Detect which xcresult JSON format is being used to ensure backwards compatibility.
89+ ///
90+ /// Xcode 16 introduced a new `get build-results` command with a flatter JSON structure.
91+ /// Older versions use the original `get` command with a deeply nested structure.
92+ /// We differentiate them by checking for the presence of top-level 'errors' or 'warnings'
93+ /// keys, which are unique to the modern format.
8794
88- final Object ? errorSummaries = issuesMap['errorSummaries' ];
89- if (errorSummaries is Map <String , Object ?>) {
95+ if (resultJson.containsKey ('errors' ) || resultJson.containsKey ('warnings' )) {
9096 issues.addAll (
91- _parseIssuesFromIssueSummariesJson (
97+ _parseIssuesFromNewFormat (
9298 type: XCResultIssueType .error,
93- issueSummariesJson : errorSummaries ,
94- issueDiscarder : issueDiscarders,
99+ jsonList : resultJson[ 'errors' ] ,
100+ issueDiscarders : issueDiscarders,
95101 ),
96102 );
97- }
98-
99- final Object ? warningSummaries = issuesMap['warningSummaries' ];
100- if (warningSummaries is Map <String , Object ?>) {
101103 issues.addAll (
102- _parseIssuesFromIssueSummariesJson (
104+ _parseIssuesFromNewFormat (
103105 type: XCResultIssueType .warning,
104- issueSummariesJson : warningSummaries ,
105- issueDiscarder : issueDiscarders,
106+ jsonList : resultJson[ 'warnings' ] ,
107+ issueDiscarders : issueDiscarders,
106108 ),
107109 );
110+ } else {
111+ final Object ? issuesMap = resultJson['issues' ];
112+
113+ if (issuesMap is ! Map <String , Object ?>) {
114+ return XCResult .failed (errorMessage: 'xcresult parser: Failed to parse the issues map.' );
115+ }
116+
117+ final Object ? errorSummaries = issuesMap['errorSummaries' ];
118+ if (errorSummaries is Map <String , Object ?>) {
119+ issues.addAll (
120+ _parseIssuesFromXcode15Format (
121+ type: XCResultIssueType .error,
122+ issueSummariesJson: errorSummaries,
123+ issueDiscarder: issueDiscarders,
124+ ),
125+ );
126+ }
127+ final Object ? warningSummaries = issuesMap['warningSummaries' ];
128+ if (warningSummaries is Map <String , Object ?>) {
129+ issues.addAll (
130+ _parseIssuesFromXcode15Format (
131+ type: XCResultIssueType .warning,
132+ issueSummariesJson: warningSummaries,
133+ issueDiscarder: issueDiscarders,
134+ ),
135+ );
136+ }
137+ final Object ? actionsMap = resultJson['actions' ];
138+ if (actionsMap is Map <String , Object ?>) {
139+ final List <XCResultIssue > actionIssues = _parseActionIssues (
140+ actionsMap,
141+ issueDiscarders: issueDiscarders,
142+ );
143+ issues.addAll (actionIssues);
144+ }
108145 }
109146
110- final Object ? actionsMap = resultJson['actions' ];
111- if (actionsMap is Map <String , Object ?>) {
112- final List <XCResultIssue > actionIssues = _parseActionIssues (
113- actionsMap,
114- issueDiscarders: issueDiscarders,
115- );
116- issues.addAll (actionIssues);
147+ if (issues.isEmpty &&
148+ resultJson['issues' ] == null &&
149+ resultJson['actions' ] == null &&
150+ resultJson['errors' ] == null &&
151+ resultJson['warnings' ] == null ) {
152+ return XCResult .failed (errorMessage: 'xcresult parser: Failed to parse the issues map.' );
117153 }
118154
119155 return XCResult ._(issues: issues);
@@ -143,7 +179,6 @@ class XCResult {
143179 this .parsingErrorMessage,
144180 });
145181
146- /// The issues in the xcresult file.
147182 final List <XCResultIssue > issues;
148183
149184 /// Indicate if the xcresult was successfully parsed.
@@ -157,33 +192,24 @@ class XCResult {
157192 final String ? parsingErrorMessage;
158193}
159194
160- /// An issue object in the XCResult
161195class XCResultIssue {
162196 /// Construct an `XCResultIssue` object from `issueJson` .
163197 ///
164198 /// `issueJson` is the object at xcresultJson[['actions']['_values'][0]['buildResult']['issues']['errorSummaries'/'warningSummaries']['_values'] .
165- factory XCResultIssue ({
199+ factory XCResultIssue . fromOldFormat ({
166200 required XCResultIssueType type,
167201 required Map <String , Object ?> issueJson,
168202 }) {
169- // Parse type.
170203 final Object ? issueSubTypeMap = issueJson['issueType' ];
171204 String ? subType;
172205 if (issueSubTypeMap is Map <String , Object ?>) {
173- final Object ? subTypeValue = issueSubTypeMap['_value' ];
174- if (subTypeValue is String ) {
175- subType = subTypeValue;
176- }
206+ subType = issueSubTypeMap['_value' ] as String ? ;
177207 }
178208
179- // Parse message.
180209 String ? message;
181210 final Object ? messageMap = issueJson['message' ];
182211 if (messageMap is Map <String , Object ?>) {
183- final Object ? messageValue = messageMap['_value' ];
184- if (messageValue is String ) {
185- message = messageValue;
186- }
212+ message = messageMap['_value' ] as String ? ;
187213 }
188214
189215 final warnings = < String > [];
@@ -215,7 +241,37 @@ class XCResultIssue {
215241 );
216242 }
217243
218- /// Create a [XCResultIssue] without JSON parsing for testing.
244+ /// Construct an `XCResultIssue` object from the (Xcode 16+) format `issueJson` .
245+ factory XCResultIssue .fromNewFormat ({
246+ required XCResultIssueType type,
247+ required Map <String , Object ?> issueJson,
248+ }) {
249+ final message = issueJson['message' ] as String ? ;
250+
251+ final subType = issueJson['issueType' ] as String ? ;
252+
253+ String ? location;
254+ final warnings = < String > [];
255+
256+ final sourceUrl = issueJson['sourceURL' ] as String ? ;
257+ if (sourceUrl != null ) {
258+ location = _convertUrlToLocationString (sourceUrl);
259+ if (location == null ) {
260+ warnings.add (
261+ '(XCResult) The `sourceURL` exists but it failed to be parsed. url: $sourceUrl ' ,
262+ );
263+ }
264+ }
265+
266+ return XCResultIssue ._(
267+ type: type,
268+ subType: subType,
269+ message: message,
270+ location: location,
271+ warnings: warnings,
272+ );
273+ }
274+
219275 @visibleForTesting
220276 factory XCResultIssue .test ({
221277 XCResultIssueType type = XCResultIssueType .error,
@@ -235,10 +291,10 @@ class XCResultIssue {
235291
236292 XCResultIssue ._({
237293 required this .type,
238- required this .subType,
239- required this .message,
240- required this .location,
241- required this .warnings,
294+ this .subType,
295+ this .message,
296+ this .location,
297+ this .warnings = const < String > [] ,
242298 });
243299
244300 /// The type of the issue.
@@ -314,11 +370,10 @@ class XCResultIssueDiscarder {
314370}
315371
316372// A typical location url string looks like file:///foo.swift#CharacterRangeLen=0&EndingColumnNumber=82&EndingLineNumber=7&StartingColumnNumber=82&StartingLineNumber=7.
317- //
318- // This function converts it to something like: /foo.swift:<StartingLineNumber>:<StartingColumnNumber>.
373+ // This function is now used by BOTH the old and new parsers.
319374String ? _convertUrlToLocationString (String url) {
320375 final Uri ? fragmentLocation = Uri .tryParse (url);
321- if (fragmentLocation == null ) {
376+ if (fragmentLocation == null || ! fragmentLocation.hasFragment ) {
322377 return null ;
323378 }
324379 // Parse the fragment as a query of key-values:
@@ -334,7 +389,6 @@ String? _convertUrlToLocationString(String url) {
334389 return '${fileLocation .path }$startingLineNumber $startingColumnNumber ' ;
335390}
336391
337- // Determine if an `issue` should be discarded based on the `discarder`.
338392bool _shouldDiscardIssue ({
339393 required XCResultIssue issue,
340394 required XCResultIssueDiscarder discarder,
@@ -361,7 +415,30 @@ bool _shouldDiscardIssue({
361415 return false ;
362416}
363417
364- List <XCResultIssue > _parseIssuesFromIssueSummariesJson ({
418+ /// Helper to parse issues from the new (Xcode 16+) flat list format.
419+ List <XCResultIssue > _parseIssuesFromNewFormat ({
420+ required XCResultIssueType type,
421+ required Object ? jsonList,
422+ required List <XCResultIssueDiscarder > issueDiscarders,
423+ }) {
424+ if (jsonList is ! List <Object ?>) {
425+ return const < XCResultIssue > [];
426+ }
427+
428+ return jsonList
429+ .whereType <Map <String , Object ?>>()
430+ .map ((issueJson) => XCResultIssue .fromNewFormat (type: type, issueJson: issueJson))
431+ .where ((issue) {
432+ final bool shouldDiscard = issueDiscarders.any (
433+ (discarder) => _shouldDiscardIssue (issue: issue, discarder: discarder),
434+ );
435+ return ! shouldDiscard;
436+ })
437+ .toList ();
438+ }
439+
440+ /// Helper to parse issues from the old (pre-Xcode 16) format.
441+ List <XCResultIssue > _parseIssuesFromXcode15Format ({
365442 required XCResultIssueType type,
366443 required Map <String , Object ?> issueSummariesJson,
367444 required List <XCResultIssueDiscarder > issueDiscarder,
@@ -370,26 +447,26 @@ List<XCResultIssue> _parseIssuesFromIssueSummariesJson({
370447 final Object ? errorsList = issueSummariesJson['_values' ];
371448 if (errorsList is List <Object ?>) {
372449 for (final Object ? issueJson in errorsList) {
373- if (issueJson == null || issueJson is ! Map <String , Object ?>) {
450+ if (issueJson is ! Map <String , Object ?>) {
374451 continue ;
375452 }
376- final resultIssue = XCResultIssue (type: type, issueJson: issueJson);
453+ final resultIssue = XCResultIssue . fromOldFormat (type: type, issueJson: issueJson);
377454 var discard = false ;
378455 for (final discarder in issueDiscarder) {
379456 if (_shouldDiscardIssue (issue: resultIssue, discarder: discarder)) {
380457 discard = true ;
381458 break ;
382459 }
383460 }
384- if (discard) {
385- continue ;
461+ if (! discard) {
462+ issues. add (resultIssue) ;
386463 }
387- issues.add (resultIssue);
388464 }
389465 }
390466 return issues;
391467}
392468
469+ /// Helper to parse issues from the `actions` block in the pre-Xcode 16 format.
393470List <XCResultIssue > _parseActionIssues (
394471 Map <String , Object ?> actionsMap, {
395472 required List <XCResultIssueDiscarder > issueDiscarders,
@@ -461,14 +538,13 @@ List<XCResultIssue> _parseActionIssues(
461538 final Object ? testFailureSummaries = actionResultIssues['testFailureSummaries' ];
462539 if (testFailureSummaries is Map <String , Object ?>) {
463540 issues.addAll (
464- _parseIssuesFromIssueSummariesJson (
541+ _parseIssuesFromXcode15Format (
465542 type: XCResultIssueType .error,
466543 issueSummariesJson: testFailureSummaries,
467544 issueDiscarder: issueDiscarders,
468545 ),
469546 );
470547 }
471548 }
472-
473549 return issues;
474550}
0 commit comments