mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
When I rewrote the format script, I forgot to filter out deleted files from the list of files that it looks at. This filters the file list to exclude deleted files
938 lines
28 KiB
Dart
938 lines
28 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.
|
|
// TODO(gspencergoog): Convert to null safety.
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:args/args.dart';
|
|
import 'package:isolate/isolate.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:process_runner/process_runner.dart';
|
|
import 'package:process/process.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) {
|
|
output.write(':\n$stderr');
|
|
}
|
|
return output.toString();
|
|
}
|
|
}
|
|
|
|
enum MessageType {
|
|
message,
|
|
error,
|
|
warning,
|
|
}
|
|
|
|
enum FormatCheck {
|
|
clang,
|
|
java,
|
|
whitespace,
|
|
gn,
|
|
}
|
|
|
|
FormatCheck nameToFormatCheck(String name) {
|
|
switch (name) {
|
|
case 'clang':
|
|
return FormatCheck.clang;
|
|
case 'java':
|
|
return FormatCheck.java;
|
|
case 'whitespace':
|
|
return FormatCheck.whitespace;
|
|
case 'gn':
|
|
return FormatCheck.gn;
|
|
}
|
|
assert(false, 'Unknown FormatCheck type $name');
|
|
return null;
|
|
}
|
|
|
|
String formatCheckToName(FormatCheck check) {
|
|
switch (check) {
|
|
case FormatCheck.clang:
|
|
return 'C++/ObjC';
|
|
case FormatCheck.java:
|
|
return 'Java';
|
|
case FormatCheck.whitespace:
|
|
return 'Trailing whitespace';
|
|
case FormatCheck.gn:
|
|
return 'GN';
|
|
}
|
|
assert(false, 'Unhandled FormatCheck type $check');
|
|
return null;
|
|
}
|
|
|
|
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,
|
|
@required this.baseGitRef,
|
|
@required this.repoDir,
|
|
@required this.srcDir,
|
|
this.allFiles = false,
|
|
this.messageCallback,
|
|
}) : _processRunner = ProcessRunner(
|
|
defaultWorkingDirectory: repoDir,
|
|
processManager: processManager ?? const LocalProcessManager(),
|
|
);
|
|
|
|
/// Factory method that creates subclass format checkers based on the type of check.
|
|
factory FormatChecker.ofType(
|
|
FormatCheck check, {
|
|
ProcessManager /*?*/ processManager,
|
|
@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,
|
|
);
|
|
break;
|
|
case FormatCheck.java:
|
|
return JavaFormatChecker(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
);
|
|
break;
|
|
case FormatCheck.whitespace:
|
|
return WhitespaceFormatChecker(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
);
|
|
break;
|
|
case FormatCheck.gn:
|
|
return GnFormatChecker(
|
|
processManager: processManager,
|
|
baseGitRef: baseGitRef,
|
|
repoDir: repoDir,
|
|
srcDir: srcDir,
|
|
allFiles: allFiles,
|
|
messageCallback: messageCallback,
|
|
);
|
|
break;
|
|
}
|
|
assert(false, 'Unhandled FormatCheck type $check');
|
|
return null;
|
|
}
|
|
|
|
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* {
|
|
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),
|
|
failOk: true,
|
|
);
|
|
}).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 files using clang-format.
|
|
class ClangFormatChecker extends FormatChecker {
|
|
ClangFormatChecker({
|
|
ProcessManager /*?*/ processManager,
|
|
@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*/ 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 formatting...');
|
|
final List<String> failures = await _getCFormatFailures(fixing: true);
|
|
if (failures.isEmpty) {
|
|
return true;
|
|
}
|
|
return await 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 formatting...');
|
|
const List<String> clangFiletypes = <String>[
|
|
'*.c',
|
|
'*.cc',
|
|
'*.cxx',
|
|
'*.cpp',
|
|
'*.h',
|
|
'*.m',
|
|
'*.mm',
|
|
];
|
|
final List<String> files = await getFileList(clangFiletypes);
|
|
if (files.isEmpty) {
|
|
message('No C++/ObjC files with changes, skipping C++/ObjC format check.');
|
|
return <String>[];
|
|
}
|
|
if (verbose) {
|
|
message('Using ${await _getClangFormatVersion()}');
|
|
}
|
|
final List<WorkerJob> clangJobs = <WorkerJob>[];
|
|
for (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 != null && completedJob.result.exitCode == 0) {
|
|
diffJobs.add(
|
|
WorkerJob(<String>['diff', '-u', completedJob.command.last, '-'],
|
|
stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw), failOk: true),
|
|
);
|
|
}
|
|
}
|
|
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 file${plural ? 's' : ''}'
|
|
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
|
|
} else {
|
|
error('Found ${failed.length} C++/ObjC file${plural ? 's' : ''}'
|
|
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
|
|
for (final WorkerJob job in failed) {
|
|
stdout.write(job.result.stdout);
|
|
}
|
|
}
|
|
} else {
|
|
message('Completed checking ${diffJobs.length} C++/ObjC 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,
|
|
@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*/ 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 await 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 (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 != null && completedJob.result.exitCode == 0) {
|
|
diffJobs.add(
|
|
WorkerJob(
|
|
<String>['diff', '-u', completedJob.command.last, '-'],
|
|
stdinRaw: codeUnitsAsStream(completedJob.result.stdoutRaw),
|
|
failOk: true,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
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.');
|
|
for (final WorkerJob job in failed) {
|
|
stdout.write(job.result.stdout);
|
|
}
|
|
}
|
|
} 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,
|
|
@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*/ 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;
|
|
}
|
|
}
|
|
|
|
@immutable
|
|
class _GrepResult {
|
|
const _GrepResult(this.file, this.hits, this.lineNumbers);
|
|
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,
|
|
@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 (File file in failures) {
|
|
stderr.writeln('Fixing $file');
|
|
String contents = file.readAsStringSync();
|
|
contents = contents.replaceAll(trailingWsRegEx, '');
|
|
file.writeAsStringSync(contents);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static Future<_GrepResult> _hasTrailingWhitespace(File file) async {
|
|
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 null;
|
|
}
|
|
return _GrepResult(file, hits, lineNumbers);
|
|
}
|
|
|
|
Stream<_GrepResult> _whereHasTrailingWhitespace(Iterable<File> files) async* {
|
|
final LoadBalancer pool =
|
|
await LoadBalancer.create(Platform.numberOfProcessors, IsolateRunner.spawn);
|
|
for (final File file in files) {
|
|
yield await pool.run<_GrepResult, File>(_hasTrailingWhitespace, file);
|
|
}
|
|
}
|
|
|
|
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;
|
|
await for (final _GrepResult result in _whereHasTrailingWhitespace(
|
|
files.map<File>(
|
|
(String file) => File(
|
|
path.join(repoDir.absolute.path, file),
|
|
),
|
|
),
|
|
)) {
|
|
if (result == null) {
|
|
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 ?? const LocalProcessManager(),
|
|
);
|
|
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, 'master'], 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.',
|
|
defaultsTo: false);
|
|
parser.addFlag('all-files',
|
|
abbr: 'a',
|
|
help: 'Instead of just checking for formatting errors in changed files, '
|
|
'check for them in all files.',
|
|
defaultsTo: false);
|
|
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);
|
|
|
|
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}) {
|
|
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,
|
|
processManager: processManager,
|
|
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);
|
|
}
|