| // 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'; |
| // ignore: import_of_legacy_library_into_null_safe |
| 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; |
| default: |
| throw FormattingException('Unknown FormatCheck type $name'); |
| } |
| } |
| |
| 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'; |
| } |
| } |
| |
| 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.java: |
| return JavaFormatChecker( |
| 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, |
| ); |
| case FormatCheck.gn: |
| return GnFormatChecker( |
| 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), |
| 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 = 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 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.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 = 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 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.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 = 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; |
| } |
| } |
| |
| @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 (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 _GrepResult(file); |
| } |
| 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.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, '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); |
| |
| 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, |
| 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); |
| } |