Unverified Commit 09dec7f5 authored by Devon Carew's avatar Devon Carew Committed by GitHub

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
parent ca94bfdf
......@@ -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
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
......
......@@ -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 @@ linter:
- 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
......
......@@ -189,6 +189,8 @@ Future<Null> main() async {
}
}
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 @@ dependencies:
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 @@ dependencies:
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 ? ' - ' : ' • ';
......
......@@ -21,21 +21,24 @@ Future<Null> main() async {
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')) {
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 if (entry.startsWith('info • Document all public members •')) {
publicMembers += 1;
} else if (entry.startsWith('[')) {
} else if (entry.startsWith('info •') || entry.startsWith('warning •') || entry.startsWith('error •')) {
otherErrors += 1;
} else if (entry.startsWith('(Ran in ')) {
} else if (entry.contains(' (ran in ')) {
// ignore this line
} else {
} 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;
......
# 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
# 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:
......
# 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
# 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_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 @@ class AnalyzeCommand extends FlutterCommand {
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 @@ class AnalyzeCommand extends FlutterCommand {
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'],
......
......@@ -383,12 +383,19 @@ class FlutterCommandRunner extends CommandRunner<Null> {
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();
}
......
......@@ -6,7 +6,7 @@ import 'dart:async';
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';
......
// 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';
void main() {
final String analyzerSeparator = platform.isWindows ? '-' : '•';
group('analyze once', () {
......@@ -55,7 +54,7 @@ void main() {
}, 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 @@ void main() {
});
// 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 @@ void main() {
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 @@ void main() {
});
// 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 @@ void main() {
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 @@ 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 {
const String contents = '''
StringBuffer bar = StringBuffer('baz');
......@@ -255,18 +222,23 @@ Future<Null> runCommand({
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();
}
......@@ -436,14 +436,13 @@ Future<Null> _createAndAnalyzeProject(
{ 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 @@ Future<Null> _analyzeProject(String workingDir, {String target}) async {
..addAll(dartVmFlags)
..add(flutterToolsPath)
..add('analyze');
if (target != null)
args.add(target);
final ProcessResult exec = await Process.run(
'$dartSdkPath/bin/dart',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment