| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:convert'; |
| import 'dart:io' as io; |
| |
| import 'package:file/file.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:meta/meta.dart'; |
| import 'package:platform/platform.dart'; |
| |
| import 'common/core.dart'; |
| import 'common/package_command.dart'; |
| import 'common/process_runner.dart'; |
| |
| /// In theory this should be 8191, but in practice that was still resulting in |
| /// "The input line is too long" errors. This was chosen as a value that worked |
| /// in practice via trial and error, but may need to be adjusted based on |
| /// further experience. |
| @visibleForTesting |
| const int windowsCommandLineMax = 8000; |
| |
| /// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a |
| /// macOS and Linux machine. If anyone encounters a lower limit in pratice, it |
| /// can be lowered accordingly. |
| @visibleForTesting |
| const int nonWindowsCommandLineMax = 1000000; |
| |
| const int _exitClangFormatFailed = 3; |
| const int _exitFlutterFormatFailed = 4; |
| const int _exitJavaFormatFailed = 5; |
| const int _exitGitFailed = 6; |
| const int _exitDependencyMissing = 7; |
| |
| final Uri _googleFormatterUrl = Uri.https('github.com', |
| '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); |
| |
| /// A command to format all package code. |
| class FormatCommand extends PackageCommand { |
| /// Creates an instance of the format command. |
| FormatCommand( |
| Directory packagesDir, { |
| ProcessRunner processRunner = const ProcessRunner(), |
| Platform platform = const LocalPlatform(), |
| }) : super(packagesDir, processRunner: processRunner, platform: platform) { |
| argParser.addFlag('fail-on-change', hide: true); |
| argParser.addOption('clang-format', |
| defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.'); |
| argParser.addOption('java', |
| defaultsTo: 'java', help: 'Path to "java" executable.'); |
| } |
| |
| @override |
| final String name = 'format'; |
| |
| @override |
| final String description = |
| 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' |
| 'This command requires "git", "flutter" and "clang-format" v5 to be in ' |
| 'your path.'; |
| |
| @override |
| Future<void> run() async { |
| final String googleFormatterPath = await _getGoogleFormatterPath(); |
| |
| // This class is not based on PackageLoopingCommand because running the |
| // formatters separately for each package is an order of magnitude slower, |
| // due to the startup overhead of the formatters. |
| final Iterable<String> files = |
| await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir); |
| await _formatDart(files); |
| await _formatJava(files, googleFormatterPath); |
| await _formatCppAndObjectiveC(files); |
| |
| if (getBoolArg('fail-on-change')) { |
| final bool modified = await _didModifyAnything(); |
| if (modified) { |
| throw ToolExit(exitCommandFoundErrors); |
| } |
| } |
| } |
| |
| Future<bool> _didModifyAnything() async { |
| final io.ProcessResult modifiedFiles = await processRunner.run( |
| 'git', |
| <String>['ls-files', '--modified'], |
| workingDir: packagesDir, |
| logOnError: true, |
| ); |
| if (modifiedFiles.exitCode != 0) { |
| printError('Unable to determine changed files.'); |
| throw ToolExit(_exitGitFailed); |
| } |
| |
| print('\n\n'); |
| |
| final String stdout = modifiedFiles.stdout as String; |
| if (stdout.isEmpty) { |
| print('All files formatted correctly.'); |
| return false; |
| } |
| |
| print('These files are not formatted correctly (see diff below):'); |
| LineSplitter.split(stdout).map((String line) => ' $line').forEach(print); |
| |
| print('\nTo fix run "dart pub global activate flutter_plugin_tools && ' |
| 'dart pub global run flutter_plugin_tools format" or copy-paste ' |
| 'this command into your terminal:'); |
| |
| final io.ProcessResult diff = await processRunner.run( |
| 'git', |
| <String>['diff'], |
| workingDir: packagesDir, |
| logOnError: true, |
| ); |
| if (diff.exitCode != 0) { |
| printError('Unable to determine diff.'); |
| throw ToolExit(_exitGitFailed); |
| } |
| print('patch -p1 <<DONE'); |
| print(diff.stdout); |
| print('DONE'); |
| return true; |
| } |
| |
| Future<void> _formatCppAndObjectiveC(Iterable<String> files) async { |
| final Iterable<String> clangFiles = _getPathsWithExtensions( |
| files, <String>{'.h', '.m', '.mm', '.cc', '.cpp'}); |
| if (clangFiles.isNotEmpty) { |
| final String clangFormat = await _findValidClangFormat(); |
| |
| print('Formatting .cc, .cpp, .h, .m, and .mm files...'); |
| final int exitCode = await _runBatched( |
| clangFormat, <String>['-i', '--style=file'], |
| files: clangFiles); |
| if (exitCode != 0) { |
| printError( |
| 'Failed to format C, C++, and Objective-C files: exit code $exitCode.'); |
| throw ToolExit(_exitClangFormatFailed); |
| } |
| } |
| } |
| |
| Future<String> _findValidClangFormat() async { |
| final String clangFormatArg = getStringArg('clang-format'); |
| if (await _hasDependency(clangFormatArg)) { |
| return clangFormatArg; |
| } |
| |
| // There is a known issue where "chromium/depot_tools/clang-format" |
| // fails with "Problem while looking for clang-format in Chromium source tree". |
| // Loop through all "clang-format"s in PATH until a working one is found, |
| // for example "/usr/local/bin/clang-format" or a "brew" installed version. |
| for (final String clangFormatPath in await _whichAll('clang-format')) { |
| if (await _hasDependency(clangFormatPath)) { |
| return clangFormatPath; |
| } |
| } |
| printError('Unable to run "clang-format". Make sure that it is in your ' |
| 'path, or provide a full path with --clang-format.'); |
| throw ToolExit(_exitDependencyMissing); |
| } |
| |
| Future<void> _formatJava( |
| Iterable<String> files, String googleFormatterPath) async { |
| final Iterable<String> javaFiles = |
| _getPathsWithExtensions(files, <String>{'.java'}); |
| if (javaFiles.isNotEmpty) { |
| final String java = getStringArg('java'); |
| if (!await _hasDependency(java)) { |
| printError( |
| 'Unable to run "java". Make sure that it is in your path, or ' |
| 'provide a full path with --java.'); |
| throw ToolExit(_exitDependencyMissing); |
| } |
| |
| print('Formatting .java files...'); |
| final int exitCode = await _runBatched( |
| java, <String>['-jar', googleFormatterPath, '--replace'], |
| files: javaFiles); |
| if (exitCode != 0) { |
| printError('Failed to format Java files: exit code $exitCode.'); |
| throw ToolExit(_exitJavaFormatFailed); |
| } |
| } |
| } |
| |
| Future<void> _formatDart(Iterable<String> files) async { |
| final Iterable<String> dartFiles = |
| _getPathsWithExtensions(files, <String>{'.dart'}); |
| if (dartFiles.isNotEmpty) { |
| print('Formatting .dart files...'); |
| final int exitCode = |
| await _runBatched('dart', <String>['format'], files: dartFiles); |
| if (exitCode != 0) { |
| printError('Failed to format Dart files: exit code $exitCode.'); |
| throw ToolExit(_exitFlutterFormatFailed); |
| } |
| } |
| } |
| |
| /// Given a stream of [files], returns the paths of any that are not in known |
| /// locations to ignore, relative to [relativeTo]. |
| Future<Iterable<String>> _getFilteredFilePaths( |
| Stream<File> files, { |
| required Directory relativeTo, |
| }) async { |
| // Returns a pattern to check for [directories] as a subset of a file path. |
| RegExp pathFragmentForDirectories(List<String> directories) { |
| String s = path.separator; |
| // Escape the separator for use in the regex. |
| if (s == r'\') { |
| s = r'\\'; |
| } |
| return RegExp('(?:^|$s)${path.joinAll(directories)}$s'); |
| } |
| |
| final String fromPath = relativeTo.path; |
| |
| // Dart files are allowed to have a pragma to disable auto-formatting. This |
| // was added because Hixie hurts when dealing with what dartfmt does to |
| // artisanally-formatted Dart, while Stuart gets really frustrated when |
| // dealing with PRs from newer contributors who don't know how to make Dart |
| // readable. After much discussion, it was decided that files in the plugins |
| // and packages repos that really benefit from hand-formatting (e.g. files |
| // with large blobs of hex literals) could be opted-out of the requirement |
| // that they be autoformatted, so long as the code's owner was willing to |
| // bear the cost of this during code reviews. |
| // In the event that code ownership moves to someone who does not hold the |
| // same views as the original owner, the pragma can be removed and the file |
| // auto-formatted. |
| const String handFormattedExtension = '.dart'; |
| const String handFormattedPragma = '// This file is hand-formatted.'; |
| |
| return files |
| .where((File file) { |
| // See comment above near [handFormattedPragma]. |
| return path.extension(file.path) != handFormattedExtension || |
| !file.readAsLinesSync().contains(handFormattedPragma); |
| }) |
| .map((File file) => path.relative(file.path, from: fromPath)) |
| .where((String path) => |
| // Ignore files in build/ directories (e.g., headers of frameworks) |
| // to avoid useless extra work in local repositories. |
| !path.contains( |
| pathFragmentForDirectories(<String>['example', 'build'])) && |
| // Ignore files in Pods, which are not part of the repository. |
| !path.contains(pathFragmentForDirectories(<String>['Pods'])) && |
| // Ignore .dart_tool/, which can have various intermediate files. |
| !path.contains(pathFragmentForDirectories(<String>['.dart_tool']))) |
| .toList(); |
| } |
| |
| Iterable<String> _getPathsWithExtensions( |
| Iterable<String> files, Set<String> extensions) { |
| return files.where( |
| (String filePath) => extensions.contains(path.extension(filePath))); |
| } |
| |
| Future<String> _getGoogleFormatterPath() async { |
| final String javaFormatterPath = path.join( |
| path.dirname(path.fromUri(platform.script)), |
| 'google-java-format-1.3-all-deps.jar'); |
| final File javaFormatterFile = |
| packagesDir.fileSystem.file(javaFormatterPath); |
| |
| if (!javaFormatterFile.existsSync()) { |
| print('Downloading Google Java Format...'); |
| final http.Response response = await http.get(_googleFormatterUrl); |
| javaFormatterFile.writeAsBytesSync(response.bodyBytes); |
| } |
| |
| return javaFormatterPath; |
| } |
| |
| /// Returns true if [command] can be run successfully. |
| Future<bool> _hasDependency(String command) async { |
| // Some versions of Java accept both -version and --version, but some only |
| // accept -version. |
| final String versionFlag = command == 'java' ? '-version' : '--version'; |
| try { |
| final io.ProcessResult result = |
| await processRunner.run(command, <String>[versionFlag]); |
| if (result.exitCode != 0) { |
| return false; |
| } |
| } on io.ProcessException { |
| // Thrown when the binary is missing entirely. |
| return false; |
| } |
| return true; |
| } |
| |
| /// Returns all instances of [command] executable found on user path. |
| Future<List<String>> _whichAll(String command) async { |
| try { |
| final io.ProcessResult result = |
| await processRunner.run('which', <String>['-a', command]); |
| |
| if (result.exitCode != 0) { |
| return <String>[]; |
| } |
| |
| final String stdout = (result.stdout as String).trim(); |
| if (stdout.isEmpty) { |
| return <String>[]; |
| } |
| return LineSplitter.split(stdout).toList(); |
| } on io.ProcessException { |
| return <String>[]; |
| } |
| } |
| |
| /// Runs [command] on [arguments] on all of the files in [files], batched as |
| /// necessary to avoid OS command-line length limits. |
| /// |
| /// Returns the exit code of the first failure, which stops the run, or 0 |
| /// on success. |
| Future<int> _runBatched( |
| String command, |
| List<String> arguments, { |
| required Iterable<String> files, |
| }) async { |
| final int commandLineMax = |
| platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax; |
| |
| // Compute the max length of the file argument portion of a batch. |
| // Add one to each argument's length for the space before it. |
| final int argumentTotalLength = |
| arguments.fold(0, (int sum, String arg) => sum + arg.length + 1); |
| final int batchMaxTotalLength = |
| commandLineMax - command.length - argumentTotalLength; |
| |
| // Run the command in batches. |
| final List<List<String>> batches = |
| _partitionFileList(files, maxStringLength: batchMaxTotalLength); |
| for (final List<String> batch in batches) { |
| batch.sort(); // For ease of testing. |
| final int exitCode = await processRunner.runAndStream( |
| command, <String>[...arguments, ...batch], |
| workingDir: packagesDir); |
| if (exitCode != 0) { |
| return exitCode; |
| } |
| } |
| return 0; |
| } |
| |
| /// Partitions [files] into batches whose max string length as parameters to |
| /// a command (including the spaces between them, and between the list and |
| /// the command itself) is no longer than [maxStringLength]. |
| List<List<String>> _partitionFileList(Iterable<String> files, |
| {required int maxStringLength}) { |
| final List<List<String>> batches = <List<String>>[<String>[]]; |
| int currentBatchTotalLength = 0; |
| for (final String file in files) { |
| final int length = file.length + 1 /* for the space */; |
| if (currentBatchTotalLength + length > maxStringLength) { |
| // Start a new batch. |
| batches.add(<String>[]); |
| currentBatchTotalLength = 0; |
| } |
| batches.last.add(file); |
| currentBatchTotalLength += length; |
| } |
| return batches; |
| } |
| } |