From a1667d43a010afbe016aca90c6839f0fe04756c2 Mon Sep 17 00:00:00 2001 From: sobolevn <mail@sobolevn.me> Date: Wed, 21 Aug 2024 18:19:18 +0300 Subject: [PATCH 1/6] Improve import error suggestions --- CHANGELOG.md | 6 ++++ compiler-core/src/analyse/imports.rs | 20 +++++++++++ compiler-core/src/error.rs | 3 +- compiler-core/src/parse.rs | 35 ++++++++++++++++++- compiler-core/src/parse/error.rs | 35 +++++++++++++++++++ ...ore__parse__tests__import_dotted_name.snap | 13 +++++++ ...e__parse__tests__import_dotted_upname.snap | 14 ++++++++ compiler-core/src/parse/tests.rs | 18 ++++++++++ compiler-core/src/type_/error.rs | 3 ++ compiler-core/src/type_/expression.rs | 2 ++ compiler-core/src/type_/tests/imports.rs | 20 +++++++++++ ...s__imports__import_value_as_submodule.snap | 12 +++++++ ...__imports__import_value_as_submodule2.snap | 12 +++++++ 13 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name.snap create mode 100644 compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_upname.snap create mode 100644 compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap create mode 100644 compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c89c1277b..b2fc10b7428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,12 @@ - Adds a hint to syntax error when defining a function inside a type. ([sobolevn](https://github.com/sobolevn)) +- Improve import error messages. + + Now we recommend fixes for `import a.b` + and have better suggestions for `import a/b` when `b` is acutally a value. + ([sobolevn](https://github.com/sobolevn)) + ### Formatter ### Language Server diff --git a/compiler-core/src/analyse/imports.rs b/compiler-core/src/analyse/imports.rs index d7f2de5676d..d16f661cde2 100644 --- a/compiler-core/src/analyse/imports.rs +++ b/compiler-core/src/analyse/imports.rs @@ -1,4 +1,5 @@ use ecow::EcoString; +use itertools::Itertools; use crate::{ ast::{SrcSpan, UnqualifiedImport, UntypedImport}, @@ -48,9 +49,28 @@ impl<'context, 'problems> Importer<'context, 'problems> { // Find imported module let Some(module_info) = self.environment.importable_modules.get(&name) else { + // Improve error message when users confuse `import one/two` + // with `import one.{two}`. + let mut modpath = name.split("/").collect_vec(); + let last_part = match modpath.last() { + Some(name) => name, + None => "", + }; + modpath.truncate(modpath.len() - 1); + let basename = EcoString::from(modpath.join("/")); + + let mut hint = None; + if let Some(module) = self.environment.importable_modules.get(&basename) { + if module.get_public_value(last_part).is_some() { + hint = Some(format!( + "Maybe you meant `import {basename}.{{{last_part}}}`?" + )); + } + } self.problems.error(Error::UnknownModule { location, name: name.clone(), + hint, imported_modules: self.environment.imported_modules.keys().cloned().collect(), }); return; diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs index 8f9f1db0d3e..e3e5d82796f 100644 --- a/compiler-core/src/error.rs +++ b/compiler-core/src/error.rs @@ -1958,10 +1958,11 @@ Private types can only be used within the module that defines them.", location, name, imported_modules, + hint, } => Diagnostic { title: "Unknown module".into(), text: format!("No module has been found with the name `{name}`."), - hint: None, + hint: hint.clone(), level: Level::Error, location: Some(Location { label: Label { diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs index b484c8bffda..b12f9c36cf8 100644 --- a/compiler-core/src/parse.rs +++ b/compiler-core/src/parse.rs @@ -2410,6 +2410,7 @@ where // import a // import a/b // import a/b.{c} + // import a/b.{type C} // import a/b.{c as d} as e fn parse_import(&mut self, import_start: u32) -> Result<Option<UntypedDefinition>, ParseError> { let mut start = 0; @@ -2450,7 +2451,9 @@ where let mut unqualified_types = vec![]; if self.maybe_one(&Token::Dot).is_some() { - let _ = self.expect_one(&Token::LeftBrace)?; + let _ = self + .expect_one(&Token::LeftBrace) + .map_err(|err| self.add_import_syntax_hint(err, &module))?; let parsed = self.parse_unqualified_imports()?; unqualified_types = parsed.types; unqualified_values = parsed.values; @@ -2487,6 +2490,36 @@ where }))) } + fn add_import_syntax_hint(&self, err: ParseError, module: &String) -> ParseError { + match err.error { + ParseErrorType::UnexpectedToken { + token: Token::Name { ref name }, + .. + } => ParseError { + location: err.location, + error: ParseErrorType::InvalidImportSyntax { + module: module.into(), + name: name.clone(), + upname: false, + }, + }, + + ParseErrorType::UnexpectedToken { + token: Token::UpName { ref name }, + .. + } => ParseError { + location: err.location, + error: ParseErrorType::InvalidImportSyntax { + module: module.into(), + name: name.clone(), + upname: true, + }, + }, + + _ => err, + } + } + // [Name (as Name)? | UpName (as Name)? ](, [Name (as Name)? | UpName (as Name)?])*,? fn parse_unqualified_imports(&mut self) -> Result<ParsedUnqualifiedImports, ParseError> { let mut imports = ParsedUnqualifiedImports::default(); diff --git a/compiler-core/src/parse/error.rs b/compiler-core/src/parse/error.rs index e4e3af964ba..9e184383b35 100644 --- a/compiler-core/src/parse/error.rs +++ b/compiler-core/src/parse/error.rs @@ -292,6 +292,36 @@ utf16_codepoint, utf32_codepoint, signed, unsigned, big, little, native, size, u "Unsupported expression", vec!["Functions cannot be called in clause guards.".into()], ), + ParseErrorType::InvalidImportSyntax { + module, + name, + upname, + } => { + let mut hint = vec![wrap( + "This syntax for import is not correct. Probably, you meant:", + )]; + if *upname { + let l_name = name.to_lowercase(); + hint.push(wrap(format!( + "- `import {module}/{l_name}` to import module `{l_name}` from package `{module}`").as_str(), + )); + hint.push(wrap(format!( + "- `import {module}.{{{name}}}` to import value `{name}` from module `{module}`").as_str(), + )); + hint.push(wrap(format!( + "- `import {module}.{{type {name}}}` to import type `{name}` from module `{module}`").as_str(), + )); + } else { + hint.push(wrap(format!( + "- `import {module}/{name}` to import module `{name}` from package `{module}`").as_str(), + )); + hint.push(wrap(format!( + "- `import {module}.{{{name}}}` to import value `{name}` from module `{module}`").as_str(), + )); + } + + ("I was not expecting this", hint) + } } } } @@ -358,6 +388,11 @@ pub enum ParseErrorType { field_type: Option<TypeAst>, }, CallInClauseGuard, // case x { _ if f() -> 1 } + InvalidImportSyntax { + module: EcoString, + name: EcoString, + upname: bool, + }, } impl LexicalError { diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name.snap new file mode 100644 index 00000000000..0f031dcd1a6 --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name.snap @@ -0,0 +1,13 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "\nimport one.two\n" +--- +error: Syntax error + ┌─ /src/parse/error.gleam:2:12 + │ +2 │ import one.two + │ ^^^ I was not expecting this + +This syntax for import is not correct. Probably, you meant: +- `import one/two` to import module `two` from package `one` +- `import one.{two}` to import value `two` from module `one` diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_upname.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_upname.snap new file mode 100644 index 00000000000..b77685feecd --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_upname.snap @@ -0,0 +1,14 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "\nimport one.Two\n" +--- +error: Syntax error + ┌─ /src/parse/error.gleam:2:12 + │ +2 │ import one.Two + │ ^^^ I was not expecting this + +This syntax for import is not correct. Probably, you meant: +- `import one/two` to import module `two` from package `one` +- `import one.{Two}` to import value `Two` from module `one` +- `import one.{type Two}` to import type `Two` from module `one` diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs index 9a65fd27391..8a040a76646 100644 --- a/compiler-core/src/parse/tests.rs +++ b/compiler-core/src/parse/tests.rs @@ -1482,3 +1482,21 @@ type Wibble { "# ); } + +#[test] +fn import_dotted_name() { + assert_module_error!( + r#" +import one.two +"# + ); +} + +#[test] +fn import_dotted_upname() { + assert_module_error!( + r#" +import one.Two +"# + ); +} diff --git a/compiler-core/src/type_/error.rs b/compiler-core/src/type_/error.rs index c36f7a7968f..1750a18a3ee 100644 --- a/compiler-core/src/type_/error.rs +++ b/compiler-core/src/type_/error.rs @@ -107,6 +107,7 @@ pub enum Error { location: SrcSpan, name: EcoString, imported_modules: Vec<EcoString>, + hint: Option<String>, }, UnknownModuleType { @@ -958,6 +959,7 @@ pub fn convert_get_value_constructor_error( location, name, imported_modules, + hint: None, }, UnknownValueConstructorError::ModuleValue { @@ -1019,6 +1021,7 @@ pub fn convert_get_type_constructor_error( location: *location, name, imported_modules, + hint: None, }, UnknownTypeConstructorError::ModuleType { diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs index 09df07cc3e8..85fd858388a 100644 --- a/compiler-core/src/type_/expression.rs +++ b/compiler-core/src/type_/expression.rs @@ -2098,6 +2098,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { name: module_alias.clone(), location: *module_location, imported_modules: self.environment.imported_modules.keys().cloned().collect(), + hint: None, })?; let constructor = @@ -2407,6 +2408,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .ok_or_else(|| Error::UnknownModule { location: *location, name: module_name.clone(), + hint: None, imported_modules: self .environment .imported_modules diff --git a/compiler-core/src/type_/tests/imports.rs b/compiler-core/src/type_/tests/imports.rs index c7c104966b0..e7acc0b9c4d 100644 --- a/compiler-core/src/type_/tests/imports.rs +++ b/compiler-core/src/type_/tests/imports.rs @@ -326,3 +326,23 @@ pub fn main() { " ); } + +#[test] +fn import_value_as_submodule() { + assert_with_module_error!( + ("one", "pub fn wibble() {}"), + " + import one/wibble + " + ); +} + +#[test] +fn import_value_as_submodule2() { + assert_with_module_error!( + ("one/two", "pub fn wibble() {}"), + " + import one/two/wibble + " + ); +} diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap new file mode 100644 index 00000000000..b0284becc4f --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap @@ -0,0 +1,12 @@ +--- +source: compiler-core/src/type_/tests/imports.rs +expression: "\n import one/wibble\n " +--- +error: Unknown module + ┌─ /src/one/two.gleam:2:9 + │ +2 │ import one/wibble + │ ^^^^^^^^^^^^^^^^^ + +No module has been found with the name `one/wibble`. +Hint: Maybe you meant `import one.{wibble}`? diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap new file mode 100644 index 00000000000..083c5123f09 --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap @@ -0,0 +1,12 @@ +--- +source: compiler-core/src/type_/tests/imports.rs +expression: "\n import one/two/wibble\n " +--- +error: Unknown module + ┌─ /src/one/two.gleam:2:9 + │ +2 │ import one/two/wibble + │ ^^^^^^^^^^^^^^^^^^^^^ + +No module has been found with the name `one/two/wibble`. +Hint: Maybe you meant `import one/two.{wibble}`? From 21afdcd679da966d66da0d23e358782a0a8958f4 Mon Sep 17 00:00:00 2001 From: sobolevn <mail@sobolevn.me> Date: Thu, 22 Aug 2024 10:41:05 +0300 Subject: [PATCH 2/6] More suggestions --- compiler-core/src/analyse/imports.rs | 31 +++++++++++++--- compiler-core/src/type_/tests/imports.rs | 35 +++++++++++++------ ...s__imports__import_value_as_submodule.snap | 12 ++++--- ...__imports__import_value_as_submodule2.snap | 12 ++++--- ...__imports__import_value_as_submodule3.snap | 14 ++++++++ ...ts__import_value_as_submodule_no_hint.snap | 11 ++++++ ...s__import_value_as_submodule_no_hint2.snap | 11 ++++++ ...s__import_value_as_submodule_no_hint3.snap | 11 ++++++ 8 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule3.snap create mode 100644 compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint.snap create mode 100644 compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint2.snap create mode 100644 compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap diff --git a/compiler-core/src/analyse/imports.rs b/compiler-core/src/analyse/imports.rs index d16f661cde2..c133330de8b 100644 --- a/compiler-core/src/analyse/imports.rs +++ b/compiler-core/src/analyse/imports.rs @@ -4,6 +4,7 @@ use itertools::Itertools; use crate::{ ast::{SrcSpan, UnqualifiedImport, UntypedImport}, build::Origin, + error::wrap, type_::{ EntityKind, Environment, Error, ModuleInterface, Problems, UnusedModuleAlias, ValueConstructorVariant, @@ -62,16 +63,38 @@ impl<'context, 'problems> Importer<'context, 'problems> { let mut hint = None; if let Some(module) = self.environment.importable_modules.get(&basename) { if module.get_public_value(last_part).is_some() { - hint = Some(format!( - "Maybe you meant `import {basename}.{{{last_part}}}`?" - )); + hint = Some( + wrap( + format!( + "Did you mean `import {basename}.{{{last_part}}}`? + +See: https://tour.gleam.run/basics/unqualified-imports + " + ) + .as_str(), + ) + .to_string(), + ); } } + let importable_modules = self + .environment + .importable_modules + .keys() + .cloned() + .collect_vec(); + // If we have a single module here, it means that it is `gleam` module. + // We don't want to suggest that. + let modules_to_suggest = if importable_modules.len() == 1 { + vec![] + } else { + importable_modules + }; self.problems.error(Error::UnknownModule { location, name: name.clone(), hint, - imported_modules: self.environment.imported_modules.keys().cloned().collect(), + imported_modules: modules_to_suggest, }); return; }; diff --git a/compiler-core/src/type_/tests/imports.rs b/compiler-core/src/type_/tests/imports.rs index e7acc0b9c4d..6f17512b1ae 100644 --- a/compiler-core/src/type_/tests/imports.rs +++ b/compiler-core/src/type_/tests/imports.rs @@ -329,20 +329,35 @@ pub fn main() { #[test] fn import_value_as_submodule() { - assert_with_module_error!( - ("one", "pub fn wibble() {}"), - " - import one/wibble - " - ); + assert_with_module_error!(("one", "pub fn wibble() {}"), "import one/wibble"); } #[test] fn import_value_as_submodule2() { + assert_with_module_error!(("one/two", "pub fn wibble() {}"), "import one/two/wibble"); +} +#[test] +fn import_value_as_submodule3() { + // Two possible suggestions: `import one/wobble` or `import one.{wibble}` + // One via hint, one via `did_you_mean`. assert_with_module_error!( - ("one/two", "pub fn wibble() {}"), - " - import one/two/wibble - " + ("one", "pub fn wibble() {}"), + ("one/wobble", ""), + "import one/wibble" ); } + +#[test] +fn import_value_as_submodule_no_hint() { + assert_with_module_error!(("one/two", ""), "import one/two/wibble"); +} + +#[test] +fn import_value_as_submodule_no_hint2() { + assert_with_module_error!(("one/two", "pub fn wibble() {}"), "import some/other"); +} + +#[test] +fn import_value_as_submodule_no_hint3() { + assert_module_error!("import some/other"); +} diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap index b0284becc4f..18e5d3b7895 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule.snap @@ -1,12 +1,14 @@ --- source: compiler-core/src/type_/tests/imports.rs -expression: "\n import one/wibble\n " +expression: import one/wibble --- error: Unknown module - ┌─ /src/one/two.gleam:2:9 + ┌─ /src/one/two.gleam:1:1 │ -2 │ import one/wibble - │ ^^^^^^^^^^^^^^^^^ +1 │ import one/wibble + │ ^^^^^^^^^^^^^^^^^ No module has been found with the name `one/wibble`. -Hint: Maybe you meant `import one.{wibble}`? +Hint: Did you mean `import one.{wibble}`? + +See: https://tour.gleam.run/basics/unqualified-imports diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap index 083c5123f09..ea4c29e5e84 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule2.snap @@ -1,12 +1,14 @@ --- source: compiler-core/src/type_/tests/imports.rs -expression: "\n import one/two/wibble\n " +expression: import one/two/wibble --- error: Unknown module - ┌─ /src/one/two.gleam:2:9 + ┌─ /src/one/two.gleam:1:1 │ -2 │ import one/two/wibble - │ ^^^^^^^^^^^^^^^^^^^^^ +1 │ import one/two/wibble + │ ^^^^^^^^^^^^^^^^^^^^^ Did you mean `one/two`? No module has been found with the name `one/two/wibble`. -Hint: Maybe you meant `import one/two.{wibble}`? +Hint: Did you mean `import one/two.{wibble}`? + +See: https://tour.gleam.run/basics/unqualified-imports diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule3.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule3.snap new file mode 100644 index 00000000000..d2abd4f6efa --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule3.snap @@ -0,0 +1,14 @@ +--- +source: compiler-core/src/type_/tests/imports.rs +expression: import one/wibble +--- +error: Unknown module + ┌─ /src/one/two.gleam:1:1 + │ +1 │ import one/wibble + │ ^^^^^^^^^^^^^^^^^ Did you mean `one/wobble`? + +No module has been found with the name `one/wibble`. +Hint: Did you mean `import one.{wibble}`? + +See: https://tour.gleam.run/basics/unqualified-imports diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint.snap new file mode 100644 index 00000000000..dd416dbee90 --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint.snap @@ -0,0 +1,11 @@ +--- +source: compiler-core/src/type_/tests/imports.rs +expression: import one/two/wibble +--- +error: Unknown module + ┌─ /src/one/two.gleam:1:1 + │ +1 │ import one/two/wibble + │ ^^^^^^^^^^^^^^^^^^^^^ Did you mean `one/two`? + +No module has been found with the name `one/two/wibble`. diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint2.snap new file mode 100644 index 00000000000..300530dea6d --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint2.snap @@ -0,0 +1,11 @@ +--- +source: compiler-core/src/type_/tests/imports.rs +expression: import some/other +--- +error: Unknown module + ┌─ /src/one/two.gleam:1:1 + │ +1 │ import some/other + │ ^^^^^^^^^^^^^^^^^ + +No module has been found with the name `some/other`. diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap new file mode 100644 index 00000000000..300530dea6d --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap @@ -0,0 +1,11 @@ +--- +source: compiler-core/src/type_/tests/imports.rs +expression: import some/other +--- +error: Unknown module + ┌─ /src/one/two.gleam:1:1 + │ +1 │ import some/other + │ ^^^^^^^^^^^^^^^^^ + +No module has been found with the name `some/other`. From bb98f794407f7d55e394320c56711d4c49cad530 Mon Sep 17 00:00:00 2001 From: sobolevn <mail@sobolevn.me> Date: Thu, 22 Aug 2024 10:49:09 +0300 Subject: [PATCH 3/6] Better changelog --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2fc10b7428..1569097fd66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,10 +106,15 @@ - Adds a hint to syntax error when defining a function inside a type. ([sobolevn](https://github.com/sobolevn)) -- Improve import error messages. +- Improve import error messages. Now this code produces: - Now we recommend fixes for `import a.b` - and have better suggestions for `import a/b` when `b` is acutally a value. + ```gleam + import gleam.io + ^^ + I was not expecting this. + + Hint: did you mean `import gleam/io`? + ``` ([sobolevn](https://github.com/sobolevn)) ### Formatter From 0c7640854e6349c077a9543a43911a827e0fda0d Mon Sep 17 00:00:00 2001 From: sobolevn <mail@sobolevn.me> Date: Fri, 23 Aug 2024 14:53:54 +0300 Subject: [PATCH 4/6] Address review --- compiler-core/src/analyse/imports.rs | 80 ++++++++----------- compiler-core/src/error.rs | 65 +++++++++++---- compiler-core/src/parse.rs | 4 +- compiler-core/src/parse/error.rs | 5 +- ...sts__import_dotted_name_multiple_dots.snap | 13 +++ compiler-core/src/parse/tests.rs | 9 +++ compiler-core/src/type_/error.rs | 11 ++- compiler-core/src/type_/expression.rs | 2 - ..._type___tests__errors__unknown_module.snap | 2 +- ...__import_errors_do_not_block_analysis.snap | 2 +- ...s__import_value_as_submodule_no_hint3.snap | 2 +- 11 files changed, 121 insertions(+), 74 deletions(-) create mode 100644 compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name_multiple_dots.snap diff --git a/compiler-core/src/analyse/imports.rs b/compiler-core/src/analyse/imports.rs index c133330de8b..cda9ab4b5ad 100644 --- a/compiler-core/src/analyse/imports.rs +++ b/compiler-core/src/analyse/imports.rs @@ -1,10 +1,8 @@ use ecow::EcoString; -use itertools::Itertools; use crate::{ ast::{SrcSpan, UnqualifiedImport, UntypedImport}, build::Origin, - error::wrap, type_::{ EntityKind, Environment, Error, ModuleInterface, Problems, UnusedModuleAlias, ValueConstructorVariant, @@ -50,52 +48,7 @@ impl<'context, 'problems> Importer<'context, 'problems> { // Find imported module let Some(module_info) = self.environment.importable_modules.get(&name) else { - // Improve error message when users confuse `import one/two` - // with `import one.{two}`. - let mut modpath = name.split("/").collect_vec(); - let last_part = match modpath.last() { - Some(name) => name, - None => "", - }; - modpath.truncate(modpath.len() - 1); - let basename = EcoString::from(modpath.join("/")); - - let mut hint = None; - if let Some(module) = self.environment.importable_modules.get(&basename) { - if module.get_public_value(last_part).is_some() { - hint = Some( - wrap( - format!( - "Did you mean `import {basename}.{{{last_part}}}`? - -See: https://tour.gleam.run/basics/unqualified-imports - " - ) - .as_str(), - ) - .to_string(), - ); - } - } - let importable_modules = self - .environment - .importable_modules - .keys() - .cloned() - .collect_vec(); - // If we have a single module here, it means that it is `gleam` module. - // We don't want to suggest that. - let modules_to_suggest = if importable_modules.len() == 1 { - vec![] - } else { - importable_modules - }; - self.problems.error(Error::UnknownModule { - location, - name: name.clone(), - hint, - imported_modules: modules_to_suggest, - }); + self.module_not_found_error(name, location); return; }; @@ -118,6 +71,37 @@ See: https://tour.gleam.run/basics/unqualified-imports } } + fn module_not_found_error(&mut self, name: EcoString, location: SrcSpan) { + let importable_modules = self + .environment + .importable_modules // many modules might not be loaded for now + .keys() + .cloned() + .collect(); + + if let Some((basename, last_part)) = name.rsplit_once("/") { + let basename = EcoString::from(basename); + if let Some(module) = self.environment.importable_modules.get(&basename) { + if module.get_public_value(last_part).is_some() { + self.problems + .error(Error::UnknownModuleWithRichSuggestions { + location, + name: name.clone(), + name_parts: (basename, EcoString::from(last_part)), + importable_modules, + }); + return; + } + }; + } + + self.problems.error(Error::UnknownModule { + location, + name: name.clone(), + imported_modules: importable_modules, + }); + } + fn register_unqualified_type(&mut self, import: &UnqualifiedImport, module: &ModuleInterface) { let imported_name = import.as_name.as_ref().unwrap_or(&import.name); diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs index e3e5d82796f..2e77ec9bdcd 100644 --- a/compiler-core/src/error.rs +++ b/compiler-core/src/error.rs @@ -1958,21 +1958,58 @@ Private types can only be used within the module that defines them.", location, name, imported_modules, - hint, } => Diagnostic { - title: "Unknown module".into(), - text: format!("No module has been found with the name `{name}`."), - hint: hint.clone(), - level: Level::Error, - location: Some(Location { - label: Label { - text: did_you_mean(name, imported_modules), - span: *location, - }, - path: path.clone(), - src: src.clone(), - extra_labels: vec![], - }), + title: "Unknown module".into(), + text: format!("No module has been found with the name `{name}`."), + hint: None, + level: Level::Error, + location: Some(Location { + label: Label { + text: did_you_mean(name, imported_modules), + span: *location, + }, + path: path.clone(), + src: src.clone(), + extra_labels: vec![], + }), + }, + + TypeError::UnknownModuleWithRichSuggestions { + location, + name, + name_parts, + importable_modules, + } => { + // Improve error message when users confuse `import one/two` + // with `import one.{two}`. + let (basename, last_part) = name_parts; + let hint = Some( + wrap( + format!( + "Did you mean `import {basename}.{{{last_part}}}`? + +See: https://tour.gleam.run/basics/unqualified-imports + " + ) + .as_str(), + ) + .to_string(), + ); + Diagnostic { + title: "Unknown module".into(), + text: format!("No module has been found with the name `{name}`."), + hint, + level: Level::Error, + location: Some(Location { + label: Label { + text: did_you_mean(name, importable_modules), + span: *location, + }, + path: path.clone(), + src: src.clone(), + extra_labels: vec![], + }), + } }, TypeError::UnknownModuleType { diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs index b12f9c36cf8..be8fe092016 100644 --- a/compiler-core/src/parse.rs +++ b/compiler-core/src/parse.rs @@ -2497,7 +2497,7 @@ where .. } => ParseError { location: err.location, - error: ParseErrorType::InvalidImportSyntax { + error: ParseErrorType::ImportDottedName { module: module.into(), name: name.clone(), upname: false, @@ -2509,7 +2509,7 @@ where .. } => ParseError { location: err.location, - error: ParseErrorType::InvalidImportSyntax { + error: ParseErrorType::ImportDottedName { module: module.into(), name: name.clone(), upname: true, diff --git a/compiler-core/src/parse/error.rs b/compiler-core/src/parse/error.rs index 9e184383b35..bdeaba80ae4 100644 --- a/compiler-core/src/parse/error.rs +++ b/compiler-core/src/parse/error.rs @@ -292,7 +292,7 @@ utf16_codepoint, utf32_codepoint, signed, unsigned, big, little, native, size, u "Unsupported expression", vec!["Functions cannot be called in clause guards.".into()], ), - ParseErrorType::InvalidImportSyntax { + ParseErrorType::ImportDottedName { module, name, upname, @@ -388,7 +388,8 @@ pub enum ParseErrorType { field_type: Option<TypeAst>, }, CallInClauseGuard, // case x { _ if f() -> 1 } - InvalidImportSyntax { + ImportDottedName { + // import x.y module: EcoString, name: EcoString, upname: bool, diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name_multiple_dots.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name_multiple_dots.snap new file mode 100644 index 00000000000..a25dca16380 --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__import_dotted_name_multiple_dots.snap @@ -0,0 +1,13 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "\nimport one.two.three\n" +--- +error: Syntax error + ┌─ /src/parse/error.gleam:2:12 + │ +2 │ import one.two.three + │ ^^^ I was not expecting this + +This syntax for import is not correct. Probably, you meant: +- `import one/two` to import module `two` from package `one` +- `import one.{two}` to import value `two` from module `one` diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs index 8a040a76646..e701d65f70a 100644 --- a/compiler-core/src/parse/tests.rs +++ b/compiler-core/src/parse/tests.rs @@ -1492,6 +1492,15 @@ import one.two ); } +#[test] +fn import_dotted_name_multiple_dots() { + assert_module_error!( + r#" +import one.two.three +"# + ); +} + #[test] fn import_dotted_upname() { assert_module_error!( diff --git a/compiler-core/src/type_/error.rs b/compiler-core/src/type_/error.rs index 1750a18a3ee..40a0121f23b 100644 --- a/compiler-core/src/type_/error.rs +++ b/compiler-core/src/type_/error.rs @@ -107,7 +107,13 @@ pub enum Error { location: SrcSpan, name: EcoString, imported_modules: Vec<EcoString>, - hint: Option<String>, + }, + + UnknownModuleWithRichSuggestions { + location: SrcSpan, + name: EcoString, + name_parts: (EcoString, EcoString), + importable_modules: Vec<EcoString>, }, UnknownModuleType { @@ -776,6 +782,7 @@ impl Error { | Error::UnknownVariable { location, .. } | Error::UnknownType { location, .. } | Error::UnknownModule { location, .. } + | Error::UnknownModuleWithRichSuggestions { location, .. } | Error::UnknownModuleType { location, .. } | Error::UnknownModuleValue { location, .. } | Error::ModuleAliasUsedAsName { location, .. } @@ -959,7 +966,6 @@ pub fn convert_get_value_constructor_error( location, name, imported_modules, - hint: None, }, UnknownValueConstructorError::ModuleValue { @@ -1021,7 +1027,6 @@ pub fn convert_get_type_constructor_error( location: *location, name, imported_modules, - hint: None, }, UnknownTypeConstructorError::ModuleType { diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs index 85fd858388a..09df07cc3e8 100644 --- a/compiler-core/src/type_/expression.rs +++ b/compiler-core/src/type_/expression.rs @@ -2098,7 +2098,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { name: module_alias.clone(), location: *module_location, imported_modules: self.environment.imported_modules.keys().cloned().collect(), - hint: None, })?; let constructor = @@ -2408,7 +2407,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .ok_or_else(|| Error::UnknownModule { location: *location, name: module_name.clone(), - hint: None, imported_modules: self .environment .imported_modules diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__unknown_module.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__unknown_module.snap index 12a4f0dd3e2..e70c77af496 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__unknown_module.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__unknown_module.snap @@ -6,6 +6,6 @@ error: Unknown module ┌─ /src/one/two.gleam:1:1 │ 1 │ import xpto - │ ^^^^^^^^^^^ + │ ^^^^^^^^^^^ Did you mean `gleam`? No module has been found with the name `xpto`. diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_errors_do_not_block_analysis.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_errors_do_not_block_analysis.snap index 6ee72e0b903..7691dafded2 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_errors_do_not_block_analysis.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_errors_do_not_block_analysis.snap @@ -6,7 +6,7 @@ error: Unknown module ┌─ /src/one/two.gleam:1:1 │ 1 │ import unknown_module - │ ^^^^^^^^^^^^^^^^^^^^^ + │ ^^^^^^^^^^^^^^^^^^^^^ Did you mean `gleam`? No module has been found with the name `unknown_module`. diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap index 300530dea6d..7bf399d39a4 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_no_hint3.snap @@ -6,6 +6,6 @@ error: Unknown module ┌─ /src/one/two.gleam:1:1 │ 1 │ import some/other - │ ^^^^^^^^^^^^^^^^^ + │ ^^^^^^^^^^^^^^^^^ Did you mean `gleam`? No module has been found with the name `some/other`. From 1679c5790b858dddc27cf318eefaaf8d379bed4f Mon Sep 17 00:00:00 2001 From: sobolevn <mail@sobolevn.me> Date: Fri, 23 Aug 2024 14:57:08 +0300 Subject: [PATCH 5/6] Add long name to the test --- compiler-core/src/type_/tests/imports.rs | 11 +++++++++++ ...import_value_as_submodule_very_long_name.snap | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_very_long_name.snap diff --git a/compiler-core/src/type_/tests/imports.rs b/compiler-core/src/type_/tests/imports.rs index 6f17512b1ae..1d9fd4bd303 100644 --- a/compiler-core/src/type_/tests/imports.rs +++ b/compiler-core/src/type_/tests/imports.rs @@ -347,6 +347,17 @@ fn import_value_as_submodule3() { ); } +#[test] +fn import_value_as_submodule_very_long_name() { + assert_with_module_error!( + ( + "this_is_a_very_long_name_for_a_module_even_hard_to_read", + "pub fn this_is_a_very_long_submodule_name() {}" + ), + "import this_is_a_very_long_name_for_a_module_even_hard_to_read/this_is_a_very_long_submodule_name" + ); +} + #[test] fn import_value_as_submodule_no_hint() { assert_with_module_error!(("one/two", ""), "import one/two/wibble"); diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_very_long_name.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_very_long_name.snap new file mode 100644 index 00000000000..c0af325e2b8 --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__imports__import_value_as_submodule_very_long_name.snap @@ -0,0 +1,16 @@ +--- +source: compiler-core/src/type_/tests/imports.rs +expression: import this_is_a_very_long_name_for_a_module_even_hard_to_read/this_is_a_very_long_submodule_name +--- +error: Unknown module + ┌─ /src/one/two.gleam:1:1 + │ +1 │ import this_is_a_very_long_name_for_a_module_even_hard_to_read/this_is_a_very_long_submodule_name + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Did you mean `this_is_a_very_long_name_for_a_module_even_hard_to_read`? + +No module has been found with the name `this_is_a_very_long_name_for_a_module_even_hard_to_read/this_is_a_very_long_submodule_name`. +Hint: Did you mean `import +this_is_a_very_long_name_for_a_module_even_hard_to_read. +{this_is_a_very_long_submodule_name}`? + +See: https://tour.gleam.run/basics/unqualified-imports From 6699cd36a82240fbbc507cce99756dec9483aa43 Mon Sep 17 00:00:00 2001 From: sobolevn <mail@sobolevn.me> Date: Fri, 23 Aug 2024 15:01:38 +0300 Subject: [PATCH 6/6] Use real output in changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1569097fd66..c84dc515dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,7 +113,9 @@ ^^ I was not expecting this. - Hint: did you mean `import gleam/io`? + This syntax for import is not correct. Probably, you meant: + - `import gleam/io` to import module `io` from package `gleam` + - `import gleam.{io}` to import value `io` from module `gleam` ``` ([sobolevn](https://github.com/sobolevn))