| // 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/third_party/dart/tools/sdks/dart-sdk/bin/dart | |
| late final String _dartBin = path.join( | |
| repoDir.absolute.parent.path, | |
| '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); | |
| } |