sortImports function
ImportSortData
sortImports(
- List<
String> lines, - String packageName,
- bool emojis,
- @Deprecated('Has no effect since 1.4.2: lib/ no longer calls exit(). bin/ checks the ' 'whole project and fails once, so every unsorted file gets reported ' '(import_sorter#87). Will be removed in 2.0.0.') bool exitIfChanged,
- bool noComments, {
- @Deprecated('Has no effect since 1.4.2; it only ever fed the message the removed ' 'exit() printed. Will be removed in 2.0.0.') String? filePath,
- bool noBlankLines = false,
- List<
CustomTier> customTiers = const [], - bool groupProjectByFolder = false,
- bool testImports = false,
- List<
String> testImportPrefixes = TidyConfig.defaultTestImportPrefixes, - bool separateRelativeImports = false,
- bool sortExports = false,
- int groupProjectByFolderDepth = 0,
- bool removeDuplicates = false,
Sort the imports of a dart file.
Returns ImportSortData containing the sorted file content and whether any changes were made. Pure: it reads nothing, writes nothing, and never terminates the process โ what an unsorted file means is the caller's call.
removeDuplicates drops a directive that repeats one already kept, keeping
the first occurrence. It compares text, not meaning, so it only ever folds
away a directive written identically twice โ the shape a merge or a
double auto-import produces. Removing an import that is merely unused
is a different question, one that needs a resolved element model;
dart fix --apply --code=unused_import answers it, and the CLI's
--remove-unused runs exactly that before sorting.
Implementation
ImportSortData sortImports(
List<String> lines,
String packageName,
bool emojis,
@Deprecated(
'Has no effect since 1.4.2: lib/ no longer calls exit(). bin/ checks the '
'whole project and fails once, so every unsorted file gets reported '
'(import_sorter#87). Will be removed in 2.0.0.',
)
bool exitIfChanged,
bool noComments, {
@Deprecated(
'Has no effect since 1.4.2; it only ever fed the message the removed '
'exit() printed. Will be removed in 2.0.0.',
)
String? filePath,
bool noBlankLines = false,
List<CustomTier> customTiers = const [],
bool groupProjectByFolder = false,
bool testImports = false,
List<String> testImportPrefixes = TidyConfig.defaultTestImportPrefixes,
bool separateRelativeImports = false,
bool sortExports = false,
int groupProjectByFolderDepth = 0,
bool removeDuplicates = false,
}) {
// Asking for a folder depth is asking for folder grouping; requiring both
// options only creates a way to set the depth and see nothing happen.
final groupByFolder = groupProjectByFolder || groupProjectByFolderDepth > 0;
String groupComment(String name, String emoji, String noun) =>
'//${emojis ? ' $emoji ' : ' '}$name $noun:';
String tierComment(CustomTier tier) =>
'//${emojis ? ' ๐งฉ ' : ' '}${tier.name}';
// Every header we have ever emitted, so a re-run strips it instead of
// stacking a second one on top.
final strippable = <String>{'// ๐ฑ Flutter imports:'};
for (final noun in const ['imports', 'exports']) {
for (final label in _groupLabels) {
strippable
..add('// ${label[0]} $noun:')
..add('// ${label[1]} ${label[0]} $noun:');
}
}
for (final tier in customTiers) {
strippable
..add('// ${tier.name}')
..add('// ๐งฉ ${tier.name}');
}
final beforeLines = <String>[];
final afterLines = <String>[];
final imports = _Buckets(customTiers);
final exports = _Buckets(customTiers);
bool startsDirective(String line) =>
line.startsWith('import ') || (sortExports && line.startsWith('export '));
// Whether a directive begins at [index], looking past any `// ignore:`
// pragmas that belong to it. Decides whether a header line above is ours.
bool directiveFollows(int index) {
var i = index;
while (i < lines.length && _isIgnorePragma(lines[i])) {
i++;
}
return i < lines.length && startsDirective(lines[i]);
}
bool noDirectives() => imports.isEmpty && exports.isEmpty;
void classify(_Directive directive, {required bool isExport}) {
final bucket = isExport ? exports : imports;
final uri = directive.uri;
if (uri.startsWith('dart:')) {
bucket.dart.add(directive);
} else if (uri.startsWith('package:flutter/')) {
bucket.flutter.add(directive);
} else if (uri.startsWith('package:$packageName/')) {
if (testImports && _isTestDouble(uri, testImportPrefixes)) {
bucket.testDoublePackageForm.add(directive);
} else {
bucket.projectPackageForm.add(directive);
}
} else if (uri.startsWith('package:')) {
final tier = _matchTier(directive.code, customTiers);
if (tier != null) {
bucket.tiers[tier]!.add(directive);
} else {
bucket.package.add(directive);
}
} else if (testImports && _isTestDouble(uri, testImportPrefixes)) {
bucket.testDoubleRelative.add(directive);
} else {
bucket.projectRelative.add(directive);
}
}
// Signatures of the directives kept so far, for [removeDuplicates].
final seen = <String>{};
var duplicatesRemoved = 0;
final scanner = _SourceScanner();
var order = 0;
var index = 0;
while (index < lines.length) {
final line = lines[index];
// Only a line that *begins* in executable code can be a directive or a
// header of ours. Inside a string literal or a `/* */` block it is text,
// and moving it would change what the file means.
if (scanner.startsInCode) {
// A header we wrote on an earlier run: drop it, the emitter re-adds it.
if (strippable.contains(line) && directiveFollows(index + 1)) {
scanner.consume(line);
index++;
continue;
}
// `// ignore:` suppresses a lint on the line below it, so it is part of
// the directive that follows โ when one actually follows.
var start = index;
while (start < lines.length && _isIgnorePragma(lines[start])) {
start++;
}
if (start < lines.length && startsDirective(lines[start])) {
final span = _scanDirective(lines, start);
if (span > 0) {
final body = lines.sublist(start, start + span);
final uri = _directiveUri(body.first);
if (uri != null) {
final directive =
_Directive(lines.sublist(index, start), body, uri, order++);
if (removeDuplicates && !seen.add(directive.signature)) {
duplicatesRemoved++;
} else {
classify(
directive,
isExport: body.first.startsWith('export '),
);
}
// A directive can carry a `/*` or a string of its own, so the
// scanner has to walk the lines the loop skips over.
for (var i = index; i < start + span; i++) {
scanner.consume(lines[i]);
}
index = start + span;
continue;
}
}
}
}
(noDirectives() ? beforeLines : afterLines).add(line);
scanner.consume(line);
index++;
}
if (noDirectives()) {
var joinedLines = lines.join('\n');
if (!joinedLines.endsWith('\n')) {
joinedLines += '\n';
}
return ImportSortData(joinedLines, false);
}
if (beforeLines.isNotEmpty && beforeLines.last.trim().isEmpty) {
beforeLines.removeLast();
}
final sortedLines = <String>[...beforeLines];
if (beforeLines.isNotEmpty) {
sortedLines.add('');
}
var hasPrevious = false;
void addSeparator() {
if (!noBlankLines && hasPrevious) sortedLines.add('');
}
void emit(List<_Directive> directives) {
for (final directive in directives) {
sortedLines
..addAll(directive.leading)
..addAll(directive.lines);
}
}
void emitGroup(List<_Directive> directives, String comment) {
if (directives.isEmpty) return;
addSeparator();
if (!noComments) sortedLines.add(comment);
_sortByUri(directives);
emit(directives);
hasPrevious = true;
}
// Since Dart 3.13 `dart format` puts a blank line between the `package:` and
// relative sections. Without one, the two tools undo each other on every run
// (issue #1), so [separateRelativeImports] emits it up front. Never fires
// when blank lines are switched off.
bool separateBefore(
List<_Directive> packageForm,
List<_Directive> relative,
) =>
separateRelativeImports &&
!noBlankLines &&
packageForm.isNotEmpty &&
relative.isNotEmpty;
// Emits one group split into a package-form and a relative-form half that
// share a single header: the project group, and the test-double group that
// mirrors it.
void emitSplitGroup(
List<_Directive> packageForm,
List<_Directive> relative,
String comment, {
required bool byFolder,
}) {
if (packageForm.isEmpty && relative.isEmpty) return;
addSeparator();
if (!noComments) sortedLines.add(comment);
_sortByUri(packageForm);
_sortByUri(relative);
if (byFolder && !noBlankLines) {
// Folder grouping already breaks at the package-form/relative-form
// boundary (a relative URI can never start with `package:`), so the two
// features never stack up two blank lines.
String? previousKey;
for (final directive in [...packageForm, ...relative]) {
final key = _folderKey(directive.uri, groupProjectByFolderDepth);
if (previousKey != null && key != previousKey) sortedLines.add('');
sortedLines
..addAll(directive.leading)
..addAll(directive.lines);
previousKey = key;
}
} else {
emit(packageForm);
if (separateBefore(packageForm, relative)) sortedLines.add('');
emit(relative);
}
hasPrevious = true;
}
void emitBlock(_Buckets bucket, String noun) {
if (bucket.isEmpty) return;
emitGroup(bucket.dart, groupComment('Dart', '๐ฏ', noun));
emitGroup(bucket.flutter, groupComment('Flutter', '๐ฆ', noun));
emitGroup(bucket.package, groupComment('Package', '๐ฆ', noun));
for (final tier in customTiers) {
emitGroup(bucket.tiers[tier]!, tierComment(tier));
}
emitSplitGroup(
bucket.projectPackageForm,
bucket.projectRelative,
groupComment('Project', '๐', noun),
byFolder: groupByFolder,
);
emitSplitGroup(
bucket.testDoublePackageForm,
bucket.testDoubleRelative,
groupComment('Test', '๐งช', noun),
byFolder: false,
);
}
emitBlock(imports, 'imports');
emitBlock(exports, 'exports');
// Everything below the directive block, with the blank lines that separated
// it from the directives dropped โ the emitter re-adds exactly one.
final trailing = <String>[];
var addedCode = false;
for (final line in afterLines) {
if (line != '') {
trailing.add(line);
addedCode = true;
} else if (addedCode) {
trailing.add(line);
}
}
// A barrel file ends on its last directive. Emitting the separator anyway
// left a blank line below it, which `dart format` then strips right back
// out โ so the two tools undid each other on every run (issue #6).
if (trailing.isNotEmpty) {
sortedLines
..add('')
..addAll(trailing);
}
sortedLines.add('');
final sortedFile = sortedLines.join('\n');
final original = '${lines.join('\n')}\n';
if (original == sortedFile) {
return ImportSortData(original, false);
}
return ImportSortData(sortedFile, true, duplicatesRemoved: duplicatesRemoved);
}