flutter analyze --watch (#6093)
This commit is contained in:
@@ -45,7 +45,7 @@ SRC_ROOT=$PWD
|
||||
|
||||
# generate and analyze our large sample app
|
||||
dart dev/tools/mega_gallery.dart
|
||||
(cd dev/benchmarks/mega_gallery; flutter watch --benchmark)
|
||||
(cd dev/benchmarks/mega_gallery; flutter analyze --watch --benchmark)
|
||||
|
||||
if [ -n "$COVERAGE_FLAG" ]; then
|
||||
GSUTIL=$HOME/google-cloud-sdk/bin/gsutil
|
||||
|
||||
@@ -38,7 +38,6 @@ import 'src/commands/test.dart';
|
||||
import 'src/commands/trace.dart';
|
||||
import 'src/commands/update_packages.dart';
|
||||
import 'src/commands/upgrade.dart';
|
||||
import 'src/commands/watch.dart';
|
||||
import 'src/device.dart';
|
||||
import 'src/doctor.dart';
|
||||
import 'src/globals.dart';
|
||||
@@ -85,8 +84,7 @@ Future<Null> main(List<String> args) async {
|
||||
..addCommand(new TestCommand())
|
||||
..addCommand(new TraceCommand())
|
||||
..addCommand(new UpdatePackagesCommand(hidden: !verboseHelp))
|
||||
..addCommand(new UpgradeCommand())
|
||||
..addCommand(new WatchCommand(verboseHelp: verboseHelp));
|
||||
..addCommand(new UpgradeCommand());
|
||||
|
||||
return Chain.capture/*<Future<Null>>*/(() async {
|
||||
// Initialize globals.
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright 2015 The Chromium 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:io';
|
||||
|
||||
import '../base/utils.dart';
|
||||
import '../globals.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
/// Common behavior for `flutter analyze` and `flutter watch`
|
||||
abstract class AnalysisCommand extends FlutterCommand {
|
||||
AnalysisCommand({bool verboseHelp: false}) {
|
||||
argParser.addFlag('flutter-repo', help: 'Include all the examples and tests from the Flutter repository.', defaultsTo: false);
|
||||
argParser.addFlag('current-directory', help: 'Include all the Dart files in the current directory, if any.', defaultsTo: true);
|
||||
argParser.addFlag('current-package', help: 'Include the lib/main.dart file from the current directory, if any.', defaultsTo: true);
|
||||
argParser.addFlag('dartdocs', help: 'List every public member that is lacking documentation (only examines files in the Flutter repository).', defaultsTo: false);
|
||||
argParser.addOption('write', valueHelp: 'file', help: 'Also output the results to a file.');
|
||||
argParser.addOption('dart-sdk', valueHelp: 'path-to-sdk', help: 'The path to the Dart SDK.', hide: !verboseHelp);
|
||||
|
||||
// Hidden option to enable a benchmarking mode.
|
||||
argParser.addFlag('benchmark', negatable: false, hide: !verboseHelp, help: 'Also output the analysis time');
|
||||
|
||||
usesPubOption();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get shouldRunPub {
|
||||
// If they're not analyzing the current project.
|
||||
if (!argResults['current-package'])
|
||||
return false;
|
||||
|
||||
// Or we're not in a project directory.
|
||||
if (!new File('pubspec.yaml').existsSync())
|
||||
return false;
|
||||
|
||||
return super.shouldRunPub;
|
||||
}
|
||||
|
||||
void dumpErrors(Iterable<String> errors) {
|
||||
if (argResults['write'] != null) {
|
||||
try {
|
||||
final RandomAccessFile resultsFile = new File(argResults['write']).openSync(mode: FileMode.WRITE);
|
||||
try {
|
||||
resultsFile.lockSync();
|
||||
resultsFile.writeStringSync(errors.join('\n'));
|
||||
} finally {
|
||||
resultsFile.close();
|
||||
}
|
||||
} catch (e) {
|
||||
printError('Failed to save output to "${argResults['write']}": $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void writeBenchmark(Stopwatch stopwatch, int errorCount, int membersMissingDocumentation) {
|
||||
final String benchmarkOut = 'analysis_benchmark.json';
|
||||
Map<String, dynamic> data = <String, dynamic>{
|
||||
'time': (stopwatch.elapsedMilliseconds / 1000.0),
|
||||
'issues': errorCount,
|
||||
'missingDartDocs': membersMissingDocumentation
|
||||
};
|
||||
new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
|
||||
printStatus('Analysis benchmark written to $benchmarkOut ($data).');
|
||||
}
|
||||
|
||||
bool get isBenchmarking => argResults['benchmark'];
|
||||
}
|
||||
@@ -3,23 +3,32 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart' as yaml;
|
||||
|
||||
import '../cache.dart';
|
||||
import '../dart/analysis.dart';
|
||||
import '../globals.dart';
|
||||
import 'analysis_common.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
import 'analyze_continuously.dart';
|
||||
import 'analyze_once.dart';
|
||||
|
||||
bool isDartFile(FileSystemEntity entry) => entry is File && entry.path.endsWith('.dart');
|
||||
|
||||
typedef bool FileFilter(FileSystemEntity entity);
|
||||
|
||||
class AnalyzeCommand extends AnalysisCommand {
|
||||
AnalyzeCommand({bool verboseHelp: false}) : super(verboseHelp: verboseHelp) {
|
||||
class AnalyzeCommand extends FlutterCommand {
|
||||
AnalyzeCommand({bool verboseHelp: false}) {
|
||||
argParser.addFlag('flutter-repo', help: 'Include all the examples and tests from the Flutter repository.', defaultsTo: false);
|
||||
argParser.addFlag('current-directory', help: 'Include all the Dart files in the current directory, if any.', defaultsTo: true);
|
||||
argParser.addFlag('current-package', help: 'Include the lib/main.dart file from the current directory, if any.', defaultsTo: true);
|
||||
argParser.addFlag('dartdocs', help: 'List every public member that is lacking documentation (only examines files in the Flutter repository).', defaultsTo: false);
|
||||
argParser.addFlag('watch', help: 'Run analysis continuously, watching the filesystem for changes.', negatable: false);
|
||||
argParser.addOption('write', valueHelp: 'file', help: 'Also output the results to a file. This is useful with --watch if you want a file to always contain the latest results.');
|
||||
argParser.addOption('dart-sdk', valueHelp: 'path-to-sdk', help: 'The path to the Dart SDK.', hide: !verboseHelp);
|
||||
|
||||
// Hidden option to enable a benchmarking mode.
|
||||
argParser.addFlag('benchmark', negatable: false, hide: !verboseHelp, help: 'Also output the analysis time');
|
||||
|
||||
usesPubOption();
|
||||
|
||||
// Not used by analyze --watch
|
||||
argParser.addFlag('congratulate', help: 'Show output even when there are no errors, warnings, hints, or lints.', defaultsTo: true);
|
||||
argParser.addFlag('preamble', help: 'Display the number of files that will be analyzed.', defaultsTo: true);
|
||||
}
|
||||
@@ -31,318 +40,24 @@ class AnalyzeCommand extends AnalysisCommand {
|
||||
String get description => 'Analyze the project\'s Dart code.';
|
||||
|
||||
@override
|
||||
Future<int> runCommand() async {
|
||||
Stopwatch stopwatch = new Stopwatch()..start();
|
||||
Set<Directory> pubSpecDirectories = new HashSet<Directory>();
|
||||
List<File> dartFiles = <File>[];
|
||||
bool get shouldRunPub {
|
||||
// If they're not analyzing the current project.
|
||||
if (!argResults['current-package'])
|
||||
return false;
|
||||
|
||||
for (String file in argResults.rest.toList()) {
|
||||
file = path.normalize(path.absolute(file));
|
||||
String root = path.rootPrefix(file);
|
||||
dartFiles.add(new File(file));
|
||||
while (file != root) {
|
||||
file = path.dirname(file);
|
||||
if (FileSystemEntity.isFileSync(path.join(file, 'pubspec.yaml'))) {
|
||||
pubSpecDirectories.add(new Directory(file));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Or we're not in a project directory.
|
||||
if (!new File('pubspec.yaml').existsSync())
|
||||
return false;
|
||||
|
||||
bool currentDirectory = argResults['current-directory'] && (argResults.wasParsed('current-directory') || dartFiles.isEmpty);
|
||||
bool currentPackage = argResults['current-package'] && (argResults.wasParsed('current-package') || dartFiles.isEmpty);
|
||||
bool flutterRepo = argResults['flutter-repo'] || inRepo(argResults.rest);
|
||||
|
||||
//TODO (pq): revisit package and directory defaults
|
||||
|
||||
if (currentDirectory && !flutterRepo) {
|
||||
// ./*.dart
|
||||
Directory currentDirectory = new Directory('.');
|
||||
bool foundOne = false;
|
||||
for (FileSystemEntity entry in currentDirectory.listSync()) {
|
||||
if (isDartFile(entry)) {
|
||||
dartFiles.add(entry);
|
||||
foundOne = true;
|
||||
}
|
||||
}
|
||||
if (foundOne)
|
||||
pubSpecDirectories.add(currentDirectory);
|
||||
}
|
||||
|
||||
if (currentPackage && !flutterRepo) {
|
||||
// **/.*dart
|
||||
Directory currentDirectory = new Directory('.');
|
||||
_collectDartFiles(currentDirectory, dartFiles);
|
||||
pubSpecDirectories.add(currentDirectory);
|
||||
}
|
||||
|
||||
// TODO(ianh): Fix the intl package resource generator
|
||||
// TODO(pq): extract this regexp from the exclude in options
|
||||
RegExp stockExampleFiles = new RegExp('examples/stocks/lib/i18n/.*\.dart\$');
|
||||
|
||||
if (flutterRepo) {
|
||||
for (Directory dir in runner.getRepoPackages()) {
|
||||
_collectDartFiles(dir, dartFiles,
|
||||
exclude: (FileSystemEntity entity) => stockExampleFiles.hasMatch(entity.path));
|
||||
pubSpecDirectories.add(dir);
|
||||
}
|
||||
}
|
||||
|
||||
// determine what all the various .packages files depend on
|
||||
PackageDependencyTracker dependencies = new PackageDependencyTracker();
|
||||
for (Directory directory in pubSpecDirectories) {
|
||||
String pubSpecYamlPath = path.join(directory.path, 'pubspec.yaml');
|
||||
File pubSpecYamlFile = new 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.
|
||||
yaml.YamlMap pubSpecYaml = yaml.loadYaml(new File(pubSpecYamlPath).readAsStringSync());
|
||||
String packageName = pubSpecYaml['name'];
|
||||
String packagePath = path.normalize(path.absolute(path.join(directory.path, 'lib')));
|
||||
dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath);
|
||||
}
|
||||
String dotPackagesPath = path.join(directory.path, '.packages');
|
||||
File dotPackages = new File(dotPackagesPath);
|
||||
if (dotPackages.existsSync()) {
|
||||
// this directory has opinions about what we should be using
|
||||
dotPackages
|
||||
.readAsStringSync()
|
||||
.split('\n')
|
||||
.where((String line) => !line.startsWith(new RegExp(r'^ *#')))
|
||||
.forEach((String line) {
|
||||
int colon = line.indexOf(':');
|
||||
if (colon > 0) {
|
||||
String packageName = line.substring(0, colon);
|
||||
String packagePath = path.fromUri(line.substring(colon+1));
|
||||
// Ensure that we only add the `analyzer` package defined in the vended SDK (and referred to with a local path directive).
|
||||
// Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored since they would produce
|
||||
// spurious conflicts.
|
||||
if (packageName != 'analyzer' || packagePath.startsWith('..'))
|
||||
dependencies.add(packageName, path.normalize(path.absolute(directory.path, path.fromUri(packagePath))), dotPackagesPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// prepare a union of all the .packages files
|
||||
if (dependencies.hasConflicts) {
|
||||
printError(dependencies.generateConflictReport());
|
||||
printError('Make sure you have run "pub upgrade" in all the directories mentioned above.');
|
||||
if (dependencies.hasConflictsAffectingFlutterRepo)
|
||||
printError('For packages in the flutter repository, try using "flutter update-packages --upgrade" to do all of them at once.');
|
||||
printError('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.');
|
||||
return 1;
|
||||
}
|
||||
Map<String, String> packages = dependencies.asPackageMap();
|
||||
|
||||
Cache.releaseLockEarly();
|
||||
|
||||
if (argResults['preamble']) {
|
||||
if (dartFiles.length == 1) {
|
||||
logger.printStatus('Analyzing ${path.relative(dartFiles.first.path)}...');
|
||||
} else {
|
||||
logger.printStatus('Analyzing ${dartFiles.length} files...');
|
||||
}
|
||||
}
|
||||
DriverOptions options = new DriverOptions();
|
||||
options.dartSdkPath = argResults['dart-sdk'];
|
||||
options.packageMap = packages;
|
||||
options.analysisOptionsFile = flutterRepo
|
||||
? path.join(Cache.flutterRoot, '.analysis_options_repo')
|
||||
: path.join(Cache.flutterRoot, '.analysis_options_user');
|
||||
AnalysisDriver analyzer = new AnalysisDriver(options);
|
||||
|
||||
// TODO(pq): consider error handling
|
||||
List<AnalysisErrorDescription> errors = analyzer.analyze(dartFiles);
|
||||
|
||||
int errorCount = 0;
|
||||
int membersMissingDocumentation = 0;
|
||||
for (AnalysisErrorDescription error in errors) {
|
||||
bool shouldIgnore = false;
|
||||
if (error.errorCode.name == 'public_member_api_docs') {
|
||||
// https://github.com/dart-lang/linter/issues/207
|
||||
// https://github.com/dart-lang/linter/issues/208
|
||||
if (isFlutterLibrary(error.source.fullName)) {
|
||||
if (!argResults['dartdocs']) {
|
||||
membersMissingDocumentation += 1;
|
||||
shouldIgnore = true;
|
||||
}
|
||||
} else {
|
||||
shouldIgnore = true;
|
||||
}
|
||||
}
|
||||
// TODO(ianh): Fix the Dart mojom compiler
|
||||
if (error.source.fullName.endsWith('.mojom.dart'))
|
||||
shouldIgnore = true;
|
||||
if (shouldIgnore)
|
||||
continue;
|
||||
printError(error.asString());
|
||||
errorCount += 1;
|
||||
}
|
||||
dumpErrors(errors.map/*<String>*/((AnalysisErrorDescription error) => error.asString()));
|
||||
|
||||
stopwatch.stop();
|
||||
String elapsed = (stopwatch.elapsedMilliseconds / 1000.0).toStringAsFixed(1);
|
||||
|
||||
if (isBenchmarking)
|
||||
writeBenchmark(stopwatch, errorCount, membersMissingDocumentation);
|
||||
|
||||
if (errorCount > 0) {
|
||||
if (membersMissingDocumentation > 0 && flutterRepo)
|
||||
printError('[lint] $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation (ran in ${elapsed}s)');
|
||||
else
|
||||
print('(Ran in ${elapsed}s)');
|
||||
return 1; // we consider any level of error to be an error exit (we don't report different levels)
|
||||
}
|
||||
if (argResults['congratulate']) {
|
||||
if (membersMissingDocumentation > 0 && flutterRepo) {
|
||||
printStatus('No analyzer warnings! (ran in ${elapsed}s; $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation)');
|
||||
} else {
|
||||
printStatus('No analyzer warnings! (ran in ${elapsed}s)');
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return super.shouldRunPub;
|
||||
}
|
||||
|
||||
List<String> flutterRootComponents;
|
||||
bool isFlutterLibrary(String filename) {
|
||||
flutterRootComponents ??= path.normalize(path.absolute(Cache.flutterRoot)).split(path.separator);
|
||||
List<String> filenameComponents = path.normalize(path.absolute(filename)).split(path.separator);
|
||||
if (filenameComponents.length < flutterRootComponents.length + 4) // the 4: 'packages', package_name, 'lib', file_name
|
||||
return false;
|
||||
for (int index = 0; index < flutterRootComponents.length; index += 1) {
|
||||
if (flutterRootComponents[index] != filenameComponents[index])
|
||||
return false;
|
||||
@override
|
||||
Future<int> runCommand() {
|
||||
if (argResults['watch']) {
|
||||
return new AnalyzeContinuously(argResults, runner.getRepoAnalysisEntryPoints()).analyze();
|
||||
} else {
|
||||
return new AnalyzeOnce(argResults, runner.getRepoPackages()).analyze();
|
||||
}
|
||||
if (filenameComponents[flutterRootComponents.length] != 'packages')
|
||||
return false;
|
||||
if (filenameComponents[flutterRootComponents.length + 1] == 'flutter_tools')
|
||||
return false;
|
||||
if (filenameComponents[flutterRootComponents.length + 2] != 'lib')
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Return `true` if [fileList] contains a path that resides inside the Flutter repository.
|
||||
/// If [fileList] is empty, then return `true` if the current directory resides inside the Flutter repository.
|
||||
bool inRepo(List<String> fileList) {
|
||||
if (fileList == null || fileList.isEmpty)
|
||||
fileList = <String>[path.current];
|
||||
String root = path.normalize(path.absolute(Cache.flutterRoot));
|
||||
String prefix = root + Platform.pathSeparator;
|
||||
for (String file in fileList) {
|
||||
file = path.normalize(path.absolute(file));
|
||||
if (file == root || file.startsWith(prefix))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<File> _collectDartFiles(Directory dir, List<File> collected, {FileFilter exclude}) {
|
||||
// Bail out in case of a .dartignore.
|
||||
if (FileSystemEntity.isFileSync(path.join(path.dirname(dir.path), '.dartignore')))
|
||||
return collected;
|
||||
|
||||
for (FileSystemEntity entity in dir.listSync(recursive: false, followLinks: false)) {
|
||||
if (isDartFile(entity) && (exclude == null || !exclude(entity)))
|
||||
collected.add(entity);
|
||||
if (entity is Directory) {
|
||||
String name = path.basename(entity.path);
|
||||
if (!name.startsWith('.') && name != 'packages')
|
||||
_collectDartFiles(entity, collected, exclude: exclude);
|
||||
}
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
}
|
||||
|
||||
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(path.isAbsolute(Cache.flutterRoot));
|
||||
for (List<String> targetSources in values.values) {
|
||||
for (String source in targetSources) {
|
||||
assert(path.isAbsolute(source));
|
||||
if (path.isWithin(Cache.flutterRoot, source))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void describeConflict(StringBuffer result) {
|
||||
assert(hasConflict);
|
||||
List<String> targets = values.keys.toList();
|
||||
targets.sort((String a, String b) => values[b].length.compareTo(values[a].length));
|
||||
for (String target in targets) {
|
||||
int count = values[target].length;
|
||||
result.writeln(' $count ${count == 1 ? 'source wants' : 'sources want'} "$target":');
|
||||
bool canonical = false;
|
||||
for (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 {
|
||||
// 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, () => new PackageDependency());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
StringBuffer result = new StringBuffer();
|
||||
for (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() {
|
||||
Map<String, String> result = <String, String>{};
|
||||
for (String package in packages.keys)
|
||||
result[package] = packages[package].target;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
68
packages/flutter_tools/lib/src/commands/analyze_base.dart
Normal file
68
packages/flutter_tools/lib/src/commands/analyze_base.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2015 The Chromium 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 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../base/utils.dart';
|
||||
import '../cache.dart';
|
||||
import '../globals.dart';
|
||||
|
||||
/// Common behavior for `flutter analyze` and `flutter analyze --watch`
|
||||
abstract class AnalyzeBase {
|
||||
/// The parsed argument results for execution.
|
||||
final ArgResults argResults;
|
||||
|
||||
AnalyzeBase(this.argResults);
|
||||
|
||||
/// Called by [AnalyzeCommand] to start the analysis process.
|
||||
Future<int> analyze();
|
||||
|
||||
void dumpErrors(Iterable<String> errors) {
|
||||
if (argResults['write'] != null) {
|
||||
try {
|
||||
final RandomAccessFile resultsFile = new File(argResults['write']).openSync(mode: FileMode.WRITE);
|
||||
try {
|
||||
resultsFile.lockSync();
|
||||
resultsFile.writeStringSync(errors.join('\n'));
|
||||
} finally {
|
||||
resultsFile.close();
|
||||
}
|
||||
} catch (e) {
|
||||
printError('Failed to save output to "${argResults['write']}": $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void writeBenchmark(Stopwatch stopwatch, int errorCount, int membersMissingDocumentation) {
|
||||
final String benchmarkOut = 'analysis_benchmark.json';
|
||||
Map<String, dynamic> data = <String, dynamic>{
|
||||
'time': (stopwatch.elapsedMilliseconds / 1000.0),
|
||||
'issues': errorCount,
|
||||
'missingDartDocs': membersMissingDocumentation
|
||||
};
|
||||
new File(benchmarkOut).writeAsStringSync(toPrettyJson(data));
|
||||
printStatus('Analysis benchmark written to $benchmarkOut ($data).');
|
||||
}
|
||||
|
||||
bool get isBenchmarking => argResults['benchmark'];
|
||||
}
|
||||
|
||||
/// Return `true` if [fileList] contains a path that resides inside the Flutter repository.
|
||||
/// If [fileList] is empty, then return `true` if the current directory resides inside the Flutter repository.
|
||||
bool inRepo(List<String> fileList) {
|
||||
if (fileList == null || fileList.isEmpty)
|
||||
fileList = <String>[path.current];
|
||||
String root = path.normalize(path.absolute(Cache.flutterRoot));
|
||||
String prefix = root + Platform.pathSeparator;
|
||||
for (String file in fileList) {
|
||||
file = path.normalize(path.absolute(file));
|
||||
if (file == root || file.startsWith(prefix))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../base/logger.dart';
|
||||
@@ -13,16 +14,12 @@ import '../base/utils.dart';
|
||||
import '../cache.dart';
|
||||
import '../dart/sdk.dart';
|
||||
import '../globals.dart';
|
||||
import 'analysis_common.dart';
|
||||
import 'analyze_base.dart';
|
||||
|
||||
class WatchCommand extends AnalysisCommand {
|
||||
WatchCommand({bool verboseHelp: false}) : super(verboseHelp: verboseHelp);
|
||||
class AnalyzeContinuously extends AnalyzeBase {
|
||||
final List<Directory> repoAnalysisEntryPoints;
|
||||
|
||||
@override
|
||||
String get name => 'watch';
|
||||
|
||||
@override
|
||||
String get description => 'Analyze the project\'s Dart code continuously, watching the filesystem for changes.';
|
||||
AnalyzeContinuously(ArgResults argResults, this.repoAnalysisEntryPoints) : super(argResults);
|
||||
|
||||
String analysisTarget;
|
||||
bool firstAnalysis = true;
|
||||
@@ -33,11 +30,11 @@ class WatchCommand extends AnalysisCommand {
|
||||
Status analysisStatus;
|
||||
|
||||
@override
|
||||
Future<int> runCommand() async {
|
||||
Future<int> analyze() async {
|
||||
List<String> directories;
|
||||
|
||||
if (argResults['flutter-repo']) {
|
||||
directories = runner.getRepoAnalysisEntryPoints().map((Directory dir) => dir.path).toList();
|
||||
directories = repoAnalysisEntryPoints.map((Directory dir) => dir.path).toList();
|
||||
analysisTarget = 'Flutter repository';
|
||||
printTrace('Analyzing Flutter repository:');
|
||||
for (String projectPath in directories)
|
||||
329
packages/flutter_tools/lib/src/commands/analyze_once.dart
Normal file
329
packages/flutter_tools/lib/src/commands/analyze_once.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
// Copyright 2015 The Chromium 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 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart' as yaml;
|
||||
|
||||
import '../cache.dart';
|
||||
import '../dart/analysis.dart';
|
||||
import '../globals.dart';
|
||||
import 'analyze.dart';
|
||||
import 'analyze_base.dart';
|
||||
|
||||
bool isDartFile(FileSystemEntity entry) => entry is File && entry.path.endsWith('.dart');
|
||||
|
||||
typedef bool FileFilter(FileSystemEntity entity);
|
||||
|
||||
/// An aspect of the [AnalyzeCommand] to perform once time analysis.
|
||||
class AnalyzeOnce extends AnalyzeBase {
|
||||
final List<Directory> repoPackages;
|
||||
|
||||
AnalyzeOnce(ArgResults argResults, this.repoPackages) : super(argResults);
|
||||
|
||||
@override
|
||||
Future<int> analyze() async {
|
||||
Stopwatch stopwatch = new Stopwatch()..start();
|
||||
Set<Directory> pubSpecDirectories = new HashSet<Directory>();
|
||||
List<File> dartFiles = <File>[];
|
||||
|
||||
for (String file in argResults.rest.toList()) {
|
||||
file = path.normalize(path.absolute(file));
|
||||
String root = path.rootPrefix(file);
|
||||
dartFiles.add(new File(file));
|
||||
while (file != root) {
|
||||
file = path.dirname(file);
|
||||
if (FileSystemEntity.isFileSync(path.join(file, 'pubspec.yaml'))) {
|
||||
pubSpecDirectories.add(new Directory(file));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool currentDirectory = argResults['current-directory'] && (argResults.wasParsed('current-directory') || dartFiles.isEmpty);
|
||||
bool currentPackage = argResults['current-package'] && (argResults.wasParsed('current-package') || dartFiles.isEmpty);
|
||||
bool flutterRepo = argResults['flutter-repo'] || inRepo(argResults.rest);
|
||||
|
||||
//TODO (pq): revisit package and directory defaults
|
||||
|
||||
if (currentDirectory && !flutterRepo) {
|
||||
// ./*.dart
|
||||
Directory currentDirectory = new Directory('.');
|
||||
bool foundOne = false;
|
||||
for (FileSystemEntity entry in currentDirectory.listSync()) {
|
||||
if (isDartFile(entry)) {
|
||||
dartFiles.add(entry);
|
||||
foundOne = true;
|
||||
}
|
||||
}
|
||||
if (foundOne)
|
||||
pubSpecDirectories.add(currentDirectory);
|
||||
}
|
||||
|
||||
if (currentPackage && !flutterRepo) {
|
||||
// **/.*dart
|
||||
Directory currentDirectory = new Directory('.');
|
||||
_collectDartFiles(currentDirectory, dartFiles);
|
||||
pubSpecDirectories.add(currentDirectory);
|
||||
}
|
||||
|
||||
// TODO(ianh): Fix the intl package resource generator
|
||||
// TODO(pq): extract this regexp from the exclude in options
|
||||
RegExp stockExampleFiles = new RegExp('examples/stocks/lib/i18n/.*\.dart\$');
|
||||
|
||||
if (flutterRepo) {
|
||||
for (Directory dir in repoPackages) {
|
||||
_collectDartFiles(dir, dartFiles,
|
||||
exclude: (FileSystemEntity entity) => stockExampleFiles.hasMatch(entity.path));
|
||||
pubSpecDirectories.add(dir);
|
||||
}
|
||||
}
|
||||
|
||||
// determine what all the various .packages files depend on
|
||||
PackageDependencyTracker dependencies = new PackageDependencyTracker();
|
||||
for (Directory directory in pubSpecDirectories) {
|
||||
String pubSpecYamlPath = path.join(directory.path, 'pubspec.yaml');
|
||||
File pubSpecYamlFile = new 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.
|
||||
yaml.YamlMap pubSpecYaml = yaml.loadYaml(new File(pubSpecYamlPath).readAsStringSync());
|
||||
String packageName = pubSpecYaml['name'];
|
||||
String packagePath = path.normalize(path.absolute(path.join(directory.path, 'lib')));
|
||||
dependencies.addCanonicalCase(packageName, packagePath, pubSpecYamlPath);
|
||||
}
|
||||
String dotPackagesPath = path.join(directory.path, '.packages');
|
||||
File dotPackages = new File(dotPackagesPath);
|
||||
if (dotPackages.existsSync()) {
|
||||
// this directory has opinions about what we should be using
|
||||
dotPackages
|
||||
.readAsStringSync()
|
||||
.split('\n')
|
||||
.where((String line) => !line.startsWith(new RegExp(r'^ *#')))
|
||||
.forEach((String line) {
|
||||
int colon = line.indexOf(':');
|
||||
if (colon > 0) {
|
||||
String packageName = line.substring(0, colon);
|
||||
String packagePath = path.fromUri(line.substring(colon+1));
|
||||
// Ensure that we only add the `analyzer` package defined in the vended SDK (and referred to with a local path directive).
|
||||
// Analyzer package versions reached via transitive dependencies (e.g., via `test`) are ignored since they would produce
|
||||
// spurious conflicts.
|
||||
if (packageName != 'analyzer' || packagePath.startsWith('..'))
|
||||
dependencies.add(packageName, path.normalize(path.absolute(directory.path, path.fromUri(packagePath))), dotPackagesPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// prepare a union of all the .packages files
|
||||
if (dependencies.hasConflicts) {
|
||||
printError(dependencies.generateConflictReport());
|
||||
printError('Make sure you have run "pub upgrade" in all the directories mentioned above.');
|
||||
if (dependencies.hasConflictsAffectingFlutterRepo)
|
||||
printError('For packages in the flutter repository, try using "flutter update-packages --upgrade" to do all of them at once.');
|
||||
printError('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.');
|
||||
return 1;
|
||||
}
|
||||
Map<String, String> packages = dependencies.asPackageMap();
|
||||
|
||||
Cache.releaseLockEarly();
|
||||
|
||||
if (argResults['preamble']) {
|
||||
if (dartFiles.length == 1) {
|
||||
logger.printStatus('Analyzing ${path.relative(dartFiles.first.path)}...');
|
||||
} else {
|
||||
logger.printStatus('Analyzing ${dartFiles.length} files...');
|
||||
}
|
||||
}
|
||||
DriverOptions options = new DriverOptions();
|
||||
options.dartSdkPath = argResults['dart-sdk'];
|
||||
options.packageMap = packages;
|
||||
options.analysisOptionsFile = flutterRepo
|
||||
? path.join(Cache.flutterRoot, '.analysis_options_repo')
|
||||
: path.join(Cache.flutterRoot, '.analysis_options_user');
|
||||
AnalysisDriver analyzer = new AnalysisDriver(options);
|
||||
|
||||
// TODO(pq): consider error handling
|
||||
List<AnalysisErrorDescription> errors = analyzer.analyze(dartFiles);
|
||||
|
||||
int errorCount = 0;
|
||||
int membersMissingDocumentation = 0;
|
||||
for (AnalysisErrorDescription error in errors) {
|
||||
bool shouldIgnore = false;
|
||||
if (error.errorCode.name == 'public_member_api_docs') {
|
||||
// https://github.com/dart-lang/linter/issues/207
|
||||
// https://github.com/dart-lang/linter/issues/208
|
||||
if (isFlutterLibrary(error.source.fullName)) {
|
||||
if (!argResults['dartdocs']) {
|
||||
membersMissingDocumentation += 1;
|
||||
shouldIgnore = true;
|
||||
}
|
||||
} else {
|
||||
shouldIgnore = true;
|
||||
}
|
||||
}
|
||||
// TODO(ianh): Fix the Dart mojom compiler
|
||||
if (error.source.fullName.endsWith('.mojom.dart'))
|
||||
shouldIgnore = true;
|
||||
if (shouldIgnore)
|
||||
continue;
|
||||
printError(error.asString());
|
||||
errorCount += 1;
|
||||
}
|
||||
dumpErrors(errors.map/*<String>*/((AnalysisErrorDescription error) => error.asString()));
|
||||
|
||||
stopwatch.stop();
|
||||
String elapsed = (stopwatch.elapsedMilliseconds / 1000.0).toStringAsFixed(1);
|
||||
|
||||
if (isBenchmarking)
|
||||
writeBenchmark(stopwatch, errorCount, membersMissingDocumentation);
|
||||
|
||||
if (errorCount > 0) {
|
||||
if (membersMissingDocumentation > 0 && flutterRepo)
|
||||
printError('[lint] $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation (ran in ${elapsed}s)');
|
||||
else
|
||||
print('(Ran in ${elapsed}s)');
|
||||
return 1; // we consider any level of error to be an error exit (we don't report different levels)
|
||||
}
|
||||
if (argResults['congratulate']) {
|
||||
if (membersMissingDocumentation > 0 && flutterRepo) {
|
||||
printStatus('No analyzer warnings! (ran in ${elapsed}s; $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation)');
|
||||
} else {
|
||||
printStatus('No analyzer warnings! (ran in ${elapsed}s)');
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<String> flutterRootComponents;
|
||||
bool isFlutterLibrary(String filename) {
|
||||
flutterRootComponents ??= path.normalize(path.absolute(Cache.flutterRoot)).split(path.separator);
|
||||
List<String> filenameComponents = path.normalize(path.absolute(filename)).split(path.separator);
|
||||
if (filenameComponents.length < flutterRootComponents.length + 4) // the 4: 'packages', package_name, 'lib', file_name
|
||||
return false;
|
||||
for (int index = 0; index < flutterRootComponents.length; index += 1) {
|
||||
if (flutterRootComponents[index] != filenameComponents[index])
|
||||
return false;
|
||||
}
|
||||
if (filenameComponents[flutterRootComponents.length] != 'packages')
|
||||
return false;
|
||||
if (filenameComponents[flutterRootComponents.length + 1] == 'flutter_tools')
|
||||
return false;
|
||||
if (filenameComponents[flutterRootComponents.length + 2] != 'lib')
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
List<File> _collectDartFiles(Directory dir, List<File> collected, {FileFilter exclude}) {
|
||||
// Bail out in case of a .dartignore.
|
||||
if (FileSystemEntity.isFileSync(path.join(path.dirname(dir.path), '.dartignore')))
|
||||
return collected;
|
||||
|
||||
for (FileSystemEntity entity in dir.listSync(recursive: false, followLinks: false)) {
|
||||
if (isDartFile(entity) && (exclude == null || !exclude(entity)))
|
||||
collected.add(entity);
|
||||
if (entity is Directory) {
|
||||
String name = path.basename(entity.path);
|
||||
if (!name.startsWith('.') && name != 'packages')
|
||||
_collectDartFiles(entity, collected, exclude: exclude);
|
||||
}
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
}
|
||||
|
||||
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(path.isAbsolute(Cache.flutterRoot));
|
||||
for (List<String> targetSources in values.values) {
|
||||
for (String source in targetSources) {
|
||||
assert(path.isAbsolute(source));
|
||||
if (path.isWithin(Cache.flutterRoot, source))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void describeConflict(StringBuffer result) {
|
||||
assert(hasConflict);
|
||||
List<String> targets = values.keys.toList();
|
||||
targets.sort((String a, String b) => values[b].length.compareTo(values[a].length));
|
||||
for (String target in targets) {
|
||||
int count = values[target].length;
|
||||
result.writeln(' $count ${count == 1 ? 'source wants' : 'sources want'} "$target":');
|
||||
bool canonical = false;
|
||||
for (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 {
|
||||
// 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, () => new PackageDependency());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
StringBuffer result = new StringBuffer();
|
||||
for (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() {
|
||||
Map<String, String> result = <String, String>{};
|
||||
for (String package in packages.keys)
|
||||
result[package] = packages[package].target;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_tools/src/base/os.dart';
|
||||
import 'package:flutter_tools/src/commands/watch.dart';
|
||||
import 'package:flutter_tools/src/commands/analyze_continuously.dart';
|
||||
import 'package:flutter_tools/src/dart/pub.dart';
|
||||
import 'package:flutter_tools/src/dart/sdk.dart';
|
||||
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
|
||||
@@ -29,7 +29,7 @@ void main() {
|
||||
return server?.dispose();
|
||||
});
|
||||
|
||||
group('watch', () {
|
||||
group('analyze --watch', () {
|
||||
testUsingContext('AnalysisServer success', () async {
|
||||
_createSampleProject(tempDir);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/commands/analyze.dart';
|
||||
import 'package:flutter_tools/src/commands/analyze_base.dart';
|
||||
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
@@ -27,27 +27,26 @@ void main() {
|
||||
group('analyze', () {
|
||||
|
||||
testUsingContext('inRepo', () {
|
||||
AnalyzeCommand cmd = new AnalyzeCommand();
|
||||
// Absolute paths
|
||||
expect(cmd.inRepo(<String>[tempDir.path]), isFalse);
|
||||
expect(cmd.inRepo(<String>[path.join(tempDir.path, 'foo')]), isFalse);
|
||||
expect(cmd.inRepo(<String>[Cache.flutterRoot]), isTrue);
|
||||
expect(cmd.inRepo(<String>[path.join(Cache.flutterRoot, 'foo')]), isTrue);
|
||||
expect(inRepo(<String>[tempDir.path]), isFalse);
|
||||
expect(inRepo(<String>[path.join(tempDir.path, 'foo')]), isFalse);
|
||||
expect(inRepo(<String>[Cache.flutterRoot]), isTrue);
|
||||
expect(inRepo(<String>[path.join(Cache.flutterRoot, 'foo')]), isTrue);
|
||||
// Relative paths
|
||||
String oldWorkingDirectory = path.current;
|
||||
try {
|
||||
Directory.current = Cache.flutterRoot;
|
||||
expect(cmd.inRepo(<String>['.']), isTrue);
|
||||
expect(cmd.inRepo(<String>['foo']), isTrue);
|
||||
expect(inRepo(<String>['.']), isTrue);
|
||||
expect(inRepo(<String>['foo']), isTrue);
|
||||
Directory.current = tempDir.path;
|
||||
expect(cmd.inRepo(<String>['.']), isFalse);
|
||||
expect(cmd.inRepo(<String>['foo']), isFalse);
|
||||
expect(inRepo(<String>['.']), isFalse);
|
||||
expect(inRepo(<String>['foo']), isFalse);
|
||||
} finally {
|
||||
Directory.current = oldWorkingDirectory;
|
||||
}
|
||||
// Ensure no exceptions
|
||||
cmd.inRepo(null);
|
||||
cmd.inRepo(<String>[]);
|
||||
inRepo(null);
|
||||
inRepo(<String>[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user