forked from firka/flutter
1045 lines
31 KiB
Dart
1045 lines
31 KiB
Dart
// 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.
|
|
|
|
// TODO(gspencergoog): Support clang formatting on Windows.
|
|
// TODO(gspencergoog): Support Java formatting on Windows.
|
|
|
|
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;
|
|
|
|
int get exitCode => result?.exitCode ?? -1;
|
|
|
|
@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 {
|
|
clang,
|
|
gn,
|
|
java,
|
|
python,
|
|
whitespace,
|
|
}
|
|
|
|
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;
|
|
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';
|
|
}
|
|
}
|
|
|
|
List<String> formatCheckNames() {
|
|
List<FormatCheck> allowed;
|
|
if (!Platform.isWindows) {
|
|
allowed = FormatCheck.values;
|
|
} else {
|
|
allowed = <FormatCheck>[FormatCheck.gn, FormatCheck.whitespace];
|
|
}
|
|
return allowed
|
|
.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 = 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,
|
|
required this.srcDir,
|
|
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,
|
|
srcDir: srcDir,
|
|
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,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
);
|
|
case FormatCheck.whitespace:
|
|
return WhitespaceFormatChecker(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
);
|
|
}
|
|
}
|
|
|
|
final ProcessRunner _processRunner;
|
|
final Directory srcDir;
|
|
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>['patch', '-p0'],
|
|
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).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);
|
|
|
|
stderr.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() {
|
|
stderr.write('\r${' ' * 100}\r');
|
|
}
|
|
}
|
|
|
|
/// Checks and formats C++/ObjC/Shader files using clang-format.
|
|
class ClangFormatChecker extends FormatChecker {
|
|
ClangFormatChecker({
|
|
ProcessManager processManager = const LocalProcessManager(),
|
|
required String baseGitRef,
|
|
required Directory repoDir,
|
|
required Directory srcDir,
|
|
bool allFiles = false,
|
|
MessageCallback? messageCallback,
|
|
}) : super(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
) {
|
|
/*late*/ String clangOs;
|
|
if (Platform.isLinux) {
|
|
clangOs = 'linux-x64';
|
|
} else if (Platform.isMacOS) {
|
|
clangOs = 'mac-x64';
|
|
} else {
|
|
throw FormattingException(
|
|
"Unknown operating system: don't know how to run clang-format here.");
|
|
}
|
|
clangFormat = File(
|
|
path.join(
|
|
srcDir.absolute.path,
|
|
'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>['diff', '-u', 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);
|
|
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:');
|
|
stdout.writeln();
|
|
stdout.writeln('patch -p0 <<DONE');
|
|
for (final WorkerJob job in failed) {
|
|
stdout.write(job.result.stdout);
|
|
}
|
|
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;
|
|
}).toList();
|
|
}
|
|
}
|
|
|
|
/// Checks the format of Java files uing the Google Java format checker.
|
|
class JavaFormatChecker extends FormatChecker {
|
|
JavaFormatChecker({
|
|
ProcessManager processManager = const LocalProcessManager(),
|
|
required String baseGitRef,
|
|
required Directory repoDir,
|
|
required Directory srcDir,
|
|
bool allFiles = false,
|
|
MessageCallback? messageCallback,
|
|
}) : super(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: 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> completedClangFormats = formatPool.startWorkers(formatJobs);
|
|
final List<WorkerJob> diffJobs = <WorkerJob>[];
|
|
await for (final WorkerJob completedJob in completedClangFormats) {
|
|
if (completedJob.result.exitCode == 0) {
|
|
diffJobs.add(
|
|
WorkerJob(
|
|
<String>['diff', '-u', 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);
|
|
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:');
|
|
stdout.writeln();
|
|
stdout.writeln('patch -p0 <<DONE');
|
|
for (final WorkerJob job in failed) {
|
|
stdout.write(job.result.stdout);
|
|
}
|
|
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;
|
|
}).toList();
|
|
}
|
|
}
|
|
|
|
/// Checks the format of any BUILD.gn files using the "gn format" command.
|
|
class GnFormatChecker extends FormatChecker {
|
|
GnFormatChecker({
|
|
ProcessManager processManager = const LocalProcessManager(),
|
|
required String baseGitRef,
|
|
required Directory repoDir,
|
|
required Directory srcDir,
|
|
bool allFiles = false,
|
|
MessageCallback? messageCallback,
|
|
}) : super(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
) {
|
|
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) '--dry-run',
|
|
];
|
|
final List<WorkerJob> jobs = <WorkerJob>[];
|
|
for (final String file in filesToCheck) {
|
|
jobs.add(WorkerJob(<String>[...cmd, file]));
|
|
}
|
|
final ProcessPool gnPool = ProcessPool(
|
|
processRunner: _processRunner,
|
|
printReport: namedReport('gn format'),
|
|
);
|
|
final List<WorkerJob> completedJobs = await gnPool.runToCompletion(jobs);
|
|
reportDone();
|
|
final List<String> incorrect = <String>[];
|
|
for (final WorkerJob job in completedJobs) {
|
|
if (job.result.exitCode == 2) {
|
|
incorrect.add(' ${job.command.last}');
|
|
}
|
|
if (job.result.exitCode == 1) {
|
|
// GN has exit code 1 if it had some problem formatting/checking the
|
|
// file.
|
|
throw FormattingException(
|
|
'Unable to format ${job.command.last}:\n${job.result.output}',
|
|
);
|
|
}
|
|
}
|
|
if (incorrect.isNotEmpty) {
|
|
final bool plural = incorrect.length > 1;
|
|
if (fixing) {
|
|
message('Fixed ${incorrect.length} GN file${plural ? 's' : ''}'
|
|
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
|
|
} else {
|
|
error('Found ${incorrect.length} GN file${plural ? 's' : ''}'
|
|
' which ${plural ? 'were' : 'was'} formatted incorrectly:');
|
|
incorrect.forEach(stderr.writeln);
|
|
}
|
|
} else {
|
|
message('All GN files formatted correctly.');
|
|
}
|
|
return incorrect.length;
|
|
}
|
|
}
|
|
|
|
/// Checks the format of any .py files using the "yapf" command.
|
|
class PythonFormatChecker extends FormatChecker {
|
|
PythonFormatChecker({
|
|
ProcessManager processManager = const LocalProcessManager(),
|
|
required String baseGitRef,
|
|
required Directory repoDir,
|
|
required Directory srcDir,
|
|
bool allFiles = false,
|
|
MessageCallback? messageCallback,
|
|
}) : super(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
) {
|
|
yapfBin = File(path.join(
|
|
repoDir.absolute.path,
|
|
'tools',
|
|
'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:');
|
|
incorrect.forEach(stderr.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({
|
|
ProcessManager processManager = const LocalProcessManager(),
|
|
required String baseGitRef,
|
|
required Directory repoDir,
|
|
required Directory srcDir,
|
|
bool allFiles = false,
|
|
MessageCallback? messageCallback,
|
|
}) : super(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: 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();
|
|
}
|
|
}
|
|
|
|
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. '
|
|
'On Windows, only whitespace and gn checks are currently supported.');
|
|
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:
|
|
stderr.writeln(message);
|
|
break;
|
|
case MessageType.error:
|
|
stderr.writeln('ERROR: $message');
|
|
break;
|
|
case MessageType.warning:
|
|
stderr.writeln('WARNING: $message');
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 {
|
|
message('Performing $humanCheckName format check');
|
|
stepResult = await checker.checkFormatting();
|
|
if (!stepResult) {
|
|
message('Found $humanCheckName format problems.');
|
|
}
|
|
}
|
|
result = result && stepResult;
|
|
}
|
|
} on FormattingException catch (e) {
|
|
message('ERROR: $e', type: MessageType.error);
|
|
}
|
|
|
|
exit(result ? 0 : 1);
|
|
}
|