Unverified Commit 21861423 authored by Gary Qian's avatar Gary Qian Committed by GitHub

[flutter_tools] analyze --suggestions --machine command (#112217)

parent 1a93ed3a
......@@ -144,7 +144,14 @@ List<FlutterCommand> generateCommands({
terminal: globals.terminal,
artifacts: globals.artifacts!,
// new ProjectValidators should be added here for the --suggestions to run
allProjectValidators: <ProjectValidator>[GeneralInfoProjectValidator()],
allProjectValidators: <ProjectValidator>[
GeneralInfoProjectValidator(),
VariableDumpMachineProjectValidator(
logger: globals.logger,
fileSystem: globals.fs,
platform: globals.platform,
),
],
),
AssembleCommand(verboseHelp: verboseHelp, buildSystem: globals.buildSystem),
AttachCommand(verboseHelp: verboseHelp),
......
......@@ -66,6 +66,12 @@ class AnalyzeCommand extends FlutterCommand {
argParser.addFlag('suggestions',
help: 'Show suggestions about the current flutter project.'
);
argParser.addFlag('machine',
negatable: false,
help: 'Dumps a JSON with a subset of relevant data about the tool, project, '
'and environment.',
hide: !verboseHelp,
);
// Hidden option to enable a benchmarking mode.
argParser.addFlag('benchmark',
......@@ -128,12 +134,18 @@ class AnalyzeCommand extends FlutterCommand {
return false;
}
// Don't run pub if asking for machine output.
if (boolArg('machine') != null && boolArg('machine')!) {
return false;
}
return super.shouldRunPub;
}
@override
Future<FlutterCommandResult> runCommand() async {
final bool? suggestionFlag = boolArg('suggestions');
final bool machineFlag = boolArg('machine') ?? false;
if (suggestionFlag != null && suggestionFlag == true) {
final String directoryPath;
final bool? watchFlag = boolArg('watch');
......@@ -159,6 +171,7 @@ class AnalyzeCommand extends FlutterCommand {
allProjectValidators: _allProjectValidators,
userPath: directoryPath,
processManager: _processManager,
machine: machineFlag,
).run();
} else if (boolArgDeprecated('watch')) {
await AnalyzeContinuously(
......
......@@ -19,11 +19,13 @@ class ValidateProject {
required this.userPath,
required this.processManager,
this.verbose = false,
this.machine = false,
});
final FileSystem fileSystem;
final Logger logger;
final bool verbose;
final bool machine;
final String userPath;
final List<ProjectValidator> allProjectValidators;
final ProcessManager processManager;
......@@ -36,6 +38,9 @@ class ValidateProject {
bool hasCrash = false;
for (final ProjectValidator validator in allProjectValidators) {
if (validator.machineOutput != machine) {
continue;
}
if (!results.containsKey(validator) && validator.supportsProject(project)) {
results[validator] = validator.start(project).catchError((Object exception, StackTrace trace) {
hasCrash = true;
......@@ -45,15 +50,30 @@ class ValidateProject {
}
final StringBuffer buffer = StringBuffer();
final List<String> resultsString = <String>[];
for (final ProjectValidator validator in results.keys) {
if (results[validator] != null) {
resultsString.add(validator.title);
addResultString(validator.title, await results[validator], resultsString);
if (machine) {
// Print properties
buffer.write('{\n');
for (final Future<List<ProjectValidatorResult>> resultListFuture in results.values) {
final List<ProjectValidatorResult> resultList = await resultListFuture;
int count = 0;
for (final ProjectValidatorResult result in resultList) {
count++;
buffer.write(' "${result.name}": ${result.value}${count < resultList.length ? ',' : ''}\n');
}
}
buffer.write('}');
logger.printStatus(buffer.toString());
} else {
final List<String> resultsString = <String>[];
for (final ProjectValidator validator in results.keys) {
if (results[validator] != null) {
resultsString.add(validator.title);
addResultString(validator.title, await results[validator], resultsString);
}
}
buffer.writeAll(resultsString, '\n');
logger.printBox(buffer.toString());
}
buffer.writeAll(resultsString, '\n');
logger.printBox(buffer.toString());
if (hasCrash) {
return const FlutterCommandResult(ExitStatus.fail);
......
......@@ -6,21 +6,199 @@ import 'dart:collection';
import 'package:process/process.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'base/platform.dart';
import 'cache.dart';
import 'convert.dart';
import 'dart_pub_json_formatter.dart';
import 'flutter_manifest.dart';
import 'project.dart';
import 'project_validator_result.dart';
import 'version.dart';
abstract class ProjectValidator {
const ProjectValidator();
String get title;
bool get machineOutput => false;
bool supportsProject(FlutterProject project);
/// Can return more than one result in case a file/command have a lot of info to share to the user
Future<List<ProjectValidatorResult>> start(FlutterProject project);
}
abstract class MachineProjectValidator extends ProjectValidator {
const MachineProjectValidator();
@override
bool get machineOutput => true;
}
/// Validator run for all platforms that extract information from the pubspec.yaml.
///
/// Specific info from different platforms should be written in their own ProjectValidator.
class VariableDumpMachineProjectValidator extends MachineProjectValidator {
VariableDumpMachineProjectValidator({
required this.logger,
required this.fileSystem,
required this.platform,
});
final Logger logger;
final FileSystem fileSystem;
final Platform platform;
String _toJsonValue(Object? obj) {
String value = obj.toString();
if (obj is String) {
value = '"$obj"';
}
value = value.replaceAll(r'\', r'\\');
return value;
}
@override
Future<List<ProjectValidatorResult>> start(FlutterProject project) async {
final List<ProjectValidatorResult> result = <ProjectValidatorResult>[];
result.add(ProjectValidatorResult(
name: 'FlutterProject.directory',
value: _toJsonValue(project.directory.absolute.path),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.metadataFile',
value: _toJsonValue(project.metadataFile.absolute.path),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.android.exists',
value: _toJsonValue(project.android.existsSync()),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.ios.exists',
value: _toJsonValue(project.ios.exists),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.web.exists',
value: _toJsonValue(project.web.existsSync()),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.macos.exists',
value: _toJsonValue(project.macos.existsSync()),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.linux.exists',
value: _toJsonValue(project.linux.existsSync()),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.windows.exists',
value: _toJsonValue(project.windows.existsSync()),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.fuchsia.exists',
value: _toJsonValue(project.fuchsia.existsSync()),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.android.isKotlin',
value: _toJsonValue(project.android.isKotlin),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.ios.isSwift',
value: _toJsonValue(project.ios.isSwift),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.isModule',
value: _toJsonValue(project.isModule),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.isPlugin',
value: _toJsonValue(project.isPlugin),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'FlutterProject.manifest.appname',
value: _toJsonValue(project.manifest.appName),
status: StatusProjectValidator.info,
));
// FlutterVersion
final FlutterVersion version = FlutterVersion(workingDirectory: project.directory.absolute.path);
result.add(ProjectValidatorResult(
name: 'FlutterVersion.frameworkRevision',
value: _toJsonValue(version.frameworkRevision),
status: StatusProjectValidator.info,
));
// Platform
result.add(ProjectValidatorResult(
name: 'Platform.operatingSystem',
value: _toJsonValue(platform.operatingSystem),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'Platform.isAndroid',
value: _toJsonValue(platform.isAndroid),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'Platform.isIOS',
value: _toJsonValue(platform.isIOS),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'Platform.isWindows',
value: _toJsonValue(platform.isWindows),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'Platform.isMacOS',
value: _toJsonValue(platform.isMacOS),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'Platform.isFuchsia',
value: _toJsonValue(platform.isFuchsia),
status: StatusProjectValidator.info,
));
result.add(ProjectValidatorResult(
name: 'Platform.pathSeparator',
value: _toJsonValue(platform.pathSeparator),
status: StatusProjectValidator.info,
));
// Cache
result.add(ProjectValidatorResult(
name: 'Cache.flutterRoot',
value: _toJsonValue(Cache.flutterRoot),
status: StatusProjectValidator.info,
));
return result;
}
@override
bool supportsProject(FlutterProject project) {
// this validator will run for any type of project
return true;
}
@override
String get title => 'Machine JSON variable dump';
}
/// Validator run for all platforms that extract information from the pubspec.yaml.
///
/// Specific info from different platforms should be written in their own ProjectValidator.
......
......@@ -277,9 +277,8 @@ class FlutterCommandRunner extends CommandRunner<void> {
globals.printStatus(status);
return;
}
if (machineFlag) {
throwToolExit('The "--machine" flag is only valid with the "--version" flag.', exitCode: 2);
if (machineFlag && topLevelResults.command?.name != 'analyze') {
throwToolExit('The "--machine" flag is only valid with the "--version" flag or the "analzye --suggestions" command.', exitCode: 2);
}
await super.runCommand(topLevelResults);
},
......
......@@ -97,7 +97,7 @@ void main() {
allProjectValidators: <ProjectValidator>[
ProjectValidatorDummy(),
ProjectValidatorSecondDummy()
]
],
);
final CommandRunner<void> runner = createTestCommandRunner(command);
......@@ -128,7 +128,7 @@ void main() {
processManager: processManager,
allProjectValidators: <ProjectValidator>[
ProjectValidatorCrash(),
]
],
);
final CommandRunner<void> runner = createTestCommandRunner(command);
......@@ -148,7 +148,7 @@ void main() {
platform: platform,
terminal: terminal,
processManager: processManager,
allProjectValidators: <ProjectValidator>[]
allProjectValidators: <ProjectValidator>[],
);
final CommandRunner<void> runner = createTestCommandRunner(command);
Future<void> result () => runner.run(<String>['analyze', '--suggestions', '--watch']);
......
......@@ -25,7 +25,7 @@ void main() {
for (final Command<void> command in runner.commands.values) {
if(command.name == 'analyze') {
final AnalyzeCommand analyze = command as AnalyzeCommand;
expect(analyze.allProjectValidators().length, 1);
expect(analyze.allProjectValidators().length, 2);
}
}
}));
......
......@@ -2,15 +2,21 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/commands/analyze.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project_validator.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/test_flutter_command_runner.dart';
import 'test_utils.dart';
void main() {
late FileSystem fileSystem;
......@@ -86,4 +92,76 @@ void main() {
expect(loggerTest.statusText, contains(expected));
});
});
group('analyze --suggestions --machine command integration', () {
late Directory tempDir;
late Platform platform;
setUpAll(() async {
platform = const LocalPlatform();
tempDir = createResolvedTempDirectorySync('run_test.');
await globals.processManager.run(<String>['flutter', 'create', 'test_project'], workingDirectory: tempDir.path);
});
tearDown(() async {
tryToDelete(tempDir);
});
testUsingContext('analyze --suggesions --machine produces expected values', () async {
final ProcessResult result = await globals.processManager.run(<String>['flutter', 'analyze', '--suggestions', '--machine'], workingDirectory: tempDir.childDirectory('test_project').path);
expect(result.stdout is String, true);
expect((result.stdout as String).startsWith('{\n'), true);
expect(result.stdout, isNot(contains(',\n}'))); // No trailing commas allowed in JSON
expect((result.stdout as String).endsWith('}\n'), true);
final Map<String, dynamic> decoded = jsonDecode(result.stdout as String) as Map<String, dynamic>;
expect(decoded.containsKey('FlutterProject.android.exists'), true);
expect(decoded.containsKey('FlutterProject.ios.exists'), true);
expect(decoded.containsKey('FlutterProject.web.exists'), true);
expect(decoded.containsKey('FlutterProject.macos.exists'), true);
expect(decoded.containsKey('FlutterProject.linux.exists'), true);
expect(decoded.containsKey('FlutterProject.windows.exists'), true);
expect(decoded.containsKey('FlutterProject.fuchsia.exists'), true);
expect(decoded.containsKey('FlutterProject.android.isKotlin'), true);
expect(decoded.containsKey('FlutterProject.ios.isSwift'), true);
expect(decoded.containsKey('FlutterProject.isModule'), true);
expect(decoded.containsKey('FlutterProject.isPlugin'), true);
expect(decoded.containsKey('FlutterProject.manifest.appname'), true);
expect(decoded.containsKey('FlutterVersion.frameworkRevision'), true);
expect(decoded.containsKey('FlutterProject.directory'), true);
expect(decoded.containsKey('FlutterProject.metadataFile'), true);
expect(decoded.containsKey('Platform.operatingSystem'), true);
expect(decoded.containsKey('Platform.isAndroid'), true);
expect(decoded.containsKey('Platform.isIOS'), true);
expect(decoded.containsKey('Platform.isWindows'), true);
expect(decoded.containsKey('Platform.isMacOS'), true);
expect(decoded.containsKey('Platform.isFuchsia'), true);
expect(decoded.containsKey('Platform.pathSeparator'), true);
expect(decoded.containsKey('Cache.flutterRoot'), true);
expect(decoded['FlutterProject.android.exists'], true);
expect(decoded['FlutterProject.ios.exists'], true);
expect(decoded['FlutterProject.web.exists'], true);
expect(decoded['FlutterProject.macos.exists'], true);
expect(decoded['FlutterProject.linux.exists'], true);
expect(decoded['FlutterProject.windows.exists'], true);
expect(decoded['FlutterProject.fuchsia.exists'], false);
expect(decoded['FlutterProject.android.isKotlin'], true);
expect(decoded['FlutterProject.ios.isSwift'], true);
expect(decoded['FlutterProject.isModule'], false);
expect(decoded['FlutterProject.isPlugin'], false);
expect(decoded['FlutterProject.manifest.appname'], 'test_project');
expect(decoded['FlutterVersion.frameworkRevision'], '');
expect(decoded['Platform.isAndroid'], false);
expect(decoded['Platform.isIOS'], false);
expect(decoded['Platform.isWindows'], platform.isWindows);
expect(decoded['Platform.isMacOS'], platform.isMacOS);
expect(decoded['Platform.isFuchsia'], platform.isFuchsia);
expect(decoded['Platform.pathSeparator'], platform.pathSeparator);
}, overrides: <Type, Generator>{});
});
}
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