Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fmt): add sort_imports config #5442

Merged
merged 4 commits into from
Dec 11, 2023
Merged
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
3 changes: 3 additions & 0 deletions crates/config/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub struct FormatterConfig {
pub ignore: Vec<String>,
/// Add new line at start and end of contract declarations
pub contract_new_lines: bool,
/// Sort import statements alphabetically in groups (a group is separated by a newline).
pub sort_imports: bool,
}

/// Style of uint/int256 types
Expand Down Expand Up @@ -176,6 +178,7 @@ impl Default for FormatterConfig {
wrap_comments: false,
ignore: vec![],
contract_new_lines: false,
sort_imports: false,
}
}
}
89 changes: 88 additions & 1 deletion crates/fmt/src/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ impl<'a, W: Write> Formatter<'a, W> {

/// Returns number of blank lines in source between two byte indexes
fn blank_lines(&self, start: usize, end: usize) -> usize {
// because of sorting import statements, start can be greater than end
if start > end {
return 0
}
self.source[start..end].trim_comments().matches('\n').count()
}

Expand Down Expand Up @@ -949,7 +953,10 @@ impl<'a, W: Write> Formatter<'a, W> {
(None, None) => return Ok(()),
}
.start();

let mut last_loc: Option<Loc> = None;
let mut visited_locs: Vec<Loc> = Vec::new();

let mut needs_space = false;
while let Some(mut line_item) = pop_next(self, &mut items) {
let loc = line_item.as_ref().either(|c| c.loc, |i| i.loc());
Expand Down Expand Up @@ -979,7 +986,19 @@ impl<'a, W: Write> Formatter<'a, W> {
Either::Right(item) => {
if !ignore_whitespace {
self.write_whitespace_separator(true)?;
if let Some(last_loc) = last_loc {
if let Some(mut last_loc) = last_loc {
// here's an edge case when we reordered items so the last_loc isn't
// necessarily the item that directly precedes the current item because
// the order might have changed, so we need to find the last item that
// is before the current item by checking the recorded locations
if let Some(last_item) = visited_locs
.iter()
.rev()
.find(|prev_item| prev_item.start() > last_loc.end())
{
last_loc = *last_item;
}

if needs_space || self.blank_lines(last_loc.end(), loc.start()) > 1 {
writeln!(self.buf())?;
}
Expand All @@ -997,6 +1016,8 @@ impl<'a, W: Write> Formatter<'a, W> {
}

last_loc = Some(loc);
visited_locs.push(loc);

last_byte_written = loc.end();
if let Some(comment) = line_item.as_ref().left() {
if comment.is_line() {
Expand Down Expand Up @@ -1593,6 +1614,69 @@ impl<'a, W: Write> Formatter<'a, W> {
}
Ok(())
}

/// Sorts grouped import statement alphabetically.
fn sort_imports(&self, source_unit: &mut SourceUnit) {
// first we need to find the grouped import statements
// A group is defined as a set of import statements that are separated by a blank line
let mut import_groups = Vec::new();
let mut current_group = Vec::new();
let mut source_unit_parts = source_unit.0.iter().enumerate().peekable();
while let Some((i, part)) = source_unit_parts.next() {
if let SourceUnitPart::ImportDirective(_) = part {
current_group.push(i);
let current_loc = part.loc();
if let Some((_, next_part)) = source_unit_parts.peek() {
let next_loc = next_part.loc();
// import statements are followed by a new line, so if there are more than one
// we have a group
if self.blank_lines(current_loc.end(), next_loc.start()) > 1 {
import_groups.push(std::mem::take(&mut current_group));
}
}
} else if !current_group.is_empty() {
import_groups.push(std::mem::take(&mut current_group));
}
}

if !current_group.is_empty() {
import_groups.push(current_group);
}

if import_groups.is_empty() {
// nothing to sort
return
}

// order all groups alphabetically
for group in import_groups.iter() {
// SAFETY: group is not empty
let first = group[0];
let last = group.last().copied().expect("group is not empty");
let import_directives = &mut source_unit.0[first..=last];

// sort rename style imports alphabetically based on the actual import and not the
// rename
for source_unit_part in import_directives.iter_mut() {
if let SourceUnitPart::ImportDirective(Import::Rename(_, renames, _)) =
source_unit_part
{
renames.sort_by_cached_key(|(og_ident, _)| og_ident.name.clone());
}
}

import_directives.sort_by_cached_key(|item| match item {
SourceUnitPart::ImportDirective(import) => match import {
Import::Plain(path, _) => path.to_string(),
Import::GlobalSymbol(path, _, _) => path.to_string(),
Import::Rename(path, _, _) => path.to_string(),
},
_ => {
unreachable!("import group contains non-import statement")
}
});
}
}
}

// Traverse the Solidity Parse Tree and write to the code formatter
Expand All @@ -1619,6 +1703,9 @@ impl<'a, W: Write> Visitor for Formatter<'a, W> {

#[instrument(name = "SU", skip_all)]
fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> {
if self.config.sort_imports {
self.sort_imports(source_unit);
}
// TODO: do we need to put pragma and import directives at the top of the file?
// source_unit.0.sort_by_key(|item| match item {
// SourceUnitPart::PragmaDirective(_, _, _) => 0,
Expand Down
34 changes: 34 additions & 0 deletions crates/fmt/testdata/SortedImports/fmt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// config: sort_imports = true
import "SomeFile0.sol" as SomeOtherFile;
import "SomeFile1.sol" as SomeOtherFile;
import "SomeFile2.sol";
import "SomeFile3.sol";

import "AnotherFile1.sol" as SomeSymbol;
import "AnotherFile2.sol" as SomeSymbol;

import {
symbol1 as alias3,
symbol2 as alias2,
symbol3 as alias1,
symbol4
} from "File0.sol";
import {symbol1 as alias, symbol2} from "File2.sol";
import {symbol1 as alias, symbol2} from "File3.sol";
import {
symbol1 as alias1,
symbol2 as alias2,
symbol3 as alias3,
symbol4
} from "File6.sol";

uint256 constant someConstant = 10;

import {Something2, Something3} from "someFile.sol";

// This is a comment
import {Something2, Something3} from "someFile.sol";

import {symbol1 as alias, symbol2} from "File3.sol";
// comment inside group is treated as a separator for now
import {symbol1 as alias, symbol2} from "File2.sol";
23 changes: 23 additions & 0 deletions crates/fmt/testdata/SortedImports/original.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "SomeFile3.sol";
import "SomeFile2.sol";
import "SomeFile1.sol" as SomeOtherFile;
import "SomeFile0.sol" as SomeOtherFile;

import "AnotherFile2.sol" as SomeSymbol;
import "AnotherFile1.sol" as SomeSymbol;

import {symbol2, symbol1 as alias} from "File3.sol";
import {symbol2, symbol1 as alias} from "File2.sol";
import {symbol2 as alias2, symbol1 as alias1, symbol3 as alias3, symbol4} from "File6.sol";
import {symbol3 as alias1, symbol2 as alias2, symbol1 as alias3, symbol4} from "File0.sol";

uint256 constant someConstant = 10;

import {Something3, Something2} from "someFile.sol";

// This is a comment
import {Something3, Something2} from "someFile.sol";

import {symbol2, symbol1 as alias} from "File3.sol";
// comment inside group is treated as a separator for now
import {symbol2, symbol1 as alias} from "File2.sol";
43 changes: 36 additions & 7 deletions crates/fmt/tests/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn tracing() {
let _ = tracing::subscriber::set_global_default(subscriber);
}

fn test_directory(base_name: &str) {
fn test_directory(base_name: &str, test_config: TestConfig) {
tracing();
let mut original = None;

Expand Down Expand Up @@ -74,6 +74,7 @@ fn test_directory(base_name: &str) {
config,
original.as_ref().expect("original.sol not found"),
&formatted,
test_config,
);
}
}
Expand All @@ -82,7 +83,13 @@ fn assert_eof(content: &str) {
assert!(content.ends_with('\n') && !content.ends_with("\n\n"));
}

fn test_formatter(filename: &str, config: FormatterConfig, source: &str, expected_source: &str) {
fn test_formatter(
filename: &str,
config: FormatterConfig,
source: &str,
expected_source: &str,
test_config: TestConfig,
) {
#[derive(Eq)]
struct PrettyString(String);

Expand All @@ -103,7 +110,7 @@ fn test_formatter(filename: &str, config: FormatterConfig, source: &str, expecte
let source_parsed = parse(source).unwrap();
let expected_parsed = parse(expected_source).unwrap();

if !source_parsed.pt.ast_eq(&expected_parsed.pt) {
if !test_config.skip_compare_ast_eq && !source_parsed.pt.ast_eq(&expected_parsed.pt) {
pretty_assertions::assert_eq!(
source_parsed.pt,
expected_parsed.pt,
Expand All @@ -118,7 +125,6 @@ fn test_formatter(filename: &str, config: FormatterConfig, source: &str, expecte
format_to(&mut source_formatted, source_parsed, config.clone()).unwrap();
assert_eof(&source_formatted);

// println!("{}", source_formatted);
let source_formatted = PrettyString(source_formatted);

pretty_assertions::assert_eq!(
Expand All @@ -142,13 +148,34 @@ fn test_formatter(filename: &str, config: FormatterConfig, source: &str, expecte
);
}

macro_rules! test_directories {
($($dir:ident),+ $(,)?) => {$(
#[derive(Default, Copy, Clone)]
struct TestConfig {
/// Whether to compare the formatted source code AST with the original AST
skip_compare_ast_eq: bool,
}

impl TestConfig {
fn skip_compare_ast_eq() -> Self {
Self { skip_compare_ast_eq: true }
}
}

macro_rules! test_dir {
($dir:ident $(,)?) => {
test_dir!($dir, Default::default());
};
($dir:ident, $config:expr $(,)?) => {
#[allow(non_snake_case)]
#[test]
fn $dir() {
test_directory(stringify!($dir));
test_directory(stringify!($dir), $config);
}
};
}

macro_rules! test_directories {
($($dir:ident),+ $(,)?) => {$(
test_dir!($dir);
)+};
}

Expand Down Expand Up @@ -202,3 +229,5 @@ test_directories! {
EmitStatement,
Repros,
}

test_dir!(SortedImports, TestConfig::skip_compare_ast_eq());