Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ import_sorter:

If you need another example check the [example app's import_sorter configuration](https://github.com/fluttercommunity/import_sorter/blob/master/example/example_app/pubspec.yaml#L76).

## Pubspec Dependency Sorting 📦

The tool now automatically organizes your `pubspec.yaml` dependencies, ensuring cleaner and more readable project configurations.

When running `import_sorter`, it will alphabetically sort package entries within the following sections, preserving comments and formatting:

- `dependencies`
- `dev_dependencies`
- `dependency_overrides`

This feature is enabled by default.

## 🚨 [`pre-commit`](https://pre-commit.com/) Hook

There are two pre-commit hooks available: `dart-import-sorter` and `flutter-import-sorter`. They use `pub run` and `flutter pub run` respectively. Use the former for a generic Dart project and the latter for a Flutter project.
Expand All @@ -124,7 +136,7 @@ Using pre-commit hooks in your project:

```yaml
- repo: https://github.com/fluttercommunity/import_sorter
rev: 'master'
rev: "master"
hooks:
- id: dart-import-sorter # use `flutter-import-sorter` for a Flutter project
```
Expand All @@ -140,8 +152,8 @@ pre-commit run --all-files
All contributions are welcome! Just make sure that it's not an already existing issue or pull request.

<!-- DO NOT REMOVE - contributor_list:start -->
## 👥 Contributors

## 👥 Contributors

- **[@gleich](https://github.com/gleich)**

Expand All @@ -161,4 +173,6 @@ All contributions are welcome! Just make sure that it's not an already existing

- **[@vHanda](https://github.com/vHanda)**

- **[@Franklyn-R-Silva](https://github.com/Franklyn-R-Silva)**

<!-- DO NOT REMOVE - contributor_list:end -->
9 changes: 9 additions & 0 deletions bin/import_sorter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io';

// 📦 Package imports:
import 'package:args/args.dart';
import 'package:import_sorter/pubspec_sort.dart' as pubspec_sort;
import 'package:tint/tint.dart';
import 'package:yaml/yaml.dart';

Expand Down Expand Up @@ -104,6 +105,14 @@ void main(List<String> args) {
sortedFiles.add(filePath);
}

stdout.write('\n┏━━ Sorting pubspec.yaml dependencies...\n');
final pubspecSorted = pubspec_sort.sortPubspec();
if (pubspecSorted) {
stdout.write('┃ ┗━━ $success Sorted dependencies in pubspec.yaml\n');
} else {
stdout.write('┃ ┗━━ pubspec.yaml is already sorted or not found\n');
}

stopwatch.stop();

// Outputting results
Expand Down
32 changes: 32 additions & 0 deletions example/example_app/ios/Flutter/ephemeral/flutter_lldb_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# Generated file, do not edit.
#

import lldb

def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()

# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'

error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return

def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")
5 changes: 5 additions & 0 deletions example/example_app/ios/Flutter/ephemeral/flutter_lldbinit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#
# Generated file, do not edit.
#

command script import --relative-to-command-file flutter_lldb_helper.py
66 changes: 66 additions & 0 deletions lib/pubspec_sort.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 🎯 Dart imports:
import 'dart:io';

// 📦 Package imports:
import 'package:yaml/yaml.dart';
import 'package:yaml_edit/yaml_edit.dart';

/// Sorts the dependencies (dependencies, dev_dependencies, dependency_overrides)
/// in pubspec.yaml alphabetically.
///
/// Returns true if changes were made to the file, and false otherwise
/// (if the file doesn't exist or is already sorted).
bool sortPubspec({String path = 'pubspec.yaml'}) {
final file = File(path);

// 1. Check if the file exists.
if (!file.existsSync()) {
return false;
}

final content = file.readAsStringSync();
final editor = YamlEditor(content);
final yaml = loadYaml(content);

Copilot AI Dec 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no error handling for malformed YAML files. If the pubspec.yaml contains invalid YAML syntax, loadYaml will throw a YamlException, which will crash the program. Consider wrapping the YAML parsing in a try-catch block and returning false on error, similar to how the function handles missing files.

Suggested change
final yaml = loadYaml(content);
dynamic yaml;
try {
yaml = loadYaml(content);
} on YamlException catch (_) {
// Malformed YAML, return false as we do for missing files
return false;
} on FormatException catch (_) {
// Malformed YAML, return false as we do for missing files
return false;
}

Copilot uses AI. Check for mistakes.

// 2. Define the sections that should be sorted.
final sectionsToCheck = [
'dependencies',
'dev_dependencies',
'dependency_overrides'
Comment thread
Franklyn-R-Silva marked this conversation as resolved.
];
Comment thread
Franklyn-R-Silva marked this conversation as resolved.

var changed = false;

// 3. Iterate over sections and apply sorting logic.
for (final section in sectionsToCheck) {
if (yaml is Map && yaml.containsKey(section)) {
final sectionMap = yaml[section];

// Skip if the section is not a Map or is empty
if (sectionMap is! Map || sectionMap.isEmpty) continue;

// Get the keys (package names)
final keys = sectionMap.keys.map((e) => e.toString()).toList();

// Create a list of sorted keys
final sortedKeys = List<String>.from(keys)..sort();

// If the current order is different from the sorted order, apply the change
if (keys.toString() != sortedKeys.toString()) {
Comment thread
Franklyn-R-Silva marked this conversation as resolved.
// Recreate the map with the new order
final sortedMap = {for (var key in sortedKeys) key: sectionMap[key]};

// Update the file virtually (using yaml_edit to preserve comments and structure)
editor.update([section], sortedMap);
changed = true;
}
}
}

// 4. If any change was made, write the new content to the file.
if (changed) {
file.writeAsStringSync(editor.toString());
}

return changed;
}
51 changes: 37 additions & 14 deletions lib/sort.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// 🎯 Dart imports:
import 'dart:io';

/// Sort the imports
/// Returns the sorted file as a string at
/// index 0 and the number of sorted imports
/// at index 1
/// Sorts the imports in a given list of lines.
/// Returns an [ImportSortData] object containing the sorted file
/// as a string and a boolean indicating if the file was updated.
ImportSortData sortImports(
List<String> lines,
String packageName,
Expand All @@ -13,6 +12,7 @@ ImportSortData sortImports(
bool noComments, {
String? filePath,
}) {
// Functions to generate the categorized import comments
String dartImportComment(bool emojis) =>
'//${emojis ? ' 🎯 ' : ' '}Dart imports:';
String flutterImportComment(bool emojis) =>
Expand Down Expand Up @@ -41,7 +41,7 @@ ImportSortData sortImports(
var isMultiLineString = false;

for (var i = 0; i < lines.length; i++) {
// Check if line is in multiline string
// Check if line is inside a multiline string literal
if (_timesContained(lines[i], "'''") == 1 ||
_timesContained(lines[i], '"""') == 1) {
isMultiLineString = !isMultiLineString;
Expand All @@ -62,7 +62,9 @@ ImportSortData sortImports(
} else {
projectRelativeImports.add(lines[i]);
}
} else if (i != lines.length - 1 &&
}
// Check for existing import comments that should be ignored
else if (i != lines.length - 1 &&
(lines[i] == dartImportComment(false) ||
lines[i] == flutterImportComment(false) ||
lines[i] == packageImportComment(false) ||
Expand All @@ -74,16 +76,21 @@ ImportSortData sortImports(
lines[i] == '// 📱 Flutter imports:') &&
lines[i + 1].startsWith('import ') &&
lines[i + 1].endsWith(';')) {
} else if (noImports()) {
}
// If no imports have been found yet, the line belongs to the file header
else if (noImports()) {
beforeImportLines.add(lines[i]);
} else {
}
// Otherwise, the line belongs to the code after the imports
else {
afterImportLines.add(lines[i]);
}
}

// If no imports return original string of lines
// If no imports were found, return the original string of lines
if (noImports()) {
var joinedLines = lines.join('\n');
// Ensure the file ends with a single newline character
if (joinedLines.endsWith('\n') && !joinedLines.endsWith('\n\n')) {
joinedLines += '\n';
} else if (!joinedLines.endsWith('\n')) {
Expand All @@ -92,7 +99,7 @@ ImportSortData sortImports(
return ImportSortData(joinedLines, false);
}

// Remove spaces
// Remove trailing empty lines from the header
if (beforeImportLines.isNotEmpty) {
if (beforeImportLines.last.trim() == '') {
beforeImportLines.removeLast();
Expand All @@ -101,21 +108,27 @@ ImportSortData sortImports(

final sortedLines = <String>[...beforeImportLines];

// Adding content conditionally
// Add a blank line if the header is not empty
if (beforeImportLines.isNotEmpty) {
sortedLines.add('');
}

// Adding Dart imports
if (dartImports.isNotEmpty) {
if (!noComments) sortedLines.add(dartImportComment(emojis));
dartImports.sort();
sortedLines.addAll(dartImports);
}

// Adding Flutter imports
if (flutterImports.isNotEmpty) {
if (dartImports.isNotEmpty) sortedLines.add('');
if (!noComments) sortedLines.add(flutterImportComment(emojis));
flutterImports.sort();
sortedLines.addAll(flutterImports);
}

// Adding Package imports
if (packageImports.isNotEmpty) {
if (dartImports.isNotEmpty || flutterImports.isNotEmpty) {
sortedLines.add('');
Expand All @@ -124,6 +137,8 @@ ImportSortData sortImports(
packageImports.sort();
sortedLines.addAll(packageImports);
}

// Adding Project imports (relative and absolute paths)
if (projectImports.isNotEmpty || projectRelativeImports.isNotEmpty) {
if (dartImports.isNotEmpty ||
flutterImports.isNotEmpty ||
Expand All @@ -137,42 +152,50 @@ ImportSortData sortImports(
sortedLines.addAll(projectRelativeImports);
}

// Add separation line before the rest of the code
sortedLines.add('');

// Add the remaining code lines
var addedCode = false;
for (var j = 0; j < afterImportLines.length; j++) {
if (afterImportLines[j] != '') {
sortedLines.add(afterImportLines[j]);
addedCode = true;
}
// Only keep internal blank lines if code (non-blank line) has been added
if (addedCode && afterImportLines[j] == '') {
sortedLines.add(afterImportLines[j]);
}
}
// Add a final newline for standard file format compliance
sortedLines.add('');

final sortedFile = sortedLines.join('\n');
final original = '${lines.join('\n')}\n';

// Check if the file changed and exit if required by the flag
if (exitIfChanged && original != sortedFile) {
if (filePath != null) {
stdout
.writeln('\n┗━━🚨 File $filePath does not have its imports sorted.');
}
exit(1);
}

// If no changes were made, return the original content
if (original == sortedFile) {
return ImportSortData(original, false);
}

// Return the sorted content
return ImportSortData(sortedFile, true);
}

/// Get the number of times a string contains another
/// string
/// Gets the number of times a string contains another string
int _timesContained(String string, String looking) =>
string.split(looking).length - 1;

/// Data to return from a sort
/// Data structure to return from a sort operation
class ImportSortData {
final String sortedFile;
final bool updated;
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ dependencies:
args: ^2.4.2
tint: ^2.0.1
yaml: ^3.1.2
yaml_edit: ^2.2.3

dev_dependencies:
lints: ^3.0.0
test: ^1.16.7


import_sorter:
emojis: true
Loading