This repository was archived by the owner on Feb 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Expand file tree
/
Copy pathformat_command.dart
More file actions
336 lines (299 loc) · 12.5 KB
/
format_command.dart
File metadata and controls
336 lines (299 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// 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 in testing with flutter/plugins, 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 "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) {
final String clangFormat = getStringArg('clang-format');
if (!await _hasDependency(clangFormat)) {
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);
}
print('Formatting .cc, .cpp, .h, .m, and .mm files...');
final int exitCode = await _runBatched(
getStringArg('clang-format'), <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> _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...');
// `flutter format` doesn't require the project to actually be a Flutter
// project.
final int exitCode = await _runBatched(flutterCommand, <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;
}
/// 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;
}
}