blob: 8b317acad2f16a928eafb85f32780b168605e200 [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.
// Checks and fixes format on files with changes.
//
// Run with --help for usage.
import 'dart:io';
import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:process/process.dart';
import 'package:process_runner/process_runner.dart';
class FormattingException implements Exception {
FormattingException(this.message, [this.result]);
final String message;
final ProcessResult? result;
@override
String toString() {
final StringBuffer output = StringBuffer(runtimeType.toString());
output.write(': $message');
final String? stderr = result?.stderr as String?;
if (stderr?.isNotEmpty ?? false) {
output.write(':\n$stderr');
}
return output.toString();
}
}
enum MessageType {
message,
error,
warning,
}
enum FormatCheck {
gn,
java,
python,
whitespace,
header,
// Run clang after the header check.
clang,
}
FormatCheck nameToFormatCheck(String name) {
switch (name) {
case 'clang':
return FormatCheck.clang;
case 'gn':
return FormatCheck.gn;
case 'java':
return FormatCheck.java;
case 'python':
return FormatCheck.python;
case 'whitespace':
return FormatCheck.whitespace;
case 'header':
return FormatCheck.header;
default:
throw FormattingException('Unknown FormatCheck type $name');
}
}
String formatCheckToName(FormatCheck check) {
switch (check) {
case FormatCheck.clang:
return 'C++/ObjC/Shader';
case FormatCheck.gn:
return 'GN';
case FormatCheck.java:
return 'Java';
case FormatCheck.python:
return 'Python';
case FormatCheck.whitespace:
return 'Trailing whitespace';
case FormatCheck.header:
return 'Header guards';
}
}
List<String> formatCheckNames() {
return FormatCheck.values
.map<String>((FormatCheck check) =>
check.toString().replaceFirst('$FormatCheck.', ''))
.toList();
}
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;
}
typedef MessageCallback = void Function(String? message, {MessageType type});
/// Base class for format checkers.
///
/// Provides services that all format checkers need.
abstract class FormatChecker {
FormatChecker({
ProcessManager processManager = const LocalProcessManager(),
required this.baseGitRef,
required this.repoDir,
this.allFiles = false,
this.messageCallback,
}) : _processRunner = ProcessRunner(
defaultWorkingDirectory: repoDir,
processManager: processManager,
);
/// Factory method that creates subclass format checkers based on the type of check.
factory FormatChecker.ofType(
FormatCheck check, {
ProcessManager processManager = const LocalProcessManager(),
required String baseGitRef,
required Directory repoDir,
required Directory srcDir,
bool allFiles = false,
MessageCallback? messageCallback,
}) {
switch (check) {
case FormatCheck.clang:
return ClangFormatChecker(
processManager: processManager,
baseGitRef: baseGitRef,
repoDir: repoDir,
srcDir: srcDir,
allFiles: allFiles,
messageCallback: messageCallback,
);
case FormatCheck.gn:
return GnFormatChecker(
processManager: processManager,
baseGitRef: baseGitRef,
repoDir: repoDir,
allFiles: allFiles,
messageCallback: messageCallback,
);
case FormatCheck.java:
return JavaFormatChecker(
processManager: processManager,
baseGitRef: baseGitRef,
repoDir: repoDir,
srcDir: srcDir,
allFiles: allFiles,
messageCallback: messageCallback,
);
case FormatCheck.python:
return PythonFormatChecker(
processManager: processManager,
baseGitRef: baseGitRef,
repoDir: repoDir,
allFiles: allFiles,
messageCallback: messageCallback,
);
case FormatCheck.whitespace:
return WhitespaceFormatChecker(
processManager: processManager,
baseGitRef: baseGitRef,
repoDir: repoDir,
allFiles: allFiles,
messageCallback: messageCallback,
);
case FormatCheck.header:
return HeaderFormatChecker(
processManager: processManager,
baseGitRef: baseGitRef,
repoDir: repoDir,
allFiles: allFiles,
messageCallback: messageCallback,
);
}
}
final ProcessRunner _processRunner;
final Directory repoDir;
final bool allFiles;
MessageCallback? messageCallback;
final String baseGitRef;
/// Override to provide format checking for a specific type.
Future<bool> checkFormatting();
/// Override to provide format fixing for a specific type.
Future<bool> fixFormatting();
@protected
void message(String? string) => messageCallback?.call(string, type: MessageType.message);
@protected
void error(String string) => messageCallback?.call(string, type: MessageType.error);
@protected
Future<String> runGit(List<String> args) async => _runGit(args, _processRunner);
/// Converts a given raw string of code units to a stream that yields those
/// code units.
///
/// Uses to convert the stdout of a previous command into an input stream for
/// the next command.
@protected
Stream<List<int>> codeUnitsAsStream(List<int>? input) async* {
if (input != null) {
yield input;
}
}
@protected
Future<bool> applyPatch(List<String> patches) async {
final ProcessPool patchPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('patch'),
);
final List<WorkerJob> jobs = patches.map<WorkerJob>((String patch) {
return WorkerJob(
<String>['git', 'apply', '--ignore-space-change'],
stdinRaw: codeUnitsAsStream(patch.codeUnits),
);
}).toList();
final List<WorkerJob> completedJobs = await patchPool.runToCompletion(jobs);
if (patchPool.failedJobs != 0) {
error('${patchPool.failedJobs} patch${patchPool.failedJobs > 1 ? 'es' : ''} '
'failed to apply.');
completedJobs
.where((WorkerJob job) => job.result.exitCode != 0)
.map<String>((WorkerJob job) => job.result.output)
.forEach(message);
}
return patchPool.failedJobs == 0;
}
/// Gets the list of files to operate on.
///
/// If [allFiles] is true, then returns all git controlled files in the repo
/// of the given types.
///
/// If [allFiles] is false, then only return those files of the given types
/// that have changed between the current working tree and the [baseGitRef].
@protected
Future<List<String>> getFileList(List<String> types) async {
String output;
if (allFiles) {
output = await runGit(<String>[
'ls-files',
'--',
...types,
]);
} else {
output = await runGit(<String>[
'diff',
'-U0',
'--no-color',
'--diff-filter=d',
'--name-only',
baseGitRef,
'--',
...types,
]);
}
return output.split('\n').where(
(String line) => line.isNotEmpty && !line.contains('third_party')
).toList();
}
/// Generates a reporting function to supply to ProcessRunner to use instead
/// of the default reporting function.
@protected
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');
};
}
/// Clears the last printed report line so garbage isn't left on the terminal.
@protected
void reportDone() {
stdout.write('\r${' ' * 100}\r');
}
}
/// Checks and formats C++/ObjC/Shader files using clang-format.
class ClangFormatChecker extends FormatChecker {
ClangFormatChecker({
super.processManager,
required super.baseGitRef,
required super.repoDir,
required Directory srcDir,
super.allFiles,
super.messageCallback,
}) {
/*late*/ String clangOs;
if (Platform.isLinux) {
clangOs = 'linux-x64';
} else if (Platform.isMacOS) {
clangOs = 'mac-x64';
} else if (Platform.isWindows) {
clangOs = 'windows-x64';
} else {
throw FormattingException(
"Unknown operating system: don't know how to run clang-format here.");
}
clangFormat = File(
path.join(
srcDir.absolute.path,
'flutter',
'buildtools',
clangOs,
'clang',
'bin',
'clang-format',
),
);
}
late final File clangFormat;
@override
Future<bool> checkFormatting() async {
final List<String> failures = await _getCFormatFailures();
failures.map(stdout.writeln);
return failures.isEmpty;
}
@override
Future<bool> fixFormatting() async {
message('Fixing C++/ObjC/Shader formatting...');
final List<String> failures = await _getCFormatFailures(fixing: true);
if (failures.isEmpty) {
return true;
}
return applyPatch(failures);
}
Future<String> _getClangFormatVersion() async {
final ProcessRunnerResult result =
await _processRunner.runProcess(<String>[clangFormat.path, '--version']);
return result.stdout.trim();
}
Future<List<String>> _getCFormatFailures({bool fixing = false}) async {
message('Checking C++/ObjC/Shader formatting...');
const List<String> clangFiletypes = <String>[
'*.c',
'*.cc',
'*.cxx',
'*.cpp',
'*.h',
'*.m',
'*.mm',
'*.glsl',
'*.hlsl',
'*.comp',
'*.tese',
'*.tesc',
'*.vert',
'*.frag',
];
final List<String> files = await getFileList(clangFiletypes);
if (files.isEmpty) {
message('No C++/ObjC/Shader files with changes, skipping C++/ObjC/Shader format check.');
return <String>[];
}
if (verbose) {
message('Using ${await _getClangFormatVersion()}');
}
final List<WorkerJob> clangJobs = <WorkerJob>[];
for (final String file in files) {
if (file.trim().isEmpty) {
continue;
}
clangJobs.add(WorkerJob(<String>[clangFormat.path, '--style=file', file.trim()]));
}
final ProcessPool clangPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('clang-format'),
);
final Stream<WorkerJob> completedClangFormats = clangPool.startWorkers(clangJobs);
final List<WorkerJob> diffJobs = <WorkerJob>[];
await for (final WorkerJob completedJob in completedClangFormats) {
if (completedJob.result.exitCode == 0) {
diffJobs.add(
WorkerJob(<String>[
'git',
'diff',
'--no-index',
'--no-color',
'--ignore-cr-at-eol',
'--',
completedJob.command.last,
'-',
],
stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw)),
);
} else {
final String formatterCommand = completedJob.command.join(' ');
error("Formatter command '$formatterCommand' failed with exit code "
'${completedJob.result.exitCode}. Command output follows:\n\n'
'${completedJob.result.output}');
}
}
final ProcessPool diffPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('diff'),
);
final List<WorkerJob> completedDiffs = await diffPool.runToCompletion(diffJobs);
final Iterable<WorkerJob> failed = completedDiffs.where((WorkerJob job) {
return job.result.exitCode != 0;
});
reportDone();
if (failed.isNotEmpty) {
final bool plural = failed.length > 1;
if (fixing) {
message('Fixing ${failed.length} C++/ObjC/Shader file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
} else {
error('Found ${failed.length} C++/ObjC/Shader file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
stdout.writeln('To fix, run `et format` or:');
stdout.writeln();
stdout.writeln('git apply <<DONE');
for (final WorkerJob job in failed) {
stdout.write(job.result.stdout
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'));
}
stdout.writeln('DONE');
stdout.writeln();
}
} else {
message('Completed checking ${diffJobs.length} C++/ObjC/Shader files with no formatting problems.');
}
return failed.map<String>((WorkerJob job) {
return job.result.stdout
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}');
}).toList();
}
}
/// Checks the format of Java files uing the Google Java format checker.
class JavaFormatChecker extends FormatChecker {
JavaFormatChecker({
super.processManager,
required super.baseGitRef,
required super.repoDir,
required Directory srcDir,
super.allFiles,
super.messageCallback,
}) {
googleJavaFormatJar = File(
path.absolute(
path.join(
srcDir.absolute.path,
'third_party',
'android_tools',
'google-java-format',
'google-java-format-1.7-all-deps.jar',
),
),
);
}
late final File googleJavaFormatJar;
Future<String> _getGoogleJavaFormatVersion() async {
final ProcessRunnerResult result = await _processRunner
.runProcess(<String>['java', '-jar', googleJavaFormatJar.path, '--version']);
return result.stderr.trim();
}
@override
Future<bool> checkFormatting() async {
final List<String> failures = await _getJavaFormatFailures();
failures.map(stdout.writeln);
return failures.isEmpty;
}
@override
Future<bool> fixFormatting() async {
message('Fixing Java formatting...');
final List<String> failures = await _getJavaFormatFailures(fixing: true);
if (failures.isEmpty) {
return true;
}
return applyPatch(failures);
}
Future<String> _getJavaVersion() async {
final ProcessRunnerResult result =
await _processRunner.runProcess(<String>['java', '-version']);
return result.stderr.trim().split('\n')[0];
}
Future<List<String>> _getJavaFormatFailures({bool fixing = false}) async {
message('Checking Java formatting...');
final List<WorkerJob> formatJobs = <WorkerJob>[];
final List<String> files = await getFileList(<String>['*.java']);
if (files.isEmpty) {
message('No Java files with changes, skipping Java format check.');
return <String>[];
}
String javaVersion = '<unknown>';
String javaFormatVersion = '<unknown>';
try {
javaVersion = await _getJavaVersion();
} on ProcessRunnerException {
error('Cannot run Java, skipping Java file formatting!');
return const <String>[];
}
try {
javaFormatVersion = await _getGoogleJavaFormatVersion();
} on ProcessRunnerException {
error('Cannot find google-java-format, skipping Java format check.');
return const <String>[];
}
if (verbose) {
message('Using $javaFormatVersion with Java $javaVersion');
}
for (final String file in files) {
if (file.trim().isEmpty) {
continue;
}
formatJobs.add(
WorkerJob(
<String>['java', '-jar', googleJavaFormatJar.path, file.trim()],
),
);
}
final ProcessPool formatPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('Java format'),
);
final Stream<WorkerJob> completedJavaFormats = formatPool.startWorkers(formatJobs);
final List<WorkerJob> diffJobs = <WorkerJob>[];
await for (final WorkerJob completedJob in completedJavaFormats) {
if (completedJob.result.exitCode == 0) {
diffJobs.add(
WorkerJob(
<String>[
'git',
'diff',
'--no-index',
'--no-color',
'--ignore-cr-at-eol',
'--',
completedJob.command.last,
'-',
],
stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw),
),
);
} else {
final String formatterCommand = completedJob.command.join(' ');
error("Formatter command '$formatterCommand' failed with exit code "
'${completedJob.result.exitCode}. Command output follows:\n\n'
'${completedJob.result.output}');
}
}
final ProcessPool diffPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('diff'),
);
final List<WorkerJob> completedDiffs = await diffPool.runToCompletion(diffJobs);
final Iterable<WorkerJob> failed = completedDiffs.where((WorkerJob job) {
return job.result.exitCode != 0;
});
reportDone();
if (failed.isNotEmpty) {
final bool plural = failed.length > 1;
if (fixing) {
error('Fixing ${failed.length} Java file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
} else {
error('Found ${failed.length} Java file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
stdout.writeln('To fix, run `et format` or:');
stdout.writeln();
stdout.writeln('git apply <<DONE');
for (final WorkerJob job in failed) {
stdout.write(job.result.stdout
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'));
}
stdout.writeln('DONE');
stdout.writeln();
}
} else {
message('Completed checking ${diffJobs.length} Java files with no formatting problems.');
}
return failed.map<String>((WorkerJob job) {
return job.result.stdout
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}');
}).toList();
}
}
/// Checks the format of any BUILD.gn files using the "gn format" command.
class GnFormatChecker extends FormatChecker {
GnFormatChecker({
super.processManager,
required super.baseGitRef,
required Directory repoDir,
super.allFiles,
super.messageCallback,
}) : super(
repoDir: repoDir,
) {
gnBinary = File(
path.join(
repoDir.absolute.path,
'third_party',
'gn',
Platform.isWindows ? 'gn.exe' : 'gn',
),
);
}
late final File gnBinary;
@override
Future<bool> checkFormatting() async {
message('Checking GN formatting...');
return (await _runGnCheck(fixing: false)) == 0;
}
@override
Future<bool> fixFormatting() async {
message('Fixing GN formatting...');
await _runGnCheck(fixing: true);
// The GN script shouldn't fail when fixing errors.
return true;
}
Future<int> _runGnCheck({required bool fixing}) async {
final List<String> filesToCheck = await getFileList(<String>['*.gn', '*.gni']);
final List<String> cmd = <String>[
gnBinary.path,
'format',
if (!fixing) '--stdin',
];
final List<WorkerJob> jobs = <WorkerJob>[];
for (final String file in filesToCheck) {
if (fixing) {
jobs.add(WorkerJob(
<String>[...cmd, file],
name: <String>[...cmd, file].join(' '),
));
} else {
final WorkerJob job = WorkerJob(
cmd,
stdinRaw: codeUnitsAsStream(
File(path.join(repoDir.absolute.path, file)).readAsBytesSync(),
),
name: <String>[...cmd, file].join(' '),
);
jobs.add(job);
}
}
final ProcessPool gnPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('gn format'),
);
final Stream<WorkerJob> completedJobs = gnPool.startWorkers(jobs);
final List<WorkerJob> diffJobs = <WorkerJob>[];
await for (final WorkerJob completedJob in completedJobs) {
if (completedJob.result.exitCode == 0) {
diffJobs.add(
WorkerJob(
<String>[
'git',
'diff',
'--no-index',
'--no-color',
'--ignore-cr-at-eol',
'--',
completedJob.name.split(' ').last,
'-'
],
stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw),
),
);
} else {
final String formatterCommand = completedJob.command.join(' ');
error("Formatter command '$formatterCommand' failed with exit code "
'${completedJob.result.exitCode}. Command output follows:\n\n'
'${completedJob.result.output}');
}
}
final ProcessPool diffPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('diff'),
);
final List<WorkerJob> completedDiffs =
await diffPool.runToCompletion(diffJobs);
final Iterable<WorkerJob> failed = completedDiffs.where((WorkerJob job) {
return job.result.exitCode != 0;
});
reportDone();
if (failed.isNotEmpty) {
final bool plural = failed.length > 1;
if (fixing) {
message('Fixed ${failed.length} GN file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
} else {
error('Found ${failed.length} GN file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
stdout.writeln('To fix, run `et format` or:');
stdout.writeln();
stdout.writeln('git apply <<DONE');
for (final WorkerJob job in failed) {
stdout.write(job.result.stdout
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}'));
}
stdout.writeln('DONE');
stdout.writeln();
}
} else {
message('Completed checking ${completedDiffs.length} GN files with no '
'formatting problems.');
}
return failed.length;
}
}
/// Checks the format of any .py files using the "yapf" command.
class PythonFormatChecker extends FormatChecker {
PythonFormatChecker({
super.processManager,
required super.baseGitRef,
required Directory repoDir,
super.allFiles,
super.messageCallback,
}) : super(
repoDir: repoDir,
) {
yapfBin = File(path.join(
repoDir.absolute.path,
'tools',
Platform.isWindows ? 'yapf.bat' : 'yapf.sh',
));
_yapfStyle = File(path.join(
repoDir.absolute.path,
'.style.yapf',
));
}
late final File yapfBin;
late final File _yapfStyle;
@override
Future<bool> checkFormatting() async {
message('Checking Python formatting...');
return (await _runYapfCheck(fixing: false)) == 0;
}
@override
Future<bool> fixFormatting() async {
message('Fixing Python formatting...');
await _runYapfCheck(fixing: true);
// The yapf script shouldn't fail when fixing errors.
return true;
}
Future<int> _runYapfCheck({required bool fixing}) async {
final List<String> filesToCheck = <String>[
...await getFileList(<String>['*.py']),
// Always include flutter/tools/gn.
'${repoDir.path}/tools/gn',
];
final List<String> cmd = <String>[
yapfBin.path,
'--style', _yapfStyle.path,
if (!fixing) '--diff',
if (fixing) '--in-place',
];
final List<WorkerJob> jobs = <WorkerJob>[];
for (final String file in filesToCheck) {
jobs.add(WorkerJob(<String>[...cmd, file]));
}
final ProcessPool yapfPool = ProcessPool(
processRunner: _processRunner,
printReport: namedReport('python format'),
);
final List<WorkerJob> completedJobs = await yapfPool.runToCompletion(jobs);
reportDone();
final List<String> incorrect = <String>[];
for (final WorkerJob job in completedJobs) {
if (job.result.exitCode == 1) {
incorrect.add(' ${job.command.last}\n${job.result.output}');
}
}
if (incorrect.isNotEmpty) {
final bool plural = incorrect.length > 1;
if (fixing) {
message('Fixed ${incorrect.length} python file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
} else {
error('Found ${incorrect.length} python file${plural ? 's' : ''}'
' which ${plural ? 'were' : 'was'} formatted incorrectly:');
stdout.writeln('To fix, run `et format` or:');
stdout.writeln();
stdout.writeln('git apply <<DONE');
incorrect.forEach(stdout.writeln);
stdout.writeln('DONE');
stdout.writeln();
}
} else {
message('All python files formatted correctly.');
}
return incorrect.length;
}
}
@immutable
class _GrepResult {
const _GrepResult(this.file, [this.hits = const <String>[], this.lineNumbers = const <int>[]]);
bool get isEmpty => hits.isEmpty && lineNumbers.isEmpty;
final File file;
final List<String> hits;
final List<int> lineNumbers;
}
/// Checks for trailing whitspace in Dart files.
class WhitespaceFormatChecker extends FormatChecker {
WhitespaceFormatChecker({
super.processManager,
required super.baseGitRef,
required super.repoDir,
super.allFiles,
super.messageCallback,
});
@override
Future<bool> checkFormatting() async {
final List<File> failures = await _getWhitespaceFailures();
return failures.isEmpty;
}
static final RegExp trailingWsRegEx = RegExp(r'[ \t]+$', multiLine: true);
@override
Future<bool> fixFormatting() async {
final List<File> failures = await _getWhitespaceFailures();
if (failures.isNotEmpty) {
for (final File file in failures) {
stderr.writeln('Fixing $file');
String contents = file.readAsStringSync();
contents = contents.replaceAll(trailingWsRegEx, '');
file.writeAsStringSync(contents);
}
}
return true;
}
static _GrepResult _hasTrailingWhitespace(File file) {
final List<String> hits = <String>[];
final List<int> lineNumbers = <int>[];
int lineNumber = 0;
for (final String line in file.readAsLinesSync()) {
if (trailingWsRegEx.hasMatch(line)) {
hits.add(line);
lineNumbers.add(lineNumber);
}
lineNumber++;
}
if (hits.isEmpty) {
return _GrepResult(file);
}
return _GrepResult(file, hits, lineNumbers);
}
Iterable<_GrepResult> _whereHasTrailingWhitespace(Iterable<File> files) {
return files.map(_hasTrailingWhitespace);
}
Future<List<File>> _getWhitespaceFailures() async {
final List<String> files = await getFileList(<String>[
'*.c',
'*.cc',
'*.cpp',
'*.cxx',
'*.dart',
'*.gn',
'*.gni',
'*.gradle',
'*.h',
'*.java',
'*.json',
'*.m',
'*.mm',
'*.py',
'*.sh',
'*.yaml',
]);
if (files.isEmpty) {
message('No files that differ, skipping whitespace check.');
return <File>[];
}
message('Checking for trailing whitespace on ${files.length} source '
'file${files.length > 1 ? 's' : ''}...');
final ProcessPoolProgressReporter reporter = namedReport('whitespace');
final List<_GrepResult> found = <_GrepResult>[];
final int total = files.length;
int completed = 0;
int inProgress = Platform.numberOfProcessors;
int pending = total;
int failed = 0;
for (final _GrepResult result in _whereHasTrailingWhitespace(
files.map<File>(
(String file) => File(
path.join(repoDir.absolute.path, file),
),
),
)) {
if (result.isEmpty) {
completed++;
} else {
failed++;
found.add(result);
}
pending--;
inProgress = pending < Platform.numberOfProcessors ? pending : Platform.numberOfProcessors;
reporter(total, completed, inProgress, pending, failed);
}
reportDone();
if (found.isNotEmpty) {
error('Whitespace check failed. The following files have trailing spaces:');
for (final _GrepResult result in found) {
for (int i = 0; i < result.hits.length; ++i) {
message(' ${result.file.path}:${result.lineNumbers[i]}:${result.hits[i]}');
}
}
} else {
message('No trailing whitespace found.');
}
return found.map<File>((_GrepResult result) => result.file).toList();
}
}
final class HeaderFormatChecker extends FormatChecker {
HeaderFormatChecker({
required super.baseGitRef,
required super.repoDir,
super.processManager,
super.allFiles,
super.messageCallback,
});
// $ENGINE/flutter/third_party/dart/tools/sdks/dart-sdk/bin/dart
late final String _dartBin = path.join(
repoDir.absolute.parent.path,
'flutter',
'third_party',
'dart',
'tools',
'sdks',
'dart-sdk',
'bin',
'dart',
);
// $ENGINE/src/flutter/tools/bin/main.dart
late final String _headerGuardCheckBin = path.join(
repoDir.absolute.path,
'tools',
'header_guard_check',
'bin',
'main.dart',
);
@override
Future<bool> checkFormatting() async {
final List<String> include = <String>[];
if (!allFiles) {
include.addAll(await getFileList(<String>[
'*.h',
]));
if (include.isEmpty) {
message('No header files with changes, skipping header guard check.');
return true;
}
}
final List<String> args = <String>[
_dartBin,
'--disable-dart-dev',
_headerGuardCheckBin,
...include.map((String f) => '--include=$f'),
];
// TIP: --exclude is encoded into the tool itself.
// see tools/header_guard_check/lib/header_guard_check.dart
final ProcessRunnerResult result = await _processRunner.runProcess(args);
if (result.exitCode != 0) {
error('Header check failed. The following files have incorrect header guards:');
message(result.stdout);
return false;
}
return true;
}
@override
Future<bool> fixFormatting() async {
final List<String> include = <String>[];
if (!allFiles) {
include.addAll(await getFileList(<String>[
'*.h',
]));
if (include.isEmpty) {
message('No header files with changes, skipping header guard fix.');
return true;
}
}
final List<String> args = <String>[
_dartBin,
'--disable-dart-dev',
_headerGuardCheckBin,
'--fix',
...include.map((String f) => '--include=$f'),
];
// TIP: --exclude is encoded into the tool itself.
// see tools/header_guard_check/lib/header_guard_check.dart
final ProcessRunnerResult result = await _processRunner.runProcess(args);
if (result.exitCode != 0) {
error('Header check fix failed:');
message(result.stdout);
return false;
}
return true;
}
}
Future<String> _getDiffBaseRevision(ProcessManager processManager, Directory repoDir) async {
final ProcessRunner processRunner = ProcessRunner(
defaultWorkingDirectory: repoDir,
processManager: processManager,
);
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();
}
void _usage(ArgParser parser, {int exitCode = 1}) {
stderr.writeln('format.dart [--help] [--fix] [--all-files] '
'[--check <${formatCheckNames().join('|')}>]');
stderr.writeln(parser.usage);
exit(exitCode);
}
bool verbose = false;
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.');
parser.addMultiOption('check',
abbr: 'c',
allowed: formatCheckNames(),
defaultsTo: formatCheckNames(),
help: 'Specifies which checks will be performed. Defaults to all checks. '
'May be specified more than once to perform multiple types of checks. ');
parser.addFlag('verbose', help: 'Print verbose output.', defaultsTo: verbose);
late final ArgResults options;
try {
options = parser.parse(arguments);
} on FormatException catch (e) {
stderr.writeln('ERROR: $e');
_usage(parser, exitCode: 0);
}
verbose = options['verbose'] as bool;
if (options['help'] as bool) {
_usage(parser, exitCode: 0);
}
final File script = File.fromUri(Platform.script).absolute;
final Directory repoDir = script.parent.parent.parent;
final Directory srcDir = repoDir.parent;
if (verbose) {
stderr.writeln('Repo: $repoDir');
stderr.writeln('Src: $srcDir');
}
void message(String? message, {MessageType type = MessageType.message}) {
message ??= '';
switch (type) {
case MessageType.message:
stdout.writeln(message);
case MessageType.error:
stderr.writeln('ERROR: $message');
case MessageType.warning:
stderr.writeln('WARNING: $message');
}
}
const ProcessManager processManager = LocalProcessManager();
final String baseGitRef = await _getDiffBaseRevision(processManager, repoDir);
bool result = true;
final List<String> checks = options['check'] as List<String>;
try {
for (final String checkName in checks) {
final FormatCheck check = nameToFormatCheck(checkName);
final String humanCheckName = formatCheckToName(check);
final FormatChecker checker = FormatChecker.ofType(check,
baseGitRef: baseGitRef,
repoDir: repoDir,
srcDir: srcDir,
allFiles: options['all-files'] as bool,
messageCallback: message);
bool stepResult;
if (options['fix'] as bool) {
message('Fixing any $humanCheckName format problems');
stepResult = await checker.fixFormatting();
if (!stepResult) {
message('Unable to apply $humanCheckName format fixes.');
}
} else {
stepResult = await checker.checkFormatting();
}
result = result && stepResult;
}
} on FormattingException catch (e) {
message('ERROR: $e', type: MessageType.error);
}
exit(result ? 0 : 1);
}