| // 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 'common/core.dart'; |
| import 'common/output_utils.dart'; |
| import 'common/package_looping_command.dart'; |
| import 'common/pub_utils.dart'; |
| import 'common/repository_package.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; |
| const int _exitSwiftFormatFailed = 8; |
| const int _exitKotlinFormatFailed = 9; |
| const int _exitSwiftLintFoundIssues = 10; |
| |
| final Uri _javaFormatterUrl = Uri.https('github.com', |
| '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); |
| final Uri _kotlinFormatterUrl = Uri.https('maven.org', |
| '/maven2/com/facebook/ktfmt/0.46/ktfmt-0.46-jar-with-dependencies.jar'); |
| |
| /// A command to format all package code. |
| class FormatCommand extends PackageLoopingCommand { |
| /// Creates an instance of the format command. |
| FormatCommand( |
| super.packagesDir, { |
| super.processRunner, |
| super.platform, |
| }) { |
| argParser.addFlag(_failonChangeArg, hide: true); |
| argParser.addFlag(_dartArg, help: 'Format Dart files', defaultsTo: true); |
| argParser.addFlag(_clangFormatArg, |
| help: 'Format with "clang-format"', defaultsTo: true); |
| argParser.addFlag(_kotlinArg, |
| help: 'Format Kotlin files', defaultsTo: true); |
| argParser.addFlag(_javaArg, help: 'Format Java files', defaultsTo: true); |
| argParser.addFlag(_swiftArg, |
| help: 'Format and lint Swift files', defaultsTo: true); |
| argParser.addOption(_clangFormatPathArg, |
| defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.'); |
| argParser.addOption(_javaPathArg, |
| defaultsTo: 'java', help: 'Path to "java" executable.'); |
| argParser.addOption(_swiftFormatPathArg, |
| defaultsTo: 'swift-format', help: 'Path to "swift-format" executable.'); |
| } |
| |
| static const String _dartArg = 'dart'; |
| static const String _clangFormatArg = 'clang-format'; |
| static const String _failonChangeArg = 'fail-on-change'; |
| static const String _kotlinArg = 'kotlin'; |
| static const String _javaArg = 'java'; |
| static const String _swiftArg = 'swift'; |
| static const String _clangFormatPathArg = 'clang-format-path'; |
| static const String _javaPathArg = 'java-path'; |
| static const String _swiftFormatPathArg = 'swift-format-path'; |
| |
| @override |
| final String name = 'format'; |
| |
| @override |
| final String description = |
| 'Formats the code of all packages (C++, Dart, Java, Kotlin, Objective-C, ' |
| 'and optionally Swift).\n\n' |
| 'This command requires "git", "flutter", "java", and "clang-format" v5 ' |
| 'to be in your path.'; |
| |
| @override |
| Future<void> initializeRun() async { |
| final String javaFormatterPath = await _getJavaFormatterPath(); |
| final String kotlinFormatterPath = await _getKotlinFormatterPath(); |
| |
| // All but Dart is formatted here rather than in runForPackage because |
| // running the formatters separately for each package is an order of |
| // magnitude slower, due to the startup overhead of the formatters. |
| // |
| // Dart has to be run per-package because the formatter can have different |
| // behavior based on the package's SDK, which can't be determined if the |
| // formatter isn't running in the context of the package. |
| final Iterable<String> files = |
| await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir); |
| if (getBoolArg(_javaArg)) { |
| await _formatJava(files, javaFormatterPath); |
| } |
| if (getBoolArg(_kotlinArg)) { |
| await _formatKotlin(files, kotlinFormatterPath); |
| } |
| if (getBoolArg(_clangFormatArg)) { |
| await _formatCppAndObjectiveC(files); |
| } |
| if (getBoolArg(_swiftArg)) { |
| await _formatAndLintSwift(files); |
| } |
| } |
| |
| @override |
| Future<PackageResult> runForPackage(RepositoryPackage package) async { |
| final Iterable<String> files = await _getFilteredFilePaths( |
| getFilesForPackage(package), |
| relativeTo: package.directory, |
| ); |
| if (getBoolArg(_dartArg)) { |
| // Ensure that .dart_tool exists, since without it `dart` doesn't know |
| // the lanugage version, so the formatter may give different output. |
| if (!package.directory.childDirectory('.dart_tool').existsSync()) { |
| if (!await runPubGet(package, processRunner, super.platform)) { |
| printError('Unable to fetch dependencies.'); |
| return PackageResult.fail(<String>['unable to fetch dependencies']); |
| } |
| } |
| |
| await _formatDart(files, workingDir: package.directory); |
| } |
| // Success or failure is determined overall in completeRun, since most code |
| // isn't being validated per-package, so just always return success at the |
| // package level. |
| // TODO(stuartmorgan): Consider doing _didModifyAnything checks per-package |
| // instead, since the other languages are already formatted by the time |
| // this code is being run. |
| return PackageResult.success(); |
| } |
| |
| @override |
| Future<void> completeRun() async { |
| if (getBoolArg(_failonChangeArg)) { |
| 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', |
| packagesDir.path, |
| thirdPartyPackagesDir.path |
| ], |
| workingDir: packagesDir.parent, |
| 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 the repository tooling `format` command: ' |
| 'https://github.com/flutter/packages/blob/main/script/tool/README.md#format-code\n' |
| 'or copy-paste this command into your terminal:'); |
| |
| final io.ProcessResult diff = await processRunner.run( |
| 'git', |
| <String>['diff', packagesDir.path, thirdPartyPackagesDir.path], |
| workingDir: packagesDir.parent, |
| 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<void> _formatAndLintSwift(Iterable<String> files) async { |
| final Iterable<String> swiftFiles = |
| _getPathsWithExtensions(files, <String>{'.swift'}); |
| if (swiftFiles.isNotEmpty) { |
| final String swiftFormat = await _findValidSwiftFormat(); |
| print('Formatting .swift files...'); |
| final int formatExitCode = |
| await _runBatched(swiftFormat, <String>['-i'], files: swiftFiles); |
| if (formatExitCode != 0) { |
| printError('Failed to format Swift files: exit code $formatExitCode.'); |
| throw ToolExit(_exitSwiftFormatFailed); |
| } |
| |
| print('Linting .swift files...'); |
| final int lintExitCode = await _runBatched( |
| swiftFormat, |
| <String>[ |
| 'lint', |
| '--parallel', |
| '--strict', |
| ], |
| files: swiftFiles); |
| if (lintExitCode == 1) { |
| printError('Swift linter found issues. See above for linter output.'); |
| throw ToolExit(_exitSwiftLintFoundIssues); |
| } else if (lintExitCode != 0) { |
| printError('Failed to lint Swift files: exit code $lintExitCode.'); |
| throw ToolExit(_exitSwiftFormatFailed); |
| } |
| } |
| } |
| |
| Future<String> _findValidClangFormat() async { |
| final String clangFormat = getStringArg(_clangFormatPathArg); |
| if (await _hasDependency(clangFormat)) { |
| return clangFormat; |
| } |
| |
| // 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 --$_clangFormatPathArg.'); |
| throw ToolExit(_exitDependencyMissing); |
| } |
| |
| Future<String> _findValidSwiftFormat() async { |
| final String swiftFormat = getStringArg(_swiftFormatPathArg); |
| if (await _hasDependency(swiftFormat)) { |
| return swiftFormat; |
| } |
| |
| printError('Unable to run "swift-format". Make sure that it is in your ' |
| 'path, or provide a full path with --$_swiftFormatPathArg.'); |
| throw ToolExit(_exitDependencyMissing); |
| } |
| |
| Future<void> _formatJava(Iterable<String> files, String formatterPath) async { |
| final Iterable<String> javaFiles = |
| _getPathsWithExtensions(files, <String>{'.java'}); |
| if (javaFiles.isNotEmpty) { |
| final String java = getStringArg(_javaPathArg); |
| if (!await _hasDependency(java)) { |
| printError( |
| 'Unable to run "java". Make sure that it is in your path, or ' |
| 'provide a full path with --$_javaPathArg.'); |
| throw ToolExit(_exitDependencyMissing); |
| } |
| |
| print('Formatting .java files...'); |
| final int exitCode = await _runBatched( |
| java, <String>['-jar', formatterPath, '--replace'], |
| files: javaFiles); |
| if (exitCode != 0) { |
| printError('Failed to format Java files: exit code $exitCode.'); |
| throw ToolExit(_exitJavaFormatFailed); |
| } |
| } |
| } |
| |
| Future<void> _formatKotlin( |
| Iterable<String> files, String formatterPath) async { |
| final Iterable<String> kotlinFiles = |
| _getPathsWithExtensions(files, <String>{'.kt'}); |
| if (kotlinFiles.isNotEmpty) { |
| final String java = getStringArg(_javaPathArg); |
| if (!await _hasDependency(java)) { |
| printError( |
| 'Unable to run "java". Make sure that it is in your path, or ' |
| 'provide a full path with --$_javaPathArg.'); |
| throw ToolExit(_exitDependencyMissing); |
| } |
| |
| print('Formatting .kt files...'); |
| final int exitCode = await _runBatched( |
| java, <String>['-jar', formatterPath], |
| files: kotlinFiles); |
| if (exitCode != 0) { |
| printError('Failed to format Kotlin files: exit code $exitCode.'); |
| throw ToolExit(_exitKotlinFormatFailed); |
| } |
| } |
| } |
| |
| Future<void> _formatDart( |
| Iterable<String> files, { |
| Directory? workingDir, |
| }) 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, workingDir: workingDir); |
| 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'])) && |
| // See https://github.com/flutter/flutter/issues/144039 |
| !path.endsWith('GeneratedPluginRegistrant.swift') && |
| // 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> _getJavaFormatterPath() 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(_javaFormatterUrl); |
| javaFormatterFile.writeAsBytesSync(response.bodyBytes); |
| } |
| |
| return javaFormatterPath; |
| } |
| |
| Future<String> _getKotlinFormatterPath() async { |
| final String kotlinFormatterPath = path.join( |
| path.dirname(path.fromUri(platform.script)), |
| 'ktfmt-0.46-jar-with-dependencies.jar'); |
| final File kotlinFormatterFile = |
| packagesDir.fileSystem.file(kotlinFormatterPath); |
| |
| if (!kotlinFormatterFile.existsSync()) { |
| print('Downloading ktfmt...'); |
| final http.Response response = await http.get(_kotlinFormatterUrl); |
| kotlinFormatterFile.writeAsBytesSync(response.bodyBytes); |
| } |
| |
| return kotlinFormatterPath; |
| } |
| |
| /// 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, Directory? workingDir}) 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: 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; |
| } |
| } |