blob: f90a7f248ae9fc903ae612020bfd2da2bd765f88 [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 'dart:async';
import 'package:file/file.dart';
import 'package:intl/intl.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../build_system/exceptions.dart';
import '../convert.dart';
import '../devfs.dart';
import '../flutter_manifest.dart';
import '../flutter_project_metadata.dart';
import '../project.dart';
import '../version.dart';
/// Provide suggested GitHub issue templates to user when Flutter encounters an error.
class GitHubTemplateCreator {
GitHubTemplateCreator({
required FileSystem fileSystem,
required Logger logger,
required FlutterProjectFactory flutterProjectFactory,
}) : _fileSystem = fileSystem,
_logger = logger,
_flutterProjectFactory = flutterProjectFactory;
final FileSystem _fileSystem;
final Logger _logger;
final FlutterProjectFactory _flutterProjectFactory;
static String toolCrashSimilarIssuesURL(String errorString) {
return 'https://github.com/flutter/flutter/issues?q=is%3Aissue+${Uri.encodeQueryComponent(errorString)}';
}
/// Restricts exception object strings to contain only information about tool internals.
static String sanitizedCrashException(Object error) {
if (error is ProcessException) {
// Suppress args.
return 'ProcessException: ${error.message} Command: ${error.executable}, OS error code: ${error.errorCode}';
} else if (error is FileSystemException) {
// Suppress path.
return 'FileSystemException: ${error.message}, ${error.osError}';
} else if (error is SocketException) {
// Suppress address and port.
return 'SocketException: ${error.message}, ${error.osError}';
} else if (error is DevFSException) {
// Suppress underlying error.
return 'DevFSException: ${error.message}';
} else if (error is NoSuchMethodError
|| error is ArgumentError
|| error is VersionCheckError
|| error is MissingDefineException
|| error is UnsupportedError
|| error is UnimplementedError
|| error is StateError
|| error is ProcessExit
|| error is OSError) {
// These exception objects only reference tool internals, print the whole error.
return '${error.runtimeType}: $error';
} else if (error is Error) {
return '${error.runtimeType}: ${LineSplitter.split(error.stackTrace.toString()).take(1)}';
} else if (error is String) {
// Force comma separator to standardize.
return 'String: <${NumberFormat(null, 'en_US').format(error.length)} characters>';
}
// Exception, other.
return error.runtimeType.toString();
}
/// GitHub URL to present to the user containing encoded suggested template.
///
/// Shorten the URL, if possible.
Future<String> toolCrashIssueTemplateGitHubURL(
String command,
Object error,
StackTrace stackTrace,
String doctorText
) async {
final String errorString = sanitizedCrashException(error);
final String title = '[tool_crash] $errorString';
final String body = '''
## Command
```
$command
```
## Steps to Reproduce
1. ...
2. ...
3. ...
## Logs
$errorString
```
${LineSplitter.split(stackTrace.toString()).take(25).join('\n')}
```
```
$doctorText
```
## Flutter Application Metadata
${_projectMetadataInformation()}
''';
return 'https://github.com/flutter/flutter/issues'
'/new' // We split this here to appease our lint that looks for bad "new bug" links.
'?title=${Uri.encodeQueryComponent(title)}'
'&body=${Uri.encodeQueryComponent(body)}'
'&labels=${Uri.encodeQueryComponent('tool,severe: crash')}';
}
/// Provide information about the Flutter project in the working directory, if present.
String _projectMetadataInformation() {
FlutterProject project;
try {
project = _flutterProjectFactory.fromDirectory(_fileSystem.currentDirectory);
} on Exception catch (exception) {
// pubspec may be malformed.
return exception.toString();
}
try {
final FlutterManifest manifest = project.manifest;
if (project == null || manifest == null || manifest.isEmpty) {
return 'No pubspec in working directory.';
}
final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.metadataFile, _logger);
final FlutterProjectType? projectType = metadata.projectType;
final StringBuffer description = StringBuffer()
..writeln('**Type**: ${projectType == null ? 'malformed' : flutterProjectTypeToString(projectType)}')
..writeln('**Version**: ${manifest.appVersion}')
..writeln('**Material**: ${manifest.usesMaterialDesign}')
..writeln('**Android X**: ${manifest.usesAndroidX}')
..writeln('**Module**: ${manifest.isModule}')
..writeln('**Plugin**: ${manifest.isPlugin}')
..writeln('**Android package**: ${manifest.androidPackage}')
..writeln('**iOS bundle identifier**: ${manifest.iosBundleIdentifier}')
..writeln('**Creation channel**: ${metadata.versionChannel}')
..writeln('**Creation framework version**: ${metadata.versionRevision}');
final File file = project.flutterPluginsFile;
if (file.existsSync()) {
description.writeln('### Plugins');
// Format is:
// camera=/path/to/.pub-cache/hosted/pub.dartlang.org/camera-0.5.7+2/
for (final String plugin in project.flutterPluginsFile.readAsLinesSync()) {
final List<String> pluginParts = plugin.split('=');
if (pluginParts.length != 2) {
continue;
}
// Write the last part of the path, which includes the plugin name and version.
// Example: camera-0.5.7+2
final List<String> pathParts = _fileSystem.path.split(pluginParts[1]);
description.writeln(pathParts.isEmpty ? pluginParts.first : pathParts.last);
}
}
return description.toString();
} on Exception catch (exception) {
return exception.toString();
}
}
}