mirror of
https://github.com/flutter/flutter
synced 2024-10-02 14:34:22 +00:00
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
This commit is contained in:
parent
ca94bfdfc6
commit
09dec7f508
|
@ -79,7 +79,7 @@ analyzer. There are two main ways to run it. In either case you will
|
||||||
want to run `flutter update-packages` first, or you will get bogus
|
want to run `flutter update-packages` first, or you will get bogus
|
||||||
error messages about core classes like Offset from `dart:ui`.
|
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.
|
at the root of the repository for its configuration.
|
||||||
|
|
||||||
For continuous analysis, use `flutter analyze --flutter-repo --watch`. This uses normal
|
For continuous analysis, use `flutter analyze --flutter-repo --watch`. This uses normal
|
||||||
|
|
|
@ -9,22 +9,18 @@
|
||||||
#
|
#
|
||||||
# There are four similar analysis options files in the flutter repos:
|
# There are four similar analysis options files in the flutter repos:
|
||||||
# - analysis_options.yaml (this file)
|
# - analysis_options.yaml (this file)
|
||||||
# - analysis_options_repo.yaml
|
|
||||||
# - packages/flutter/lib/analysis_options_user.yaml
|
# - packages/flutter/lib/analysis_options_user.yaml
|
||||||
# - https://github.com/flutter/plugins/blob/master/analysis_options.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,
|
# This file contains the analysis options used by Flutter tools, such as IntelliJ,
|
||||||
# Android Studio, and the `flutter analyze` command.
|
# 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
|
# The flutter/plugins repo contains a copy of this file, which should be kept
|
||||||
# in sync with this file.
|
# in sync with this file.
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
language:
|
language:
|
||||||
enableStrictCallChecks: true
|
|
||||||
enableSuperMixins: true
|
enableSuperMixins: true
|
||||||
strong-mode:
|
strong-mode:
|
||||||
implicit-dynamic: false
|
implicit-dynamic: false
|
||||||
|
@ -131,7 +127,6 @@ linter:
|
||||||
- prefer_is_not_empty
|
- prefer_is_not_empty
|
||||||
- prefer_single_quotes
|
- prefer_single_quotes
|
||||||
- prefer_typing_uninitialized_variables
|
- prefer_typing_uninitialized_variables
|
||||||
# - public_member_api_docs # this is the only difference from analysis_options_repo.yaml
|
|
||||||
- recursive_getters
|
- recursive_getters
|
||||||
- slash_for_doc_comments
|
- slash_for_doc_comments
|
||||||
- sort_constructors_first
|
- sort_constructors_first
|
||||||
|
|
|
@ -189,6 +189,8 @@ Future<Null> main() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer.add('');
|
buffer.add('');
|
||||||
|
buffer.add('// ignore_for_file: unused_element');
|
||||||
|
buffer.add('');
|
||||||
final List<Line> lines = new List<Line>.filled(buffer.length, null, growable: true);
|
final List<Line> lines = new List<Line>.filled(buffer.length, null, growable: true);
|
||||||
for (Section section in sections) {
|
for (Section section in sections) {
|
||||||
buffer.addAll(section.strings);
|
buffer.addAll(section.strings);
|
||||||
|
@ -207,7 +209,7 @@ dependencies:
|
||||||
print('Found $sampleCodeSections sample code sections.');
|
print('Found $sampleCodeSections sample code sections.');
|
||||||
final Process process = await Process.start(
|
final Process process = await Process.start(
|
||||||
_flutter,
|
_flutter,
|
||||||
<String>['analyze', '--no-preamble', mainDart.path],
|
<String>['analyze', '--no-preamble', '--no-congratulate', mainDart.parent.path],
|
||||||
workingDirectory: temp.path,
|
workingDirectory: temp.path,
|
||||||
);
|
);
|
||||||
stderr.addStream(process.stderr);
|
stderr.addStream(process.stderr);
|
||||||
|
@ -216,10 +218,6 @@ dependencies:
|
||||||
errors.removeAt(0);
|
errors.removeAt(0);
|
||||||
if (errors.first.startsWith('Running "flutter packages get" in '))
|
if (errors.first.startsWith('Running "flutter packages get" in '))
|
||||||
errors.removeAt(0);
|
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;
|
int errorCount = 0;
|
||||||
for (String error in errors) {
|
for (String error in errors) {
|
||||||
final String kBullet = Platform.isWindows ? ' - ' : ' • ';
|
final String kBullet = Platform.isWindows ? ' - ' : ' • ';
|
||||||
|
|
|
@ -21,21 +21,24 @@ Future<Null> main() async {
|
||||||
int publicMembers = 0;
|
int publicMembers = 0;
|
||||||
int otherErrors = 0;
|
int otherErrors = 0;
|
||||||
int otherLines = 0;
|
int otherLines = 0;
|
||||||
await for (String entry in analysis.stderr.transform(utf8.decoder).transform(const LineSplitter())) {
|
await for (String entry in analysis.stdout.transform(utf8.decoder).transform(const LineSplitter())) {
|
||||||
print('analyzer stderr: $entry');
|
entry = entry.trim();
|
||||||
if (entry.startsWith('[lint] Document all public members')) {
|
print('analyzer stdout: $entry');
|
||||||
publicMembers += 1;
|
if (entry == 'Building flutter tool...') {
|
||||||
} else if (entry.startsWith('[')) {
|
|
||||||
otherErrors += 1;
|
|
||||||
} else if (entry.startsWith('(Ran in ')) {
|
|
||||||
// ignore this line
|
// 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;
|
otherLines += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await for (String entry in analysis.stdout.transform(utf8.decoder).transform(const LineSplitter())) {
|
await for (String entry in analysis.stderr.transform(utf8.decoder).transform(const LineSplitter())) {
|
||||||
print('analyzer stdout: $entry');
|
print('analyzer stderr: $entry');
|
||||||
if (entry == 'Building flutter tool...') {
|
if (entry.startsWith('[lint] ')) {
|
||||||
// ignore this line
|
// ignore this line
|
||||||
} else {
|
} else {
|
||||||
otherLines += 1;
|
otherLines += 1;
|
||||||
|
|
8
packages/analysis_options.yaml
Normal file
8
packages/analysis_options.yaml
Normal file
|
@ -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
|
|
@ -7,21 +7,21 @@
|
||||||
# See the configuration guide for more
|
# See the configuration guide for more
|
||||||
# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
|
# 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.yaml
|
||||||
# - analysis_options_repo.yaml
|
|
||||||
# - packages/flutter/lib/analysis_options_user.yaml (this file)
|
# - 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"
|
# This file contains the analysis options used by "flutter analyze" and the
|
||||||
# and the dartanalyzer when analyzing code outside the flutter repository.
|
# dartanalyzer when analyzing code outside the flutter repository. It isn't named
|
||||||
# It isn't named 'analysis_options.yaml' because otherwise editors like Atom
|
# 'analysis_options.yaml' because otherwise editors would use it when analyzing
|
||||||
# would use it when analyzing the flutter tool itself.
|
# 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:
|
analyzer:
|
||||||
language:
|
language:
|
||||||
enableStrictCallChecks: true
|
|
||||||
enableSuperMixins: true
|
enableSuperMixins: true
|
||||||
strong-mode: true
|
strong-mode: true
|
||||||
errors:
|
errors:
|
||||||
|
|
4
packages/flutter_goldens/analysis_options.yaml
Normal file
4
packages/flutter_goldens/analysis_options.yaml
Normal file
|
@ -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
|
4
packages/flutter_tools/analysis_options.yaml
Normal file
4
packages/flutter_tools/analysis_options.yaml
Normal file
|
@ -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
|
|
@ -9,28 +9,51 @@ import '../runner/flutter_command.dart';
|
||||||
import 'analyze_continuously.dart';
|
import 'analyze_continuously.dart';
|
||||||
import 'analyze_once.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 {
|
class AnalyzeCommand extends FlutterCommand {
|
||||||
AnalyzeCommand({ bool verboseHelp: false, this.workingDirectory }) {
|
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('flutter-repo',
|
||||||
argParser.addFlag('current-package', help: 'Include the lib/main.dart file from the current directory, if any.', defaultsTo: true);
|
negatable: false,
|
||||||
argParser.addFlag('dartdocs', help: 'List every public member that is lacking documentation (only works with --flutter-repo and without --watch).', defaultsTo: false, hide: !verboseHelp);
|
help: 'Include all the examples and tests from the Flutter repository.',
|
||||||
argParser.addFlag('watch', help: 'Run analysis continuously, watching the filesystem for changes.', negatable: false);
|
defaultsTo: false,
|
||||||
argParser.addFlag('preview-dart-2', defaultsTo: true, help: 'Preview Dart 2.0 functionality.');
|
hide: !verboseHelp);
|
||||||
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.addFlag('current-package',
|
||||||
argParser.addOption('dart-sdk', valueHelp: 'path-to-sdk', help: 'The path to the Dart SDK.', hide: !verboseHelp);
|
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.
|
// 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();
|
usesPubOption();
|
||||||
|
|
||||||
// Not used by analyze --watch
|
// 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('congratulate',
|
||||||
argParser.addFlag('preamble', help: 'When analyzing the flutter repository, display the number of files that will be analyzed.', defaultsTo: true);
|
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.
|
/// The working directory for testing analysis using dartanalyzer.
|
||||||
|
@ -40,17 +63,19 @@ class AnalyzeCommand extends FlutterCommand {
|
||||||
String get name => 'analyze';
|
String get name => 'analyze';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get description => 'Analyze the project\'s Dart code.';
|
String get description => "Analyze the project's Dart code.";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get shouldRunPub {
|
bool get shouldRunPub {
|
||||||
// If they're not analyzing the current project.
|
// If they're not analyzing the current project.
|
||||||
if (!argResults['current-package'])
|
if (!argResults['current-package']) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Or we're not in a project directory.
|
// Or we're not in a project directory.
|
||||||
if (!fs.file('pubspec.yaml').existsSync())
|
if (!fs.file('pubspec.yaml').existsSync()) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return super.shouldRunPub;
|
return super.shouldRunPub;
|
||||||
}
|
}
|
||||||
|
@ -59,11 +84,15 @@ class AnalyzeCommand extends FlutterCommand {
|
||||||
Future<Null> runCommand() {
|
Future<Null> runCommand() {
|
||||||
if (argResults['watch']) {
|
if (argResults['watch']) {
|
||||||
return new AnalyzeContinuously(
|
return new AnalyzeContinuously(
|
||||||
argResults, runner.getRepoPackages(), previewDart2: argResults['preview-dart-2']
|
argResults,
|
||||||
|
runner.getRepoRoots(),
|
||||||
|
runner.getRepoPackages(),
|
||||||
|
previewDart2: argResults['preview-dart-2'],
|
||||||
).analyze();
|
).analyze();
|
||||||
} else {
|
} else {
|
||||||
return new AnalyzeOnce(
|
return new AnalyzeOnce(
|
||||||
argResults,
|
argResults,
|
||||||
|
runner.getRepoRoots(),
|
||||||
runner.getRepoPackages(),
|
runner.getRepoPackages(),
|
||||||
workingDirectory: workingDirectory,
|
workingDirectory: workingDirectory,
|
||||||
previewDart2: argResults['preview-dart-2'],
|
previewDart2: argResults['preview-dart-2'],
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
|
|
||||||
|
@ -11,17 +10,20 @@ import '../base/common.dart';
|
||||||
import '../base/file_system.dart';
|
import '../base/file_system.dart';
|
||||||
import '../base/io.dart';
|
import '../base/io.dart';
|
||||||
import '../base/logger.dart';
|
import '../base/logger.dart';
|
||||||
import '../base/process_manager.dart';
|
|
||||||
import '../base/terminal.dart';
|
import '../base/terminal.dart';
|
||||||
import '../base/utils.dart';
|
import '../base/utils.dart';
|
||||||
import '../cache.dart';
|
import '../cache.dart';
|
||||||
|
import '../dart/analysis.dart';
|
||||||
import '../dart/sdk.dart' as sdk;
|
import '../dart/sdk.dart' as sdk;
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import 'analyze_base.dart';
|
import 'analyze_base.dart';
|
||||||
|
|
||||||
class AnalyzeContinuously extends AnalyzeBase {
|
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 List<Directory> repoPackages;
|
||||||
final bool previewDart2;
|
final bool previewDart2;
|
||||||
|
|
||||||
|
@ -43,11 +45,14 @@ class AnalyzeContinuously extends AnalyzeBase {
|
||||||
if (argResults['flutter-repo']) {
|
if (argResults['flutter-repo']) {
|
||||||
final PackageDependencyTracker dependencies = new PackageDependencyTracker();
|
final PackageDependencyTracker dependencies = new PackageDependencyTracker();
|
||||||
dependencies.checkForConflictingDependencies(repoPackages, dependencies);
|
dependencies.checkForConflictingDependencies(repoPackages, dependencies);
|
||||||
directories = repoPackages.map((Directory dir) => dir.path).toList();
|
|
||||||
|
directories = repoRoots;
|
||||||
analysisTarget = 'Flutter repository';
|
analysisTarget = 'Flutter repository';
|
||||||
|
|
||||||
printTrace('Analyzing Flutter repository:');
|
printTrace('Analyzing Flutter repository:');
|
||||||
for (String projectPath in directories)
|
for (String projectPath in repoRoots) {
|
||||||
printTrace(' ${fs.path.relative(projectPath)}');
|
printTrace(' ${fs.path.relative(projectPath)}');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
directories = <String>[fs.currentDirectory.path];
|
directories = <String>[fs.currentDirectory.path];
|
||||||
analysisTarget = fs.currentDirectory.path;
|
analysisTarget = fs.currentDirectory.path;
|
||||||
|
@ -107,10 +112,14 @@ class AnalyzeContinuously extends AnalyzeBase {
|
||||||
// Print an analysis summary.
|
// Print an analysis summary.
|
||||||
String errorsMessage;
|
String errorsMessage;
|
||||||
|
|
||||||
final int issueCount = errors.length;
|
int issueCount = errors.length;
|
||||||
final int issueDiff = issueCount - lastErrorCount;
|
final int issueDiff = issueCount - lastErrorCount;
|
||||||
lastErrorCount = issueCount;
|
lastErrorCount = issueCount;
|
||||||
|
|
||||||
|
final int undocumentedCount = errors.where((AnalysisError issue) {
|
||||||
|
return issue.code == 'public_member_api_docs';
|
||||||
|
}).length;
|
||||||
|
|
||||||
if (firstAnalysis)
|
if (firstAnalysis)
|
||||||
errorsMessage = '$issueCount ${pluralize('issue', issueCount)} found';
|
errorsMessage = '$issueCount ${pluralize('issue', issueCount)} found';
|
||||||
else if (issueDiff > 0)
|
else if (issueDiff > 0)
|
||||||
|
@ -124,10 +133,13 @@ class AnalyzeContinuously extends AnalyzeBase {
|
||||||
|
|
||||||
final String files = '${analyzedPaths.length} ${pluralize('file', analyzedPaths.length)}';
|
final String files = '${analyzedPaths.length} ${pluralize('file', analyzedPaths.length)}';
|
||||||
final String seconds = (analysisTimer.elapsedMilliseconds / 1000.0).toStringAsFixed(2);
|
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) {
|
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); });
|
server.dispose().whenComplete(() { exit(issueCount > 0 ? 1 : 0); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,209 +147,10 @@ class AnalyzeContinuously extends AnalyzeBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
void _handleAnalysisErrors(FileAnalysisErrors fileErrors) {
|
||||||
fileErrors.errors.removeWhere(_filterError);
|
fileErrors.errors.removeWhere((AnalysisError error) => error.type == 'TODO');
|
||||||
|
|
||||||
analyzedPaths.add(fileErrors.file);
|
analyzedPaths.add(fileErrors.file);
|
||||||
analysisErrors[fileErrors.file] = fileErrors.errors;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,13 +3,12 @@
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
|
|
||||||
import '../base/common.dart';
|
import '../base/common.dart';
|
||||||
import '../base/file_system.dart';
|
import '../base/file_system.dart';
|
||||||
import '../base/process.dart';
|
import '../base/logger.dart';
|
||||||
import '../base/utils.dart';
|
import '../base/utils.dart';
|
||||||
import '../cache.dart';
|
import '../cache.dart';
|
||||||
import '../dart/analysis.dart';
|
import '../dart/analysis.dart';
|
||||||
|
@ -18,281 +17,178 @@ import '../globals.dart';
|
||||||
import 'analyze.dart';
|
import 'analyze.dart';
|
||||||
import 'analyze_base.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.
|
/// An aspect of the [AnalyzeCommand] to perform once time analysis.
|
||||||
class AnalyzeOnce extends AnalyzeBase {
|
class AnalyzeOnce extends AnalyzeBase {
|
||||||
AnalyzeOnce(ArgResults argResults, this.repoPackages, {
|
AnalyzeOnce(
|
||||||
|
ArgResults argResults,
|
||||||
|
this.repoRoots,
|
||||||
|
this.repoPackages, {
|
||||||
this.workingDirectory,
|
this.workingDirectory,
|
||||||
this.previewDart2: false,
|
this.previewDart2: false,
|
||||||
}) : super(argResults);
|
}) : super(argResults);
|
||||||
|
|
||||||
|
final List<String> repoRoots;
|
||||||
final List<Directory> repoPackages;
|
final List<Directory> repoPackages;
|
||||||
|
|
||||||
/// The working directory for testing analysis using dartanalyzer
|
/// The working directory for testing analysis using dartanalyzer.
|
||||||
final Directory workingDirectory;
|
final Directory workingDirectory;
|
||||||
|
|
||||||
final bool previewDart2;
|
final bool previewDart2;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Null> analyze() async {
|
Future<Null> analyze() async {
|
||||||
final Stopwatch stopwatch = new Stopwatch()..start();
|
final String currentDirectory =
|
||||||
final Set<Directory> pubSpecDirectories = new HashSet<Directory>();
|
(workingDirectory ?? fs.currentDirectory).path;
|
||||||
final List<File> dartFiles = <File>[];
|
|
||||||
for (String file in argResults.rest.toList()) {
|
// find directories from argResults.rest
|
||||||
file = fs.path.normalize(fs.path.absolute(file));
|
final Set<String> directories = new Set<String>.from(argResults.rest
|
||||||
final String root = fs.path.rootPrefix(file);
|
.map<String>((String path) => fs.path.canonicalize(path)));
|
||||||
dartFiles.add(fs.file(file));
|
if (directories.isNotEmpty) {
|
||||||
while (file != root) {
|
for (String directory in directories) {
|
||||||
file = fs.path.dirname(file);
|
final FileSystemEntityType type = fs.typeSync(directory);
|
||||||
if (fs.isFileSync(fs.path.join(file, 'pubspec.yaml'))) {
|
|
||||||
pubSpecDirectories.add(fs.directory(file));
|
if (type == FileSystemEntityType.notFound) {
|
||||||
break;
|
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);
|
if (argResults['flutter-repo']) {
|
||||||
final bool flutterRepo = argResults['flutter-repo'] || (workingDirectory == null && inRepo(argResults.rest));
|
// check for conflicting dependencies
|
||||||
|
final PackageDependencyTracker dependencies =
|
||||||
|
new PackageDependencyTracker();
|
||||||
|
dependencies.checkForConflictingDependencies(repoPackages, dependencies);
|
||||||
|
|
||||||
// Use dartanalyzer directly except when analyzing the Flutter repository.
|
directories.addAll(repoRoots);
|
||||||
// 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.');
|
|
||||||
|
|
||||||
final List<String> arguments = <String>[];
|
if (argResults.wasParsed('current-package') &&
|
||||||
arguments.addAll(dartFiles.map((FileSystemEntity f) => f.path));
|
argResults['current-package']) {
|
||||||
|
directories.add(currentDirectory);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// If the files being analyzed are outside of the current directory hierarchy
|
if (argResults['current-package']) {
|
||||||
// then dartanalyzer does not yet know how to find the ".packages" file.
|
directories.add(currentDirectory);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (argResults['dartdocs'] && !argResults['flutter-repo']) {
|
||||||
_collectDartFiles(dir, dartFiles);
|
throwToolExit(
|
||||||
pubSpecDirectories.add(dir);
|
'The --dartdocs option is currently only supported with --flutter-repo.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine what all the various .packages files depend on
|
if (directories.isEmpty) {
|
||||||
final PackageDependencyTracker dependencies = new PackageDependencyTracker();
|
throwToolExit('Nothing to analyze.', exitCode: 0);
|
||||||
dependencies.checkForConflictingDependencies(pubSpecDirectories, dependencies);
|
}
|
||||||
final Map<String, String> packages = dependencies.asPackageMap();
|
|
||||||
|
// 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();
|
Cache.releaseLockEarly();
|
||||||
|
|
||||||
if (argResults['preamble']) {
|
// collect results
|
||||||
if (dartFiles.length == 1) {
|
final Stopwatch timer = new Stopwatch()..start();
|
||||||
logger.printStatus('Analyzing ${fs.path.relative(dartFiles.first.path)}...');
|
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 {
|
} 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 (argResults['congratulate']) {
|
||||||
if (membersMissingDocumentation > 0) {
|
if (undocumentedMembers > 0) {
|
||||||
printStatus('No analyzer warnings! (ran in ${elapsed}s; $membersMissingDocumentation public ${ membersMissingDocumentation == 1 ? "member lacks" : "members lack" } documentation)');
|
printStatus('No issues found! (ran in ${seconds}s; '
|
||||||
|
'$undocumentedMembers public ${ undocumentedMembers ==
|
||||||
|
1 ? "member lacks" : "members lack" } documentation)');
|
||||||
} else {
|
} 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,269 +2,220 @@
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:collection';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
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 '../base/file_system.dart' hide IOSink;
|
import '../base/file_system.dart' hide IOSink;
|
||||||
|
import '../base/file_system.dart';
|
||||||
import '../base/io.dart';
|
import '../base/io.dart';
|
||||||
|
import '../base/platform.dart';
|
||||||
|
import '../base/process_manager.dart';
|
||||||
|
import '../globals.dart';
|
||||||
|
|
||||||
class AnalysisDriver {
|
class AnalysisServer {
|
||||||
AnalysisDriver(this.options) {
|
AnalysisServer(this.sdkPath, this.directories, {this.previewDart2: false});
|
||||||
AnalysisEngine.instance.logger =
|
|
||||||
new _StdLogger(outSink: options.outSink, errorSink: options.errorSink);
|
|
||||||
_processPlugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
final Set<Source> _analyzedSources = new HashSet<Source>();
|
final String sdkPath;
|
||||||
|
final List<String> directories;
|
||||||
|
final bool previewDart2;
|
||||||
|
|
||||||
AnalysisOptionsProvider analysisOptionsProvider =
|
Process _process;
|
||||||
new AnalysisOptionsProvider();
|
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;
|
if (previewDart2) {
|
||||||
|
command.add('--preview-dart-2');
|
||||||
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));
|
|
||||||
} else {
|
} else {
|
||||||
// Fall back to a standard SDK if no embedder is found.
|
command.add('--no-preview-dart-2');
|
||||||
final FolderBasedDartSdk sdk = new FolderBasedDartSdk(resourceProvider,
|
|
||||||
PhysicalResourceProvider.INSTANCE.getFolder(sdkDir));
|
|
||||||
sdk.analysisOptions = context.analysisOptions;
|
|
||||||
|
|
||||||
resolvers.add(new DartUriResolver(sdk));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.packageRootPath != null) {
|
printTrace('dart ${command.skip(1).join(' ')}');
|
||||||
final ContextBuilderOptions builderOptions = new ContextBuilderOptions();
|
_process = await processManager.start(command);
|
||||||
builderOptions.defaultPackagesDirectoryPath = options.packageRootPath;
|
// This callback hookup can't throw.
|
||||||
final ContextBuilder builder = new ContextBuilder(resourceProvider, null, null,
|
_process.exitCode
|
||||||
options: builderOptions);
|
.whenComplete(() => _process = null); // ignore: unawaited_futures
|
||||||
final PackageMapUriResolver packageUriResolver = new PackageMapUriResolver(resourceProvider,
|
|
||||||
builder.convertPackagesToMap(builder.createPackageMap('')));
|
|
||||||
|
|
||||||
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));
|
final Stream<String> inStream =
|
||||||
return resolvers;
|
_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) {
|
Stream<bool> get onAnalyzing => _analyzingController.stream;
|
||||||
final ErrorProcessor processor = ErrorProcessor.getProcessor(context.analysisOptions, error);
|
Stream<FileAnalysisErrors> get onErrors => _errorsController.stream;
|
||||||
// Filtered errors are processed to a severity of null.
|
|
||||||
return processor != null && processor.severity == null;
|
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() {
|
void _handleServerResponse(String line) {
|
||||||
final String optionsPath = options.analysisOptionsFile;
|
printTrace('<== $line');
|
||||||
if (optionsPath != null) {
|
|
||||||
final file_system.File file =
|
final dynamic response = json.decode(line);
|
||||||
PhysicalResourceProvider.INSTANCE.getFile(optionsPath);
|
|
||||||
final Map<Object, Object> optionMap =
|
if (response is Map<dynamic, dynamic>) {
|
||||||
analysisOptionsProvider.getOptionsFromFile(file);
|
if (response['event'] != null) {
|
||||||
if (optionMap != null)
|
final String event = response['event'];
|
||||||
applyToAnalysisOptions(options, optionMap);
|
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() {
|
void _handleStatus(Map<String, dynamic> statusInfo) {
|
||||||
final List<Plugin> plugins = <Plugin>[];
|
// {"event":"server.status","params":{"analysis":{"isAnalyzing":true}}}
|
||||||
plugins.addAll(AnalysisEngine.instance.requiredPlugins);
|
if (statusInfo['analysis'] != null && !_analyzingController.isClosed) {
|
||||||
final ExtensionManager manager = new ExtensionManager();
|
final bool isAnalyzing = statusInfo['analysis']['isAnalyzing'];
|
||||||
manager.processPlugins(plugins);
|
_analyzingController.add(isAnalyzing);
|
||||||
linter.registerLintRules();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
class AnalysisError implements Comparable<AnalysisError> {
|
||||||
AnalysisDriverException([this.message]);
|
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
|
@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 {
|
if (offset != other.offset)
|
||||||
AnalysisErrorDescription(this.error, this.line);
|
return offset - other.offset;
|
||||||
|
|
||||||
static Directory cwd = fs.currentDirectory.absolute;
|
final int diff = other.severityLevel - severityLevel;
|
||||||
|
if (diff != 0)
|
||||||
|
return diff;
|
||||||
|
|
||||||
final AnalysisError error;
|
return message.compareTo(other.message);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@override
|
||||||
void logError(String message, [Exception exception]) =>
|
String toString() {
|
||||||
errorSink.writeln(message);
|
return '${severity.toLowerCase().padLeft(7)} $_separator '
|
||||||
|
'$messageSentenceFragment $_separator '
|
||||||
|
'${fs.path.relative(file)}:$startLine:$startColumn';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
String toLegacyString() {
|
||||||
void logInformation(String message, [Exception exception]) {
|
return '[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)';
|
||||||
// TODO(pq): remove once addressed in analyzer (http://dartbug.com/28285)
|
|
||||||
if (message != 'No definition of type FutureOr')
|
|
||||||
outSink.writeln(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FileAnalysisErrors {
|
||||||
|
FileAnalysisErrors(this.file, this.errors);
|
||||||
|
|
||||||
|
final String file;
|
||||||
|
final List<AnalysisError> errors;
|
||||||
|
}
|
||||||
|
|
|
@ -383,12 +383,19 @@ class FlutterCommandRunner extends CommandRunner<Null> {
|
||||||
Cache.flutterRoot ??= _defaultFlutterRoot;
|
Cache.flutterRoot ??= _defaultFlutterRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all pub packages in the Flutter repo.
|
/// Get the root directories of the repo - the directories containing Dart packages.
|
||||||
List<Directory> getRepoPackages() {
|
List<String> getRepoRoots() {
|
||||||
final String root = fs.path.absolute(Cache.flutterRoot);
|
final String root = fs.path.absolute(Cache.flutterRoot);
|
||||||
// not bin, and not the root
|
// not bin, and not the root
|
||||||
return <String>['dev', 'examples', 'packages']
|
return <String>['dev', 'examples', 'packages'].map((String item) {
|
||||||
.expand<String>((String path) => _gatherProjectPaths(fs.path.join(root, path)))
|
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))
|
.map((String dir) => fs.directory(dir))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/os.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/pub.dart';
|
||||||
import 'package:flutter_tools/src/dart/sdk.dart';
|
import 'package:flutter_tools/src/dart/sdk.dart';
|
||||||
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
|
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
|
||||||
|
|
|
@ -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!'));
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -17,7 +17,6 @@ import '../src/common.dart';
|
||||||
import '../src/context.dart';
|
import '../src/context.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
||||||
final String analyzerSeparator = platform.isWindows ? '-' : '•';
|
final String analyzerSeparator = platform.isWindows ? '-' : '•';
|
||||||
|
|
||||||
group('analyze once', () {
|
group('analyze once', () {
|
||||||
|
@ -55,7 +54,7 @@ void main() {
|
||||||
}, timeout: allowForRemotePubInvocation);
|
}, timeout: allowForRemotePubInvocation);
|
||||||
|
|
||||||
// Analyze in the current directory - no arguments
|
// Analyze in the current directory - no arguments
|
||||||
testUsingContext('flutter analyze working directory', () async {
|
testUsingContext('working directory', () async {
|
||||||
await runCommand(
|
await runCommand(
|
||||||
command: new AnalyzeCommand(workingDirectory: fs.directory(projectPath)),
|
command: new AnalyzeCommand(workingDirectory: fs.directory(projectPath)),
|
||||||
arguments: <String>['analyze'],
|
arguments: <String>['analyze'],
|
||||||
|
@ -64,17 +63,17 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Analyze a specific file outside the current directory
|
// Analyze a specific file outside the current directory
|
||||||
testUsingContext('flutter analyze one file', () async {
|
testUsingContext('passing one file throws', () async {
|
||||||
await runCommand(
|
await runCommand(
|
||||||
command: new AnalyzeCommand(),
|
command: new AnalyzeCommand(),
|
||||||
arguments: <String>['analyze', libMain.path],
|
arguments: <String>['analyze', libMain.path],
|
||||||
statusTextContains: <String>['No issues found!'],
|
toolExit: true,
|
||||||
|
exitMessageContains: 'is not a directory',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Analyze in the current directory - no arguments
|
// 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
|
// 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
|
// that is upgraded to a warning in package:flutter/analysis_options_user.yaml
|
||||||
// to assert that we are using the default Flutter analysis options.
|
// to assert that we are using the default Flutter analysis options.
|
||||||
|
@ -98,22 +97,7 @@ void main() {
|
||||||
statusTextContains: <String>[
|
statusTextContains: <String>[
|
||||||
'Analyzing',
|
'Analyzing',
|
||||||
'warning $analyzerSeparator The parameter \'onPressed\' is required',
|
'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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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',
|
|
||||||
'2 issues found.',
|
'2 issues found.',
|
||||||
],
|
],
|
||||||
toolExit: true,
|
toolExit: true,
|
||||||
|
@ -121,8 +105,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Analyze in the current directory - no arguments
|
// 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
|
// Insert an analysis_options.yaml file in the project
|
||||||
// which will trigger a lint for broken code that was inserted earlier
|
// which will trigger a lint for broken code that was inserted earlier
|
||||||
final File optionsFile = fs.file(fs.path.join(projectPath, 'analysis_options.yaml'));
|
final File optionsFile = fs.file(fs.path.join(projectPath, 'analysis_options.yaml'));
|
||||||
|
@ -140,15 +123,15 @@ void main() {
|
||||||
statusTextContains: <String>[
|
statusTextContains: <String>[
|
||||||
'Analyzing',
|
'Analyzing',
|
||||||
'warning $analyzerSeparator The parameter \'onPressed\' is required',
|
'warning $analyzerSeparator The parameter \'onPressed\' is required',
|
||||||
'hint $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
|
'info $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
|
||||||
'lint $analyzerSeparator Only throw instances of classes extending either Exception or Error',
|
'info $analyzerSeparator Only throw instances of classes extending either Exception or Error',
|
||||||
'3 issues found.',
|
'3 issues found.',
|
||||||
],
|
],
|
||||||
toolExit: true,
|
toolExit: true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('flutter analyze no duplicate issues', () async {
|
testUsingContext('no duplicate issues', () async {
|
||||||
final Directory tempDir = fs.systemTempDirectory.createTempSync('analyze_once_test_').absolute;
|
final Directory tempDir = fs.systemTempDirectory.createTempSync('analyze_once_test_').absolute;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -182,22 +165,6 @@ void bar() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 {
|
testUsingContext('--preview-dart-2', () async {
|
||||||
const String contents = '''
|
const String contents = '''
|
||||||
StringBuffer bar = StringBuffer('baz');
|
StringBuffer bar = StringBuffer('baz');
|
||||||
|
@ -255,18 +222,23 @@ Future<Null> runCommand({
|
||||||
List<String> statusTextContains,
|
List<String> statusTextContains,
|
||||||
List<String> errorTextContains,
|
List<String> errorTextContains,
|
||||||
bool toolExit: false,
|
bool toolExit: false,
|
||||||
|
String exitMessageContains,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
arguments.insert(0, '--flutter-root=${Cache.flutterRoot}');
|
arguments.insert(0, '--flutter-root=${Cache.flutterRoot}');
|
||||||
await createTestCommandRunner(command).run(arguments);
|
await createTestCommandRunner(command).run(arguments);
|
||||||
expect(toolExit, isFalse, reason: 'Expected ToolExit exception');
|
expect(toolExit, isFalse, reason: 'Expected ToolExit exception');
|
||||||
} on ToolExit {
|
} on ToolExit catch (e) {
|
||||||
if (!toolExit) {
|
if (!toolExit) {
|
||||||
testLogger.clear();
|
testLogger.clear();
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
if (exitMessageContains != null) {
|
||||||
|
expect(e.message, contains(exitMessageContains));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assertContains(testLogger.statusText, statusTextContains);
|
assertContains(testLogger.statusText, statusTextContains);
|
||||||
assertContains(testLogger.errorText, errorTextContains);
|
assertContains(testLogger.errorText, errorTextContains);
|
||||||
|
|
||||||
testLogger.clear();
|
testLogger.clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -436,14 +436,13 @@ Future<Null> _createAndAnalyzeProject(
|
||||||
{ List<String> unexpectedPaths = const <String>[], bool plugin = false }) async {
|
{ List<String> unexpectedPaths = const <String>[], bool plugin = false }) async {
|
||||||
await _createProject(dir, createArgs, expectedPaths, unexpectedPaths: unexpectedPaths, plugin: plugin);
|
await _createProject(dir, createArgs, expectedPaths, unexpectedPaths: unexpectedPaths, plugin: plugin);
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
await _analyzeProject(dir.path, target: fs.path.join(dir.path, 'lib', 'flutter_project.dart'));
|
await _analyzeProject(dir.path);
|
||||||
await _analyzeProject(fs.path.join(dir.path, 'example'));
|
|
||||||
} else {
|
} else {
|
||||||
await _analyzeProject(dir.path);
|
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(
|
final String flutterToolsPath = fs.path.absolute(fs.path.join(
|
||||||
'bin',
|
'bin',
|
||||||
'flutter_tools.dart',
|
'flutter_tools.dart',
|
||||||
|
@ -453,8 +452,6 @@ Future<Null> _analyzeProject(String workingDir, {String target}) async {
|
||||||
..addAll(dartVmFlags)
|
..addAll(dartVmFlags)
|
||||||
..add(flutterToolsPath)
|
..add(flutterToolsPath)
|
||||||
..add('analyze');
|
..add('analyze');
|
||||||
if (target != null)
|
|
||||||
args.add(target);
|
|
||||||
|
|
||||||
final ProcessResult exec = await Process.run(
|
final ProcessResult exec = await Process.run(
|
||||||
'$dartSdkPath/bin/dart',
|
'$dartSdkPath/bin/dart',
|
||||||
|
|
Loading…
Reference in a new issue