re-write flutter analyze to use the analysis server (#16979)
re-write flutter analyze (the single-shot and --flutter-repo) to use the analysis server
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index eca9cc5..80d2cce 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -79,7 +79,7 @@
want to run `flutter update-packages` first, or you will get bogus
error messages about core classes like Offset from `dart:ui`.
-For a one-off, use `flutter analyze --flutter-repo`. This uses the `analysis_options_repo.yaml` file
+For a one-off, use `flutter analyze --flutter-repo`. This uses the `analysis_options.yaml` file
at the root of the repository for its configuration.
For continuous analysis, use `flutter analyze --flutter-repo --watch`. This uses normal
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 95dc52e..14cebb5 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -9,22 +9,18 @@
#
# There are four similar analysis options files in the flutter repos:
# - analysis_options.yaml (this file)
-# - analysis_options_repo.yaml
# - packages/flutter/lib/analysis_options_user.yaml
# - https://github.com/flutter/plugins/blob/master/analysis_options.yaml
+# - https://github.com/flutter/engine/blob/master/analysis_options.yaml
#
# This file contains the analysis options used by Flutter tools, such as IntelliJ,
# Android Studio, and the `flutter analyze` command.
-# It is very similar to the analysis_options_repo.yaml file in this same directory;
-# the only difference (currently) is the public_member_api_docs option,
-# which triggers too many messages to be used in editors.
#
# The flutter/plugins repo contains a copy of this file, which should be kept
# in sync with this file.
analyzer:
language:
- enableStrictCallChecks: true
enableSuperMixins: true
strong-mode:
implicit-dynamic: false
@@ -131,7 +127,6 @@
- prefer_is_not_empty
- prefer_single_quotes
- prefer_typing_uninitialized_variables
- # - public_member_api_docs # this is the only difference from analysis_options_repo.yaml
- recursive_getters
- slash_for_doc_comments
- sort_constructors_first
diff --git a/dev/bots/analyze-sample-code.dart b/dev/bots/analyze-sample-code.dart
index a3dc4c1..5bf2bf5 100644
--- a/dev/bots/analyze-sample-code.dart
+++ b/dev/bots/analyze-sample-code.dart
@@ -189,6 +189,8 @@
}
}
buffer.add('');
+ buffer.add('// ignore_for_file: unused_element');
+ buffer.add('');
final List<Line> lines = new List<Line>.filled(buffer.length, null, growable: true);
for (Section section in sections) {
buffer.addAll(section.strings);
@@ -207,7 +209,7 @@
print('Found $sampleCodeSections sample code sections.');
final Process process = await Process.start(
_flutter,
- <String>['analyze', '--no-preamble', mainDart.path],
+ <String>['analyze', '--no-preamble', '--no-congratulate', mainDart.parent.path],
workingDirectory: temp.path,
);
stderr.addStream(process.stderr);
@@ -216,10 +218,6 @@
errors.removeAt(0);
if (errors.first.startsWith('Running "flutter packages get" in '))
errors.removeAt(0);
- if (errors.first.startsWith('Analyzing '))
- errors.removeAt(0);
- if (errors.last.endsWith(' issues found.') || errors.last.endsWith(' issue found.'))
- errors.removeLast();
int errorCount = 0;
for (String error in errors) {
final String kBullet = Platform.isWindows ? ' - ' : ' • ';
diff --git a/dev/devicelab/bin/tasks/dartdocs.dart b/dev/devicelab/bin/tasks/dartdocs.dart
index a2d0833..36ccf17 100644
--- a/dev/devicelab/bin/tasks/dartdocs.dart
+++ b/dev/devicelab/bin/tasks/dartdocs.dart
@@ -21,21 +21,24 @@
int publicMembers = 0;
int otherErrors = 0;
int otherLines = 0;
- await for (String entry in analysis.stderr.transform(utf8.decoder).transform(const LineSplitter())) {
- print('analyzer stderr: $entry');
- if (entry.startsWith('[lint] Document all public members')) {
- publicMembers += 1;
- } else if (entry.startsWith('[')) {
- otherErrors += 1;
- } else if (entry.startsWith('(Ran in ')) {
+ await for (String entry in analysis.stdout.transform(utf8.decoder).transform(const LineSplitter())) {
+ entry = entry.trim();
+ print('analyzer stdout: $entry');
+ if (entry == 'Building flutter tool...') {
// ignore this line
- } else {
+ } else if (entry.startsWith('info • Document all public members •')) {
+ publicMembers += 1;
+ } else if (entry.startsWith('info •') || entry.startsWith('warning •') || entry.startsWith('error •')) {
+ otherErrors += 1;
+ } else if (entry.contains(' (ran in ')) {
+ // ignore this line
+ } else if (entry.isNotEmpty) {
otherLines += 1;
}
}
- await for (String entry in analysis.stdout.transform(utf8.decoder).transform(const LineSplitter())) {
- print('analyzer stdout: $entry');
- if (entry == 'Building flutter tool...') {
+ await for (String entry in analysis.stderr.transform(utf8.decoder).transform(const LineSplitter())) {
+ print('analyzer stderr: $entry');
+ if (entry.startsWith('[lint] ')) {
// ignore this line
} else {
otherLines += 1;
diff --git a/packages/analysis_options.yaml b/packages/analysis_options.yaml
new file mode 100644
index 0000000..217831d
--- /dev/null
+++ b/packages/analysis_options.yaml
@@ -0,0 +1,8 @@
+# Take our settings from the repo's main analysis_options.yaml file, but include
+# an additional rule to validate that public members are documented.
+
+include: ../analysis_options.yaml
+
+linter:
+ rules:
+ - public_member_api_docs
diff --git a/packages/flutter/lib/analysis_options_user.yaml b/packages/flutter/lib/analysis_options_user.yaml
index 41cb657..504456d 100644
--- a/packages/flutter/lib/analysis_options_user.yaml
+++ b/packages/flutter/lib/analysis_options_user.yaml
@@ -7,21 +7,21 @@
# See the configuration guide for more
# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
#
-# There are three similar analysis options files in the flutter repo:
+# There are four similar analysis options files in the flutter repos:
# - analysis_options.yaml
-# - analysis_options_repo.yaml
# - packages/flutter/lib/analysis_options_user.yaml (this file)
+# - https://github.com/flutter/plugins/blob/master/analysis_options.yaml
+# - https://github.com/flutter/engine/blob/master/analysis_options.yaml
#
-# This file contains the analysis options used by "flutter analyze"
-# and the dartanalyzer when analyzing code outside the flutter repository.
-# It isn't named 'analysis_options.yaml' because otherwise editors like Atom
-# would use it when analyzing the flutter tool itself.
+# This file contains the analysis options used by "flutter analyze" and the
+# dartanalyzer when analyzing code outside the flutter repository. It isn't named
+# 'analysis_options.yaml' because otherwise editors would use it when analyzing
+# the flutter tool itself.
#
-# When editing, make sure you keep /analysis_options.yaml consistent.
+# When editing, make sure you keep this and /analysis_options.yaml consistent.
analyzer:
language:
- enableStrictCallChecks: true
enableSuperMixins: true
strong-mode: true
errors:
diff --git a/packages/flutter_goldens/analysis_options.yaml b/packages/flutter_goldens/analysis_options.yaml
new file mode 100644
index 0000000..b8591ca
--- /dev/null
+++ b/packages/flutter_goldens/analysis_options.yaml
@@ -0,0 +1,4 @@
+# Use the analysis options settings from the top level of the repo (not
+# the ones from above, which include the `public_member_api_docs` rule).
+
+include: ../../analysis_options.yaml
diff --git a/packages/flutter_tools/analysis_options.yaml b/packages/flutter_tools/analysis_options.yaml
new file mode 100644
index 0000000..b8591ca
--- /dev/null
+++ b/packages/flutter_tools/analysis_options.yaml
@@ -0,0 +1,4 @@
+# Use the analysis options settings from the top level of the repo (not
+# the ones from above, which include the `public_member_api_docs` rule).
+
+include: ../../analysis_options.yaml
diff --git a/packages/flutter_tools/lib/src/commands/analyze.dart b/packages/flutter_tools/lib/src/commands/analyze.dart
index 0f4969e..4968cbf 100644
--- a/packages/flutter_tools/lib/src/commands/analyze.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze.dart
@@ -9,28 +9,51 @@
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 FlutterCommand {
- AnalyzeCommand({ bool verboseHelp: false, this.workingDirectory }) {
- argParser.addFlag('flutter-repo', help: 'Include all the examples and tests from the Flutter repository.', defaultsTo: false);
- 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 works with --flutter-repo and without --watch).', defaultsTo: false, hide: !verboseHelp);
- argParser.addFlag('watch', help: 'Run analysis continuously, watching the filesystem for changes.', negatable: false);
- argParser.addFlag('preview-dart-2', defaultsTo: true, help: 'Preview Dart 2.0 functionality.');
- 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);
+ AnalyzeCommand({bool verboseHelp: false, this.workingDirectory}) {
+ argParser.addFlag('flutter-repo',
+ negatable: false,
+ help: 'Include all the examples and tests from the Flutter repository.',
+ defaultsTo: false,
+ hide: !verboseHelp);
+ argParser.addFlag('current-package',
+ help: 'Analyze the current project, if applicable.', defaultsTo: true);
+ argParser.addFlag('dartdocs',
+ negatable: false,
+ help: 'List every public member that is lacking documentation '
+ '(only works with --flutter-repo).',
+ hide: !verboseHelp);
+ argParser.addFlag('watch',
+ help: 'Run analysis continuously, watching the filesystem for changes.',
+ negatable: false);
+ argParser.addFlag('preview-dart-2',
+ defaultsTo: true, help: 'Preview Dart 2.0 functionality.');
+ 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.');
+ argParser.addFlag('benchmark',
+ negatable: false,
+ hide: !verboseHelp,
+ help: 'Also output the analysis time.');
usesPubOption();
// Not used by analyze --watch
- argParser.addFlag('congratulate', help: 'When analyzing the flutter repository, show output even when there are no errors, warnings, hints, or lints.', defaultsTo: true);
- argParser.addFlag('preamble', help: 'When analyzing the flutter repository, display the number of files that will be analyzed.', defaultsTo: true);
+ argParser.addFlag('congratulate',
+ help: 'When analyzing the flutter repository, show output even when '
+ 'there are no errors, warnings, hints, or lints.',
+ defaultsTo: true);
+ argParser.addFlag('preamble',
+ defaultsTo: true,
+ help: 'When analyzing the flutter repository, display the number of '
+ 'files that will be analyzed.');
}
/// The working directory for testing analysis using dartanalyzer.
@@ -40,17 +63,19 @@
String get name => 'analyze';
@override
- String get description => 'Analyze the project\'s Dart code.';
+ String get description => "Analyze the project's Dart code.";
@override
bool get shouldRunPub {
// If they're not analyzing the current project.
- if (!argResults['current-package'])
+ if (!argResults['current-package']) {
return false;
+ }
// Or we're not in a project directory.
- if (!fs.file('pubspec.yaml').existsSync())
+ if (!fs.file('pubspec.yaml').existsSync()) {
return false;
+ }
return super.shouldRunPub;
}
@@ -59,11 +84,15 @@
Future<Null> runCommand() {
if (argResults['watch']) {
return new AnalyzeContinuously(
- argResults, runner.getRepoPackages(), previewDart2: argResults['preview-dart-2']
+ argResults,
+ runner.getRepoRoots(),
+ runner.getRepoPackages(),
+ previewDart2: argResults['preview-dart-2'],
).analyze();
} else {
return new AnalyzeOnce(
argResults,
+ runner.getRepoRoots(),
runner.getRepoPackages(),
workingDirectory: workingDirectory,
previewDart2: argResults['preview-dart-2'],
diff --git a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart
index a9bd2dc..9d83765 100644
--- a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart
@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
-import 'dart:convert';
import 'package:args/args.dart';
@@ -11,17 +10,20 @@
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
-import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../cache.dart';
+import '../dart/analysis.dart';
import '../dart/sdk.dart' as sdk;
import '../globals.dart';
import 'analyze_base.dart';
class AnalyzeContinuously extends AnalyzeBase {
- AnalyzeContinuously(ArgResults argResults, this.repoPackages, { this.previewDart2: false }) : super(argResults);
+ AnalyzeContinuously(ArgResults argResults, this.repoRoots, this.repoPackages, {
+ this.previewDart2: false,
+ }) : super(argResults);
+ final List<String> repoRoots;
final List<Directory> repoPackages;
final bool previewDart2;
@@ -43,11 +45,14 @@
if (argResults['flutter-repo']) {
final PackageDependencyTracker dependencies = new PackageDependencyTracker();
dependencies.checkForConflictingDependencies(repoPackages, dependencies);
- directories = repoPackages.map((Directory dir) => dir.path).toList();
+
+ directories = repoRoots;
analysisTarget = 'Flutter repository';
+
printTrace('Analyzing Flutter repository:');
- for (String projectPath in directories)
+ for (String projectPath in repoRoots) {
printTrace(' ${fs.path.relative(projectPath)}');
+ }
} else {
directories = <String>[fs.currentDirectory.path];
analysisTarget = fs.currentDirectory.path;
@@ -107,10 +112,14 @@
// Print an analysis summary.
String errorsMessage;
- final int issueCount = errors.length;
+ int issueCount = errors.length;
final int issueDiff = issueCount - lastErrorCount;
lastErrorCount = issueCount;
+ final int undocumentedCount = errors.where((AnalysisError issue) {
+ return issue.code == 'public_member_api_docs';
+ }).length;
+
if (firstAnalysis)
errorsMessage = '$issueCount ${pluralize('issue', issueCount)} found';
else if (issueDiff > 0)
@@ -124,10 +133,13 @@
final String files = '${analyzedPaths.length} ${pluralize('file', analyzedPaths.length)}';
final String seconds = (analysisTimer.elapsedMilliseconds / 1000.0).toStringAsFixed(2);
- printStatus('$errorsMessage • analyzed $files, $seconds seconds');
+ printStatus('$errorsMessage • analyzed $files in $seconds seconds');
if (firstAnalysis && isBenchmarking) {
- writeBenchmark(analysisTimer, issueCount, -1); // TODO(ianh): track members missing dartdocs instead of saying -1
+ // We don't want to return a failing exit code based on missing documentation.
+ issueCount -= undocumentedCount;
+
+ writeBenchmark(analysisTimer, issueCount, undocumentedCount);
server.dispose().whenComplete(() { exit(issueCount > 0 ? 1 : 0); });
}
@@ -135,209 +147,10 @@
}
}
- bool _filterError(AnalysisError error) {
- // TODO(devoncarew): Also filter the regex items from `analyzeOnce()`.
-
- if (error.type == 'TODO')
- return true;
-
- return false;
- }
-
void _handleAnalysisErrors(FileAnalysisErrors fileErrors) {
- fileErrors.errors.removeWhere(_filterError);
+ fileErrors.errors.removeWhere((AnalysisError error) => error.type == 'TODO');
analyzedPaths.add(fileErrors.file);
analysisErrors[fileErrors.file] = fileErrors.errors;
}
}
-
-class AnalysisServer {
- AnalysisServer(this.sdkPath, this.directories, { this.previewDart2: false });
-
- final String sdkPath;
- final List<String> directories;
- final bool previewDart2;
-
- Process _process;
- final StreamController<bool> _analyzingController = new StreamController<bool>.broadcast();
- final StreamController<FileAnalysisErrors> _errorsController = new StreamController<FileAnalysisErrors>.broadcast();
-
- int _id = 0;
-
- Future<Null> start() async {
- final String snapshot = fs.path.join(sdkPath, 'bin/snapshots/analysis_server.dart.snapshot');
- final List<String> command = <String>[
- fs.path.join(sdkPath, 'bin', 'dart'),
- snapshot,
- '--sdk',
- sdkPath,
- ];
-
- if (previewDart2) {
- command.add('--preview-dart-2');
- } else {
- command.add('--no-preview-dart-2');
- }
-
- printTrace('dart ${command.skip(1).join(' ')}');
- _process = await processManager.start(command);
- // This callback hookup can't throw.
- _process.exitCode.whenComplete(() => _process = null); // ignore: unawaited_futures
-
- final Stream<String> errorStream = _process.stderr.transform(utf8.decoder).transform(const LineSplitter());
- errorStream.listen(printError);
-
- final Stream<String> inStream = _process.stdout.transform(utf8.decoder).transform(const LineSplitter());
- inStream.listen(_handleServerResponse);
-
- // Available options (many of these are obsolete):
- // enableAsync, enableDeferredLoading, enableEnums, enableNullAwareOperators,
- // enableSuperMixins, generateDart2jsHints, generateHints, generateLints
- _sendCommand('analysis.updateOptions', <String, dynamic>{
- 'options': <String, dynamic>{
- 'enableSuperMixins': true
- }
- });
-
- _sendCommand('server.setSubscriptions', <String, dynamic>{
- 'subscriptions': <String>['STATUS']
- });
-
- _sendCommand('analysis.setAnalysisRoots', <String, dynamic>{
- 'included': directories,
- 'excluded': <String>[]
- });
- }
-
- Stream<bool> get onAnalyzing => _analyzingController.stream;
- Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
-
- Future<int> get onExit => _process.exitCode;
-
- void _sendCommand(String method, Map<String, dynamic> params) {
- final String message = json.encode(<String, dynamic> {
- 'id': (++_id).toString(),
- 'method': method,
- 'params': params
- });
- _process.stdin.writeln(message);
- printTrace('==> $message');
- }
-
- void _handleServerResponse(String line) {
- printTrace('<== $line');
-
- final dynamic response = json.decode(line);
-
- if (response is Map<dynamic, dynamic>) {
- if (response['event'] != null) {
- final String event = response['event'];
- final dynamic params = response['params'];
-
- if (params is Map<dynamic, dynamic>) {
- if (event == 'server.status')
- _handleStatus(response['params']);
- else if (event == 'analysis.errors')
- _handleAnalysisIssues(response['params']);
- else if (event == 'server.error')
- _handleServerError(response['params']);
- }
- } else if (response['error'] != null) {
- // Fields are 'code', 'message', and 'stackTrace'.
- final Map<String, dynamic> error = response['error'];
- printError('Error response from the server: ${error['code']} ${error['message']}');
- if (error['stackTrace'] != null)
- printError(error['stackTrace']);
- }
- }
- }
-
- void _handleStatus(Map<String, dynamic> statusInfo) {
- // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
- if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
- final bool isAnalyzing = statusInfo['analysis']['isAnalyzing'];
- _analyzingController.add(isAnalyzing);
- }
- }
-
- void _handleServerError(Map<String, dynamic> error) {
- // Fields are 'isFatal', 'message', and 'stackTrace'.
- printError('Error from the analysis server: ${error['message']}');
- if (error['stackTrace'] != null)
- printError(error['stackTrace']);
- }
-
- void _handleAnalysisIssues(Map<String, dynamic> issueInfo) {
- // {"event":"analysis.errors","params":{"file":"/Users/.../lib/main.dart","errors":[]}}
- final String file = issueInfo['file'];
- final List<AnalysisError> errors = issueInfo['errors'].map((Map<String, dynamic> json) => new AnalysisError(json)).toList();
- if (!_errorsController.isClosed)
- _errorsController.add(new FileAnalysisErrors(file, errors));
- }
-
- Future<bool> dispose() async {
- await _analyzingController.close();
- await _errorsController.close();
- return _process?.kill();
- }
-}
-
-class AnalysisError implements Comparable<AnalysisError> {
- AnalysisError(this.json);
-
- static final Map<String, int> _severityMap = <String, int> {
- 'ERROR': 3,
- 'WARNING': 2,
- 'INFO': 1
- };
-
- // "severity":"INFO","type":"TODO","location":{
- // "file":"/Users/.../lib/test.dart","offset":362,"length":72,"startLine":15,"startColumn":4
- // },"message":"...","hasFix":false}
- Map<String, dynamic> json;
-
- String get severity => json['severity'];
- int get severityLevel => _severityMap[severity] ?? 0;
- String get type => json['type'];
- String get message => json['message'];
- String get code => json['code'];
-
- String get file => json['location']['file'];
- int get startLine => json['location']['startLine'];
- int get startColumn => json['location']['startColumn'];
- int get offset => json['location']['offset'];
-
- @override
- int compareTo(AnalysisError other) {
- // Sort in order of file path, error location, severity, and message.
- if (file != other.file)
- return file.compareTo(other.file);
-
- if (offset != other.offset)
- return offset - other.offset;
-
- final int diff = other.severityLevel - severityLevel;
- if (diff != 0)
- return diff;
-
- return message.compareTo(other.message);
- }
-
- @override
- String toString() {
- final String relativePath = fs.path.relative(file);
- return '${severity.toLowerCase().padLeft(7)} • $message • $relativePath:$startLine:$startColumn';
- }
-
- String toLegacyString() {
- return '[${severity.toLowerCase()}] $message ($file:$startLine:$startColumn)';
- }
-}
-
-class FileAnalysisErrors {
- FileAnalysisErrors(this.file, this.errors);
-
- final String file;
- final List<AnalysisError> errors;
-}
diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart
index fa58a59..82b71ee 100644
--- a/packages/flutter_tools/lib/src/commands/analyze_once.dart
+++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart
@@ -3,13 +3,12 @@
// found in the LICENSE file.
import 'dart:async';
-import 'dart:collection';
import 'package:args/args.dart';
import '../base/common.dart';
import '../base/file_system.dart';
-import '../base/process.dart';
+import '../base/logger.dart';
import '../base/utils.dart';
import '../cache.dart';
import '../dart/analysis.dart';
@@ -18,281 +17,178 @@
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 {
- AnalyzeOnce(ArgResults argResults, this.repoPackages, {
+ AnalyzeOnce(
+ ArgResults argResults,
+ this.repoRoots,
+ this.repoPackages, {
this.workingDirectory,
this.previewDart2: false,
}) : super(argResults);
+ final List<String> repoRoots;
final List<Directory> repoPackages;
- /// The working directory for testing analysis using dartanalyzer
+ /// The working directory for testing analysis using dartanalyzer.
final Directory workingDirectory;
final bool previewDart2;
@override
Future<Null> analyze() async {
- final Stopwatch stopwatch = new Stopwatch()..start();
- final Set<Directory> pubSpecDirectories = new HashSet<Directory>();
- final List<File> dartFiles = <File>[];
- for (String file in argResults.rest.toList()) {
- file = fs.path.normalize(fs.path.absolute(file));
- final String root = fs.path.rootPrefix(file);
- dartFiles.add(fs.file(file));
- while (file != root) {
- file = fs.path.dirname(file);
- if (fs.isFileSync(fs.path.join(file, 'pubspec.yaml'))) {
- pubSpecDirectories.add(fs.directory(file));
- break;
+ final String currentDirectory =
+ (workingDirectory ?? fs.currentDirectory).path;
+
+ // find directories from argResults.rest
+ final Set<String> directories = new Set<String>.from(argResults.rest
+ .map<String>((String path) => fs.path.canonicalize(path)));
+ if (directories.isNotEmpty) {
+ for (String directory in directories) {
+ final FileSystemEntityType type = fs.typeSync(directory);
+
+ if (type == FileSystemEntityType.notFound) {
+ throwToolExit("'$directory' does not exist");
+ } else if (type != FileSystemEntityType.directory) {
+ throwToolExit("'$directory' is not a directory");
}
}
}
- final bool currentPackage = argResults['current-package'] && (argResults.wasParsed('current-package') || dartFiles.isEmpty);
- final bool flutterRepo = argResults['flutter-repo'] || (workingDirectory == null && inRepo(argResults.rest));
+ if (argResults['flutter-repo']) {
+ // check for conflicting dependencies
+ final PackageDependencyTracker dependencies =
+ new PackageDependencyTracker();
+ dependencies.checkForConflictingDependencies(repoPackages, dependencies);
- // Use dartanalyzer directly except when analyzing the Flutter repository.
- // Analyzing the repository requires a more complex report than dartanalyzer
- // currently supports (e.g. missing member dartdoc summary).
- // TODO(danrubel): enhance dartanalyzer to provide this type of summary
- if (!flutterRepo) {
- if (argResults['dartdocs'])
- throwToolExit('The --dartdocs option is currently only supported with --flutter-repo.');
+ directories.addAll(repoRoots);
- final List<String> arguments = <String>[];
- arguments.addAll(dartFiles.map((FileSystemEntity f) => f.path));
-
- if (arguments.isEmpty || currentPackage) {
- // workingDirectory is non-null only when testing flutter analyze
- final Directory currentDirectory = workingDirectory ?? fs.currentDirectory.absolute;
- final Directory projectDirectory = await projectDirectoryContaining(currentDirectory);
- if (projectDirectory != null) {
- arguments.add(projectDirectory.path);
- } else if (arguments.isEmpty) {
- arguments.add(currentDirectory.path);
- }
+ if (argResults.wasParsed('current-package') &&
+ argResults['current-package']) {
+ directories.add(currentDirectory);
}
-
- // If the files being analyzed are outside of the current directory hierarchy
- // then dartanalyzer does not yet know how to find the ".packages" file.
- // TODO(danrubel): fix dartanalyzer to find the .packages file
- final File packagesFile = await packagesFileFor(arguments);
- if (packagesFile != null) {
- arguments.insert(0, '--packages');
- arguments.insert(1, packagesFile.path);
+ } else {
+ if (argResults['current-package']) {
+ directories.add(currentDirectory);
}
-
- if (previewDart2) {
- arguments.add('--preview-dart-2');
- } else {
- arguments.add('--no-preview-dart-2');
- }
-
- final String sdkPath = argResults['dart-sdk'] ?? sdk.dartSdkPath;
-
- final String dartanalyzer = fs.path.join(sdkPath, 'bin', 'dartanalyzer');
- arguments.insert(0, dartanalyzer);
- bool noErrors = false;
- final Set<String> issues = new Set<String>();
- int exitCode = await runCommandAndStreamOutput(
- arguments,
- workingDirectory: workingDirectory?.path,
- mapFunction: (String line) {
- // De-duplicate the dartanalyzer command output (https://github.com/dart-lang/sdk/issues/25697).
- if (line.startsWith(' ')) {
- if (!issues.add(line.trim()))
- return null;
- }
-
- // Workaround for the fact that dartanalyzer does not exit with a non-zero exit code
- // when errors are found.
- // TODO(danrubel): Fix dartanalyzer to return non-zero exit code
- if (line == 'No issues found!')
- noErrors = true;
-
- // Remove text about the issue count ('2 hints found.'); with the duplicates
- // above, the printed count would be incorrect.
- if (line.endsWith(' found.'))
- return null;
-
- return line;
- },
- );
- stopwatch.stop();
- if (issues.isNotEmpty)
- printStatus('${issues.length} ${pluralize('issue', issues.length)} found.');
- final String elapsed = (stopwatch.elapsedMilliseconds / 1000.0).toStringAsFixed(1);
- // Workaround for the fact that dartanalyzer does not exit with a non-zero exit code
- // when errors are found.
- // TODO(danrubel): Fix dartanalyzer to return non-zero exit code
- if (exitCode == 0 && !noErrors)
- exitCode = 1;
- if (exitCode != 0)
- throwToolExit('(Ran in ${elapsed}s)', exitCode: exitCode);
- printStatus('Ran in ${elapsed}s');
- return;
}
- for (Directory dir in repoPackages) {
- _collectDartFiles(dir, dartFiles);
- pubSpecDirectories.add(dir);
+ if (argResults['dartdocs'] && !argResults['flutter-repo']) {
+ throwToolExit(
+ 'The --dartdocs option is currently only supported with --flutter-repo.');
}
- // determine what all the various .packages files depend on
- final PackageDependencyTracker dependencies = new PackageDependencyTracker();
- dependencies.checkForConflictingDependencies(pubSpecDirectories, dependencies);
- final Map<String, String> packages = dependencies.asPackageMap();
+ if (directories.isEmpty) {
+ throwToolExit('Nothing to analyze.', exitCode: 0);
+ }
+
+ // analyze all
+ final Completer<Null> analysisCompleter = new Completer<Null>();
+ final List<AnalysisError> errors = <AnalysisError>[];
+
+ final String sdkPath = argResults['dart-sdk'] ?? sdk.dartSdkPath;
+
+ final AnalysisServer server = new AnalysisServer(
+ sdkPath,
+ directories.toList(),
+ previewDart2: previewDart2,
+ );
+
+ StreamSubscription<bool> subscription;
+ subscription = server.onAnalyzing.listen((bool isAnalyzing) {
+ if (!isAnalyzing) {
+ analysisCompleter.complete();
+ subscription?.cancel();
+ subscription = null;
+ }
+ });
+ server.onErrors.listen((FileAnalysisErrors fileErrors) {
+ fileErrors.errors
+ .removeWhere((AnalysisError error) => error.type == 'TODO');
+ errors.addAll(fileErrors.errors);
+ });
+
+ await server.start();
+ server.onExit.then((int exitCode) {
+ if (!analysisCompleter.isCompleted) {
+ analysisCompleter.completeError('analysis server exited: $exitCode');
+ }
+ });
Cache.releaseLockEarly();
- if (argResults['preamble']) {
- if (dartFiles.length == 1) {
- logger.printStatus('Analyzing ${fs.path.relative(dartFiles.first.path)}...');
+ // collect results
+ final Stopwatch timer = new Stopwatch()..start();
+ final String message = directories.length > 1
+ ? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
+ : fs.path.basename(directories.first);
+ final Status progress = argResults['preamble']
+ ? logger.startProgress('Analyzing $message...')
+ : null;
+
+ await analysisCompleter.future;
+ progress?.cancel();
+ timer.stop();
+
+ // report dartdocs
+ int undocumentedMembers = 0;
+
+ if (argResults['flutter-repo']) {
+ undocumentedMembers = errors.where((AnalysisError error) {
+ return error.code == 'public_member_api_docs';
+ }).length;
+
+ if (!argResults['dartdocs']) {
+ errors.removeWhere(
+ (AnalysisError error) => error.code == 'public_member_api_docs');
+ }
+ }
+
+ // emit benchmarks
+ if (isBenchmarking) {
+ writeBenchmark(timer, errors.length, undocumentedMembers);
+ }
+
+ // report results
+ dumpErrors(
+ errors.map<String>((AnalysisError error) => error.toLegacyString()));
+
+ if (errors.isNotEmpty && argResults['preamble']) {
+ printStatus('');
+ }
+ errors.sort();
+ for (AnalysisError error in errors) {
+ printStatus(error.toString());
+ }
+
+ final String seconds =
+ (timer.elapsedMilliseconds / 1000.0).toStringAsFixed(1);
+
+ // We consider any level of error to be an error exit (we don't report different levels).
+ if (errors.isNotEmpty) {
+ printStatus('');
+
+ printStatus(
+ '${errors.length} ${pluralize('issue', errors.length)} found. (ran in ${seconds}s)');
+
+ if (undocumentedMembers > 0) {
+ throwToolExit('[lint] $undocumentedMembers public '
+ '${ undocumentedMembers == 1
+ ? "member lacks"
+ : "members lack" } documentation');
} else {
- logger.printStatus('Analyzing ${dartFiles.length} files...');
+ throwToolExit(null);
}
}
- final DriverOptions options = new DriverOptions();
- options.dartSdkPath = argResults['dart-sdk'];
- options.packageMap = packages;
- options.analysisOptionsFile = fs.path.join(Cache.flutterRoot, 'analysis_options_repo.yaml');
- final AnalysisDriver analyzer = new AnalysisDriver(options);
- // TODO(pq): consider error handling
- final 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/208
- if (isFlutterLibrary(error.source.fullName)) {
- if (!argResults['dartdocs']) {
- membersMissingDocumentation += 1;
- shouldIgnore = true;
- }
- } else {
- shouldIgnore = true;
- }
- }
- if (shouldIgnore)
- continue;
- printError(error.asString());
- errorCount += 1;
- }
- dumpErrors(errors.map<String>((AnalysisErrorDescription error) => error.asString()));
-
- stopwatch.stop();
- final String elapsed = (stopwatch.elapsedMilliseconds / 1000.0).toStringAsFixed(1);
-
- if (isBenchmarking)
- writeBenchmark(stopwatch, errorCount, membersMissingDocumentation);
-
- if (errorCount > 0) {
- // we consider any level of error to be an error exit (we don't report different levels)
- if (membersMissingDocumentation > 0)
- throwToolExit('[lint] $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation (ran in ${elapsed}s)');
- else
- throwToolExit('(Ran in ${elapsed}s)');
- }
if (argResults['congratulate']) {
- if (membersMissingDocumentation > 0) {
- printStatus('No analyzer warnings! (ran in ${elapsed}s; $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation)');
+ if (undocumentedMembers > 0) {
+ printStatus('No issues found! (ran in ${seconds}s; '
+ '$undocumentedMembers public ${ undocumentedMembers ==
+ 1 ? "member lacks" : "members lack" } documentation)');
} else {
- printStatus('No analyzer warnings! (ran in ${elapsed}s)');
+ printStatus('No issues found! (ran in ${seconds}s)');
}
}
}
-
- /// Return a path to the ".packages" file for use by dartanalyzer when analyzing the specified files.
- /// Report an error if there are file paths that belong to different projects.
- Future<File> packagesFileFor(List<String> filePaths) async {
- String projectPath = await projectPathContaining(filePaths.first);
- if (projectPath != null) {
- if (projectPath.endsWith(fs.path.separator))
- projectPath = projectPath.substring(0, projectPath.length - 1);
- final String projectPrefix = projectPath + fs.path.separator;
- // Assert that all file paths are contained in the same project directory
- for (String filePath in filePaths) {
- if (!filePath.startsWith(projectPrefix) && filePath != projectPath)
- throwToolExit('Files in different projects cannot be analyzed at the same time.\n'
- ' Project: $projectPath\n File outside project: $filePath');
- }
- } else {
- // Assert that all file paths are not contained in any project
- for (String filePath in filePaths) {
- final String otherProjectPath = await projectPathContaining(filePath);
- if (otherProjectPath != null)
- throwToolExit('Files inside a project cannot be analyzed at the same time as files not in any project.\n'
- ' File inside a project: $filePath');
- }
- }
-
- if (projectPath == null)
- return null;
- final File packagesFile = fs.file(fs.path.join(projectPath, '.packages'));
- return await packagesFile.exists() ? packagesFile : null;
- }
-
- Future<String> projectPathContaining(String targetPath) async {
- final FileSystemEntity target = await fs.isDirectory(targetPath) ? fs.directory(targetPath) : fs.file(targetPath);
- final Directory projectDirectory = await projectDirectoryContaining(target);
- return projectDirectory?.path;
- }
-
- Future<Directory> projectDirectoryContaining(FileSystemEntity entity) async {
- Directory dir = entity is Directory ? entity : entity.parent;
- dir = dir.absolute;
- while (!await dir.childFile('pubspec.yaml').exists()) {
- final Directory parent = dir.parent;
- if (parent == null || parent.path == dir.path)
- return null;
- dir = parent;
- }
- return dir;
- }
-
- List<String> flutterRootComponents;
- bool isFlutterLibrary(String filename) {
- flutterRootComponents ??= fs.path.normalize(fs.path.absolute(Cache.flutterRoot)).split(fs.path.separator);
- final List<String> filenameComponents = fs.path.normalize(fs.path.absolute(filename)).split(fs.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) {
- // Bail out in case of a .dartignore.
- if (fs.isFileSync(fs.path.join(dir.path, '.dartignore')))
- return collected;
-
- for (FileSystemEntity entity in dir.listSync(recursive: false, followLinks: false)) {
- if (isDartFile(entity))
- collected.add(entity);
- if (entity is Directory) {
- final String name = fs.path.basename(entity.path);
- if (!name.startsWith('.') && name != 'packages')
- _collectDartFiles(entity, collected);
- }
- }
-
- return collected;
- }
}
diff --git a/packages/flutter_tools/lib/src/dart/analysis.dart b/packages/flutter_tools/lib/src/dart/analysis.dart
index c0d9822..09d2425 100644
--- a/packages/flutter_tools/lib/src/dart/analysis.dart
+++ b/packages/flutter_tools/lib/src/dart/analysis.dart
@@ -2,269 +2,220 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:collection';
-
-import 'package:analyzer/error/error.dart';
-import 'package:analyzer/file_system/file_system.dart' as file_system;
-import 'package:analyzer/file_system/physical_file_system.dart';
-// TODO(goderbauer): update import path when deprecation has landed on stable
-import 'package:analyzer/source/analysis_options_provider.dart'; // ignore: deprecated_member_use
-import 'package:analyzer/source/error_processor.dart';
-import 'package:analyzer/source/line_info.dart';
-import 'package:analyzer/source/package_map_resolver.dart'; // ignore: deprecated_member_use
-import 'package:analyzer/src/context/builder.dart'; // ignore: implementation_imports
-import 'package:analyzer/src/dart/sdk/sdk.dart'; // ignore: implementation_imports
-import 'package:analyzer/src/generated/engine.dart'; // ignore: implementation_imports
-import 'package:analyzer/src/generated/java_io.dart'; // ignore: implementation_imports
-import 'package:analyzer/src/generated/source.dart'; // ignore: implementation_imports
-import 'package:analyzer/src/generated/source_io.dart'; // ignore: implementation_imports
-import 'package:analyzer/src/task/options.dart'; // ignore: implementation_imports
-import 'package:linter/src/rules.dart' as linter; // ignore: implementation_imports
-import 'package:cli_util/cli_util.dart' as cli_util;
-import 'package:package_config/packages.dart' show Packages;
-import 'package:package_config/src/packages_impl.dart' show MapPackages; // ignore: implementation_imports
-import 'package:plugin/manager.dart';
-import 'package:plugin/plugin.dart';
+import 'dart:async';
+import 'dart:convert';
import '../base/file_system.dart' hide IOSink;
+import '../base/file_system.dart';
import '../base/io.dart';
+import '../base/platform.dart';
+import '../base/process_manager.dart';
+import '../globals.dart';
-class AnalysisDriver {
- AnalysisDriver(this.options) {
- AnalysisEngine.instance.logger =
- new _StdLogger(outSink: options.outSink, errorSink: options.errorSink);
- _processPlugins();
- }
+class AnalysisServer {
+ AnalysisServer(this.sdkPath, this.directories, {this.previewDart2: false});
- final Set<Source> _analyzedSources = new HashSet<Source>();
+ final String sdkPath;
+ final List<String> directories;
+ final bool previewDart2;
- AnalysisOptionsProvider analysisOptionsProvider =
- new AnalysisOptionsProvider();
+ Process _process;
+ final StreamController<bool> _analyzingController =
+ new StreamController<bool>.broadcast();
+ final StreamController<FileAnalysisErrors> _errorsController =
+ new StreamController<FileAnalysisErrors>.broadcast();
- file_system.ResourceProvider resourceProvider = PhysicalResourceProvider.INSTANCE;
+ int _id = 0;
- AnalysisContext context;
+ Future<Null> start() async {
+ final String snapshot =
+ fs.path.join(sdkPath, 'bin/snapshots/analysis_server.dart.snapshot');
+ final List<String> command = <String>[
+ fs.path.join(sdkPath, 'bin', 'dart'),
+ snapshot,
+ '--sdk',
+ sdkPath,
+ ];
- DriverOptions options;
-
- String get sdkDir => options.dartSdkPath ?? cli_util.getSdkPath();
-
- List<AnalysisErrorDescription> analyze(Iterable<File> files) {
- final List<AnalysisErrorInfo> infos = _analyze(files);
- final List<AnalysisErrorDescription> errors = <AnalysisErrorDescription>[];
- for (AnalysisErrorInfo info in infos) {
- for (AnalysisError error in info.errors) {
- if (!_isFiltered(error))
- errors.add(new AnalysisErrorDescription(error, info.lineInfo));
- }
- }
- return errors;
- }
-
- List<AnalysisErrorInfo> _analyze(Iterable<File> files) {
- context = AnalysisEngine.instance.createAnalysisContext();
- _processAnalysisOptions();
- context.analysisOptions = options;
- final PackageInfo packageInfo = new PackageInfo(options.packageMap);
- final List<UriResolver> resolvers = _getResolvers(context, packageInfo.asMap());
- context.sourceFactory =
- new SourceFactory(resolvers, packageInfo.asPackages());
-
- final List<Source> sources = <Source>[];
- final ChangeSet changeSet = new ChangeSet();
- for (File file in files) {
- final JavaFile sourceFile = new JavaFile(fs.path.normalize(file.absolute.path));
- Source source = new FileBasedSource(sourceFile, sourceFile.toURI());
- final Uri uri = context.sourceFactory.restoreUri(source);
- if (uri != null) {
- source = new FileBasedSource(sourceFile, uri);
- }
- sources.add(source);
- changeSet.addedSource(source);
- }
- context.applyChanges(changeSet);
-
- final List<AnalysisErrorInfo> infos = <AnalysisErrorInfo>[];
- for (Source source in sources) {
- context.computeErrors(source);
- infos.add(context.getErrors(source));
- _analyzedSources.add(source);
- }
-
- return infos;
- }
-
- List<UriResolver> _getResolvers(InternalAnalysisContext context,
- Map<String, List<file_system.Folder>> packageMap) {
-
- // Create our list of resolvers.
- final List<UriResolver> resolvers = <UriResolver>[];
-
- // Look for an embedder.
- final EmbedderYamlLocator locator = new EmbedderYamlLocator(packageMap);
- if (locator.embedderYamls.isNotEmpty) {
- // Create and configure an embedded SDK.
- final EmbedderSdk sdk = new EmbedderSdk(PhysicalResourceProvider.INSTANCE, locator.embedderYamls);
- // Fail fast if no URI mappings are found.
- assert(sdk.libraryMap.size() > 0);
- sdk.analysisOptions = context.analysisOptions;
-
- resolvers.add(new DartUriResolver(sdk));
+ if (previewDart2) {
+ command.add('--preview-dart-2');
} else {
- // Fall back to a standard SDK if no embedder is found.
- final FolderBasedDartSdk sdk = new FolderBasedDartSdk(resourceProvider,
- PhysicalResourceProvider.INSTANCE.getFolder(sdkDir));
- sdk.analysisOptions = context.analysisOptions;
-
- resolvers.add(new DartUriResolver(sdk));
+ command.add('--no-preview-dart-2');
}
- if (options.packageRootPath != null) {
- final ContextBuilderOptions builderOptions = new ContextBuilderOptions();
- builderOptions.defaultPackagesDirectoryPath = options.packageRootPath;
- final ContextBuilder builder = new ContextBuilder(resourceProvider, null, null,
- options: builderOptions);
- final PackageMapUriResolver packageUriResolver = new PackageMapUriResolver(resourceProvider,
- builder.convertPackagesToMap(builder.createPackageMap('')));
+ printTrace('dart ${command.skip(1).join(' ')}');
+ _process = await processManager.start(command);
+ // This callback hookup can't throw.
+ _process.exitCode
+ .whenComplete(() => _process = null); // ignore: unawaited_futures
- resolvers.add(packageUriResolver);
- }
+ final Stream<String> errorStream =
+ _process.stderr.transform(utf8.decoder).transform(const LineSplitter());
+ errorStream.listen(printError);
- resolvers.add(new file_system.ResourceUriResolver(resourceProvider));
- return resolvers;
+ final Stream<String> inStream =
+ _process.stdout.transform(utf8.decoder).transform(const LineSplitter());
+ inStream.listen(_handleServerResponse);
+
+ // Available options (many of these are obsolete):
+ // enableAsync, enableDeferredLoading, enableEnums, enableNullAwareOperators,
+ // enableSuperMixins, generateDart2jsHints, generateHints, generateLints
+ _sendCommand('analysis.updateOptions', <String, dynamic>{
+ 'options': <String, dynamic>{'enableSuperMixins': true}
+ });
+
+ _sendCommand('server.setSubscriptions', <String, dynamic>{
+ 'subscriptions': <String>['STATUS']
+ });
+
+ _sendCommand('analysis.setAnalysisRoots',
+ <String, dynamic>{'included': directories, 'excluded': <String>[]});
}
- bool _isFiltered(AnalysisError error) {
- final ErrorProcessor processor = ErrorProcessor.getProcessor(context.analysisOptions, error);
- // Filtered errors are processed to a severity of null.
- return processor != null && processor.severity == null;
+ Stream<bool> get onAnalyzing => _analyzingController.stream;
+ Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
+
+ Future<int> get onExit => _process.exitCode;
+
+ void _sendCommand(String method, Map<String, dynamic> params) {
+ final String message = json.encode(<String, dynamic>{
+ 'id': (++_id).toString(),
+ 'method': method,
+ 'params': params
+ });
+ _process.stdin.writeln(message);
+ printTrace('==> $message');
}
- void _processAnalysisOptions() {
- final String optionsPath = options.analysisOptionsFile;
- if (optionsPath != null) {
- final file_system.File file =
- PhysicalResourceProvider.INSTANCE.getFile(optionsPath);
- final Map<Object, Object> optionMap =
- analysisOptionsProvider.getOptionsFromFile(file);
- if (optionMap != null)
- applyToAnalysisOptions(options, optionMap);
+ void _handleServerResponse(String line) {
+ printTrace('<== $line');
+
+ final dynamic response = json.decode(line);
+
+ if (response is Map<dynamic, dynamic>) {
+ if (response['event'] != null) {
+ final String event = response['event'];
+ final dynamic params = response['params'];
+
+ if (params is Map<dynamic, dynamic>) {
+ if (event == 'server.status')
+ _handleStatus(response['params']);
+ else if (event == 'analysis.errors')
+ _handleAnalysisIssues(response['params']);
+ else if (event == 'server.error')
+ _handleServerError(response['params']);
+ }
+ } else if (response['error'] != null) {
+ // Fields are 'code', 'message', and 'stackTrace'.
+ final Map<String, dynamic> error = response['error'];
+ printError(
+ 'Error response from the server: ${error['code']} ${error['message']}');
+ if (error['stackTrace'] != null) {
+ printError(error['stackTrace']);
+ }
+ }
}
}
- void _processPlugins() {
- final List<Plugin> plugins = <Plugin>[];
- plugins.addAll(AnalysisEngine.instance.requiredPlugins);
- final ExtensionManager manager = new ExtensionManager();
- manager.processPlugins(plugins);
- linter.registerLintRules();
+ void _handleStatus(Map<String, dynamic> statusInfo) {
+ // {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
+ if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
+ final bool isAnalyzing = statusInfo['analysis']['isAnalyzing'];
+ _analyzingController.add(isAnalyzing);
+ }
+ }
+
+ void _handleServerError(Map<String, dynamic> error) {
+ // Fields are 'isFatal', 'message', and 'stackTrace'.
+ printError('Error from the analysis server: ${error['message']}');
+ if (error['stackTrace'] != null) {
+ printError(error['stackTrace']);
+ }
+ }
+
+ void _handleAnalysisIssues(Map<String, dynamic> issueInfo) {
+ // {"event":"analysis.errors","params":{"file":"/Users/.../lib/main.dart","errors":[]}}
+ final String file = issueInfo['file'];
+ final List<AnalysisError> errors = issueInfo['errors']
+ .map((Map<String, dynamic> json) => new AnalysisError(json))
+ .toList();
+ if (!_errorsController.isClosed)
+ _errorsController.add(new FileAnalysisErrors(file, errors));
+ }
+
+ Future<bool> dispose() async {
+ await _analyzingController.close();
+ await _errorsController.close();
+ return _process?.kill();
}
}
-class AnalysisDriverException implements Exception {
- AnalysisDriverException([this.message]);
+class AnalysisError implements Comparable<AnalysisError> {
+ AnalysisError(this.json);
- final String message;
+ static final Map<String, int> _severityMap = <String, int>{
+ 'ERROR': 3,
+ 'WARNING': 2,
+ 'INFO': 1
+ };
+
+ static final String _separator = platform.isWindows ? '-' : '•';
+
+ // "severity":"INFO","type":"TODO","location":{
+ // "file":"/Users/.../lib/test.dart","offset":362,"length":72,"startLine":15,"startColumn":4
+ // },"message":"...","hasFix":false}
+ Map<String, dynamic> json;
+
+ String get severity => json['severity'];
+ int get severityLevel => _severityMap[severity] ?? 0;
+ String get type => json['type'];
+ String get message => json['message'];
+ String get code => json['code'];
+
+ String get file => json['location']['file'];
+ int get startLine => json['location']['startLine'];
+ int get startColumn => json['location']['startColumn'];
+ int get offset => json['location']['offset'];
+
+ String get messageSentenceFragment {
+ if (message.endsWith('.')) {
+ return message.substring(0, message.length - 1);
+ } else {
+ return message;
+ }
+ }
@override
- String toString() => message == null ? 'Exception' : 'Exception: $message';
-}
+ int compareTo(AnalysisError other) {
+ // Sort in order of file path, error location, severity, and message.
+ if (file != other.file)
+ return file.compareTo(other.file);
-class AnalysisErrorDescription {
- AnalysisErrorDescription(this.error, this.line);
+ if (offset != other.offset)
+ return offset - other.offset;
- static Directory cwd = fs.currentDirectory.absolute;
+ final int diff = other.severityLevel - severityLevel;
+ if (diff != 0)
+ return diff;
- final AnalysisError error;
- final LineInfo line;
-
- ErrorCode get errorCode => error.errorCode;
-
- String get errorType {
- final ErrorSeverity severity = errorCode.errorSeverity;
- if (severity == ErrorSeverity.INFO) {
- if (errorCode.type == ErrorType.HINT || errorCode.type == ErrorType.LINT)
- return errorCode.type.displayName;
- }
- return severity.displayName;
+ return message.compareTo(other.message);
}
- CharacterLocation get location => line.getLocation(error.offset);
-
- String get path => _shorten(cwd.path, error.source.fullName);
-
- Source get source => error.source;
-
- String asString() => '[$errorType] ${error.message} ($path, '
- 'line ${location.lineNumber}, col ${location.columnNumber})';
-
- static String _shorten(String root, String path) =>
- path.startsWith(root) ? path.substring(root.length + 1) : path;
-}
-
-class DriverOptions extends AnalysisOptionsImpl {
- DriverOptions() {
- // Set defaults.
- lint = true;
- generateSdkErrors = false;
- trackCacheDependencies = false;
- }
-
- /// The path to the dart SDK.
- String dartSdkPath;
-
- /// Map of packages to folder paths.
- Map<String, String> packageMap;
-
- /// The path to the package root.
- String packageRootPath;
-
- /// The path to analysis options.
- String analysisOptionsFile;
-
- /// Out sink for logging.
- IOSink outSink = stdout;
-
- /// Error sink for logging.
- IOSink errorSink = stderr;
-}
-
-class PackageInfo {
- PackageInfo(Map<String, String> packageMap) {
- final Map<String, Uri> packages = new HashMap<String, Uri>();
- for (String package in packageMap.keys) {
- final String path = packageMap[package];
- packages[package] = new Uri.directory(path);
- _map[package] = <file_system.Folder>[
- PhysicalResourceProvider.INSTANCE.getFolder(path)
- ];
- }
- _packages = new MapPackages(packages);
- }
-
- Packages _packages;
-
- Map<String, List<file_system.Folder>> asMap() => _map;
- final HashMap<String, List<file_system.Folder>> _map =
- new HashMap<String, List<file_system.Folder>>();
-
- Packages asPackages() => _packages;
-}
-
-class _StdLogger extends Logger {
- _StdLogger({this.outSink, this.errorSink});
-
- final IOSink outSink;
- final IOSink errorSink;
-
@override
- void logError(String message, [Exception exception]) =>
- errorSink.writeln(message);
-
- @override
- void logInformation(String message, [Exception exception]) {
- // TODO(pq): remove once addressed in analyzer (http://dartbug.com/28285)
- if (message != 'No definition of type FutureOr')
- outSink.writeln(message);
+ String toString() {
+ return '${severity.toLowerCase().padLeft(7)} $_separator '
+ '$messageSentenceFragment $_separator '
+ '${fs.path.relative(file)}:$startLine:$startColumn';
}
+
+ String toLegacyString() {
+ return '[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)';
+ }
+}
+
+class FileAnalysisErrors {
+ FileAnalysisErrors(this.file, this.errors);
+
+ final String file;
+ final List<AnalysisError> errors;
}
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index 9a02aaf..4ae4304 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -383,12 +383,19 @@
Cache.flutterRoot ??= _defaultFlutterRoot;
}
- /// Get all pub packages in the Flutter repo.
- List<Directory> getRepoPackages() {
+ /// Get the root directories of the repo - the directories containing Dart packages.
+ List<String> getRepoRoots() {
final String root = fs.path.absolute(Cache.flutterRoot);
// not bin, and not the root
- return <String>['dev', 'examples', 'packages']
- .expand<String>((String path) => _gatherProjectPaths(fs.path.join(root, path)))
+ return <String>['dev', 'examples', 'packages'].map((String item) {
+ return fs.path.join(root, item);
+ }).toList();
+ }
+
+ /// Get all pub packages in the Flutter repo.
+ List<Directory> getRepoPackages() {
+ return getRepoRoots()
+ .expand<String>((String root) => _gatherProjectPaths(root))
.map((String dir) => fs.directory(dir))
.toList();
}
diff --git a/packages/flutter_tools/test/commands/analyze_continuously_test.dart b/packages/flutter_tools/test/commands/analyze_continuously_test.dart
index 3686de4..2a67faa 100644
--- a/packages/flutter_tools/test/commands/analyze_continuously_test.dart
+++ b/packages/flutter_tools/test/commands/analyze_continuously_test.dart
@@ -6,7 +6,7 @@
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/os.dart';
-import 'package:flutter_tools/src/commands/analyze_continuously.dart';
+import 'package:flutter_tools/src/dart/analysis.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';
diff --git a/packages/flutter_tools/test/commands/analyze_duplicate_names_test.dart b/packages/flutter_tools/test/commands/analyze_duplicate_names_test.dart
deleted file mode 100644
index c56ff8b..0000000
--- a/packages/flutter_tools/test/commands/analyze_duplicate_names_test.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2016 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 'package:flutter_tools/src/cache.dart';
-import 'package:flutter_tools/src/base/file_system.dart';
-import 'package:flutter_tools/src/commands/analyze.dart';
-import 'package:test/test.dart';
-
-import '../src/common.dart';
-import '../src/context.dart';
-import '../src/mocks.dart';
-
-void main() {
- Directory tempDir;
-
- setUpAll(() {
- Cache.disableLocking();
- });
-
- setUp(() {
- tempDir = fs.systemTempDirectory.createTempSync('analysis_duplicate_names_test');
- });
-
- tearDown(() {
- tempDir?.deleteSync(recursive: true);
- });
-
- group('analyze', () {
- testUsingContext('flutter analyze with two files with the same name', () async {
- final File dartFileA = fs.file(fs.path.join(tempDir.path, 'a.dart'));
- dartFileA.parent.createSync();
- dartFileA.writeAsStringSync('library test;');
- final File dartFileB = fs.file(fs.path.join(tempDir.path, 'b.dart'));
- dartFileB.writeAsStringSync('library test;');
-
- final AnalyzeCommand command = new AnalyzeCommand();
- applyMocksToCommand(command);
- return createTestCommandRunner(command).run(
- <String>['analyze', '--no-current-package', dartFileA.path, dartFileB.path]
- ).then<Null>((Null value) {
- expect(testLogger.statusText, contains('Analyzing'));
- expect(testLogger.statusText, contains('No issues found!'));
- });
-
- });
- });
-}
diff --git a/packages/flutter_tools/test/commands/analyze_once_test.dart b/packages/flutter_tools/test/commands/analyze_once_test.dart
index 8f40955..c8c574e 100644
--- a/packages/flutter_tools/test/commands/analyze_once_test.dart
+++ b/packages/flutter_tools/test/commands/analyze_once_test.dart
@@ -17,7 +17,6 @@
import '../src/context.dart';
void main() {
-
final String analyzerSeparator = platform.isWindows ? '-' : '•';
group('analyze once', () {
@@ -55,7 +54,7 @@
}, timeout: allowForRemotePubInvocation);
// Analyze in the current directory - no arguments
- testUsingContext('flutter analyze working directory', () async {
+ testUsingContext('working directory', () async {
await runCommand(
command: new AnalyzeCommand(workingDirectory: fs.directory(projectPath)),
arguments: <String>['analyze'],
@@ -64,17 +63,17 @@
});
// Analyze a specific file outside the current directory
- testUsingContext('flutter analyze one file', () async {
+ testUsingContext('passing one file throws', () async {
await runCommand(
command: new AnalyzeCommand(),
arguments: <String>['analyze', libMain.path],
- statusTextContains: <String>['No issues found!'],
+ toolExit: true,
+ exitMessageContains: 'is not a directory',
);
});
// Analyze in the current directory - no arguments
- testUsingContext('flutter analyze working directory with errors', () async {
-
+ testUsingContext('working directory with errors', () async {
// Break the code to produce the "The parameter 'onPressed' is required" hint
// that is upgraded to a warning in package:flutter/analysis_options_user.yaml
// to assert that we are using the default Flutter analysis options.
@@ -98,22 +97,7 @@
statusTextContains: <String>[
'Analyzing',
'warning $analyzerSeparator The parameter \'onPressed\' is required',
- 'hint $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
- '2 issues found.',
- ],
- toolExit: true,
- );
- });
-
- // Analyze a specific file outside the current directory
- testUsingContext('flutter analyze one file with errors', () async {
- await runCommand(
- command: new AnalyzeCommand(),
- arguments: <String>['analyze', libMain.path],
- statusTextContains: <String>[
- 'Analyzing',
- 'warning $analyzerSeparator The parameter \'onPressed\' is required',
- 'hint $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
+ 'info $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
'2 issues found.',
],
toolExit: true,
@@ -121,8 +105,7 @@
});
// Analyze in the current directory - no arguments
- testUsingContext('flutter analyze working directory with local options', () async {
-
+ testUsingContext('working directory with local options', () async {
// Insert an analysis_options.yaml file in the project
// which will trigger a lint for broken code that was inserted earlier
final File optionsFile = fs.file(fs.path.join(projectPath, 'analysis_options.yaml'));
@@ -140,15 +123,15 @@
statusTextContains: <String>[
'Analyzing',
'warning $analyzerSeparator The parameter \'onPressed\' is required',
- 'hint $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
- 'lint $analyzerSeparator Only throw instances of classes extending either Exception or Error',
+ 'info $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
+ 'info $analyzerSeparator Only throw instances of classes extending either Exception or Error',
'3 issues found.',
],
toolExit: true,
);
});
- testUsingContext('flutter analyze no duplicate issues', () async {
+ testUsingContext('no duplicate issues', () async {
final Directory tempDir = fs.systemTempDirectory.createTempSync('analyze_once_test_').absolute;
try {
@@ -182,22 +165,6 @@
}
});
- // Analyze a specific file outside the current directory
- testUsingContext('flutter analyze one file with local options', () async {
- await runCommand(
- command: new AnalyzeCommand(),
- arguments: <String>['analyze', libMain.path],
- statusTextContains: <String>[
- 'Analyzing',
- 'warning $analyzerSeparator The parameter \'onPressed\' is required',
- 'hint $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
- 'lint $analyzerSeparator Only throw instances of classes extending either Exception or Error',
- '3 issues found.',
- ],
- toolExit: true,
- );
- });
-
testUsingContext('--preview-dart-2', () async {
const String contents = '''
StringBuffer bar = StringBuffer('baz');
@@ -255,18 +222,23 @@
List<String> statusTextContains,
List<String> errorTextContains,
bool toolExit: false,
+ String exitMessageContains,
}) async {
try {
arguments.insert(0, '--flutter-root=${Cache.flutterRoot}');
await createTestCommandRunner(command).run(arguments);
expect(toolExit, isFalse, reason: 'Expected ToolExit exception');
- } on ToolExit {
+ } on ToolExit catch (e) {
if (!toolExit) {
testLogger.clear();
rethrow;
}
+ if (exitMessageContains != null) {
+ expect(e.message, contains(exitMessageContains));
+ }
}
assertContains(testLogger.statusText, statusTextContains);
assertContains(testLogger.errorText, errorTextContains);
+
testLogger.clear();
}
diff --git a/packages/flutter_tools/test/commands/create_test.dart b/packages/flutter_tools/test/commands/create_test.dart
index b155050..a893d95 100644
--- a/packages/flutter_tools/test/commands/create_test.dart
+++ b/packages/flutter_tools/test/commands/create_test.dart
@@ -436,14 +436,13 @@
{ List<String> unexpectedPaths = const <String>[], bool plugin = false }) async {
await _createProject(dir, createArgs, expectedPaths, unexpectedPaths: unexpectedPaths, plugin: plugin);
if (plugin) {
- await _analyzeProject(dir.path, target: fs.path.join(dir.path, 'lib', 'flutter_project.dart'));
- await _analyzeProject(fs.path.join(dir.path, 'example'));
+ await _analyzeProject(dir.path);
} else {
await _analyzeProject(dir.path);
}
}
-Future<Null> _analyzeProject(String workingDir, {String target}) async {
+Future<Null> _analyzeProject(String workingDir) async {
final String flutterToolsPath = fs.path.absolute(fs.path.join(
'bin',
'flutter_tools.dart',
@@ -453,8 +452,6 @@
..addAll(dartVmFlags)
..add(flutterToolsPath)
..add('analyze');
- if (target != null)
- args.add(target);
final ProcessResult exec = await Process.run(
'$dartSdkPath/bin/dart',