mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add script to check format of changed dart files (#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.
This commit is contained in:
parent
2f9e2d9bc9
commit
c4dc2c9890
@ -2246,6 +2246,7 @@ const Set<String> kExecutableAllowlist = <String>{
|
||||
'dev/tools/gen_keycodes/bin/gen_keycodes',
|
||||
'dev/tools/repackage_gradle_wrapper.sh',
|
||||
'dev/tools/bin/engine_hash.sh',
|
||||
'dev/tools/format.sh',
|
||||
|
||||
'packages/flutter_tools/bin/macos_assemble.sh',
|
||||
'packages/flutter_tools/bin/tool_backend.sh',
|
||||
|
256
dev/tools/bin/format.dart
Normal file
256
dev/tools/bin/format.dart
Normal file
@ -0,0 +1,256 @@
|
||||
// Copyright 2014 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:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:process_runner/process_runner.dart';
|
||||
|
||||
Future<int> main(List<String> arguments) async {
|
||||
final ArgParser parser = ArgParser();
|
||||
parser.addFlag('help', help: 'Print help.', abbr: 'h');
|
||||
parser.addFlag('fix',
|
||||
abbr: 'f',
|
||||
help: 'Instead of just checking for formatting errors, fix them in place.');
|
||||
parser.addFlag('all-files',
|
||||
abbr: 'a',
|
||||
help: 'Instead of just checking for formatting errors in changed files, '
|
||||
'check for them in all files.');
|
||||
|
||||
late final ArgResults options;
|
||||
try {
|
||||
options = parser.parse(arguments);
|
||||
} on FormatException catch (e) {
|
||||
stderr.writeln('ERROR: $e');
|
||||
_usage(parser, exitCode: 0);
|
||||
}
|
||||
|
||||
if (options['help'] as bool) {
|
||||
_usage(parser, exitCode: 0);
|
||||
}
|
||||
|
||||
final File script = File.fromUri(Platform.script).absolute;
|
||||
final Directory flutterRoot = script.parent.parent.parent.parent;
|
||||
|
||||
final bool result = (await DartFormatChecker(
|
||||
flutterRoot: flutterRoot,
|
||||
allFiles: options['all-files'] as bool,
|
||||
).check(fix: options['fix'] as bool)) == 0;
|
||||
|
||||
exit(result ? 0 : 1);
|
||||
}
|
||||
|
||||
void _usage(ArgParser parser, {int exitCode = 1}) {
|
||||
stderr.writeln('format.dart [--help] [--fix] [--all-files]');
|
||||
stderr.writeln(parser.usage);
|
||||
exit(exitCode);
|
||||
}
|
||||
|
||||
class DartFormatChecker {
|
||||
DartFormatChecker({
|
||||
required this.flutterRoot,
|
||||
required this.allFiles,
|
||||
}) : processRunner = ProcessRunner(
|
||||
defaultWorkingDirectory: flutterRoot,
|
||||
);
|
||||
|
||||
final Directory flutterRoot;
|
||||
final bool allFiles;
|
||||
final ProcessRunner processRunner;
|
||||
|
||||
Future<int> check({required bool fix}) async {
|
||||
final String baseGitRef = await _getDiffBaseRevision();
|
||||
final List<String> filesToCheck = await _getFileList(
|
||||
types: <String>['*.dart'],
|
||||
allFiles: allFiles,
|
||||
baseGitRef: baseGitRef,
|
||||
);
|
||||
return _checkFormat(
|
||||
filesToCheck: filesToCheck,
|
||||
fix: fix,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getDiffBaseRevision() async {
|
||||
String upstream = 'upstream';
|
||||
final String upstreamUrl = await _runGit(
|
||||
<String>['remote', 'get-url', upstream],
|
||||
processRunner,
|
||||
failOk: true,
|
||||
);
|
||||
if (upstreamUrl.isEmpty) {
|
||||
upstream = 'origin';
|
||||
}
|
||||
await _runGit(<String>['fetch', upstream, 'main'], processRunner);
|
||||
String result = '';
|
||||
try {
|
||||
// This is the preferred command to use, but developer checkouts often do
|
||||
// not have a clear fork point, so we fall back to just the regular
|
||||
// merge-base in that case.
|
||||
result = await _runGit(
|
||||
<String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
|
||||
processRunner,
|
||||
);
|
||||
} on ProcessRunnerException {
|
||||
result = await _runGit(<String>['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner);
|
||||
}
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
Future<String> _runGit(
|
||||
List<String> args,
|
||||
ProcessRunner processRunner, {
|
||||
bool failOk = false,
|
||||
}) async {
|
||||
final ProcessRunnerResult result = await processRunner.runProcess(
|
||||
<String>['git', ...args],
|
||||
failOk: failOk,
|
||||
);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
Future<List<String>> _getFileList({
|
||||
required List<String> types,
|
||||
required bool allFiles,
|
||||
required String baseGitRef,
|
||||
}) async {
|
||||
String output;
|
||||
if (allFiles) {
|
||||
output = await _runGit(<String>[
|
||||
'ls-files',
|
||||
'--',
|
||||
...types,
|
||||
], processRunner);
|
||||
} else {
|
||||
output = await _runGit(<String>[
|
||||
'diff',
|
||||
'-U0',
|
||||
'--no-color',
|
||||
'--diff-filter=d',
|
||||
'--name-only',
|
||||
baseGitRef,
|
||||
'--',
|
||||
...types,
|
||||
], processRunner);
|
||||
}
|
||||
return output.split('\n').where((String line) => line.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
Future<int> _checkFormat({
|
||||
required List<String> filesToCheck,
|
||||
required bool fix,
|
||||
}) async {
|
||||
final List<String> cmd = <String>[
|
||||
path.join(flutterRoot.path, 'bin', 'dart'),
|
||||
'format',
|
||||
'--set-exit-if-changed',
|
||||
'--show=none',
|
||||
if (!fix) '--output=show',
|
||||
if (fix) '--output=write',
|
||||
];
|
||||
final List<WorkerJob> jobs = <WorkerJob>[];
|
||||
for (final String file in filesToCheck) {
|
||||
jobs.add(WorkerJob(<String>[...cmd, file]));
|
||||
}
|
||||
final ProcessPool dartFmt = ProcessPool(
|
||||
processRunner: processRunner,
|
||||
printReport: _namedReport('dart format'),
|
||||
);
|
||||
|
||||
Iterable<WorkerJob> incorrect;
|
||||
if (!fix) {
|
||||
final Stream<WorkerJob> completedJobs = dartFmt.startWorkers(jobs);
|
||||
final List<WorkerJob> diffJobs = <WorkerJob>[];
|
||||
await for (final WorkerJob completedJob in completedJobs) {
|
||||
if (completedJob.result.exitCode == 1) {
|
||||
diffJobs.add(
|
||||
WorkerJob(
|
||||
<String>[
|
||||
'git',
|
||||
'diff',
|
||||
'--no-index',
|
||||
'--no-color',
|
||||
'--ignore-cr-at-eol',
|
||||
'--',
|
||||
completedJob.command.last,
|
||||
'-',
|
||||
],
|
||||
stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
final ProcessPool diffPool = ProcessPool(
|
||||
processRunner: processRunner,
|
||||
printReport: _namedReport('diff'),
|
||||
);
|
||||
final List<WorkerJob> completedDiffs = await diffPool.runToCompletion(diffJobs);
|
||||
incorrect = completedDiffs.where((WorkerJob job) => job.result.exitCode != 0);
|
||||
} else {
|
||||
final List<WorkerJob> completedJobs = await dartFmt.runToCompletion(jobs);
|
||||
incorrect = completedJobs.where((WorkerJob job) => job.result.exitCode == 1);
|
||||
}
|
||||
|
||||
_clearOutput();
|
||||
|
||||
if (incorrect.isNotEmpty) {
|
||||
final bool plural = incorrect.length > 1;
|
||||
if (fix) {
|
||||
stdout.writeln('Fixing ${incorrect.length} dart file${plural ? 's' : ''}'
|
||||
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
|
||||
} else {
|
||||
stderr.writeln('Found ${incorrect.length} Dart file${plural ? 's' : ''}'
|
||||
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
|
||||
final String fileList = incorrect.map(
|
||||
(WorkerJob job) => job.command[job.command.length - 2]
|
||||
).join(' ');
|
||||
stdout.writeln();
|
||||
stdout.writeln('To fix, run `dart format $fileList` or:');
|
||||
stdout.writeln();
|
||||
stdout.writeln('git apply <<DONE');
|
||||
for (final WorkerJob job in incorrect) {
|
||||
stdout.write(job.result.stdout
|
||||
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
|
||||
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
|
||||
.replaceFirst(RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n'), '')
|
||||
);
|
||||
}
|
||||
stdout.writeln('DONE');
|
||||
stdout.writeln();
|
||||
}
|
||||
} else {
|
||||
stdout.writeln('All dart files formatted correctly.');
|
||||
}
|
||||
return incorrect.length;
|
||||
}
|
||||
}
|
||||
|
||||
ProcessPoolProgressReporter _namedReport(String name) {
|
||||
return (int total, int completed, int inProgress, int pending, int failed) {
|
||||
final String percent =
|
||||
total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3);
|
||||
final String completedStr = completed.toString().padLeft(3);
|
||||
final String totalStr = total.toString().padRight(3);
|
||||
final String inProgressStr = inProgress.toString().padLeft(2);
|
||||
final String pendingStr = pending.toString().padLeft(3);
|
||||
final String failedStr = failed.toString().padLeft(3);
|
||||
|
||||
stdout.write('$name Jobs: $percent% done, '
|
||||
'$completedStr/$totalStr completed, '
|
||||
'$inProgressStr in progress, '
|
||||
'$pendingStr pending, '
|
||||
'$failedStr failed.${' ' * 20}\r');
|
||||
};
|
||||
}
|
||||
|
||||
void _clearOutput() {
|
||||
stdout.write('\r${' ' * 100}\r');
|
||||
}
|
||||
|
||||
Stream<List<int>> _codeUnitsAsStream(List<int>? input) async* {
|
||||
if (input != null) {
|
||||
yield input;
|
||||
}
|
||||
}
|
22
dev/tools/format.bat
Normal file
22
dev/tools/format.bat
Normal file
@ -0,0 +1,22 @@
|
||||
@ECHO off
|
||||
REM Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
REM Use of this source code is governed by a BSD-style license that can be
|
||||
REM found in the LICENSE file.
|
||||
|
||||
SETLOCAL ENABLEDELAYEDEXPANSION
|
||||
|
||||
FOR %%i IN ("%~dp0..\..") DO SET FLUTTER_ROOT=%%~fi
|
||||
|
||||
REM Test if Git is available on the Host
|
||||
where /q git || ECHO Error: Unable to find git in your PATH. && EXIT /B 1
|
||||
|
||||
SET tools_dir=%FLUTTER_ROOT%\dev\tools
|
||||
|
||||
SET dart=%FLUTTER_ROOT%\bin\dart.bat
|
||||
|
||||
cd "%tools_dir%"
|
||||
|
||||
REM Do not use the CALL command in the next line to execute Dart. CALL causes
|
||||
REM Windows to re-read the line from disk after the CALL command has finished
|
||||
REM regardless of the ampersand chain.
|
||||
"%dart%" pub get > NUL && "%dart%" "%tools_dir%\bin\format.dart" %* & exit /B !ERRORLEVEL!
|
37
dev/tools/format.sh
Executable file
37
dev/tools/format.sh
Executable file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2014 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.
|
||||
|
||||
set -e
|
||||
|
||||
# Needed because if it is set, cd may print the path it changed to.
|
||||
unset CDPATH
|
||||
|
||||
# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one
|
||||
# link at a time, and then cds into the link destination and find out where it
|
||||
# ends up.
|
||||
#
|
||||
# The function is enclosed in a subshell to avoid changing the working directory
|
||||
# of the caller.
|
||||
function follow_links() (
|
||||
cd -P "$(dirname -- "$1")"
|
||||
file="$PWD/$(basename -- "$1")"
|
||||
while [[ -h "$file" ]]; do
|
||||
cd -P "$(dirname -- "$file")"
|
||||
file="$(readlink -- "$file")"
|
||||
cd -P "$(dirname -- "$file")"
|
||||
file="$PWD/$(basename -- "$file")"
|
||||
done
|
||||
echo "$file"
|
||||
)
|
||||
|
||||
SCRIPT_DIR=$(follow_links "$(dirname -- "${BASH_SOURCE[0]}")")
|
||||
FLUTTER_DIR="$(cd "$SCRIPT_DIR/../.."; pwd -P)"
|
||||
DART="${FLUTTER_DIR}/bin/dart"
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
"$DART" pub get > /dev/null
|
||||
"$DART" \
|
||||
bin/format.dart \
|
||||
"$@"
|
@ -12,6 +12,7 @@ dependencies:
|
||||
meta: 1.15.0
|
||||
path: 1.9.1
|
||||
process: 5.0.3
|
||||
process_runner: 4.2.0
|
||||
pub_semver: 2.1.4
|
||||
yaml: 3.1.2
|
||||
|
||||
@ -63,4 +64,4 @@ dev_dependencies:
|
||||
web_socket_channel: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||
|
||||
# PUBSPEC CHECKSUM: f620
|
||||
# PUBSPEC CHECKSUM: 2d6b
|
||||
|
94
dev/tools/test/format_test.dart
Normal file
94
dev/tools/test/format_test.dart
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright 2014 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:io' as io;
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class FileContentPair {
|
||||
FileContentPair({required this.name, required this.original, required this.formatted});
|
||||
|
||||
final String name;
|
||||
final String original;
|
||||
final String formatted;
|
||||
}
|
||||
|
||||
final FileContentPair dartContentPair = FileContentPair(
|
||||
name: 'format_test.dart',
|
||||
original: 'enum \n\nfoo {\n entry1,\n entry2,\n}',
|
||||
formatted: 'enum foo { entry1, entry2 }\n',
|
||||
);
|
||||
|
||||
class TestFileFixture {
|
||||
TestFileFixture(this.filePairs, this.baseDir) {
|
||||
for (final FileContentPair filePair in filePairs) {
|
||||
final io.File file = io.File(path.join(baseDir.path, filePair.name));
|
||||
file.writeAsStringSync(filePair.original);
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
final List<io.File> files = <io.File>[];
|
||||
final io.Directory baseDir;
|
||||
final List<FileContentPair> filePairs;
|
||||
|
||||
void gitAdd() {
|
||||
final List<String> args = <String>['add'];
|
||||
for (final io.File file in files) {
|
||||
args.add(file.path);
|
||||
}
|
||||
|
||||
io.Process.runSync('git', args);
|
||||
}
|
||||
|
||||
void gitRemove() {
|
||||
final List<String> args = <String>['rm', '-f'];
|
||||
for (final io.File file in files) {
|
||||
args.add(file.path);
|
||||
}
|
||||
io.Process.runSync('git', args);
|
||||
}
|
||||
|
||||
Iterable<FileContentPair> getFileContents() {
|
||||
final List<FileContentPair> results = <FileContentPair>[];
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
final io.File file = files[i];
|
||||
final FileContentPair filePair = filePairs[i];
|
||||
final String content = file.readAsStringSync().replaceAll('\r\n', '\n');
|
||||
results.add(
|
||||
FileContentPair(name: filePair.name, original: content, formatted: filePair.formatted),
|
||||
);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
final io.File script = io.File(path.current).absolute;
|
||||
final io.Directory flutterRoot = script.parent.parent;
|
||||
final String formatterPath = path.join(
|
||||
flutterRoot.path,
|
||||
'dev',
|
||||
'tools',
|
||||
'format.${io.Platform.isWindows ? 'bat' : 'sh'}',
|
||||
);
|
||||
|
||||
test('Can fix Dart formatting errors', () {
|
||||
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[
|
||||
dartContentPair,
|
||||
], flutterRoot);
|
||||
try {
|
||||
fixture.gitAdd();
|
||||
io.Process.runSync(formatterPath, <String>['--fix'], workingDirectory: flutterRoot.path);
|
||||
|
||||
final Iterable<FileContentPair> files = fixture.getFileContents();
|
||||
for (final FileContentPair pair in files) {
|
||||
expect(pair.original, equals(pair.formatted));
|
||||
}
|
||||
} finally {
|
||||
fixture.gitRemove();
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user