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