flutter analyze command
Other changes in this patch:
- Make the 'flutter' tool say "Updating flutter tool..." when it calls
pub get, to avoid confusion about what the pub get output is about.
- Make the bash flutter tool call pub get when the revision has
changed. (This was already happening on Windows.)
- Fix a raft of bugs found by the analyzer.
- Fix some style nits in various bits of code that happened to be near
things the analyzer noticed.
- Remove the logic in "flutter test" that would run "pub get", since
upon further reflexion it was determined it didn't work anyway.
We'll probably have to add better diagnostics here and say to run the
updater script.
- Remove the native velocity tracker script, since it was testing code
that has since been removed.
Notes on ignored warnings:
- We ignore warnings in any packages that are not in the Flutter repo or
in the author's current directory.
- We ignore various irrelevant Strong Mode warnings. We still enable
strong mode because even though it's not really relevant to our needs,
it does (more or less accidentally) catch a few things that are
helpful to us.
- We allow CONSTANTS_LIKE_THIS, since we get some of those from other
platforms that we are copying for sanity and consistency.
- We allow one-member abstract classes since we have a number of them
where it's perfectly reasonable.
- We unfortunately still ignore warnings in mojom.dart autogenerated
files. We should really fix those but that's a separate patch.
- We verify the actual source file when we see the 'Name non-constant
identifiers using lowerCamelCase.' lint, to allow one-letter variables
that use capital letters (e.g. for physics expressions) and to allow
multiple-underscore variable names.
- We ignore all errors on lines that contain the following magic
incantation and a "#" character:
// analyzer doesn't like constructor tear-offs
- For all remaining errors, if the line contains a comment of the form
// analyzer says "..."
...then we ignore any errors that have that "..." string in them.
diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart
new file mode 100644
index 0000000..f4fa293
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/analyze.dart
@@ -0,0 +1,302 @@
+// 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:convert';
+import 'dart:io';
+
+import 'package:logging/logging.dart';
+import 'package:path/path.dart' as path;
+
+import '../artifacts.dart';
+import '../build_configuration.dart';
+import '../process.dart';
+import 'flutter_command.dart';
+
+final Logger _logging = new Logger('sky_tools.analyze');
+
+class AnalyzeCommand extends FlutterCommand {
+ String get name => 'analyze';
+ String get description => 'Runs a carefully configured dartanalyzer over the current project\'s dart code.';
+
+ AnalyzeCommand() {
+ 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('congratulate', help: 'Show output even when there are no errors, warnings, hints, or lints.', defaultsTo: true);
+ }
+
+ bool get requiresProjectRoot => false;
+
+ @override
+ Future<int> runInProject() async {
+ Set<String> pubSpecDirectories = new Set<String>();
+ List<String> dartFiles = argResults.rest.toList();
+
+ for (String file in dartFiles) {
+ // TODO(ianh): figure out how dartanalyzer decides which .packages file to use when given a random file
+ pubSpecDirectories.add(path.dirname(file));
+ }
+
+ if (argResults['flutter-repo']) {
+ // .../examples/*/*.dart
+ // .../examples/*/lib/main.dart
+ Directory examples = new Directory(path.join(ArtifactStore.flutterRoot, 'examples'));
+ for (FileSystemEntity entry in examples.listSync()) {
+ if (entry is Directory) {
+ bool foundOne = false;
+ for (FileSystemEntity subentry in entry.listSync()) {
+ if (subentry is File && subentry.path.endsWith('.dart')) {
+ dartFiles.add(subentry.path);
+ foundOne = true;
+ } else if (subentry is Directory && path.basename(subentry.path) == 'lib') {
+ String mainPath = path.join(subentry.path, 'main.dart');
+ if (FileSystemEntity.isFileSync(mainPath)) {
+ dartFiles.add(mainPath);
+ foundOne = true;
+ }
+ }
+ }
+ if (foundOne)
+ pubSpecDirectories.add(entry.path);
+ }
+ }
+
+ bool foundTest = false;
+ Directory flutterDir = new Directory(path.join(ArtifactStore.flutterRoot, 'packages/unit')); // See https://github.com/flutter/flutter/issues/50
+
+ // .../packages/unit/test/*/*_test.dart
+ Directory tests = new Directory(path.join(flutterDir.path, 'test'));
+ for (FileSystemEntity entry in tests.listSync()) {
+ if (entry is Directory) {
+ for (FileSystemEntity subentry in entry.listSync()) {
+ if (subentry is File && subentry.path.endsWith('_test.dart')) {
+ dartFiles.add(subentry.path);
+ foundTest = true;
+ }
+ }
+ }
+ }
+
+ // .../packages/unit/benchmark/*/*_bench.dart
+ Directory benchmarks = new Directory(path.join(flutterDir.path, 'benchmark'));
+ for (FileSystemEntity entry in benchmarks.listSync()) {
+ if (entry is Directory) {
+ for (FileSystemEntity subentry in entry.listSync()) {
+ if (subentry is File && subentry.path.endsWith('_bench.dart')) {
+ dartFiles.add(subentry.path);
+ foundTest = true;
+ }
+ }
+ }
+ }
+
+ if (foundTest)
+ pubSpecDirectories.add(flutterDir.path);
+
+ // .../packages/*/bin/*.dart
+ Directory packages = new Directory(path.join(ArtifactStore.flutterRoot, 'packages'));
+ for (FileSystemEntity entry in packages.listSync()) {
+ if (entry is Directory) {
+ bool foundOne = false;
+ Directory binDirectory = new Directory(path.join(entry.path, 'bin'));
+ if (binDirectory.existsSync()) {
+ for (FileSystemEntity subentry in binDirectory.listSync()) {
+ if (subentry is File && subentry.path.endsWith('.dart')) {
+ dartFiles.add(subentry.path);
+ foundOne = true;
+ }
+ }
+ }
+ if (foundOne)
+ pubSpecDirectories.add(entry.path);
+ }
+ }
+ }
+
+ if (argResults['current-directory']) {
+ // ./*.dart
+ Directory currentDirectory = new Directory('.');
+ bool foundOne = false;
+ for (FileSystemEntity entry in currentDirectory.listSync()) {
+ if (entry is File && entry.path.endsWith('.dart')) {
+ dartFiles.add(entry.path);
+ foundOne = true;
+ }
+ }
+ if (foundOne)
+ pubSpecDirectories.add('.');
+ }
+
+ if (argResults['current-package']) {
+ // ./lib/main.dart
+ String mainPath = 'lib/main.dart';
+ if (FileSystemEntity.isFileSync(mainPath)) {
+ dartFiles.add(mainPath);
+ pubSpecDirectories.add('.');
+ }
+ }
+
+ // prepare a Dart file that references all the above Dart files
+ StringBuffer mainBody = new StringBuffer();
+ for (int index = 0; index < dartFiles.length; index += 1) {
+ mainBody.writeln('import \'${path.normalize(path.absolute(dartFiles[index]))}\' as file$index;');
+ }
+ mainBody.writeln('void main() { }');
+
+ // prepare a union of all the .packages files
+ Map<String, String> packages = <String, String>{};
+ bool hadInconsistentRequirements = false;
+ for (Directory directory in pubSpecDirectories.map((path) => new Directory(path))) {
+ File dotPackages = new File(path.join(directory.path, '.packages'));
+ if (dotPackages.existsSync()) {
+ Map<String, String> dependencies = <String, String>{};
+ dotPackages
+ .readAsStringSync()
+ .split('\n')
+ .where((line) => !line.startsWith(new RegExp(r'^ *#')))
+ .forEach((line) {
+ int colon = line.indexOf(':');
+ if (colon > 0)
+ dependencies[line.substring(0, colon)] = path.normalize(path.absolute(directory.path, path.fromUri(line.substring(colon+1))));
+ });
+ for (String package in dependencies.keys) {
+ if (packages.containsKey(package)) {
+ if (packages[package] != dependencies[package]) {
+ _logging.warning('Inconsistent requirements for $package; using ${packages[package]} (and not ${dependencies[package]}).');
+ hadInconsistentRequirements = true;
+ }
+ } else {
+ packages[package] = dependencies[package];
+ }
+ }
+ }
+ }
+ if (hadInconsistentRequirements) {
+ if (argResults['flutter-repo'])
+ _logging.warning('You may need to run "dart ${path.normalize(path.relative(path.join(ArtifactStore.flutterRoot, 'dev/update_packages.dart')))}".');
+ if (argResults['current-directory'] || argResults['current-package'])
+ _logging.warning('You may need to run "pub get".');
+ }
+
+ String buildDir = buildConfigurations.firstWhere((BuildConfiguration config) => config.testable, orElse: () => null)?.buildDir;
+ if (buildDir != null) {
+ packages['sky_engine'] = path.join(buildDir, 'gen/dart-pkg/sky_engine/lib');
+ packages['sky_services'] = path.join(buildDir, 'gen/dart-pkg/sky_services/lib');
+ }
+
+ StringBuffer packagesBody = new StringBuffer();
+ for (String package in packages.keys)
+ packagesBody.writeln('$package:${path.toUri(packages[package])}');
+
+ // save the Dart file and the .packages file to disk
+ Directory host = Directory.systemTemp.createTempSync('flutter-analyze-');
+ File mainFile = new File(path.join(host.path, 'main.dart'))..writeAsStringSync(mainBody.toString());
+ File packagesFile = new File(path.join(host.path, '.packages'))..writeAsStringSync(packagesBody.toString());
+
+ List<String> cmd = <String>[
+ sdkBinaryName('dartanalyzer'),
+ // do not set '--warnings', since that will include the entire Dart SDK
+ '--ignore-unrecognized-flags',
+ '--supermixin',
+ '--enable-strict-call-checks',
+ '--enable_type_checks',
+ '--strong',
+ '--package-warnings',
+ '--fatal-warnings',
+ '--strong-hints',
+ '--fatal-hints',
+ '--lints',
+ '--packages', packagesFile.path,
+ mainFile.path
+ ];
+
+ _logging.info(cmd.join(' '));
+ Process process = await Process.start(
+ cmd[0],
+ cmd.sublist(1),
+ workingDirectory: host.path
+ );
+ int errorCount = 0;
+ StringBuffer output = new StringBuffer();
+ process.stdout.transform(UTF8.decoder).listen((String data) {
+ output.write(data);
+ });
+ process.stderr.transform(UTF8.decoder).listen((String data) {
+ // dartanalyzer doesn't seem to ever output anything on stderr
+ errorCount += 1;
+ print(data);
+ });
+
+ // host.deleteSync();
+
+ int exitCode = await process.exitCode;
+
+ List<Pattern> patternsToSkip = <Pattern>[
+ 'Analyzing [${mainFile.path}]...',
+ new RegExp('^\\[hint\\] Unused import \\(${mainFile.path},'),
+ new RegExp(r'^\[.+\] .+ \(.+/\.pub-cache/.+'),
+ new RegExp(r'^\[error\] Invalid override\. The type of [^ ]+ \(.+\) is not a subtype of [^ ]+ \(.+\)\.'), // we allow type narrowing
+ new RegExp(r'^\[warning\] .+ will need runtime check to cast to type .+'), // https://github.com/dart-lang/sdk/issues/24542
+ new RegExp(r'^\[error\] Type check failed: .*\(dynamic\) is not of type'), // allow unchecked casts from dynamic
+ new RegExp('^\\[error\\] Target of URI does not exist: \'dart:ui_internals\''), // https://github.com/flutter/flutter/issues/83
+ new RegExp(r'\[lint\] Prefer using lowerCamelCase for constant names.'), // sometimes we have no choice (e.g. when matching other platforms)
+ new RegExp(r'\[lint\] Avoid defining a one-member abstract class when a simple function will do.'), // too many false-positives; code review should catch real instances
+ new RegExp(r'\[0-9]+ (error|warning|hint|lint).+found\.'),
+ '',
+ ];
+
+ RegExp generalPattern = new RegExp(r'^\[(error|warning|hint|lint)\] (.+) \(([^(),]+), line ([0-9]+), col ([0-9]+)\)$');
+ RegExp ignorePattern = new RegExp(r'// analyzer says "([^"]+)"');
+ RegExp constructorTearOffsPattern = new RegExp('.+#.+// analyzer doesn\'t like constructor tear-offs');
+ RegExp allowedIdentifiers = new RegExp(r'_?([A-Z]|_+)\b');
+
+ List<String> errorLines = output.toString().split('\n');
+ for (String errorLine in errorLines) {
+ if (patternsToSkip.every((Pattern pattern) => pattern.allMatches(errorLine).isEmpty)) {
+ Match groups = generalPattern.firstMatch(errorLine);
+ if (groups != null) {
+ String level = groups[1];
+ String filename = groups[3];
+ String errorMessage = groups[2];
+ int lineNumber = int.parse(groups[4]);
+ int colNumber = int.parse(groups[5]);
+ File source = new File(filename);
+ List<String> sourceLines = source.readAsLinesSync();
+ String sourceLine = sourceLines[lineNumber-1];
+ bool shouldIgnore = false;
+ if (filename.endsWith('.mojom.dart')) {
+ shouldIgnore = true;
+ } else if (level == 'lint' && errorMessage == 'Name non-constant identifiers using lowerCamelCase.') {
+ if (allowedIdentifiers.matchAsPrefix(sourceLine, colNumber-1) != null)
+ shouldIgnore = true;
+ } else if (constructorTearOffsPattern.allMatches(sourceLine).isNotEmpty) {
+ shouldIgnore = true;
+ } else {
+ Iterable<Match> ignoreGroups = ignorePattern.allMatches(sourceLine);
+ for (Match ignoreGroup in ignoreGroups) {
+ if (errorMessage.contains(ignoreGroup[1])) {
+ shouldIgnore = true;
+ break;
+ }
+ }
+ }
+ if (shouldIgnore)
+ continue;
+ }
+ print(errorLine);
+ errorCount += 1;
+ }
+ }
+
+ if (exitCode < 0 || exitCode > 3) // 1 = hints, 2 = warnings, 3 = errors
+ return exitCode;
+
+ if (errorCount > 1)
+ return 1;
+ if (argResults['congratulate'])
+ print('No analyzer warnings!');
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart
index caee7e6..b892c10 100644
--- a/packages/flutter_tools/lib/src/commands/build.dart
+++ b/packages/flutter_tools/lib/src/commands/build.dart
@@ -93,7 +93,7 @@
}
ArchiveFile _createFile(String key, String assetBase) {
- File file = new File('${assetBase}/${key}');
+ File file = new File('$assetBase/$key');
if (!file.existsSync())
return null;
List<int> content = file.readAsBytesSync();
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index cd82c5f..e391296 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -60,7 +60,7 @@
stdout.writeln('[${JSON.encode(command)}]');
}, daemonCommand: this);
- return daemon.onExit;
+ return await daemon.onExit;
}
}
@@ -98,19 +98,19 @@
var id = command['id'];
if (id == null) {
- _logging.severe('no id for command: ${command}');
+ _logging.severe('no id for command: $command');
return;
}
try {
String event = command['event'];
if (event.indexOf('.') == -1)
- throw 'command not understood: ${event}';
+ throw 'command not understood: $event';
String prefix = event.substring(0, event.indexOf('.'));
String name = event.substring(event.indexOf('.') + 1);
if (_domains[prefix] == null)
- throw 'no domain for command: ${command}';
+ throw 'no domain for command: $command';
_domains[prefix].handleEvent(name, id, command['params']);
} catch (error, trace) {
@@ -144,7 +144,7 @@
new Future.sync(() {
if (_handlers.containsKey(name))
return _handlers[name](args);
- throw 'command not understood: ${name}';
+ throw 'command not understood: $name';
}).then((result) {
if (result == null) {
_send({'id': id});
@@ -153,7 +153,7 @@
}
}).catchError((error, trace) {
_send({'id': id, 'error': _toJsonable(error)});
- _logging.warning('error handling ${name}', error, trace);
+ _logging.warning('error handling $name', error, trace);
});
}
@@ -210,5 +210,5 @@
dynamic _toJsonable(dynamic obj) {
if (obj is String || obj is int || obj is bool || obj is Map || obj is List || obj == null)
return obj;
- return '${obj}';
+ return '$obj';
}
diff --git a/packages/flutter_tools/lib/src/commands/flutter_command.dart b/packages/flutter_tools/lib/src/commands/flutter_command.dart
index f31ad56..4ff335a 100644
--- a/packages/flutter_tools/lib/src/commands/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/commands/flutter_command.dart
@@ -56,7 +56,7 @@
}
}
- return runInProject();
+ return await runInProject();
}
Future<int> runInProject();
diff --git a/packages/flutter_tools/lib/src/commands/init.dart b/packages/flutter_tools/lib/src/commands/init.dart
index 85ef441..3c70531 100644
--- a/packages/flutter_tools/lib/src/commands/init.dart
+++ b/packages/flutter_tools/lib/src/commands/init.dart
@@ -6,12 +6,15 @@
import 'dart:io';
import 'package:args/command_runner.dart';
+import 'package:logging/logging.dart';
import 'package:mustache4dart/mustache4dart.dart' as mustache;
import 'package:path/path.dart' as p;
import '../artifacts.dart';
import '../process.dart';
+final Logger _logging = new Logger('sky_tools.init');
+
class InitCommand extends Command {
final String name = 'init';
final String description = 'Create a new Flutter project.';
@@ -20,7 +23,7 @@
argParser.addOption('out', abbr: 'o', help: 'The output directory.');
argParser.addFlag('pub',
defaultsTo: true,
- help: 'Whether to run pub after the project has been created.');
+ help: 'Whether to run "pub get" after the project has been created.');
}
@override
@@ -40,7 +43,7 @@
String flutterPackagePath = p.join(flutterRoot, 'packages', 'flutter');
if (!FileSystemEntity.isFileSync(p.join(flutterPackagePath, 'pubspec.yaml'))) {
- print('Unable to find package:flutter in ${flutterPackagePath}');
+ print('Unable to find package:flutter in $flutterPackagePath');
return 2;
}
@@ -58,11 +61,7 @@
''';
if (argResults['pub']) {
- print("Running pub get...");
- int code = await runCommandAndStreamOutput(
- [sdkBinaryName('pub'), 'get'],
- workingDirectory: out.path
- );
+ int code = await pubGet(directory: out.path);
if (code != 0)
return code;
}
@@ -70,6 +69,41 @@
print(message);
return 0;
}
+
+ Future<int> pubGet({
+ String directory: '',
+ bool skipIfAbsent: false,
+ bool verbose: true
+ }) async {
+ File pubSpecYaml = new File(p.join(directory, 'pubspec.yaml'));
+ File pubSpecLock = new File(p.join(directory, 'pubspec.lock'));
+ File dotPackages = new File(p.join(directory, '.packages'));
+
+ if (!pubSpecYaml.existsSync()) {
+ if (skipIfAbsent)
+ return 0;
+ _logging.severe('$directory: no pubspec.yaml found');
+ return 1;
+ }
+
+ if (!pubSpecLock.existsSync() || pubSpecYaml.lastModifiedSync().isAfter(pubSpecLock.lastModifiedSync())) {
+ if (verbose)
+ print("Running pub get in $directory...");
+ int code = await runCommandAndStreamOutput(
+ [sdkBinaryName('pub'), 'get'],
+ workingDirectory: directory
+ );
+ if (code != 0)
+ return code;
+ }
+
+ if ((pubSpecLock.existsSync() && pubSpecLock.lastModifiedSync().isAfter(pubSpecYaml.lastModifiedSync())) &&
+ (dotPackages.existsSync() && dotPackages.lastModifiedSync().isAfter(pubSpecYaml.lastModifiedSync())))
+ return 0;
+
+ _logging.severe('$directory: pubspec.yaml, pubspec.lock, and .packages are in an inconsistent state');
+ return 1;
+ }
}
abstract class Template {
diff --git a/packages/flutter_tools/lib/src/commands/run_mojo.dart b/packages/flutter_tools/lib/src/commands/run_mojo.dart
index ce0a187..516673e 100644
--- a/packages/flutter_tools/lib/src/commands/run_mojo.dart
+++ b/packages/flutter_tools/lib/src/commands/run_mojo.dart
@@ -34,7 +34,7 @@
String _makePathAbsolute(String relativePath) {
File file = new File(relativePath);
if (!file.existsSync()) {
- throw new Exception("Path \"${relativePath}\" does not exist");
+ throw new Exception('Path "$relativePath" does not exist');
}
return file.absolute.path;
}
@@ -79,8 +79,8 @@
final appPath = _makePathAbsolute(argResults['app']);
Artifact artifact = ArtifactStore.getArtifact(type: ArtifactType.viewer, targetPlatform: TargetPlatform.linux);
final viewerPath = _makePathAbsolute(await ArtifactStore.getPath(artifact));
- args.add('file://${appPath}');
- args.add('--url-mappings=mojo:sky_viewer=file://${viewerPath}');
+ args.add('file://$appPath');
+ args.add('--url-mappings=mojo:sky_viewer=file://$viewerPath');
}
if (useDevtools) {
@@ -115,6 +115,6 @@
return 1;
}
- return runCommandAndStreamOutput(await _getShellConfig());
+ return await runCommandAndStreamOutput(await _getShellConfig());
}
}
diff --git a/packages/flutter_tools/lib/src/commands/start.dart b/packages/flutter_tools/lib/src/commands/start.dart
index ce60e43..842d909 100644
--- a/packages/flutter_tools/lib/src/commands/start.dart
+++ b/packages/flutter_tools/lib/src/commands/start.dart
@@ -68,7 +68,7 @@
if (FileSystemEntity.isDirectorySync(target))
mainPath = path.join(target, 'lib', 'main.dart');
if (!FileSystemEntity.isFileSync(mainPath)) {
- String message = 'Tried to run ${mainPath}, but that file does not exist.';
+ String message = 'Tried to run $mainPath, but that file does not exist.';
if (!argResults.wasParsed('target'))
message += '\nConsider using the -t option to specify that Dart file to start.';
stderr.writeln(message);
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 8c3202c..5d26834 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -11,7 +11,6 @@
import '../artifacts.dart';
import '../build_configuration.dart';
-import '../process.dart';
import '../test/loader.dart' as loader;
import 'flutter_command.dart';
@@ -49,25 +48,6 @@
testArgs.insert(0, '--no-color');
List<BuildConfiguration> configs = buildConfigurations;
bool foundOne = false;
-
- File pubSpecYaml = new File(path.join(flutterDir.path, 'pubspec.yaml'));
- File pubSpecLock = new File(path.join(flutterDir.path, 'pubspec.lock'));
-
- if (!pubSpecYaml.existsSync()) {
- print('${flutterDir.path} has no pubspec.yaml');
- return 1;
- }
-
- if (!pubSpecLock.existsSync() || pubSpecYaml.lastModifiedSync().isAfter(pubSpecLock.lastModifiedSync())) {
- print("Running pub get...");
- int code = await runCommandAndStreamOutput(
- [sdkBinaryName('pub'), 'get'],
- workingDirectory: flutterDir.path
- );
- if (code != 0)
- return code;
- }
-
String currentDirectory = Directory.current.path;
Directory.current = flutterDir.path;
loader.installHook();