// Copyright 2014 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. import 'dart:io'; import 'package:args/args.dart'; import 'package:path/path.dart' as path; import 'package:process_runner/process_runner.dart'; Future main(List 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.'); late final ArgResults options; try { options = parser.parse(arguments); } on FormatException catch (e) { stderr.writeln('ERROR: $e'); _usage(parser, exitCode: 0); } if (options['help'] as bool) { _usage(parser, exitCode: 0); } final File script = File.fromUri(Platform.script).absolute; final Directory flutterRoot = script.parent.parent.parent.parent; final bool result = (await DartFormatChecker( flutterRoot: flutterRoot, allFiles: options['all-files'] as bool, ).check(fix: options['fix'] as bool)) == 0; exit(result ? 0 : 1); } void _usage(ArgParser parser, {int exitCode = 1}) { stderr.writeln('format.dart [--help] [--fix] [--all-files]'); stderr.writeln(parser.usage); exit(exitCode); } class DartFormatChecker { DartFormatChecker({ required this.flutterRoot, required this.allFiles, }) : processRunner = ProcessRunner( defaultWorkingDirectory: flutterRoot, ); final Directory flutterRoot; final bool allFiles; final ProcessRunner processRunner; Future check({required bool fix}) async { final String baseGitRef = await _getDiffBaseRevision(); final List filesToCheck = await _getFileList( types: ['*.dart'], allFiles: allFiles, baseGitRef: baseGitRef, ); return _checkFormat( filesToCheck: filesToCheck, fix: fix, ); } Future _getDiffBaseRevision() async { String upstream = 'upstream'; final String upstreamUrl = await _runGit( ['remote', 'get-url', upstream], processRunner, failOk: true, ); if (upstreamUrl.isEmpty) { upstream = 'origin'; } await _runGit(['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( ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], processRunner, ); } on ProcessRunnerException { result = await _runGit(['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner); } return result.trim(); } Future _runGit( List args, ProcessRunner processRunner, { bool failOk = false, }) async { final ProcessRunnerResult result = await processRunner.runProcess( ['git', ...args], failOk: failOk, ); return result.stdout; } Future> _getFileList({ required List types, required bool allFiles, required String baseGitRef, }) async { String output; if (allFiles) { output = await _runGit([ 'ls-files', '--', ...types, ], processRunner); } else { output = await _runGit([ 'diff', '-U0', '--no-color', '--diff-filter=d', '--name-only', baseGitRef, '--', ...types, ], processRunner); } return output.split('\n').where((String line) => line.isNotEmpty).toList(); } Future _checkFormat({ required List filesToCheck, required bool fix, }) async { final List cmd = [ path.join(flutterRoot.path, 'bin', 'dart'), 'format', '--set-exit-if-changed', '--show=none', if (!fix) '--output=show', if (fix) '--output=write', ]; final List jobs = []; for (final String file in filesToCheck) { jobs.add(WorkerJob([...cmd, file])); } final ProcessPool dartFmt = ProcessPool( processRunner: processRunner, printReport: _namedReport('dart format'), ); Iterable incorrect; if (!fix) { final Stream completedJobs = dartFmt.startWorkers(jobs); final List diffJobs = []; await for (final WorkerJob completedJob in completedJobs) { if (completedJob.result.exitCode == 1) { diffJobs.add( WorkerJob( [ 'git', 'diff', '--no-index', '--no-color', '--ignore-cr-at-eol', '--', completedJob.command.last, '-', ], stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw), ), ); } } final ProcessPool diffPool = ProcessPool( processRunner: processRunner, printReport: _namedReport('diff'), ); final List completedDiffs = await diffPool.runToCompletion(diffJobs); incorrect = completedDiffs.where((WorkerJob job) => job.result.exitCode != 0); } else { final List completedJobs = await dartFmt.runToCompletion(jobs); incorrect = completedJobs.where((WorkerJob job) => job.result.exitCode == 1); } _clearOutput(); if (incorrect.isNotEmpty) { final bool plural = incorrect.length > 1; if (fix) { stdout.writeln('Fixing ${incorrect.length} dart file${plural ? 's' : ''}' ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); } else { stderr.writeln('Found ${incorrect.length} Dart file${plural ? 's' : ''}' ' which ${plural ? 'were' : 'was'} formatted incorrectly.'); final String fileList = incorrect.map( (WorkerJob job) => job.command[job.command.length - 2] ).join(' '); stdout.writeln(); stdout.writeln('To fix, run `dart format $fileList` or:'); stdout.writeln(); stdout.writeln('git apply <> _codeUnitsAsStream(List? input) async* { if (input != null) { yield input; } }