Skip to content

Commit 71ac085

Browse files
authored
Add an optional configuration to preserve trailing commas. (#1672)
Add an optional configuration to preserve trailing commas. In the old short formatter, when a delimited construct had a trailing comma, it had two effects: 1. The construct would be formatted "Flutter style" where the closing bracket would be moved to the next line and the contents indented +2, as opposed to the earlier style where the bracket stays on the same line as the last element and the elements are indented +4). 2. The construct is forced to split even if it otherwise wouldn't. The new formatter always uses a Flutter-like style for commas-separated constructors, so trailing commas are no longer needed for point 1. When it does split a construct, it always adds a trailing comma. This means that point 2 breaks reversibility. Because of that, and because many users don't want to decide for every construct whether it should split or not, the new tall formatter no longer does point 2. It will remove a trailing comma and collapse a construct if it decides to. However, some users rely heavily on that feature and strongly prefer control over forcing argument lists and other constructs to split. This PR restores that functionality with an opt-in configuration: ``` formatter: trailing_commas: preserve ``` When enabled, if a construct has a trailing comma, the formatter will always split it. Users that preferred that behavior from the old formatter can enable the option and continue to work that way. This does not mean we are generally moving in the direction of a more configurable formatter. This was a behavior that the formatter already supported and we removed it to the detriment of some users' experience. We're restoring that existing behavior for those users who want it. Fix #1652.
1 parent 2772c8d commit 71ac085

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1439
-297
lines changed

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## 3.0.2-wip
1+
## 3.1.0-wip
22

33
* Format null-aware elements.
44

@@ -63,10 +63,26 @@
6363
);
6464
```
6565

66+
* Allow preserving trailing commas and forcing the surrounding construct to
67+
split even when it would otherwise fit on one line. This is off by default
68+
(because it breaks [reversibility][] among other reasons) but can be enabled
69+
by adding this to a surrounding `analysis_options.yaml` file:
70+
71+
```yaml
72+
formatter:
73+
trailing_commas: preserve
74+
```
75+
76+
This is similar to how trailing commas worked in the old short style
77+
formatter.
78+
6679
* Don't add a trailing comma in lists that don't allow it, even when there is
6780
a trailing comment (#1639).
81+
6882
* Add tests for digit separators.
6983
84+
[reversibility]: https://github.com/dart-lang/dart_style/wiki/Reversibility-principle
85+
7086
## 3.0.1
7187
7288
* Handle trailing commas in for-loop updaters (#1354).

example/format.dart

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,42 @@ void main(List<String> args) {
3030
_runTest('other/selection.stmt', 2);
3131
}
3232

33-
void _formatStmt(String source, {bool tall = true, int pageWidth = 40}) {
34-
_runFormatter(source, pageWidth, tall: tall, isCompilationUnit: false);
33+
void _formatStmt(
34+
String source, {
35+
bool tall = true,
36+
int pageWidth = 40,
37+
TrailingCommas trailingCommas = TrailingCommas.automate,
38+
}) {
39+
_runFormatter(
40+
source,
41+
pageWidth,
42+
tall: tall,
43+
isCompilationUnit: false,
44+
trailingCommas: trailingCommas,
45+
);
3546
}
3647

37-
void _formatUnit(String source, {bool tall = true, int pageWidth = 40}) {
38-
_runFormatter(source, pageWidth, tall: tall, isCompilationUnit: true);
48+
void _formatUnit(
49+
String source, {
50+
bool tall = true,
51+
int pageWidth = 40,
52+
TrailingCommas trailingCommas = TrailingCommas.automate,
53+
}) {
54+
_runFormatter(
55+
source,
56+
pageWidth,
57+
tall: tall,
58+
isCompilationUnit: true,
59+
trailingCommas: trailingCommas,
60+
);
3961
}
4062

4163
void _runFormatter(
4264
String source,
4365
int pageWidth, {
4466
required bool tall,
4567
required bool isCompilationUnit,
68+
TrailingCommas trailingCommas = TrailingCommas.automate,
4669
}) {
4770
try {
4871
var formatter = DartFormatter(
@@ -51,6 +74,7 @@ void _runFormatter(
5174
? DartFormatter.latestLanguageVersion
5275
: DartFormatter.latestShortStyleLanguageVersion,
5376
pageWidth: pageWidth,
77+
trailingCommas: trailingCommas,
5478
);
5579

5680
String result;
@@ -84,13 +108,7 @@ Future<void> _runTest(
84108
}) async {
85109
var testFile = await TestFile.read('${tall ? 'tall' : 'short'}/$path');
86110
var formatTest = testFile.tests.firstWhere((test) => test.line == line);
87-
88-
var formatter = DartFormatter(
89-
languageVersion: formatTest.languageVersion,
90-
pageWidth: testFile.pageWidth,
91-
indent: formatTest.leadingIndent,
92-
experimentFlags: formatTest.experimentFlags,
93-
);
111+
var formatter = testFile.formatterForTest(formatTest);
94112

95113
var actual = formatter.formatSource(formatTest.input);
96114

lib/src/analysis_options/analysis_options_file.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ typedef AnalysisOptions = Map<Object?, Object?>;
1717
/// passed to [FileSystem.join()].
1818
typedef ResolvePackageUri = Future<String?> Function(Uri packageUri);
1919

20-
/// Reads an `analysis_options.yaml` file in [directory] or in the nearest
20+
/// Reads an "analysis_options.yaml" file in [directory] or in the nearest
2121
/// surrounding folder that contains that file using [fileSystem].
2222
///
2323
/// Stops walking parent directories as soon as it finds one that contains an
24-
/// `analysis_options.yaml` file. If it reaches the root directory without
24+
/// "analysis_options.yaml" file. If it reaches the root directory without
2525
/// finding one, returns an empty [YamlMap].
2626
///
27-
/// If an `analysis_options.yaml` file is found, reads it and parses it to a
27+
/// If an "analysis_options.yaml" file is found, reads it and parses it to a
2828
/// [YamlMap]. If the map contains an `include` key whose value is a list, then
2929
/// reads any of the other referenced YAML files and merges them into this one.
3030
/// Returns the resulting map with the `include` key removed.

lib/src/ast_extensions.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ extension PatternExtensions on DartPattern {
454454
}
455455

456456
extension TokenExtensions on Token {
457+
/// Whether the token before this one is a comma.
458+
bool get hasCommaBefore => previous?.type == TokenType.COMMA;
459+
457460
/// Whether this token has a preceding comment that is a line comment.
458461
bool get hasLineCommentBefore {
459462
for (

lib/src/cli/format_command.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,22 @@ final class FormatCommand extends Command<int> {
106106
hide: true,
107107
);
108108

109+
argParser.addOption(
110+
'trailing-commas',
111+
help: 'How trailing commas in input affect formatting.',
112+
defaultsTo: 'automate',
113+
allowedHelp: {
114+
'automate':
115+
'The formatter adds and removes trailing commas based on\n'
116+
'its decision to split the surrounding construct.',
117+
'preserve':
118+
'A trailing comma forces the surrounding construct to split.\n'
119+
'The formatter will add a trailing comma when it splits a\n'
120+
'construct but will not remove one.',
121+
},
122+
hide: !verbose,
123+
);
124+
109125
argParser.addOption(
110126
'indent',
111127
abbr: 'i',
@@ -255,6 +271,19 @@ final class FormatCommand extends Command<int> {
255271
}
256272
}
257273

274+
TrailingCommas? trailingCommas;
275+
if (argResults.wasParsed('trailing-commas')) {
276+
// We check the values explicitly here instead of using `allowedValues`
277+
// from [ArgParser] because this provides a better error message.
278+
trailingCommas = switch (argResults['trailing-commas']) {
279+
'automate' => TrailingCommas.automate,
280+
'preserve' => TrailingCommas.preserve,
281+
var mode => usageException(
282+
'--trailing-commas must be "automate" or "preserve", was "$mode".',
283+
),
284+
};
285+
}
286+
258287
var indent =
259288
int.tryParse(argResults['indent'] as String) ??
260289
usageException(
@@ -300,6 +329,7 @@ final class FormatCommand extends Command<int> {
300329
languageVersion: languageVersion,
301330
indent: indent,
302331
pageWidth: pageWidth,
332+
trailingCommas: trailingCommas,
303333
followLinks: followLinks,
304334
show: show,
305335
output: output,

lib/src/cli/formatter_options.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import 'dart:io';
66

77
import 'package:pub_semver/pub_semver.dart';
88

9+
import '../dart_formatter.dart';
910
import '../source_code.dart';
1011
import 'output.dart';
1112
import 'show.dart';
1213
import 'summary.dart';
1314

1415
// Note: The following line of code is modified by tool/grind.dart.
15-
const dartStyleVersion = '3.0.2-wip';
16+
const dartStyleVersion = '3.1.0-wip';
1617

17-
/// Global options that affect how the formatter produces and uses its outputs.
18+
/// Global options parsed from the command line that affect how the formatter
19+
/// produces and uses its outputs.
1820
final class FormatterOptions {
1921
/// The language version formatted code should be parsed at or `null` if not
2022
/// specified.
@@ -30,6 +32,9 @@ final class FormatterOptions {
3032
/// [DartFormatter.defaultPageWidth].
3133
final int? pageWidth;
3234

35+
/// How trailing commas in the input source code affect formatting.
36+
final TrailingCommas? trailingCommas;
37+
3338
/// Whether symlinks should be traversed when formatting a directory.
3439
final bool followLinks;
3540

@@ -53,6 +58,7 @@ final class FormatterOptions {
5358
this.languageVersion,
5459
this.indent = 0,
5560
this.pageWidth,
61+
this.trailingCommas,
5662
this.followLinks = false,
5763
this.show = Show.changed,
5864
this.output = Output.write,

lib/src/config_cache.dart

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import 'package:pub_semver/pub_semver.dart';
99

1010
import 'analysis_options/analysis_options_file.dart';
1111
import 'analysis_options/io_file_system.dart';
12+
import 'dart_formatter.dart';
1213
import 'profile.dart';
1314

1415
/// Caches the nearest surrounding package config file for files in directories.
1516
///
1617
/// The formatter reads `.dart_tool/package_config.json` files in order to
1718
/// determine the default language version of files in that package and to
18-
/// resolve "package:" URIs in `analysis_options.yaml` files.
19+
/// resolve "package:" URIs in "analysis_options.yaml" files.
1920
///
2021
/// Walking the file system to find the package config and then reading it off
2122
/// disk is very slow. We know that every formatted file in the same directory
@@ -40,13 +41,9 @@ final class ConfigCache {
4041
/// discovered that there is no surrounding package.
4142
final Map<String, Version?> _directoryVersions = {};
4243

43-
/// The previously cached page width for all files immediately within a given
44-
/// directory.
45-
///
46-
/// The width may be `null` if we formatted a file in that directory and
47-
/// discovered that there is no surrounding analysis options that sets a
48-
/// page width.
49-
final Map<String, int?> _directoryPageWidths = {};
44+
/// The previously cached configured options for all files immediately within
45+
/// a given directory.
46+
final Map<String, _FormatterOptions> _directoryOptions = {};
5047

5148
final IOFileSystem _fileSystem = IOFileSystem();
5249

@@ -89,35 +86,77 @@ final class ConfigCache {
8986
/// formatter:
9087
/// page_width: 123
9188
Future<int?> findPageWidth(File file) async {
92-
// Use the cached version (which may be `null`) if present.
89+
return (await _findFormatterOptions(file)).pageWidth;
90+
}
91+
92+
/// Looks for an "analysis_options.yaml" file surrounding [file] and, if
93+
/// found and valid, returns the trailing comma handling specified by that
94+
/// config file.
95+
///
96+
/// Otherwise returns `null`.
97+
///
98+
/// The schema looks like:
99+
///
100+
/// formatter:
101+
/// trailing_commas: preserve # Or "automate".
102+
Future<TrailingCommas?> findTrailingCommas(File file) async {
103+
return (await _findFormatterOptions(file)).trailingCommas;
104+
}
105+
106+
/// Looks for an "analysis_options.yaml" file surrounding [file] and, if
107+
/// found and valid, returns the configured options.
108+
///
109+
/// If no options file could be found or it doesn't contain a "formatter" key
110+
/// whose value is a map, returns a default set of options where all settings
111+
/// are `null`.
112+
Future<_FormatterOptions> _findFormatterOptions(File file) async {
113+
// Use the cached version if present.
93114
var directory = file.parent.path;
94-
if (_directoryPageWidths.containsKey(directory)) {
95-
return _directoryPageWidths[directory];
96-
}
115+
if (_directoryOptions[directory] case var options?) return options;
116+
117+
int? pageWidth;
118+
TrailingCommas? trailingCommas;
97119

98120
try {
99-
// Look for a surrounding analysis_options.yaml file.
100-
var options = await findAnalysisOptions(
121+
// Look for a surrounding "analysis_options.yaml" file.
122+
var optionsFile = await findAnalysisOptions(
101123
_fileSystem,
102124
await _fileSystem.makePath(file.path),
103125
resolvePackageUri: (uri) => _resolvePackageUri(file, uri),
104126
);
105127

106-
if (options['formatter'] case Map<Object?, Object?> formatter) {
107-
if (formatter['page_width'] case int pageWidth) {
108-
return _directoryPageWidths[directory] = pageWidth;
128+
if (optionsFile['formatter'] case Map<Object?, Object?> formatter) {
129+
if (formatter case {'page_width': int width}) {
130+
pageWidth = width;
131+
}
132+
133+
if (formatter case {'trailing_commas': var commas}) {
134+
switch (commas) {
135+
case 'automate':
136+
trailingCommas = TrailingCommas.automate;
137+
case 'preserve':
138+
trailingCommas = TrailingCommas.preserve;
139+
default:
140+
stderr.writeln(
141+
'Warning: "trailing_commas" option should be "automate" or '
142+
'"preserve", but was "$commas".',
143+
);
144+
}
109145
}
110146
}
111-
} on PackageResolutionException {
112-
// Silently ignore any errors coming from the processing the analyis
113-
// options. If there are any issues, we just use the default page width
114-
// and keep going.
147+
} on PackageResolutionException catch (exception) {
148+
// Report the error, but use the default settings and keep going.
149+
stderr.writeln(
150+
'Warning: Package resolution error when reading '
151+
'"analysis_options.yaml" file:\n$exception',
152+
);
115153
}
116154

117-
// If we get here, the options file either doesn't specify the page width,
118-
// or specifies it in some invalid way. When that happens, silently ignore
119-
// the config file and use the default width.
120-
return _directoryPageWidths[directory] = null;
155+
// Cache whichever options we found (or `null` if we didn't find them).
156+
return _directoryOptions[directory] = _FormatterOptions(
157+
pageWidth,
158+
trailingCommas,
159+
);
121160
}
122161

123162
/// Look for and cache the nearest package surrounding [file].
@@ -176,3 +215,17 @@ final class ConfigCache {
176215
return config.resolve(packageUri)?.toFilePath();
177216
}
178217
}
218+
219+
/// The formatter options that can be configured in the "analysis_options.yaml"
220+
/// file.
221+
final class _FormatterOptions {
222+
/// The configured page width, or `null` if there is no options file or the
223+
/// options file doesn't specify it.
224+
final int? pageWidth;
225+
226+
/// The configured comma handling, or `null` if there is no options file or
227+
/// the options file doesn't specify it.
228+
final TrailingCommas? trailingCommas;
229+
230+
_FormatterOptions(this.pageWidth, this.trailingCommas);
231+
}

0 commit comments

Comments
 (0)