Commit 9f673ad4 authored by Dan Rubel's avatar Dan Rubel Committed by GitHub

flutter analyze --watch (#6093)

parent 67f97b45
......@@ -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.
......
......@@ -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>[];
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 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;
}
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;
}
/// 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;
}
bool get shouldRunPub {
// If they're not analyzing the current project.
if (!argResults['current-package'])
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;
}
}
// Or we're not in a project directory.
if (!new File('pubspec.yaml').existsSync())
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);
return super.shouldRunPub;
}
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();
@override
Future<int> runCommand() {
if (argResults['watch']) {
return new AnalyzeContinuously(argResults, runner.getRepoAnalysisEntryPoints()).analyze();
} else {
return new AnalyzeOnce(argResults, runner.getRepoPackages()).analyze();
}
Map<String, String> asPackageMap() {
Map<String, String> result = <String, String>{};
for (String package in packages.keys)
result[package] = packages[package].target;
return result;
}
}
......@@ -2,40 +2,25 @@
// 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';
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;
/// Common behavior for `flutter analyze` and `flutter analyze --watch`
abstract class AnalyzeBase {
/// The parsed argument results for execution.
final ArgResults argResults;
// Or we're not in a project directory.
if (!new File('pubspec.yaml').existsSync())
return false;
AnalyzeBase(this.argResults);
return super.shouldRunPub;
}
/// Called by [AnalyzeCommand] to start the analysis process.
Future<int> analyze();
void dumpErrors(Iterable<String> errors) {
if (argResults['write'] != null) {
......@@ -66,3 +51,18 @@ abstract class AnalysisCommand extends FlutterCommand {
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;
}
\ No newline at end of file
......@@ -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)
......
// 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>[]);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment