1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:io'; |
6 | |
7 | import 'package:args/args.dart' ; |
8 | import 'package:path/path.dart' as path; |
9 | import 'package:process_runner/process_runner.dart' ; |
10 | |
11 | Future<int> main(List<String> arguments) async { |
12 | final ArgParser parser = ArgParser(); |
13 | parser.addFlag('help' , help: 'Print help.' , abbr: 'h' ); |
14 | parser.addFlag( |
15 | 'fix' , |
16 | abbr: 'f' , |
17 | help: 'Instead of just checking for formatting errors, fix them in place.' , |
18 | ); |
19 | parser.addFlag( |
20 | 'all-files' , |
21 | abbr: 'a' , |
22 | help: |
23 | 'Instead of just checking for formatting errors in changed files, ' |
24 | 'check for them in all files.' , |
25 | ); |
26 | |
27 | late final ArgResults options; |
28 | try { |
29 | options = parser.parse(arguments); |
30 | } on FormatException catch (e) { |
31 | stderr.writeln('ERROR: $e' ); |
32 | _usage(parser, exitCode: 0); |
33 | } |
34 | |
35 | if (options['help' ] as bool) { |
36 | _usage(parser, exitCode: 0); |
37 | } |
38 | |
39 | final File script = File.fromUri(Platform.script).absolute; |
40 | final Directory flutterRoot = script.parent.parent.parent.parent; |
41 | |
42 | final bool result = |
43 | (await DartFormatChecker( |
44 | flutterRoot: flutterRoot, |
45 | allFiles: options['all-files' ] as bool, |
46 | ).check(fix: options['fix' ] as bool)) == |
47 | 0; |
48 | |
49 | exit(result ? 0 : 1); |
50 | } |
51 | |
52 | void _usage(ArgParser parser, {int exitCode = 1}) { |
53 | stderr.writeln('format.dart [--help] [--fix] [--all-files]' ); |
54 | stderr.writeln(parser.usage); |
55 | exit(exitCode); |
56 | } |
57 | |
58 | class DartFormatChecker { |
59 | DartFormatChecker({required this.flutterRoot, required this.allFiles}) |
60 | : processRunner = ProcessRunner(defaultWorkingDirectory: flutterRoot); |
61 | |
62 | final Directory flutterRoot; |
63 | final bool allFiles; |
64 | final ProcessRunner processRunner; |
65 | |
66 | Future<int> check({required bool fix}) async { |
67 | final String baseGitRef = await _getDiffBaseRevision(); |
68 | final List<String> filesToCheck = await _getFileList( |
69 | types: <String>['*.dart' ], |
70 | allFiles: allFiles, |
71 | baseGitRef: baseGitRef, |
72 | ); |
73 | return _checkFormat(filesToCheck: filesToCheck, fix: fix); |
74 | } |
75 | |
76 | Future<String> _getDiffBaseRevision() async { |
77 | String upstream = 'upstream' ; |
78 | final String upstreamUrl = await _runGit( |
79 | <String>['remote' , 'get-url' , upstream], |
80 | processRunner, |
81 | failOk: true, |
82 | ); |
83 | if (upstreamUrl.isEmpty) { |
84 | upstream = 'origin' ; |
85 | } |
86 | await _runGit(<String>['fetch' , upstream, 'main' ], processRunner); |
87 | String result = '' ; |
88 | try { |
89 | // This is the preferred command to use, but developer checkouts often do |
90 | // not have a clear fork point, so we fall back to just the regular |
91 | // merge-base in that case. |
92 | result = await _runGit(<String>[ |
93 | 'merge-base' , |
94 | '--fork-point' , |
95 | 'FETCH_HEAD' , |
96 | 'HEAD' , |
97 | ], processRunner); |
98 | } on ProcessRunnerException { |
99 | result = await _runGit(<String>['merge-base' , 'FETCH_HEAD' , 'HEAD' ], processRunner); |
100 | } |
101 | return result.trim(); |
102 | } |
103 | |
104 | Future<String> _runGit( |
105 | List<String> args, |
106 | ProcessRunner processRunner, { |
107 | bool failOk = false, |
108 | }) async { |
109 | final ProcessRunnerResult result = await processRunner.runProcess(<String>[ |
110 | 'git' , |
111 | ...args, |
112 | ], failOk: failOk); |
113 | return result.stdout; |
114 | } |
115 | |
116 | Future<List<String>> _getFileList({ |
117 | required List<String> types, |
118 | required bool allFiles, |
119 | required String baseGitRef, |
120 | }) async { |
121 | String output; |
122 | if (allFiles) { |
123 | output = await _runGit(<String>['ls-files' , '--' , ...types], processRunner); |
124 | } else { |
125 | output = await _runGit(<String>[ |
126 | 'diff' , |
127 | '-U0' , |
128 | '--no-color' , |
129 | '--diff-filter=d' , |
130 | '--name-only' , |
131 | baseGitRef, |
132 | '--' , |
133 | ...types, |
134 | ], processRunner); |
135 | } |
136 | return output |
137 | .split('\n' ) |
138 | .where((String line) => line.isNotEmpty && !line.startsWith('engine' )) |
139 | .toList(); |
140 | } |
141 | |
142 | Future<int> _checkFormat({required List<String> filesToCheck, required bool fix}) async { |
143 | final List<String> cmd = <String>[ |
144 | path.join(flutterRoot.path, 'bin' , 'dart' ), |
145 | 'format' , |
146 | '--set-exit-if-changed' , |
147 | '--show=none' , |
148 | if (!fix) '--output=show' , |
149 | if (fix) '--output=write' , |
150 | ]; |
151 | final List<WorkerJob> jobs = <WorkerJob>[]; |
152 | for (final String file in filesToCheck) { |
153 | jobs.add(WorkerJob(<String>[...cmd, file])); |
154 | } |
155 | final ProcessPool dartFmt = ProcessPool( |
156 | processRunner: processRunner, |
157 | printReport: _namedReport('dart format' ), |
158 | ); |
159 | |
160 | Iterable<WorkerJob> incorrect; |
161 | final List<WorkerJob> errorJobs = <WorkerJob>[]; |
162 | if (!fix) { |
163 | final Stream<WorkerJob> completedJobs = dartFmt.startWorkers(jobs); |
164 | final List<WorkerJob> diffJobs = <WorkerJob>[]; |
165 | await for (final WorkerJob completedJob in completedJobs) { |
166 | if (completedJob.result.exitCode != 0 && completedJob.result.exitCode != 1) { |
167 | // The formatter had a problem formatting the file. |
168 | errorJobs.add(completedJob); |
169 | } else if (completedJob.result.exitCode == 1) { |
170 | diffJobs.add( |
171 | WorkerJob(<String>[ |
172 | 'git' , |
173 | 'diff' , |
174 | '--no-index' , |
175 | '--no-color' , |
176 | '--ignore-cr-at-eol' , |
177 | '--' , |
178 | completedJob.command.last, |
179 | '-' , |
180 | ], stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw)), |
181 | ); |
182 | } |
183 | } |
184 | final ProcessPool diffPool = ProcessPool( |
185 | processRunner: processRunner, |
186 | printReport: _namedReport('diff' ), |
187 | ); |
188 | final List<WorkerJob> completedDiffs = await diffPool.runToCompletion(diffJobs); |
189 | incorrect = completedDiffs.where((WorkerJob job) => job.result.exitCode != 0); |
190 | } else { |
191 | final List<WorkerJob> completedJobs = await dartFmt.runToCompletion(jobs); |
192 | final List<WorkerJob> incorrectJobs = incorrect = <WorkerJob>[]; |
193 | for (final WorkerJob job in completedJobs) { |
194 | if (job.result.exitCode != 0 && job.result.exitCode != 1) { |
195 | // The formatter had a problem formatting the file. |
196 | errorJobs.add(job); |
197 | } else if (job.result.exitCode == 1) { |
198 | incorrectJobs.add(job); |
199 | } |
200 | } |
201 | } |
202 | |
203 | _clearOutput(); |
204 | |
205 | if (incorrect.isNotEmpty) { |
206 | final bool plural = incorrect.length > 1; |
207 | if (fix) { |
208 | stdout.writeln( |
209 | 'Fixing ${incorrect.length} dart file ${plural ? 's' : '' }' |
210 | ' which ${plural ? 'were' : 'was' } formatted incorrectly.' , |
211 | ); |
212 | } else { |
213 | stderr.writeln( |
214 | 'Found ${incorrect.length} Dart file ${plural ? 's' : '' }' |
215 | ' which ${plural ? 'were' : 'was' } formatted incorrectly.' , |
216 | ); |
217 | final String fileList = incorrect |
218 | .map((WorkerJob job) => job.command[job.command.length - 2]) |
219 | .join(' ' ); |
220 | stdout.writeln(); |
221 | stdout.writeln('To fix, run `dart format $fileList` or:' ); |
222 | stdout.writeln(); |
223 | stdout.writeln('git apply <<DONE' ); |
224 | for (final WorkerJob job in incorrect) { |
225 | stdout.write( |
226 | job.result.stdout |
227 | .replaceFirst('b/-' , 'b/ ${job.command[job.command.length - 2]}' ) |
228 | .replaceFirst('b/-' , 'b/ ${job.command[job.command.length - 2]}' ) |
229 | .replaceFirst( |
230 | RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n' ), |
231 | '' , |
232 | ), |
233 | ); |
234 | } |
235 | stdout.writeln('DONE' ); |
236 | stdout.writeln(); |
237 | } |
238 | _printErrorJobs(errorJobs); |
239 | } else if (errorJobs.isNotEmpty) { |
240 | _printErrorJobs(errorJobs); |
241 | } else { |
242 | stdout.writeln('All dart files formatted correctly.' ); |
243 | } |
244 | return fix ? errorJobs.length : (incorrect.length + errorJobs.length); |
245 | } |
246 | |
247 | void _printErrorJobs(List<WorkerJob> errorJobs) { |
248 | if (errorJobs.isNotEmpty) { |
249 | final bool plural = errorJobs.length > 1; |
250 | stderr.writeln( |
251 | 'The formatter failed to run on ${errorJobs.length} Dart file ${plural ? 's' : '' }.' , |
252 | ); |
253 | stdout.writeln(); |
254 | for (final WorkerJob job in errorJobs) { |
255 | stdout.writeln('--> ${job.command.last} produced the following error:' ); |
256 | stdout.write(job.result.stderr); |
257 | stdout.writeln(); |
258 | } |
259 | } |
260 | } |
261 | } |
262 | |
263 | ProcessPoolProgressReporter _namedReport(String name) { |
264 | return (int total, int completed, int inProgress, int pending, int failed) { |
265 | final String percent = total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3); |
266 | final String completedStr = completed.toString().padLeft(3); |
267 | final String totalStr = total.toString().padRight(3); |
268 | final String inProgressStr = inProgress.toString().padLeft(2); |
269 | final String pendingStr = pending.toString().padLeft(3); |
270 | final String failedStr = failed.toString().padLeft(3); |
271 | |
272 | stdout.write( |
273 | ' $name Jobs: $percent% done, ' |
274 | ' $completedStr/ $totalStr completed, ' |
275 | ' $inProgressStr in progress, ' |
276 | ' $pendingStr pending, ' |
277 | ' $failedStr failed. ${' ' * 20}\r' , |
278 | ); |
279 | }; |
280 | } |
281 | |
282 | void _clearOutput() { |
283 | stdout.write('\r ${' ' * 100}\r' ); |
284 | } |
285 | |
286 | Stream<List<int>> _codeUnitsAsStream(List<int>? input) async* { |
287 | if (input != null) { |
288 | yield input; |
289 | } |
290 | } |
291 | |