Skip to content

Feature/error pythonic import #4401

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

Merged
merged 12 commits into from
Jun 16, 2025
Merged
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@
the source of the problem.
([Giacomo Cavalieri](https://github.com/giacomocavalieri))

- Gleam will now emit a helpful message when attempting to import modules using
`.` instead of `/`.

```gleam
error: Syntax error
┌─ /src/parse/error.gleam:1:11
1 │ import one.two.three
│ ^ I was expecting either `/` or `.{` here.

Perhaps you meant one of:

import one/two
import one.{item}
```

([Zij-IT](https://github.com/zij-it))

### Build tool

- `gleam update`, `gleam deps update`, and `gleam deps download` will now print
Expand Down
32 changes: 28 additions & 4 deletions compiler-core/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2706,7 +2706,7 @@ where
fn parse_import(&mut self, import_start: u32) -> Result<Option<UntypedDefinition>, ParseError> {
let mut start = 0;
let mut end;
let mut module = String::new();
let mut module = EcoString::new();
// Gather module names
loop {
let (s, name, e) = self.expect_name()?;
Expand Down Expand Up @@ -2741,8 +2741,32 @@ where
let mut unqualified_values = vec![];
let mut unqualified_types = vec![];

if self.maybe_one(&Token::Dot).is_some() {
let _ = self.expect_one(&Token::LeftBrace)?;
if let Some((dot_start, dot_end)) = self.maybe_one(&Token::Dot) {
if let Err(e) = self.expect_one(&Token::LeftBrace) {
// If the module does contain a '/', then it's unlikely that the user
// intended for the import to be pythonic, so skip this.
if module.contains('/') {
return Err(e);
}

// Catch `import gleam.io` and provide a more helpful error...
let ParseErrorType::UnexpectedToken {
token: Token::Name { name } | Token::UpName { name },
..
} = &e.error
else {
return Err(e);
};

return Err(ParseError {
error: ParseErrorType::IncorrectImportModuleSeparator {
module,
item: name.clone(),
},
location: SrcSpan::new(dot_start, dot_end),
});
};

let parsed = self.parse_unqualified_imports()?;
unqualified_types = parsed.types;
unqualified_values = parsed.values;
Expand Down Expand Up @@ -2773,7 +2797,7 @@ where
},
unqualified_values,
unqualified_types,
module: module.into(),
module,
as_name,
package: (),
})))
Expand Down
14 changes: 14 additions & 0 deletions compiler-core/src/parse/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ utf16_codepoint, utf32_codepoint, signed, unsigned, big, little, native, size, u
"This attribute cannot be used on a variant.",
vec!["Hint: Did you mean `@deprecated`?".into()],
),
ParseErrorType::IncorrectImportModuleSeparator { module, item } => (
"I was expecting either `/` or `.{` here.",
vec![
"Perhaps you meant one of:".into(),
"".into(),
format!(" import {module}/{item}"),
format!(" import {module}.{{item}}"),
]
)
}
}
}
Expand Down Expand Up @@ -411,6 +420,11 @@ pub enum ParseErrorType {
TypeConstructorNoArguments, // let a : Int()
TypeDefinitionNoArguments, // pub type Wibble() { ... }
UnknownAttributeRecordVariant, // an attribute was used that is not know for a custom type variant
// a Python-like import was written, such as `import gleam.io`, instead of `import gleam/io`
IncorrectImportModuleSeparator {
module: EcoString,
item: EcoString,
},
}

impl LexicalError {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: compiler-core/src/parse/tests.rs
expression: import one/two.three
---
----- SOURCE CODE
import one/two.three

----- ERROR
error: Syntax error
┌─ /src/parse/error.gleam:1:16
1 │ import one/two.three
│ ^^^^^ I was not expecting this

Found a name, expected one of:
- `{`
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
source: compiler-core/src/parse/tests.rs
assertion_line: 1792
expression: import gleam.io
snapshot_kind: text
---
----- SOURCE CODE
import gleam.io

----- ERROR
error: Syntax error
┌─ /src/parse/error.gleam:1:13
1 │ import gleam.io
│ ^ I was expecting either `/` or `.{` here.

Perhaps you meant one of:

import gleam/io
import gleam.{item}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
source: compiler-core/src/parse/tests.rs
assertion_line: 1797
expression: import one.two.three
snapshot_kind: text
---
----- SOURCE CODE
import one.two.three

----- ERROR
error: Syntax error
┌─ /src/parse/error.gleam:1:11
1 │ import one.two.three
│ ^ I was expecting either `/` or `.{` here.

Perhaps you meant one of:

import one/two
import one.{item}
15 changes: 15 additions & 0 deletions compiler-core/src/parse/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1786,3 +1786,18 @@ fn assert_statement_without_expression() {
fn assert_statement_followed_by_statement() {
assert_error!("assert let a = 10");
}

#[test]
fn special_error_for_pythonic_import() {
assert_module_error!("import gleam.io");
}

#[test]
fn special_error_for_pythonic_neste_import() {
assert_module_error!("import one.two.three");
}

#[test]
fn doesnt_issue_special_error_for_pythonic_import_if_slash() {
assert_module_error!("import one/two.three");
}
Loading