| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:args/command_runner.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| import '../dart_formatter.dart'; |
| import '../io.dart'; |
| import 'formatter_options.dart'; |
| import 'output.dart'; |
| import 'show.dart'; |
| import 'summary.dart'; |
| |
| final class FormatCommand extends Command<int> { |
| @override |
| String get name => 'format'; |
| |
| @override |
| String get description => 'Idiomatically format Dart source code.'; |
| |
| @override |
| String get invocation => |
| '${runner!.executableName} $name [options...] <files or directories...>'; |
| |
| FormatCommand({bool verbose = false}) { |
| argParser.addFlag( |
| 'verbose', |
| abbr: 'v', |
| negatable: false, |
| help: 'Show all options and flags with --help.', |
| ); |
| |
| if (verbose) argParser.addSeparator('Output options:'); |
| |
| argParser.addOption( |
| 'output', |
| abbr: 'o', |
| help: 'Set where to write formatted output.', |
| allowed: ['write', 'show', 'json', 'none'], |
| allowedHelp: { |
| 'write': 'Overwrite formatted files on disk.', |
| 'show': 'Print code to terminal.', |
| 'json': 'Print code and selection as JSON.', |
| 'none': 'Discard output.', |
| }, |
| defaultsTo: 'write', |
| ); |
| argParser.addOption( |
| 'show', |
| help: 'Set which filenames to print.', |
| allowed: ['all', 'changed', 'none'], |
| allowedHelp: { |
| 'all': 'All visited files and directories.', |
| 'changed': 'Only the names of files whose formatting is changed.', |
| 'none': 'No file names or directories.', |
| }, |
| defaultsTo: 'changed', |
| hide: !verbose, |
| ); |
| argParser.addOption( |
| 'summary', |
| help: 'Show the specified summary after formatting.', |
| allowed: ['line', 'diff', 'profile', 'none'], |
| allowedHelp: { |
| 'line': 'Single-line summary.', |
| 'diff': 'The number of changed lines.', |
| 'profile': 'How long it took for format each file.', |
| 'none': 'No summary.', |
| }, |
| defaultsTo: 'line', |
| hide: !verbose, |
| ); |
| |
| argParser.addOption( |
| 'language-version', |
| help: |
| 'Language version of formatted code.\n' |
| 'Use "latest" to parse as the latest supported version.\n' |
| 'Omit to look for a surrounding package config.', |
| hide: !verbose, |
| ); |
| |
| argParser.addFlag( |
| 'set-exit-if-changed', |
| negatable: false, |
| help: 'Return exit code 1 if there are any formatting changes.', |
| ); |
| |
| if (verbose) argParser.addSeparator('Other options:'); |
| |
| argParser.addOption( |
| 'page-width', |
| help: 'Try to keep lines no longer than this.', |
| defaultsTo: '80', |
| hide: !verbose, |
| ); |
| // This is the old name for "--page-width". We keep it for backwards |
| // compatibility but don't show it in the help output. |
| argParser.addOption( |
| 'line-length', |
| abbr: 'l', |
| help: 'Wrap lines longer than this.', |
| defaultsTo: '80', |
| hide: true, |
| ); |
| |
| argParser.addOption( |
| 'trailing-commas', |
| help: 'How trailing commas in input affect formatting.', |
| defaultsTo: 'automate', |
| allowedHelp: { |
| 'automate': |
| 'The formatter adds and removes trailing commas based on\n' |
| 'its decision to split the surrounding construct.', |
| 'preserve': |
| 'A trailing comma forces the surrounding construct to split.\n' |
| 'The formatter will add a trailing comma when it splits a\n' |
| 'construct but will not remove one.', |
| }, |
| hide: !verbose, |
| ); |
| |
| argParser.addOption( |
| 'indent', |
| abbr: 'i', |
| help: 'Add this many spaces of leading indentation.', |
| defaultsTo: '0', |
| hide: !verbose, |
| ); |
| |
| argParser.addFlag( |
| 'follow-links', |
| negatable: false, |
| help: |
| 'Follow links to files and directories.\n' |
| 'If unset, links will be ignored.', |
| hide: !verbose, |
| ); |
| argParser.addFlag( |
| 'version', |
| negatable: false, |
| help: 'Show dart_style version.', |
| hide: !verbose, |
| ); |
| argParser.addMultiOption( |
| 'enable-experiment', |
| help: |
| 'Enable one or more experimental features.\n' |
| 'See dart.dev/go/experiments.', |
| hide: !verbose, |
| ); |
| |
| if (verbose) argParser.addSeparator('Options when formatting from stdin:'); |
| |
| argParser.addOption( |
| 'selection', |
| help: 'Track selection (given as "start:length") through formatting.', |
| hide: !verbose, |
| ); |
| argParser.addOption( |
| 'stdin-name', |
| help: |
| 'The path that code read from stdin is treated as coming from.\n\n' |
| 'This path is used in error messages and also to locate a\n' |
| 'surrounding package to infer the code\'s language version.\n' |
| 'To avoid searching for a surrounding package config, pass\n' |
| 'in a language version using --language-version.', |
| hide: !verbose, |
| ); |
| } |
| |
| @override |
| Future<int> run() async { |
| var argResults = this.argResults!; |
| |
| if (argResults['version'] as bool) { |
| print(dartStyleVersion); |
| return 0; |
| } |
| |
| var show = |
| const { |
| 'all': Show.all, |
| 'changed': Show.changed, |
| 'none': Show.none, |
| }[argResults['show']]!; |
| |
| var output = |
| const { |
| 'write': Output.write, |
| 'show': Output.show, |
| 'none': Output.none, |
| 'json': Output.json, |
| }[argResults['output']]!; |
| |
| var summary = Summary.none; |
| switch (argResults['summary'] as String) { |
| case 'diff': |
| summary = Summary.diff(); |
| break; |
| case 'line': |
| summary = Summary.line(); |
| break; |
| case 'profile': |
| summary = Summary.profile(); |
| break; |
| } |
| |
| // If the user is sending code through stdin, default the output to stdout. |
| if (!argResults.wasParsed('output') && argResults.rest.isEmpty) { |
| output = Output.show; |
| } |
| |
| // If the user wants to print the code and didn't indicate how the files |
| // should be printed, default to only showing the code. |
| if (!argResults.wasParsed('show') && |
| (output == Output.show || output == Output.json)) { |
| show = Show.none; |
| } |
| |
| // If the user wants JSON output, default to no summary. |
| if (!argResults.wasParsed('summary') && output == Output.json) { |
| summary = Summary.none; |
| } |
| |
| // Can't use --verbose with anything but --help. |
| if (argResults['verbose'] as bool && !(argResults['help'] as bool)) { |
| usageException('Can only use --verbose with --help.'); |
| } |
| |
| // Can't use any summary with JSON output. |
| if (output == Output.json && summary != Summary.none) { |
| usageException('Cannot print a summary with JSON output.'); |
| } |
| |
| Version? languageVersion; |
| if (argResults['language-version'] case String version) { |
| var versionPattern = RegExp(r'^([0-9]+)\.([0-9]+)$'); |
| if (version == 'latest') { |
| languageVersion = DartFormatter.latestLanguageVersion; |
| } else if (versionPattern.firstMatch(version) case var match?) { |
| languageVersion = Version( |
| int.parse(match[1]!), |
| int.parse(match[2]!), |
| 0, |
| ); |
| } else { |
| usageException( |
| '--language-version must be a version like "3.2" or ' |
| '"latest", was "$version".', |
| ); |
| } |
| } |
| |
| // Allow the old option name if the new one wasn't passed. |
| String? pageWidthString; |
| if (argResults.wasParsed('page-width')) { |
| pageWidthString = argResults['page-width'] as String; |
| } else if (argResults.wasParsed('line-length')) { |
| pageWidthString = argResults['line-length'] as String; |
| } |
| |
| int? pageWidth; |
| if (pageWidthString != null) { |
| pageWidth = int.tryParse(pageWidthString); |
| if (pageWidth == null) { |
| usageException( |
| 'Page width must be an integer, was "$pageWidthString".', |
| ); |
| } else if (pageWidth <= 0) { |
| usageException('Page width must be a positive number, was $pageWidth.'); |
| } |
| } |
| |
| TrailingCommas? trailingCommas; |
| if (argResults.wasParsed('trailing-commas')) { |
| // We check the values explicitly here instead of using `allowedValues` |
| // from [ArgParser] because this provides a better error message. |
| trailingCommas = switch (argResults['trailing-commas']) { |
| 'automate' => TrailingCommas.automate, |
| 'preserve' => TrailingCommas.preserve, |
| var mode => usageException( |
| '--trailing-commas must be "automate" or "preserve", was "$mode".', |
| ), |
| }; |
| } |
| |
| var indent = |
| int.tryParse(argResults['indent'] as String) ?? |
| usageException( |
| '--indent must be an integer, was ' |
| '"${argResults['indent']}".', |
| ); |
| |
| if (indent < 0) { |
| usageException( |
| '--indent must be non-negative, was ' |
| '"${argResults['indent']}".', |
| ); |
| } |
| |
| List<int>? selection; |
| try { |
| selection = _parseSelection(argResults, 'selection'); |
| } on FormatException catch (exception) { |
| usageException(exception.message); |
| } |
| |
| var followLinks = argResults['follow-links'] as bool; |
| var setExitIfChanged = argResults['set-exit-if-changed'] as bool; |
| |
| var experimentFlags = argResults['enable-experiment'] as List<String>; |
| |
| // If stdin isn't connected to a pipe, then the user is not passing |
| // anything to stdin, so let them know they made a mistake. |
| if (argResults.rest.isEmpty && stdin.hasTerminal) { |
| usageException('Missing paths to code to format.'); |
| } |
| |
| if (argResults.rest.isEmpty && output == Output.write) { |
| usageException('Cannot use --output=write when reading from stdin.'); |
| } |
| |
| if (argResults.wasParsed('stdin-name') && argResults.rest.isNotEmpty) { |
| usageException('Cannot pass --stdin-name when not reading from stdin.'); |
| } |
| var stdinName = argResults['stdin-name'] as String?; |
| |
| var options = FormatterOptions( |
| languageVersion: languageVersion, |
| indent: indent, |
| pageWidth: pageWidth, |
| trailingCommas: trailingCommas, |
| followLinks: followLinks, |
| show: show, |
| output: output, |
| summary: summary, |
| setExitIfChanged: setExitIfChanged, |
| experimentFlags: experimentFlags, |
| ); |
| |
| if (argResults.rest.isEmpty) { |
| await formatStdin(options, selection, stdinName); |
| } else { |
| await formatPaths(options, argResults.rest); |
| options.summary.show(); |
| } |
| |
| // Return the exitCode explicitly for tools which embed dart_style |
| // and set their own exitCode. |
| return exitCode; |
| } |
| |
| List<int>? _parseSelection(ArgResults argResults, String optionName) { |
| var option = argResults[optionName] as String?; |
| if (option == null) return null; |
| |
| // Can only preserve a selection when parsing from stdin. |
| if (argResults.rest.isNotEmpty) { |
| throw FormatException( |
| 'Can only use --$optionName when reading from stdin.', |
| ); |
| } |
| |
| try { |
| var coordinates = option.split(':'); |
| if (coordinates.length != 2) { |
| throw const FormatException( |
| 'Selection should be a colon-separated pair of integers, ' |
| '"123:45".', |
| ); |
| } |
| |
| return coordinates.map<int>((coord) => int.parse(coord.trim())).toList(); |
| } on FormatException catch (_) { |
| throw FormatException( |
| '--$optionName must be a colon-separated pair of integers, was ' |
| '"${argResults[optionName]}".', |
| ); |
| } |
| } |
| } |