blob: 7954fd044ce4e4fca9a49d6a1cf7a17c1ce3afd9 [file] [log] [blame]
// 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:platform/platform.dart';
import 'package:quiver/iterables.dart';
import 'common/core.dart';
import 'common/plugin_command.dart';
import 'common/process_runner.dart';
const int _exitClangFormatFailed = 3;
const int _exitFlutterFormatFailed = 4;
const int _exitJavaFormatFailed = 5;
const int _exitGitFailed = 6;
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 PluginCommand {
/// 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 executable of clang-format.');
}
@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());
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 "pub global activate flutter_plugin_tools && '
'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) {
print('Formatting .cc, .cpp, .h, .m, and .mm files...');
final Iterable<List<String>> batches = partition(clangFiles, 100);
int exitCode = 0;
for (final List<String> batch in batches) {
batch.sort(); // For ease of testing; partition changes the order.
exitCode = await processRunner.runAndStream(
getStringArg('clang-format'),
<String>['-i', '--style=Google', ...batch],
workingDir: packagesDir);
if (exitCode != 0) {
break;
}
}
if (exitCode != 0) {
printError(
'Failed to format C, C++, and Objective-C files: exit code $exitCode.');
throw ToolExit(_exitClangFormatFailed);
}
}
}
Future<void> _formatJava(
Iterable<String> files, String googleFormatterPath) async {
final Iterable<String> javaFiles =
_getPathsWithExtensions(files, <String>{'.java'});
if (javaFiles.isNotEmpty) {
print('Formatting .java files...');
final int exitCode = await processRunner.runAndStream('java',
<String>['-jar', googleFormatterPath, '--replace', ...javaFiles],
workingDir: packagesDir);
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...');
// `flutter format` doesn't require the project to actually be a Flutter
// project.
final int exitCode = await processRunner.runAndStream(
flutterCommand, <String>['format', ...dartFiles],
workingDir: packagesDir);
if (exitCode != 0) {
printError('Failed to format Dart files: exit code $exitCode.');
throw ToolExit(_exitFlutterFormatFailed);
}
}
}
Future<Iterable<String>> _getFilteredFilePaths(Stream<File> files) 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');
}
return files
.map((File file) => file.path)
.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;
}
}