Skip to content

Commit 31e2eda

Browse files
Merge pull request #381 from Workiva/import_sort_v371backpatch
Add import sorting to format tool (backpatch to v3.7.1)
2 parents 3d8aec6 + 68fab5a commit 31e2eda

35 files changed

+1732
-69
lines changed

.github/workflows/dart_ci.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
sdk: [ 2.7.2, 2.13.4, stable, dev ]
18+
sdk: [ 2.7.2, 2.13.4 ]
1919
steps:
2020
- uses: actions/checkout@v2
2121
- uses: dart-lang/[email protected]
@@ -30,7 +30,7 @@ jobs:
3030
run: pub get
3131

3232
- name: Validate dependencies
33-
run: pub run dependency_validator -i pedantic,over_react_format,meta
33+
run: pub run dependency_validator
3434
if: always() && steps.install.outcome == 'success'
3535

3636
- name: Analyze project source
@@ -60,6 +60,9 @@ jobs:
6060
run: dart run dart_dev format --check
6161
if: always() && steps.install.outcome == 'success'
6262

63-
- name: Publish dry run
64-
run: dart pub publish --dry-run
65-
if: always() && steps.install.outcome == 'success'
63+
# Disabled for backpatch releases since pubspec contains dependency_validator
64+
# key and cannot update to dependency_validator ^3.0.0
65+
#
66+
# - name: Publish dry run
67+
# run: dart pub publish --dry-run
68+
# if: always() && steps.install.outcome == 'success'

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [3.7.2](https://github.com/Workiva/dart_dev/compare/3.7.1...3.7.2)
4+
5+
- Added an `organizeDirectives` option to the `FormatTool` (default false). When
6+
true, the formatter will also sort a file's imports and exports.
7+
38
## [3.7.1](https://github.com/Workiva/dart_dev/compare/3.7.0...3.7.1)
49

510
- Update `TestTool` to allow arguments after a separator (`--`). These arguments

doc/tools/format-tool.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ final config = {
9292
};
9393
```
9494

95+
### Organizing directives
96+
97+
By default, the format tool will not sort imports/export. They can be automatically
98+
sorted by setting `organizeDirectives`.
99+
100+
```dart
101+
// tool/dart_dev/config.dart
102+
import 'package:dart_dev/dart_dev.dart';
103+
104+
final config = {
105+
'format': FormatTool()
106+
..organizeDirectives = true
107+
};
108+
```
109+
95110
## Command-line options
96111

97112
```bash

lib/src/dart_dev_runner.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import 'dart_dev_tool.dart';
77
import 'events.dart' as events;
88
import 'utils/version.dart';
99

10-
// import 'package:completion/completion.dart' as completion;
11-
1210
class DartDevRunner extends CommandRunner<int> {
1311
DartDevRunner(Map<String, DevTool> commands)
1412
: super('dart_dev', 'Dart tool runner.') {

lib/src/executable.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'dart:async';
22
import 'dart:io';
3-
import 'dart:math';
43

54
import 'package:analyzer/dart/analysis/utilities.dart';
65
import 'package:args/command_runner.dart';

lib/src/tools/format_tool.dart

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../dart_dev_tool.dart';
1313
import '../utils/arg_results_utils.dart';
1414
import '../utils/assert_no_positional_args_nor_args_after_separator.dart';
1515
import '../utils/logging.dart';
16+
import '../utils/organize_directives/organize_directives_in_paths.dart';
1617
import '../utils/package_is_immediate_dependency.dart';
1718
import '../utils/process_declaration.dart';
1819
import '../utils/run_process_and_ensure_exit.dart';
@@ -68,6 +69,11 @@ class FormatTool extends DevTool {
6869
/// Run `dartfmt -h -v` to see all available args.
6970
List<String> formatterArgs;
7071

72+
/// If the formatter should also organize imports and exports.
73+
///
74+
/// By default, this is disabled.
75+
bool organizeDirectives = false;
76+
7177
// ---------------------------------------------------------------------------
7278
// DevTool Overrides
7379
// ---------------------------------------------------------------------------
@@ -97,17 +103,34 @@ class FormatTool extends DevTool {
97103
String description = 'Format dart files in this package.';
98104

99105
@override
100-
FutureOr<int> run([DevToolExecutionContext context]) {
106+
FutureOr<int> run([DevToolExecutionContext context]) async {
101107
context ??= DevToolExecutionContext();
102-
final execution = buildExecution(
108+
final formatExecution = buildExecution(
103109
context,
104110
configuredFormatterArgs: formatterArgs,
105111
defaultMode: defaultMode,
106112
exclude: exclude,
107113
formatter: formatter,
114+
organizeDirectives: organizeDirectives,
115+
);
116+
if (formatExecution.exitCode != null) {
117+
return formatExecution.exitCode;
118+
}
119+
var exitCode = await runProcessAndEnsureExit(
120+
formatExecution.formatProcess,
121+
log: _log,
108122
);
109-
return execution.exitCode ??
110-
runProcessAndEnsureExit(execution.process, log: _log);
123+
if (exitCode != 0) {
124+
return exitCode;
125+
}
126+
if (formatExecution.directiveOrganization != null) {
127+
exitCode = organizeDirectivesInPaths(
128+
formatExecution.directiveOrganization.inputs,
129+
check: formatExecution.directiveOrganization.check,
130+
verbose: context.verbose,
131+
);
132+
}
133+
return exitCode;
111134
}
112135

113136
/// Builds and returns the object that contains:
@@ -283,14 +306,17 @@ class FormatterInputs {
283306
/// A declarative representation of an execution of the [FormatTool].
284307
///
285308
/// This class allows the [FormatTool] to break its execution up into two steps:
286-
/// 1. Validation of confg/inputs and creation of this class.
309+
/// 1. Validation of config/inputs and creation of this class.
287310
/// 2. Execution of expensive or hard-to-test logic based on step 1.
288311
///
289312
/// As a result, nearly all of the logic in [FormatTool] can be tested via the
290313
/// output of step 1 (an instance of this class) with very simple unit tests.
291314
class FormatExecution {
292-
FormatExecution.exitEarly(this.exitCode) : process = null;
293-
FormatExecution.process(this.process) : exitCode = null;
315+
FormatExecution.exitEarly(this.exitCode)
316+
: formatProcess = null,
317+
directiveOrganization = null;
318+
FormatExecution.process(this.formatProcess, [this.directiveOrganization])
319+
: exitCode = null;
294320

295321
/// If non-null, the execution is already complete and the [FormatTool] should
296322
/// exit with this code.
@@ -300,8 +326,20 @@ class FormatExecution {
300326

301327
/// A declarative representation of the formatter process that should be run.
302328
///
303-
/// This process' result should become the final result of the [FormatTool].
304-
final ProcessDeclaration process;
329+
/// If this process results in a non-zero exit code, [FormatTool] should return it.
330+
final ProcessDeclaration formatProcess;
331+
332+
/// A declarative representation of the directive organization work to be done
333+
/// (if enabled) after running the formatter.
334+
final DirectiveOrganization directiveOrganization;
335+
}
336+
337+
/// A declarative representation of the directive organization work.
338+
class DirectiveOrganization {
339+
DirectiveOrganization(this.inputs, {this.check});
340+
341+
final bool check;
342+
final Set<String> inputs;
305343
}
306344

307345
/// Modes supported by the dart formatter.
@@ -385,6 +423,8 @@ Iterable<String> buildArgs(
385423
///
386424
/// [include] will be populated from [FormatTool.include].
387425
///
426+
/// [organizeDirectives] will be populated from [FormatTool.organizeDirectives].
427+
///
388428
/// If non-null, [path] will override the current working directory for any
389429
/// operations that require it. This is intended for use by tests.
390430
///
@@ -397,6 +437,7 @@ FormatExecution buildExecution(
397437
FormatMode defaultMode,
398438
List<Glob> exclude,
399439
Formatter formatter,
440+
bool organizeDirectives = false,
400441
String path,
401442
}) {
402443
FormatMode mode;
@@ -463,23 +504,37 @@ FormatExecution buildExecution(
463504
'${inputs.hiddenDirectories.join('\n ')}');
464505
}
465506

466-
final dartfmt = buildProcess(formatter);
507+
final dartfmt = buildFormatProcess(formatter);
467508
final args = buildArgs(dartfmt.args, mode,
468509
argResults: context.argResults,
469510
configuredFormatterArgs: configuredFormatterArgs);
470511
logCommand(dartfmt.executable, inputs.includedFiles, args,
471512
verbose: context.verbose);
472-
return FormatExecution.process(ProcessDeclaration(
473-
dartfmt.executable, [...args, ...inputs.includedFiles],
474-
mode: ProcessStartMode.inheritStdio));
513+
514+
final formatProcess = ProcessDeclaration(
515+
dartfmt.executable,
516+
[...args, ...inputs.includedFiles],
517+
mode: ProcessStartMode.inheritStdio,
518+
);
519+
DirectiveOrganization directiveOrganization;
520+
if (organizeDirectives) {
521+
directiveOrganization = DirectiveOrganization(
522+
inputs.includedFiles,
523+
check: mode == FormatMode.check,
524+
);
525+
}
526+
return FormatExecution.process(
527+
formatProcess,
528+
directiveOrganization,
529+
);
475530
}
476531

477532
/// Returns a representation of the process that will be run by [FormatTool]
478533
/// based on the given [formatter].
479534
///
480535
/// - [Formatter.dartfmt] -> `dartfmt`
481536
/// - [Formatter.dartStyle] -> `pub run dart_style:format`
482-
ProcessDeclaration buildProcess([Formatter formatter]) {
537+
ProcessDeclaration buildFormatProcess([Formatter formatter]) {
483538
switch (formatter) {
484539
case Formatter.dartStyle:
485540
return ProcessDeclaration('pub', ['run', 'dart_style:format']);

lib/src/tools/function_tool.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'dart:async';
22

33
import 'package:args/args.dart';
4-
import 'package:dart_dev/src/utils/assert_no_args_after_separator.dart';
54
import 'package:io/io.dart';
65
import 'package:logging/logging.dart';
76

lib/src/tools/test_tool.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import 'package:logging/logging.dart';
99

1010
import '../dart_dev_tool.dart';
1111
import '../utils/arg_results_utils.dart';
12-
import '../utils/assert_no_args_after_separator.dart';
1312
import '../utils/logging.dart';
1413
import '../utils/package_is_immediate_dependency.dart';
1514
import '../utils/process_declaration.dart';

lib/src/tools/webdev_serve_tool.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import 'package:pub_semver/pub_semver.dart';
1010
import '../dart_dev_tool.dart';
1111
import '../utils/arg_results_utils.dart';
1212
import '../utils/assert_no_positional_args_nor_args_after_separator.dart';
13-
import '../utils/logging.dart';
1413
import '../utils/global_package_is_active_and_compatible.dart';
14+
import '../utils/logging.dart';
1515
import '../utils/process_declaration.dart';
1616
import '../utils/run_process_and_ensure_exit.dart';
1717

@@ -91,7 +91,7 @@ class WebdevServeTool extends DevTool {
9191
///
9292
/// This class allows the [WebdevServeTool] to break its execution up into two
9393
/// steps:
94-
/// 1. Validation of confg/inputs and creation of this class.
94+
/// 1. Validation of config/inputs and creation of this class.
9595
/// 2. Execution of expensive or hard-to-test logic based on step 1.
9696
///
9797
/// As a result, nearly all of the logic in [WebdevServeTool] can be tested via
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/token.dart';
3+
4+
/// A representation of an namespace directive.
5+
///
6+
/// Capable of tracking comments that should be associated with an namespace
7+
/// during organization (which cannot be represented by the AST)
8+
class Namespace {
9+
/// The AST node that represents an namespace in a file.
10+
final NamespaceDirective directive;
11+
12+
/// Comments that appear before the namespace that should stay with the
13+
/// namespace when organized.
14+
List<Token> beforeComments = [];
15+
16+
/// Comments that appear after the namespace that should stay with the
17+
/// namespace when organized.
18+
List<Token> afterComments = [];
19+
20+
/// The file being imported/exported.
21+
String get target {
22+
return directive.uri.stringValue;
23+
}
24+
25+
/// If the namespace is a dart namespace. Memoized for performance.
26+
bool _isDart;
27+
bool get isDart {
28+
return _isDart ??= target.startsWith('dart:');
29+
}
30+
31+
/// If the namespace is an external package namespace. Memoized for performance.
32+
bool _isExternalPkg;
33+
bool get isExternalPkg {
34+
return _isExternalPkg ??= target.startsWith('package:');
35+
}
36+
37+
/// If the namespace is a relative namespace. Memoized for performance.
38+
bool _isRelative;
39+
bool get isRelative {
40+
return _isRelative ??= !isExternalPkg && !isDart;
41+
}
42+
43+
Namespace(this.directive);
44+
45+
/// The character offset of the start of the namespace statement in source text.
46+
/// Excludes comments associated with this namespace.
47+
int get statementStart => directive.beginToken.charOffset;
48+
49+
/// The character offset of the end of the namespace statement in source text.
50+
/// Excludes comments associated with this namespace.
51+
int get statementEnd => directive.endToken.end;
52+
53+
/// The character offset of the end of this namespace in source text.
54+
/// Includes comments associated with this namespace.
55+
int end() {
56+
var end = directive.endToken.end;
57+
for (final afterComment in afterComments) {
58+
if (afterComment.end > end) {
59+
end = afterComment.end;
60+
}
61+
}
62+
return end;
63+
}
64+
65+
/// The character offset of the start of this namespace in source text.
66+
/// Includes comments associated with this namespace.
67+
int start() {
68+
var charOffset = directive.beginToken.charOffset;
69+
for (final beforeComment in beforeComments) {
70+
if (beforeComment.charOffset < charOffset) {
71+
charOffset = beforeComment.charOffset;
72+
}
73+
}
74+
return charOffset;
75+
}
76+
}

0 commit comments

Comments
 (0)