diff --git a/dev/bots/test.sh b/dev/bots/test.sh index 5692684755..0ae9f9d627 100755 --- a/dev/bots/test.sh +++ b/dev/bots/test.sh @@ -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 diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 57f32cf304..dc65fc3df9 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -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 main(List 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/*>*/(() async { // Initialize globals. diff --git a/packages/flutter_tools/lib/src/commands/analysis_common.dart b/packages/flutter_tools/lib/src/commands/analysis_common.dart deleted file mode 100644 index 5c1aaa9fff..0000000000 --- a/packages/flutter_tools/lib/src/commands/analysis_common.dart +++ /dev/null @@ -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 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 data = { - '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']; -} \ No newline at end of file diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart index b2b8d0eeb4..a4e3dce17c 100644 --- a/packages/flutter_tools/lib/src/commands/analyze.dart +++ b/packages/flutter_tools/lib/src/commands/analyze.dart @@ -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 runCommand() async { - Stopwatch stopwatch = new Stopwatch()..start(); - Set pubSpecDirectories = new HashSet(); - List dartFiles = []; + 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 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 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/**/((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 flutterRootComponents; - bool isFlutterLibrary(String filename) { - flutterRootComponents ??= path.normalize(path.absolute(Cache.flutterRoot)).split(path.separator); - List 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 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 fileList) { - if (fileList == null || fileList.isEmpty) - fileList = [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 _collectDartFiles(Directory dir, List 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> values = >{}; - 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, () => []).add(sourcePath); - } - bool get hasConflict => values.length > 1; - bool get hasConflictAffectingFlutterRepo { - assert(path.isAbsolute(Cache.flutterRoot)); - for (List 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 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 packages = {}; - - 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 asPackageMap() { - Map result = {}; - for (String package in packages.keys) - result[package] = packages[package].target; - return result; } } diff --git a/packages/flutter_tools/lib/src/commands/analyze_base.dart b/packages/flutter_tools/lib/src/commands/analyze_base.dart new file mode 100644 index 0000000000..da8407b99d --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/analyze_base.dart @@ -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 analyze(); + + void dumpErrors(Iterable 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 data = { + '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 fileList) { + if (fileList == null || fileList.isEmpty) + fileList = [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; +} \ No newline at end of file diff --git a/packages/flutter_tools/lib/src/commands/watch.dart b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart similarity index 95% rename from packages/flutter_tools/lib/src/commands/watch.dart rename to packages/flutter_tools/lib/src/commands/analyze_continuously.dart index 0c6f84d3fb..e0dcdd4944 100644 --- a/packages/flutter_tools/lib/src/commands/watch.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart @@ -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 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 runCommand() async { + Future analyze() async { List 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) diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart new file mode 100644 index 0000000000..006ecd9e6a --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart @@ -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 repoPackages; + + AnalyzeOnce(ArgResults argResults, this.repoPackages) : super(argResults); + + @override + Future analyze() async { + Stopwatch stopwatch = new Stopwatch()..start(); + Set pubSpecDirectories = new HashSet(); + List dartFiles = []; + + 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 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 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/**/((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 flutterRootComponents; + bool isFlutterLibrary(String filename) { + flutterRootComponents ??= path.normalize(path.absolute(Cache.flutterRoot)).split(path.separator); + List 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 _collectDartFiles(Directory dir, List 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> values = >{}; + 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, () => []).add(sourcePath); + } + bool get hasConflict => values.length > 1; + bool get hasConflictAffectingFlutterRepo { + assert(path.isAbsolute(Cache.flutterRoot)); + for (List 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 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 packages = {}; + + 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 asPackageMap() { + Map result = {}; + for (String package in packages.keys) + result[package] = packages[package].target; + return result; + } +} diff --git a/packages/flutter_tools/test/watch_test.dart b/packages/flutter_tools/test/analyze_continuously_test.dart similarity index 95% rename from packages/flutter_tools/test/watch_test.dart rename to packages/flutter_tools/test/analyze_continuously_test.dart index 7651c4841e..88dc1ddee2 100644 --- a/packages/flutter_tools/test/watch_test.dart +++ b/packages/flutter_tools/test/analyze_continuously_test.dart @@ -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); diff --git a/packages/flutter_tools/test/analyze_test.dart b/packages/flutter_tools/test/analyze_test.dart index 8decda8301..7ab9bb56cf 100644 --- a/packages/flutter_tools/test/analyze_test.dart +++ b/packages/flutter_tools/test/analyze_test.dart @@ -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([tempDir.path]), isFalse); - expect(cmd.inRepo([path.join(tempDir.path, 'foo')]), isFalse); - expect(cmd.inRepo([Cache.flutterRoot]), isTrue); - expect(cmd.inRepo([path.join(Cache.flutterRoot, 'foo')]), isTrue); + expect(inRepo([tempDir.path]), isFalse); + expect(inRepo([path.join(tempDir.path, 'foo')]), isFalse); + expect(inRepo([Cache.flutterRoot]), isTrue); + expect(inRepo([path.join(Cache.flutterRoot, 'foo')]), isTrue); // Relative paths String oldWorkingDirectory = path.current; try { Directory.current = Cache.flutterRoot; - expect(cmd.inRepo(['.']), isTrue); - expect(cmd.inRepo(['foo']), isTrue); + expect(inRepo(['.']), isTrue); + expect(inRepo(['foo']), isTrue); Directory.current = tempDir.path; - expect(cmd.inRepo(['.']), isFalse); - expect(cmd.inRepo(['foo']), isFalse); + expect(inRepo(['.']), isFalse); + expect(inRepo(['foo']), isFalse); } finally { Directory.current = oldWorkingDirectory; } // Ensure no exceptions - cmd.inRepo(null); - cmd.inRepo([]); + inRepo(null); + inRepo([]); }); }); }