|
| 1 | +// Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +import 'dart:io'; |
| 6 | + |
| 7 | +import 'package:args/args.dart'; |
| 8 | +import 'package:path/path.dart' as path; |
| 9 | +import 'package:process_runner/process_runner.dart'; |
| 10 | + |
| 11 | +Future<int> main(List<String> arguments) async { |
| 12 | + final ArgParser parser = ArgParser(); |
| 13 | + parser.addFlag('help', help: 'Print help.', abbr: 'h'); |
| 14 | + parser.addFlag('fix', |
| 15 | + abbr: 'f', |
| 16 | + help: 'Instead of just checking for formatting errors, fix them in place.'); |
| 17 | + parser.addFlag('all-files', |
| 18 | + abbr: 'a', |
| 19 | + help: 'Instead of just checking for formatting errors in changed files, ' |
| 20 | + 'check for them in all files.'); |
| 21 | + |
| 22 | + late final ArgResults options; |
| 23 | + try { |
| 24 | + options = parser.parse(arguments); |
| 25 | + } on FormatException catch (e) { |
| 26 | + stderr.writeln('ERROR: $e'); |
| 27 | + _usage(parser, exitCode: 0); |
| 28 | + } |
| 29 | + |
| 30 | + if (options['help'] as bool) { |
| 31 | + _usage(parser, exitCode: 0); |
| 32 | + } |
| 33 | + |
| 34 | + final File script = File.fromUri(Platform.script).absolute; |
| 35 | + final Directory flutterRoot = script.parent.parent.parent.parent; |
| 36 | + |
| 37 | + final bool result = (await DartFormatChecker( |
| 38 | + flutterRoot: flutterRoot, |
| 39 | + allFiles: options['all-files'] as bool, |
| 40 | + ).check(fix: options['fix'] as bool)) == 0; |
| 41 | + |
| 42 | + exit(result ? 0 : 1); |
| 43 | +} |
| 44 | + |
| 45 | +void _usage(ArgParser parser, {int exitCode = 1}) { |
| 46 | + stderr.writeln('format.dart [--help] [--fix] [--all-files]'); |
| 47 | + stderr.writeln(parser.usage); |
| 48 | + exit(exitCode); |
| 49 | +} |
| 50 | + |
| 51 | +class DartFormatChecker { |
| 52 | + DartFormatChecker({ |
| 53 | + required this.flutterRoot, |
| 54 | + required this.allFiles, |
| 55 | + }) : processRunner = ProcessRunner( |
| 56 | + defaultWorkingDirectory: flutterRoot, |
| 57 | + ); |
| 58 | + |
| 59 | + final Directory flutterRoot; |
| 60 | + final bool allFiles; |
| 61 | + final ProcessRunner processRunner; |
| 62 | + |
| 63 | + Future<int> check({required bool fix}) async { |
| 64 | + final String baseGitRef = await _getDiffBaseRevision(); |
| 65 | + final List<String> filesToCheck = await _getFileList( |
| 66 | + types: <String>['*.dart'], |
| 67 | + allFiles: allFiles, |
| 68 | + baseGitRef: baseGitRef, |
| 69 | + ); |
| 70 | + return _checkFormat( |
| 71 | + filesToCheck: filesToCheck, |
| 72 | + fix: fix, |
| 73 | + ); |
| 74 | + } |
| 75 | + |
| 76 | + Future<String> _getDiffBaseRevision() async { |
| 77 | + String upstream = 'upstream'; |
| 78 | + final String upstreamUrl = await _runGit( |
| 79 | + <String>['remote', 'get-url', upstream], |
| 80 | + processRunner, |
| 81 | + failOk: true, |
| 82 | + ); |
| 83 | + if (upstreamUrl.isEmpty) { |
| 84 | + upstream = 'origin'; |
| 85 | + } |
| 86 | + await _runGit(<String>['fetch', upstream, 'main'], processRunner); |
| 87 | + String result = ''; |
| 88 | + try { |
| 89 | + // This is the preferred command to use, but developer checkouts often do |
| 90 | + // not have a clear fork point, so we fall back to just the regular |
| 91 | + // merge-base in that case. |
| 92 | + result = await _runGit( |
| 93 | + <String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], |
| 94 | + processRunner, |
| 95 | + ); |
| 96 | + } on ProcessRunnerException { |
| 97 | + result = await _runGit(<String>['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner); |
| 98 | + } |
| 99 | + return result.trim(); |
| 100 | + } |
| 101 | + |
| 102 | + Future<String> _runGit( |
| 103 | + List<String> args, |
| 104 | + ProcessRunner processRunner, { |
| 105 | + bool failOk = false, |
| 106 | + }) async { |
| 107 | + final ProcessRunnerResult result = await processRunner.runProcess( |
| 108 | + <String>['git', ...args], |
| 109 | + failOk: failOk, |
| 110 | + ); |
| 111 | + return result.stdout; |
| 112 | + } |
| 113 | + |
| 114 | + Future<List<String>> _getFileList({ |
| 115 | + required List<String> types, |
| 116 | + required bool allFiles, |
| 117 | + required String baseGitRef, |
| 118 | + }) async { |
| 119 | + String output; |
| 120 | + if (allFiles) { |
| 121 | + output = await _runGit(<String>[ |
| 122 | + 'ls-files', |
| 123 | + '--', |
| 124 | + ...types, |
| 125 | + ], processRunner); |
| 126 | + } else { |
| 127 | + output = await _runGit(<String>[ |
| 128 | + 'diff', |
| 129 | + '-U0', |
| 130 | + '--no-color', |
| 131 | + '--diff-filter=d', |
| 132 | + '--name-only', |
| 133 | + baseGitRef, |
| 134 | + '--', |
| 135 | + ...types, |
| 136 | + ], processRunner); |
| 137 | + } |
| 138 | + return output.split('\n').where((String line) => line.isNotEmpty).toList(); |
| 139 | + } |
| 140 | + |
| 141 | + Future<int> _checkFormat({ |
| 142 | + required List<String> filesToCheck, |
| 143 | + required bool fix, |
| 144 | + }) async { |
| 145 | + final List<String> cmd = <String>[ |
| 146 | + path.join(flutterRoot.path, 'bin', 'dart'), |
| 147 | + 'format', |
| 148 | + '--set-exit-if-changed', |
| 149 | + '--show=none', |
| 150 | + if (!fix) '--output=show', |
| 151 | + if (fix) '--output=write', |
| 152 | + ]; |
| 153 | + final List<WorkerJob> jobs = <WorkerJob>[]; |
| 154 | + for (final String file in filesToCheck) { |
| 155 | + jobs.add(WorkerJob(<String>[...cmd, file])); |
| 156 | + } |
| 157 | + final ProcessPool dartFmt = ProcessPool( |
| 158 | + processRunner: processRunner, |
| 159 | + printReport: _namedReport('dart format'), |
| 160 | + ); |
| 161 | + |
| 162 | + Iterable<WorkerJob> incorrect; |
| 163 | + if (!fix) { |
| 164 | + final Stream<WorkerJob> completedJobs = dartFmt.startWorkers(jobs); |
| 165 | + final List<WorkerJob> diffJobs = <WorkerJob>[]; |
| 166 | + await for (final WorkerJob completedJob in completedJobs) { |
| 167 | + if (completedJob.result.exitCode == 1) { |
| 168 | + diffJobs.add( |
| 169 | + WorkerJob( |
| 170 | + <String>[ |
| 171 | + 'git', |
| 172 | + 'diff', |
| 173 | + '--no-index', |
| 174 | + '--no-color', |
| 175 | + '--ignore-cr-at-eol', |
| 176 | + '--', |
| 177 | + completedJob.command.last, |
| 178 | + '-', |
| 179 | + ], |
| 180 | + stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw), |
| 181 | + ), |
| 182 | + ); |
| 183 | + } |
| 184 | + } |
| 185 | + final ProcessPool diffPool = ProcessPool( |
| 186 | + processRunner: processRunner, |
| 187 | + printReport: _namedReport('diff'), |
| 188 | + ); |
| 189 | + final List<WorkerJob> completedDiffs = await diffPool.runToCompletion(diffJobs); |
| 190 | + incorrect = completedDiffs.where((WorkerJob job) => job.result.exitCode != 0); |
| 191 | + } else { |
| 192 | + final List<WorkerJob> completedJobs = await dartFmt.runToCompletion(jobs); |
| 193 | + incorrect = completedJobs.where((WorkerJob job) => job.result.exitCode == 1); |
| 194 | + } |
| 195 | + |
| 196 | + _clearOutput(); |
| 197 | + |
| 198 | + if (incorrect.isNotEmpty) { |
| 199 | + final bool plural = incorrect.length > 1; |
| 200 | + if (fix) { |
| 201 | + stdout.writeln('Fixing ${incorrect.length} dart file${plural ? 's' : ''}' |
| 202 | + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); |
| 203 | + } else { |
| 204 | + stderr.writeln('Found ${incorrect.length} Dart file${plural ? 's' : ''}' |
| 205 | + ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); |
| 206 | + final String fileList = incorrect.map( |
| 207 | + (WorkerJob job) => job.command[job.command.length - 2] |
| 208 | + ).join(' '); |
| 209 | + stdout.writeln(); |
| 210 | + stdout.writeln('To fix, run `dart format $fileList` or:'); |
| 211 | + stdout.writeln(); |
| 212 | + stdout.writeln('git apply <<DONE'); |
| 213 | + for (final WorkerJob job in incorrect) { |
| 214 | + stdout.write(job.result.stdout |
| 215 | + .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}') |
| 216 | + .replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}') |
| 217 | + .replaceFirst(RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n'), '') |
| 218 | + ); |
| 219 | + } |
| 220 | + stdout.writeln('DONE'); |
| 221 | + stdout.writeln(); |
| 222 | + } |
| 223 | + } else { |
| 224 | + stdout.writeln('All dart files formatted correctly.'); |
| 225 | + } |
| 226 | + return incorrect.length; |
| 227 | + } |
| 228 | +} |
| 229 | + |
| 230 | +ProcessPoolProgressReporter _namedReport(String name) { |
| 231 | + return (int total, int completed, int inProgress, int pending, int failed) { |
| 232 | + final String percent = |
| 233 | + total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3); |
| 234 | + final String completedStr = completed.toString().padLeft(3); |
| 235 | + final String totalStr = total.toString().padRight(3); |
| 236 | + final String inProgressStr = inProgress.toString().padLeft(2); |
| 237 | + final String pendingStr = pending.toString().padLeft(3); |
| 238 | + final String failedStr = failed.toString().padLeft(3); |
| 239 | + |
| 240 | + stdout.write('$name Jobs: $percent% done, ' |
| 241 | + '$completedStr/$totalStr completed, ' |
| 242 | + '$inProgressStr in progress, ' |
| 243 | + '$pendingStr pending, ' |
| 244 | + '$failedStr failed.${' ' * 20}\r'); |
| 245 | + }; |
| 246 | +} |
| 247 | + |
| 248 | +void _clearOutput() { |
| 249 | + stdout.write('\r${' ' * 100}\r'); |
| 250 | +} |
| 251 | + |
| 252 | +Stream<List<int>> _codeUnitsAsStream(List<int>? input) async* { |
| 253 | + if (input != null) { |
| 254 | + yield input; |
| 255 | + } |
| 256 | +} |
0 commit comments