Skip to content

Commit c4dc2c9

Browse files
authored
Add script to check format of changed dart files (flutter#160007)
The script is modeled after a similar script in the engine (see engine's [format.dart](https://github.com/flutter/engine/blob/main/ci/bin/format.dart)). It identifies the files that have been changed and checks if their formatting is correct. It also offers an option to correct formatting (`--fix`) and an option to check the formatting of all files in the repro (not just changed ones, `--all-files`). When we are enforcing dart format this script will be called as part of presubmit.
1 parent 2f9e2d9 commit c4dc2c9

File tree

6 files changed

+412
-1
lines changed

6 files changed

+412
-1
lines changed

dev/bots/analyze.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2246,6 +2246,7 @@ const Set<String> kExecutableAllowlist = <String>{
22462246
'dev/tools/gen_keycodes/bin/gen_keycodes',
22472247
'dev/tools/repackage_gradle_wrapper.sh',
22482248
'dev/tools/bin/engine_hash.sh',
2249+
'dev/tools/format.sh',
22492250

22502251
'packages/flutter_tools/bin/macos_assemble.sh',
22512252
'packages/flutter_tools/bin/tool_backend.sh',

dev/tools/bin/format.dart

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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('fix',
15+
abbr: 'f',
16+
help: 'Instead of just checking for formatting errors, fix them in place.');
17+
parser.addFlag('all-files',
18+
abbr: 'a',
19+
help: 'Instead of just checking for formatting errors in changed files, '
20+
'check for them in all files.');
21+
22+
late final ArgResults options;
23+
try {
24+
options = parser.parse(arguments);
25+
} on FormatException catch (e) {
26+
stderr.writeln('ERROR: $e');
27+
_usage(parser, exitCode: 0);
28+
}
29+
30+
if (options['help'] as bool) {
31+
_usage(parser, exitCode: 0);
32+
}
33+
34+
final File script = File.fromUri(Platform.script).absolute;
35+
final Directory flutterRoot = script.parent.parent.parent.parent;
36+
37+
final bool result = (await DartFormatChecker(
38+
flutterRoot: flutterRoot,
39+
allFiles: options['all-files'] as bool,
40+
).check(fix: options['fix'] as bool)) == 0;
41+
42+
exit(result ? 0 : 1);
43+
}
44+
45+
void _usage(ArgParser parser, {int exitCode = 1}) {
46+
stderr.writeln('format.dart [--help] [--fix] [--all-files]');
47+
stderr.writeln(parser.usage);
48+
exit(exitCode);
49+
}
50+
51+
class DartFormatChecker {
52+
DartFormatChecker({
53+
required this.flutterRoot,
54+
required this.allFiles,
55+
}) : processRunner = ProcessRunner(
56+
defaultWorkingDirectory: flutterRoot,
57+
);
58+
59+
final Directory flutterRoot;
60+
final bool allFiles;
61+
final ProcessRunner processRunner;
62+
63+
Future<int> check({required bool fix}) async {
64+
final String baseGitRef = await _getDiffBaseRevision();
65+
final List<String> filesToCheck = await _getFileList(
66+
types: <String>['*.dart'],
67+
allFiles: allFiles,
68+
baseGitRef: baseGitRef,
69+
);
70+
return _checkFormat(
71+
filesToCheck: filesToCheck,
72+
fix: fix,
73+
);
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(
93+
<String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
94+
processRunner,
95+
);
96+
} on ProcessRunnerException {
97+
result = await _runGit(<String>['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner);
98+
}
99+
return result.trim();
100+
}
101+
102+
Future<String> _runGit(
103+
List<String> args,
104+
ProcessRunner processRunner, {
105+
bool failOk = false,
106+
}) async {
107+
final ProcessRunnerResult result = await processRunner.runProcess(
108+
<String>['git', ...args],
109+
failOk: failOk,
110+
);
111+
return result.stdout;
112+
}
113+
114+
Future<List<String>> _getFileList({
115+
required List<String> types,
116+
required bool allFiles,
117+
required String baseGitRef,
118+
}) async {
119+
String output;
120+
if (allFiles) {
121+
output = await _runGit(<String>[
122+
'ls-files',
123+
'--',
124+
...types,
125+
], processRunner);
126+
} else {
127+
output = await _runGit(<String>[
128+
'diff',
129+
'-U0',
130+
'--no-color',
131+
'--diff-filter=d',
132+
'--name-only',
133+
baseGitRef,
134+
'--',
135+
...types,
136+
], processRunner);
137+
}
138+
return output.split('\n').where((String line) => line.isNotEmpty).toList();
139+
}
140+
141+
Future<int> _checkFormat({
142+
required List<String> filesToCheck,
143+
required bool fix,
144+
}) async {
145+
final List<String> cmd = <String>[
146+
path.join(flutterRoot.path, 'bin', 'dart'),
147+
'format',
148+
'--set-exit-if-changed',
149+
'--show=none',
150+
if (!fix) '--output=show',
151+
if (fix) '--output=write',
152+
];
153+
final List<WorkerJob> jobs = <WorkerJob>[];
154+
for (final String file in filesToCheck) {
155+
jobs.add(WorkerJob(<String>[...cmd, file]));
156+
}
157+
final ProcessPool dartFmt = ProcessPool(
158+
processRunner: processRunner,
159+
printReport: _namedReport('dart format'),
160+
);
161+
162+
Iterable<WorkerJob> incorrect;
163+
if (!fix) {
164+
final Stream<WorkerJob> completedJobs = dartFmt.startWorkers(jobs);
165+
final List<WorkerJob> diffJobs = <WorkerJob>[];
166+
await for (final WorkerJob completedJob in completedJobs) {
167+
if (completedJob.result.exitCode == 1) {
168+
diffJobs.add(
169+
WorkerJob(
170+
<String>[
171+
'git',
172+
'diff',
173+
'--no-index',
174+
'--no-color',
175+
'--ignore-cr-at-eol',
176+
'--',
177+
completedJob.command.last,
178+
'-',
179+
],
180+
stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw),
181+
),
182+
);
183+
}
184+
}
185+
final ProcessPool diffPool = ProcessPool(
186+
processRunner: processRunner,
187+
printReport: _namedReport('diff'),
188+
);
189+
final List<WorkerJob> completedDiffs = await diffPool.runToCompletion(diffJobs);
190+
incorrect = completedDiffs.where((WorkerJob job) => job.result.exitCode != 0);
191+
} else {
192+
final List<WorkerJob> completedJobs = await dartFmt.runToCompletion(jobs);
193+
incorrect = completedJobs.where((WorkerJob job) => job.result.exitCode == 1);
194+
}
195+
196+
_clearOutput();
197+
198+
if (incorrect.isNotEmpty) {
199+
final bool plural = incorrect.length > 1;
200+
if (fix) {
201+
stdout.writeln('Fixing ${incorrect.length} dart file${plural ? 's' : ''}'
202+
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
203+
} else {
204+
stderr.writeln('Found ${incorrect.length} Dart file${plural ? 's' : ''}'
205+
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
206+
final String fileList = incorrect.map(
207+
(WorkerJob job) => job.command[job.command.length - 2]
208+
).join(' ');
209+
stdout.writeln();
210+
stdout.writeln('To fix, run `dart format $fileList` or:');
211+
stdout.writeln();
212+
stdout.writeln('git apply <<DONE');
213+
for (final WorkerJob job in incorrect) {
214+
stdout.write(job.result.stdout
215+
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
216+
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
217+
.replaceFirst(RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n'), '')
218+
);
219+
}
220+
stdout.writeln('DONE');
221+
stdout.writeln();
222+
}
223+
} else {
224+
stdout.writeln('All dart files formatted correctly.');
225+
}
226+
return incorrect.length;
227+
}
228+
}
229+
230+
ProcessPoolProgressReporter _namedReport(String name) {
231+
return (int total, int completed, int inProgress, int pending, int failed) {
232+
final String percent =
233+
total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3);
234+
final String completedStr = completed.toString().padLeft(3);
235+
final String totalStr = total.toString().padRight(3);
236+
final String inProgressStr = inProgress.toString().padLeft(2);
237+
final String pendingStr = pending.toString().padLeft(3);
238+
final String failedStr = failed.toString().padLeft(3);
239+
240+
stdout.write('$name Jobs: $percent% done, '
241+
'$completedStr/$totalStr completed, '
242+
'$inProgressStr in progress, '
243+
'$pendingStr pending, '
244+
'$failedStr failed.${' ' * 20}\r');
245+
};
246+
}
247+
248+
void _clearOutput() {
249+
stdout.write('\r${' ' * 100}\r');
250+
}
251+
252+
Stream<List<int>> _codeUnitsAsStream(List<int>? input) async* {
253+
if (input != null) {
254+
yield input;
255+
}
256+
}

dev/tools/format.bat

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@ECHO off
2+
REM Copyright 2014 The Flutter Authors. All rights reserved.
3+
REM Use of this source code is governed by a BSD-style license that can be
4+
REM found in the LICENSE file.
5+
6+
SETLOCAL ENABLEDELAYEDEXPANSION
7+
8+
FOR %%i IN ("%~dp0..\..") DO SET FLUTTER_ROOT=%%~fi
9+
10+
REM Test if Git is available on the Host
11+
where /q git || ECHO Error: Unable to find git in your PATH. && EXIT /B 1
12+
13+
SET tools_dir=%FLUTTER_ROOT%\dev\tools
14+
15+
SET dart=%FLUTTER_ROOT%\bin\dart.bat
16+
17+
cd "%tools_dir%"
18+
19+
REM Do not use the CALL command in the next line to execute Dart. CALL causes
20+
REM Windows to re-read the line from disk after the CALL command has finished
21+
REM regardless of the ampersand chain.
22+
"%dart%" pub get > NUL && "%dart%" "%tools_dir%\bin\format.dart" %* & exit /B !ERRORLEVEL!

dev/tools/format.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2014 The Flutter Authors. All rights reserved.
3+
# Use of this source code is governed by a BSD-style license that can be
4+
# found in the LICENSE file.
5+
6+
set -e
7+
8+
# Needed because if it is set, cd may print the path it changed to.
9+
unset CDPATH
10+
11+
# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one
12+
# link at a time, and then cds into the link destination and find out where it
13+
# ends up.
14+
#
15+
# The function is enclosed in a subshell to avoid changing the working directory
16+
# of the caller.
17+
function follow_links() (
18+
cd -P "$(dirname -- "$1")"
19+
file="$PWD/$(basename -- "$1")"
20+
while [[ -h "$file" ]]; do
21+
cd -P "$(dirname -- "$file")"
22+
file="$(readlink -- "$file")"
23+
cd -P "$(dirname -- "$file")"
24+
file="$PWD/$(basename -- "$file")"
25+
done
26+
echo "$file"
27+
)
28+
29+
SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")")
30+
FLUTTER_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)"
31+
DART="${FLUTTER_DIR}/bin/dart"
32+
33+
cd "$SCRIPT_DIR"
34+
"$DART" pub get > /dev/null
35+
"$DART" \
36+
bin/format.dart \
37+
"$@"

dev/tools/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies:
1212
meta: 1.15.0
1313
path: 1.9.1
1414
process: 5.0.3
15+
process_runner: 4.2.0
1516
pub_semver: 2.1.4
1617
yaml: 3.1.2
1718

@@ -63,4 +64,4 @@ dev_dependencies:
6364
web_socket_channel: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
6465
webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
6566

66-
# PUBSPEC CHECKSUM: f620
67+
# PUBSPEC CHECKSUM: 2d6b

0 commit comments

Comments
 (0)