blob: 4f505735f915956197211a9a0d0cf38e5198f6ce [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../src/base/process.dart';
import '../../src/convert.dart' show json;
import '../../src/macos/xcode.dart';
import '../convert.dart';
/// The generator of xcresults.
///
/// Call [generate] after an iOS/MacOS build will generate a [XCResult].
/// This only works when the `-resultBundleVersion` is set to 3.
/// * See also: [XCResult].
class XCResultGenerator {
/// Construct the [XCResultGenerator].
XCResultGenerator({
required this.resultPath,
required this.xcode,
required this.processUtils,
});
/// The file path that used to store the xcrun result.
///
/// There's usually a `resultPath.xcresult` file in the same folder.
final String resultPath;
/// The [ProcessUtils] to run commands.
final ProcessUtils processUtils;
/// [Xcode] object used to run xcode command.
final Xcode xcode;
/// Generates the XCResult.
///
/// Calls `xcrun xcresulttool get --path <resultPath> --format json`,
/// then stores the useful information the json into an [XCResult] object.
///
/// A`issueDiscarders` can be passed to discard any issues that matches the description of any [XCResultIssueDiscarder] in the list.
Future<XCResult> generate(
{List<XCResultIssueDiscarder> issueDiscarders =
const <XCResultIssueDiscarder>[]}) async {
final RunResult result = await processUtils.run(
<String>[
...xcode.xcrunCommand(),
'xcresulttool',
'get',
'--path',
resultPath,
'--format',
'json',
],
);
if (result.exitCode != 0) {
return XCResult.failed(errorMessage: result.stderr);
}
if (result.stdout.isEmpty) {
return XCResult.failed(
errorMessage: 'xcresult parser: Unrecognized top level json format.');
}
final Object? resultJson = json.decode(result.stdout);
if (resultJson == null || resultJson is! Map<String, Object?>) {
// If json parsing failed, indicate such error.
// This also includes the top level json object is an array, which indicates
// the structure of the json is changed and this parser class possibly needs to update for this change.
return XCResult.failed(
errorMessage: 'xcresult parser: Unrecognized top level json format.');
}
return XCResult(resultJson: resultJson, issueDiscarders: issueDiscarders);
}
}
/// The xcresult of an `xcodebuild` command.
///
/// This is the result from an `xcrun xcresulttool get --path <resultPath> --format json` run.
/// The result contains useful information such as build errors and warnings.
class XCResult {
/// Parse the `resultJson` and stores useful informations in the returned `XCResult`.
factory XCResult({required Map<String, Object?> resultJson, List<XCResultIssueDiscarder> issueDiscarders = const <XCResultIssueDiscarder>[]}) {
final List<XCResultIssue> issues = <XCResultIssue>[];
final Object? issuesMap = resultJson['issues'];
if (issuesMap == null || issuesMap is! Map<String, Object?>) {
return XCResult.failed(
errorMessage: 'xcresult parser: Failed to parse the issues map.');
}
final Object? errorSummaries = issuesMap['errorSummaries'];
if (errorSummaries is Map<String, Object?>) {
issues.addAll(_parseIssuesFromIssueSummariesJson(
type: XCResultIssueType.error,
issueSummariesJson: errorSummaries,
issueDiscarder: issueDiscarders,
));
}
final Object? warningSummaries = issuesMap['warningSummaries'];
if (warningSummaries is Map<String, Object?>) {
issues.addAll(_parseIssuesFromIssueSummariesJson(
type: XCResultIssueType.warning,
issueSummariesJson: warningSummaries,
issueDiscarder: issueDiscarders,
));
}
return XCResult._(issues: issues);
}
factory XCResult.failed({required String errorMessage}) {
return XCResult._(
parseSuccess: false,
parsingErrorMessage: errorMessage,
);
}
XCResult._({
this.issues = const <XCResultIssue>[],
this.parseSuccess = true,
this.parsingErrorMessage,
});
/// The issues in the xcresult file.
final List<XCResultIssue> issues;
/// Indicate if the xcresult was successfully parsed.
///
/// See also: [parsingErrorMessage] for the error message if the parsing was unsuccessful.
final bool parseSuccess;
/// The error message describes why the parse if unsuccessful.
///
/// This is `null` if [parseSuccess] is `true`.
final String? parsingErrorMessage;
}
/// An issue object in the XCResult
class XCResultIssue {
/// Construct an `XCResultIssue` object from `issueJson`.
///
/// `issueJson` is the object at xcresultJson[['actions']['_values'][0]['buildResult']['issues']['errorSummaries'/'warningSummaries']['_values'].
factory XCResultIssue({
required XCResultIssueType type,
required Map<String, Object?> issueJson,
}) {
// Parse type.
final Object? issueSubTypeMap = issueJson['issueType'];
String? subType;
if (issueSubTypeMap is Map<String, Object?>) {
final Object? subTypeValue = issueSubTypeMap['_value'];
if (subTypeValue is String) {
subType = subTypeValue;
}
}
// Parse message.
String? message;
final Object? messageMap = issueJson['message'];
if (messageMap is Map<String, Object?>) {
final Object? messageValue = messageMap['_value'];
if (messageValue is String) {
message = messageValue;
}
}
final List<String> warnings = <String>[];
// Parse url and convert it to a location String.
String? location;
final Object? documentLocationInCreatingWorkspaceMap =
issueJson['documentLocationInCreatingWorkspace'];
if (documentLocationInCreatingWorkspaceMap is Map<String, Object?>) {
final Object? urlMap = documentLocationInCreatingWorkspaceMap['url'];
if (urlMap is Map<String, Object?>) {
final Object? urlValue = urlMap['_value'];
if (urlValue is String) {
location = _convertUrlToLocationString(urlValue);
if (location == null) {
warnings.add(
'(XCResult) The `url` exists but it was failed to be parsed. url: $urlValue');
}
}
}
}
return XCResultIssue._(
type: type,
subType: subType,
message: message,
location: location,
warnings: warnings,
);
}
XCResultIssue._({
required this.type,
required this.subType,
required this.message,
required this.location,
required this.warnings,
});
/// The type of the issue.
final XCResultIssueType type;
/// The sub type of the issue.
///
/// This is a more detailed category about the issue.
/// The possible values are `Warning`, `Semantic Issue'` etc.
final String? subType;
/// Human readable message for the issue.
///
/// This can be displayed to user for their information.
final String? message;
/// The location where the issue occurs.
///
/// This is a re-formatted version of the "url" value in the json.
/// The format looks like <FileLocation>:<StartingLineNumber>:<StartingColumnNumber>.
final String? location;
/// Warnings when constructing the issue object.
final List<String> warnings;
}
/// The type of an `XCResultIssue`.
enum XCResultIssueType {
/// The issue is an warning.
///
/// This is for all the issues under the `warningSummaries` key in the xcresult.
warning,
/// The issue is an warning.
///
/// This is for all the issues under the `errorSummaries` key in the xcresult.
error,
}
/// Discards the [XCResultIssue] that matches any of the matchers.
class XCResultIssueDiscarder {
XCResultIssueDiscarder(
{this.typeMatcher,
this.subTypeMatcher,
this.messageMatcher,
this.locationMatcher})
: assert(typeMatcher != null ||
subTypeMatcher != null ||
messageMatcher != null ||
locationMatcher != null);
/// The type of the discarder.
///
/// A [XCResultIssue] should be discarded if its `type` equals to this.
final XCResultIssueType? typeMatcher;
/// The subType of the discarder.
///
/// A [XCResultIssue] should be discarded if its `subType` matches the RegExp.
final RegExp? subTypeMatcher;
/// The message of the discarder.
///
/// A [XCResultIssue] should be discarded if its `message` matches the RegExp.
final RegExp? messageMatcher;
/// The location of the discarder.
///
/// A [XCResultIssue] should be discarded if its `location` matches the RegExp.
final RegExp? locationMatcher;
}
// A typical location url string looks like file:///foo.swift#CharacterRangeLen=0&EndingColumnNumber=82&EndingLineNumber=7&StartingColumnNumber=82&StartingLineNumber=7.
//
// This function converts it to something like: /foo.swift:<StartingLineNumber>:<StartingColumnNumber>.
String? _convertUrlToLocationString(String url) {
final Uri? fragmentLocation = Uri.tryParse(url);
if (fragmentLocation == null) {
return null;
}
// Parse the fragment as a query of key-values:
final Uri fileLocation = Uri(
path: fragmentLocation.path,
query: fragmentLocation.fragment,
);
String startingLineNumber =
fileLocation.queryParameters['StartingLineNumber'] ?? '';
if (startingLineNumber.isNotEmpty) {
startingLineNumber = ':$startingLineNumber';
}
String startingColumnNumber =
fileLocation.queryParameters['StartingColumnNumber'] ?? '';
if (startingColumnNumber.isNotEmpty) {
startingColumnNumber = ':$startingColumnNumber';
}
return '${fileLocation.path}$startingLineNumber$startingColumnNumber';
}
// Determine if an `issue` should be discarded based on the `discarder`.
bool _shouldDiscardIssue(
{required XCResultIssue issue, required XCResultIssueDiscarder discarder}) {
if (issue.type == discarder.typeMatcher) {
return true;
}
if (issue.subType != null &&
discarder.subTypeMatcher != null &&
discarder.subTypeMatcher!.hasMatch(issue.subType!)) {
return true;
}
if (issue.message != null &&
discarder.messageMatcher != null &&
discarder.messageMatcher!.hasMatch(issue.message!)) {
return true;
}
if (issue.location != null &&
discarder.locationMatcher != null &&
discarder.locationMatcher!.hasMatch(issue.location!)) {
return true;
}
return false;
}
List<XCResultIssue> _parseIssuesFromIssueSummariesJson({
required XCResultIssueType type,
required Map<String, Object?> issueSummariesJson,
required List<XCResultIssueDiscarder> issueDiscarder,
}) {
final List<XCResultIssue> issues = <XCResultIssue>[];
final Object? errorsList = issueSummariesJson['_values'];
if (errorsList is List<Object?>) {
for (final Object? issueJson in errorsList) {
if (issueJson == null || issueJson is! Map<String, Object?>) {
continue;
}
final XCResultIssue resultIssue = XCResultIssue(
type: type,
issueJson: issueJson,
);
bool discard = false;
for (final XCResultIssueDiscarder discarder in issueDiscarder) {
if (_shouldDiscardIssue(issue: resultIssue, discarder: discarder)) {
discard = true;
break;
}
}
if (discard) {
continue;
}
issues.add(resultIssue);
}
}
return issues;
}