mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Our current top crasher is an unclear error when ProcessManager fails to resolve an executable path. To fix this, we'd like to being adjusting the process resolution logic and adding more instrumentation to track failures. In order to begin the process, the ProcessManager has been folded back into the flutter tool
314 lines
12 KiB
Dart
314 lines
12 KiB
Dart
// 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.
|
|
|
|
// @dart = 2.8
|
|
|
|
import 'package:args/args.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:yaml/yaml.dart' as yaml;
|
|
|
|
import '../artifacts.dart';
|
|
import '../base/common.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/platform.dart';
|
|
import '../base/process.dart';
|
|
import '../base/terminal.dart';
|
|
import '../base/utils.dart';
|
|
import '../cache.dart';
|
|
import '../dart/analysis.dart';
|
|
import '../globals.dart' as globals;
|
|
|
|
/// Common behavior for `flutter analyze` and `flutter analyze --watch`
|
|
abstract class AnalyzeBase {
|
|
AnalyzeBase(this.argResults, {
|
|
@required this.repoRoots,
|
|
@required this.repoPackages,
|
|
@required this.fileSystem,
|
|
@required this.logger,
|
|
@required this.platform,
|
|
@required this.processManager,
|
|
@required this.terminal,
|
|
@required this.artifacts,
|
|
});
|
|
|
|
/// The parsed argument results for execution.
|
|
final ArgResults argResults;
|
|
@protected
|
|
final List<String> repoRoots;
|
|
@protected
|
|
final List<Directory> repoPackages;
|
|
@protected
|
|
final FileSystem fileSystem;
|
|
@protected
|
|
final Logger logger;
|
|
@protected
|
|
final ProcessManager processManager;
|
|
@protected
|
|
final Platform platform;
|
|
@protected
|
|
final Terminal terminal;
|
|
@protected
|
|
final Artifacts artifacts;
|
|
|
|
/// Called by [AnalyzeCommand] to start the analysis process.
|
|
Future<void> analyze();
|
|
|
|
void dumpErrors(Iterable<String> errors) {
|
|
if (argResults['write'] != null) {
|
|
try {
|
|
final RandomAccessFile resultsFile = fileSystem.file(argResults['write']).openSync(mode: FileMode.write);
|
|
try {
|
|
resultsFile.lockSync();
|
|
resultsFile.writeStringSync(errors.join('\n'));
|
|
} finally {
|
|
resultsFile.close();
|
|
}
|
|
} on Exception catch (e) {
|
|
logger.printError('Failed to save output to "${argResults['write']}": $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
void writeBenchmark(Stopwatch stopwatch, int errorCount, int membersMissingDocumentation) {
|
|
const String benchmarkOut = 'analysis_benchmark.json';
|
|
final Map<String, dynamic> data = <String, dynamic>{
|
|
'time': stopwatch.elapsedMilliseconds / 1000.0,
|
|
'issues': errorCount,
|
|
'missingDartDocs': membersMissingDocumentation,
|
|
};
|
|
fileSystem.file(benchmarkOut).writeAsStringSync(toPrettyJson(data));
|
|
logger.printStatus('Analysis benchmark written to $benchmarkOut ($data).');
|
|
}
|
|
|
|
bool get isFlutterRepo => argResults['flutter-repo'] as bool;
|
|
String get sdkPath => argResults['dart-sdk'] as String ?? artifacts.getArtifactPath(Artifact.engineDartSdkPath);
|
|
bool get isBenchmarking => argResults['benchmark'] as bool;
|
|
bool get isDartDocs => argResults['dartdocs'] as bool;
|
|
|
|
static int countMissingDartDocs(List<AnalysisError> errors) {
|
|
return errors.where((AnalysisError error) {
|
|
return error.code == 'public_member_api_docs';
|
|
}).length;
|
|
}
|
|
|
|
static String generateDartDocMessage(int undocumentedMembers) {
|
|
String dartDocMessage;
|
|
|
|
assert(undocumentedMembers >= 0);
|
|
switch (undocumentedMembers) {
|
|
case 0:
|
|
dartDocMessage = 'all public member have documentation';
|
|
break;
|
|
case 1:
|
|
dartDocMessage = 'one public member lacks documentation';
|
|
break;
|
|
default:
|
|
dartDocMessage = '$undocumentedMembers public members lack documentation';
|
|
}
|
|
|
|
return dartDocMessage;
|
|
}
|
|
|
|
/// Generate an analysis summary for both [AnalyzeOnce], [AnalyzeContinuously].
|
|
static String generateErrorsMessage({
|
|
@required int issueCount,
|
|
int issueDiff,
|
|
int files,
|
|
@required String seconds,
|
|
int undocumentedMembers = 0,
|
|
String dartDocMessage = '',
|
|
}) {
|
|
final StringBuffer errorsMessage = StringBuffer(issueCount > 0
|
|
? '$issueCount ${pluralize('issue', issueCount)} found.'
|
|
: 'No issues found!');
|
|
|
|
// Only [AnalyzeContinuously] has issueDiff message.
|
|
if (issueDiff != null) {
|
|
if (issueDiff > 0) {
|
|
errorsMessage.write(' ($issueDiff new)');
|
|
} else if (issueDiff < 0) {
|
|
errorsMessage.write(' (${-issueDiff} fixed)');
|
|
}
|
|
}
|
|
|
|
// Only [AnalyzeContinuously] has files message.
|
|
if (files != null) {
|
|
errorsMessage.write(' • analyzed $files ${pluralize('file', files)}');
|
|
}
|
|
|
|
if (undocumentedMembers > 0) {
|
|
errorsMessage.write(' (ran in ${seconds}s; $dartDocMessage)');
|
|
} else {
|
|
errorsMessage.write(' (ran in ${seconds}s)');
|
|
}
|
|
return errorsMessage.toString();
|
|
}
|
|
}
|
|
|
|
class PackageDependency {
|
|
// This is a map from dependency targets (lib directories) to a list
|
|
// of places that ask for that target (.packages or pubspec.yaml files)
|
|
Map<String, List<String>> values = <String, List<String>>{};
|
|
String canonicalSource;
|
|
void addCanonicalCase(String packagePath, String pubSpecYamlPath) {
|
|
assert(canonicalSource == null);
|
|
add(packagePath, pubSpecYamlPath);
|
|
canonicalSource = pubSpecYamlPath;
|
|
}
|
|
void add(String packagePath, String sourcePath) {
|
|
values.putIfAbsent(packagePath, () => <String>[]).add(sourcePath);
|
|
}
|
|
bool get hasConflict => values.length > 1;
|
|
bool get hasConflictAffectingFlutterRepo {
|
|
assert(globals.fs.path.isAbsolute(Cache.flutterRoot));
|
|
for (final List<String> targetSources in values.values) {
|
|
for (final String source in targetSources) {
|
|
assert(globals.fs.path.isAbsolute(source));
|
|
if (globals.fs.path.isWithin(Cache.flutterRoot, source)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
void describeConflict(StringBuffer result) {
|
|
assert(hasConflict);
|
|
final List<String> targets = values.keys.toList();
|
|
targets.sort((String a, String b) => values[b].length.compareTo(values[a].length));
|
|
for (final String target in targets) {
|
|
final int count = values[target].length;
|
|
result.writeln(' $count ${count == 1 ? 'source wants' : 'sources want'} "$target":');
|
|
bool canonical = false;
|
|
for (final String source in values[target]) {
|
|
result.writeln(' $source');
|
|
if (source == canonicalSource) {
|
|
canonical = true;
|
|
}
|
|
}
|
|
if (canonical) {
|
|
result.writeln(' (This is the actual package definition, so it is considered the canonical "right answer".)');
|
|
}
|
|
}
|
|
}
|
|
String get target => values.keys.single;
|
|
}
|
|
|
|
class PackageDependencyTracker {
|
|
/// Packages whose source is defined in the vended SDK.
|
|
static const List<String> _vendedSdkPackages = <String>['analyzer', 'front_end', 'kernel'];
|
|
|
|
// This is a map from package names to objects that track the paths
|
|
// involved (sources and targets).
|
|
Map<String, PackageDependency> packages = <String, PackageDependency>{};
|
|
|
|
PackageDependency getPackageDependency(String packageName) {
|
|
return packages.putIfAbsent(packageName, () => PackageDependency());
|
|
}
|
|
|
|
/// Read the .packages file in [directory] and add referenced packages to [dependencies].
|
|
void addDependenciesFromPackagesFileIn(Directory directory) {
|
|
final String dotPackagesPath = globals.fs.path.join(directory.path, '.packages');
|
|
final File dotPackages = globals.fs.file(dotPackagesPath);
|
|
if (dotPackages.existsSync()) {
|
|
// this directory has opinions about what we should be using
|
|
final Iterable<String> lines = dotPackages
|
|
.readAsStringSync()
|
|
.split('\n')
|
|
.where((String line) => !line.startsWith(RegExp(r'^ *#')));
|
|
for (final String line in lines) {
|
|
final int colon = line.indexOf(':');
|
|
if (colon > 0) {
|
|
final String packageName = line.substring(0, colon);
|
|
final String packagePath = globals.fs.path.fromUri(line.substring(colon+1));
|
|
// Ensure that we only add `analyzer` and dependent packages defined in the vended SDK (and referred to with a local
|
|
// globals.fs.path. directive). Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored
|
|
// since they would produce spurious conflicts.
|
|
if (!_vendedSdkPackages.contains(packageName) || packagePath.startsWith('..')) {
|
|
add(packageName, globals.fs.path.normalize(globals.fs.path.absolute(directory.path, packagePath)), dotPackagesPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void addCanonicalCase(String packageName, String packagePath, String pubSpecYamlPath) {
|
|
getPackageDependency(packageName).addCanonicalCase(packagePath, pubSpecYamlPath);
|
|
}
|
|
|
|
void add(String packageName, String packagePath, String dotPackagesPath) {
|
|
getPackageDependency(packageName).add(packagePath, dotPackagesPath);
|
|
}
|
|
|
|
void checkForConflictingDependencies(Iterable<Directory> pubSpecDirectories, PackageDependencyTracker dependencies) {
|
|
for (final Directory directory in pubSpecDirectories) {
|
|
final String pubSpecYamlPath = globals.fs.path.join(directory.path, 'pubspec.yaml');
|
|
final File pubSpecYamlFile = globals.fs.file(pubSpecYamlPath);
|
|
if (pubSpecYamlFile.existsSync()) {
|
|
// we are analyzing the actual canonical source for this package;
|
|
// make sure we remember that, in case all the packages are actually
|
|
// pointing elsewhere somehow.
|
|
final dynamic pubSpecYaml = yaml.loadYaml(globals.fs.file(pubSpecYamlPath).readAsStringSync());
|
|
if (pubSpecYaml is yaml.YamlMap) {
|
|
final dynamic packageName = pubSpecYaml['name'];
|
|
if (packageName is String) {
|
|
final String packagePath = globals.fs.path.normalize(globals.fs.path.absolute(globals.fs.path.join(directory.path, 'lib')));
|
|
dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath);
|
|
} else {
|
|
throwToolExit('pubspec.yaml is malformed. The name should be a String.');
|
|
}
|
|
} else {
|
|
throwToolExit('pubspec.yaml is malformed.');
|
|
}
|
|
}
|
|
dependencies.addDependenciesFromPackagesFileIn(directory);
|
|
}
|
|
|
|
// prepare a union of all the .packages files
|
|
if (dependencies.hasConflicts) {
|
|
final StringBuffer message = StringBuffer();
|
|
message.writeln(dependencies.generateConflictReport());
|
|
message.writeln('Make sure you have run "pub upgrade" in all the directories mentioned above.');
|
|
if (dependencies.hasConflictsAffectingFlutterRepo) {
|
|
message.writeln(
|
|
'For packages in the flutter repository, try using "flutter update-packages" to do all of them at once.\n'
|
|
'If you need to actually upgrade them, consider "flutter update-packages --force-upgrade". '
|
|
'(This will update your pubspec.yaml files as well, so you may wish to do this on a separate branch.)'
|
|
);
|
|
}
|
|
message.write(
|
|
'If this does not help, to track down the conflict you can use '
|
|
'"pub deps --style=list" and "pub upgrade --verbosity=solver" in the affected directories.'
|
|
);
|
|
throwToolExit(message.toString());
|
|
}
|
|
}
|
|
|
|
bool get hasConflicts {
|
|
return packages.values.any((PackageDependency dependency) => dependency.hasConflict);
|
|
}
|
|
|
|
bool get hasConflictsAffectingFlutterRepo {
|
|
return packages.values.any((PackageDependency dependency) => dependency.hasConflictAffectingFlutterRepo);
|
|
}
|
|
|
|
String generateConflictReport() {
|
|
assert(hasConflicts);
|
|
final StringBuffer result = StringBuffer();
|
|
for (final String package in packages.keys.where((String package) => packages[package].hasConflict)) {
|
|
result.writeln('Package "$package" has conflicts:');
|
|
packages[package].describeConflict(result);
|
|
}
|
|
return result.toString();
|
|
}
|
|
|
|
Map<String, String> asPackageMap() {
|
|
final Map<String, String> result = <String, String>{};
|
|
for (final String package in packages.keys) {
|
|
result[package] = packages[package].target;
|
|
}
|
|
return result;
|
|
}
|
|
}
|