diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9ef41a..f1ba3bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - **aiken-lang**: the keyword `fail` on property-based test semantic has changed and now consider a test to succeed only if **every** execution of the test failed (instead of just one). The previous behavior can be recovered by adding the keyword `once` after `fail`. @KtorZ +- **aiken-lang**: duplicate import lines are now automatically merged instead of raising a warning. However, imports can no longer appear anywhere in the file and must come as the first definitions. @KtorZ + ### Fixed - **aiken-lang**: fixed the number of 'after x tests' number reported on property test failure, which was off by one. @KtorZ diff --git a/crates/aiken-lang/src/parser.rs b/crates/aiken-lang/src/parser.rs index d27aabc0..564045d7 100644 --- a/crates/aiken-lang/src/parser.rs +++ b/crates/aiken-lang/src/parser.rs @@ -13,10 +13,11 @@ mod utils; use crate::{ast, line_numbers::LineNumbers}; pub use annotation::parser as annotation; use chumsky::prelude::*; -pub use definition::parser as definition; +pub use definition::{import::parser as import, parser as definition}; use error::ParseError; pub use expr::parser as expression; use extra::ModuleExtra; +use indexmap::IndexMap; pub use pattern::parser as pattern; pub fn module( @@ -27,7 +28,48 @@ pub fn module( let stream = chumsky::Stream::from_iter(ast::Span::create(tokens.len(), 1), tokens.into_iter()); - let definitions = definition().repeated().then_ignore(end()).parse(stream)?; + let definitions = import() + .repeated() + .map(|imports| { + let mut store = IndexMap::new(); + + for import in imports.into_iter() { + let key = (import.module, import.as_name); + match store.remove(&key) { + None => { + store.insert(key, (import.location, import.unqualified)); + } + Some((location, unqualified)) => { + let mut merged_unqualified = Vec::new(); + merged_unqualified.extend(unqualified); + merged_unqualified.extend(import.unqualified); + store.insert(key, (location, merged_unqualified)); + } + } + } + + store + .into_iter() + .map(|((module, as_name), (location, unqualified))| { + ast::Definition::Use(ast::Use { + module, + as_name, + location, + unqualified, + package: (), + }) + }) + .collect::>() + }) + .then(definition().repeated()) + .map(|(imports, others)| { + let mut defs = Vec::new(); + defs.extend(imports); + defs.extend(others); + defs + }) + .then_ignore(end()) + .parse(stream)?; let lines = LineNumbers::new(src); @@ -47,6 +89,16 @@ pub fn module( mod tests { use crate::assert_module; + #[test] + fn merge_imports() { + assert_module!( + r#" + use aiken/list.{bar, foo} + use aiken/list.{baz} + "# + ); + } + #[test] fn windows_newline() { assert_module!("use aiken/list\r\n"); diff --git a/crates/aiken-lang/src/parser/definition/import.rs b/crates/aiken-lang/src/parser/definition/import.rs index d98366c1..22a84d36 100644 --- a/crates/aiken-lang/src/parser/definition/import.rs +++ b/crates/aiken-lang/src/parser/definition/import.rs @@ -4,7 +4,7 @@ use crate::{ }; use chumsky::prelude::*; -pub fn parser() -> impl Parser { +pub fn parser() -> impl Parser { let unqualified_import = choice(( select! {Token::Name { name } => name}.then( just(Token::As) @@ -42,30 +42,28 @@ pub fn parser() -> impl Parser impl Parser { choice(( - import(), data_type(), type_alias(), validator(), diff --git a/crates/aiken-lang/src/parser/definition/snapshots/import_alias.snap b/crates/aiken-lang/src/parser/definition/snapshots/import_alias.snap index 72220ba3..9e32d734 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/import_alias.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/import_alias.snap @@ -2,17 +2,15 @@ source: crates/aiken-lang/src/parser/definition/import.rs description: "Code:\n\nuse aiken/list as foo" --- -Use( - Use { - as_name: Some( - "foo", - ), - location: 0..21, - module: [ - "aiken", - "list", - ], - package: (), - unqualified: [], - }, -) +Use { + as_name: Some( + "foo", + ), + location: 0..21, + module: [ + "aiken", + "list", + ], + package: (), + unqualified: [], +} diff --git a/crates/aiken-lang/src/parser/definition/snapshots/import_basic.snap b/crates/aiken-lang/src/parser/definition/snapshots/import_basic.snap index 49015b8f..cac12581 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/import_basic.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/import_basic.snap @@ -2,15 +2,13 @@ source: crates/aiken-lang/src/parser/definition/import.rs description: "Code:\n\nuse aiken/list" --- -Use( - Use { - as_name: None, - location: 0..14, - module: [ - "aiken", - "list", - ], - package: (), - unqualified: [], - }, -) +Use { + as_name: None, + location: 0..14, + module: [ + "aiken", + "list", + ], + package: (), + unqualified: [], +} diff --git a/crates/aiken-lang/src/parser/definition/snapshots/import_unqualified.snap b/crates/aiken-lang/src/parser/definition/snapshots/import_unqualified.snap index a296b3a2..2310c47f 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/import_unqualified.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/import_unqualified.snap @@ -2,30 +2,28 @@ source: crates/aiken-lang/src/parser/definition/import.rs description: "Code:\n\nuse std/address.{Address as A, thing as w}\n" --- -Use( - Use { - as_name: None, - location: 0..42, - module: [ - "std", - "address", - ], - package: (), - unqualified: [ - UnqualifiedImport { - location: 17..29, - name: "Address", - as_name: Some( - "A", - ), - }, - UnqualifiedImport { - location: 31..41, - name: "thing", - as_name: Some( - "w", - ), - }, - ], - }, -) +Use { + as_name: None, + location: 0..42, + module: [ + "std", + "address", + ], + package: (), + unqualified: [ + UnqualifiedImport { + location: 17..29, + name: "Address", + as_name: Some( + "A", + ), + }, + UnqualifiedImport { + location: 31..41, + name: "thing", + as_name: Some( + "w", + ), + }, + ], +} diff --git a/crates/aiken-lang/src/parser/utils.rs b/crates/aiken-lang/src/parser/utils.rs index 08853bd4..49016fd3 100644 --- a/crates/aiken-lang/src/parser/utils.rs +++ b/crates/aiken-lang/src/parser/utils.rs @@ -1,6 +1,5 @@ -use chumsky::prelude::*; - use super::{error::ParseError, token::Token}; +use chumsky::prelude::*; pub fn optional_flag(token: Token) -> impl Parser { just(token).ignored().or_not().map(|v| v.is_some()) @@ -132,6 +131,27 @@ macro_rules! assert_definition { }; } +#[macro_export] +macro_rules! assert_import { + ($code:expr) => { + use chumsky::Parser; + + let $crate::parser::lexer::LexInfo { tokens, .. } = $crate::parser::lexer::run(indoc::indoc! { $code }).unwrap(); + + let stream = chumsky::Stream::from_iter($crate::ast::Span::create(tokens.len(), 1), tokens.into_iter()); + + let result = $crate::parser::import().parse(stream).unwrap(); + + insta::with_settings!({ + description => concat!("Code:\n\n", indoc::indoc! { $code }), + prepend_module_to_snapshot => false, + omit_expression => true + }, { + insta::assert_debug_snapshot!(result); + }); + }; +} + #[macro_export] macro_rules! assert_format { ($code:expr) => { diff --git a/crates/aiken-lang/src/snapshots/merge_imports.snap b/crates/aiken-lang/src/snapshots/merge_imports.snap new file mode 100644 index 00000000..d8b27c86 --- /dev/null +++ b/crates/aiken-lang/src/snapshots/merge_imports.snap @@ -0,0 +1,51 @@ +--- +source: crates/aiken-lang/src/parser.rs +description: "Code:\n\nuse aiken/list.{bar, foo}\nuse aiken/list.{baz}\n" +--- +Module { + name: "", + docs: [], + type_info: (), + definitions: [ + Use( + Use { + as_name: None, + location: 0..25, + module: [ + "aiken", + "list", + ], + package: (), + unqualified: [ + UnqualifiedImport { + location: 16..19, + name: "bar", + as_name: None, + }, + UnqualifiedImport { + location: 21..24, + name: "foo", + as_name: None, + }, + UnqualifiedImport { + location: 42..45, + name: "baz", + as_name: None, + }, + ], + }, + ), + ], + lines: LineNumbers { + line_starts: [ + 0, + 26, + 47, + ], + length: 47, + last: Some( + 47, + ), + }, + kind: Validator, +} diff --git a/crates/aiken-lang/src/tests/format.rs b/crates/aiken-lang/src/tests/format.rs index ce0b7128..c8a5f0c2 100644 --- a/crates/aiken-lang/src/tests/format.rs +++ b/crates/aiken-lang/src/tests/format.rs @@ -309,8 +309,6 @@ fn format_else_if() { #[test] fn format_imports() { - // TODO: Fix this case, this is behaving weirdly, not keeping the order for the comments and - // imports. assert_format!( r#" use aiken/list @@ -324,6 +322,59 @@ fn format_imports() { ); } +#[test] +fn format_merge_imports() { + assert_format!( + r#" + use aiken/list.{bar, foo} + use aiken/list.{baz} + "# + ); +} + +#[test] +fn format_merge_imports_2() { + assert_format!( + r#" + use aiken/list.{bar, foo} + use aiken/dict + use aiken/list + "# + ); +} + +#[test] +fn format_merge_imports_alias() { + assert_format!( + r#" + use aiken/list.{bar, foo} + use aiken/list.{baz} as vector + "# + ); +} + +#[test] +fn format_merge_imports_alias_2() { + assert_format!( + r#" + use aiken/list.{bar, foo} as vector + use aiken/list.{baz} as vector + "# + ); +} + +#[test] +fn format_merge_imports_comments() { + assert_format!( + r#" + // foo + use aiken/list.{bar, foo} + // bar + use aiken/list.{baz} + "# + ); +} + #[test] fn format_negate() { assert_format!( diff --git a/crates/aiken-lang/src/tests/snapshots/format_merge_imports.snap b/crates/aiken-lang/src/tests/snapshots/format_merge_imports.snap new file mode 100644 index 00000000..41d9d73b --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_merge_imports.snap @@ -0,0 +1,5 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nuse aiken/list.{bar, foo}\nuse aiken/list.{baz}\n" +--- +use aiken/list.{bar, baz, foo} diff --git a/crates/aiken-lang/src/tests/snapshots/format_merge_imports_2.snap b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_2.snap new file mode 100644 index 00000000..70e57a99 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_2.snap @@ -0,0 +1,6 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nuse aiken/list.{bar, foo}\nuse aiken/dict\nuse aiken/list\n" +--- +use aiken/dict +use aiken/list.{bar, foo} diff --git a/crates/aiken-lang/src/tests/snapshots/format_merge_imports_alias.snap b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_alias.snap new file mode 100644 index 00000000..a7d66b99 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_alias.snap @@ -0,0 +1,6 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nuse aiken/list.{bar, foo}\nuse aiken/list.{baz} as vector\n" +--- +use aiken/list.{bar, foo} +use aiken/list.{baz} as vector diff --git a/crates/aiken-lang/src/tests/snapshots/format_merge_imports_alias_2.snap b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_alias_2.snap new file mode 100644 index 00000000..3dd1a37f --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_alias_2.snap @@ -0,0 +1,5 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nuse aiken/list.{bar, foo} as vector\nuse aiken/list.{baz} as vector\n" +--- +use aiken/list.{bar, baz, foo} as vector diff --git a/crates/aiken-lang/src/tests/snapshots/format_merge_imports_comments.snap b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_comments.snap new file mode 100644 index 00000000..528d29b8 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_merge_imports_comments.snap @@ -0,0 +1,7 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\n// foo\nuse aiken/list.{bar, foo}\n// bar\nuse aiken/list.{baz}\n" +--- +// foo +use aiken/list.{bar, baz, foo} +// bar