mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-03 00:24:34 +00:00
Merge pull request #2574 from rtfeldman/fix-panic-on-unimported-module
[Bug] Handle unimported modules properly
This commit is contained in:
commit
1aec997843
6 changed files with 117 additions and 32 deletions
|
@ -160,12 +160,17 @@ impl<'a> Env<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => Err(RuntimeError::ModuleNotImported {
|
||||||
panic!(
|
module_name,
|
||||||
"Module {} exists, but is not recorded in dep_idents",
|
imported_modules: self
|
||||||
module_name
|
.dep_idents
|
||||||
)
|
.keys()
|
||||||
}
|
.filter_map(|module_id| self.module_ids.get_name(*module_id))
|
||||||
|
.map(|module_name| module_name.as_ref().into())
|
||||||
|
.collect(),
|
||||||
|
region,
|
||||||
|
module_exists: true,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,6 +182,7 @@ impl<'a> Env<'a> {
|
||||||
.map(|string| string.as_ref().into())
|
.map(|string| string.as_ref().into())
|
||||||
.collect(),
|
.collect(),
|
||||||
region,
|
region,
|
||||||
|
module_exists: false,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,7 +130,9 @@ fn make_apply_symbol(
|
||||||
// it was imported but it doesn't expose this ident.
|
// it was imported but it doesn't expose this ident.
|
||||||
env.problem(roc_problem::can::Problem::RuntimeError(problem));
|
env.problem(roc_problem::can::Problem::RuntimeError(problem));
|
||||||
|
|
||||||
Err(Type::Erroneous(Problem::UnrecognizedIdent((*ident).into())))
|
// A failed import should have already been reported through
|
||||||
|
// roc_can::env::Env::qualified_lookup's checks
|
||||||
|
Err(Type::Erroneous(Problem::SolvedTypeError))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,12 +124,17 @@ impl<'a> Env<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => Err(RuntimeError::ModuleNotImported {
|
||||||
panic!(
|
module_name,
|
||||||
"Module {} exists, but is not recorded in dep_idents",
|
imported_modules: self
|
||||||
module_name
|
.dep_idents
|
||||||
)
|
.keys()
|
||||||
}
|
.filter_map(|module_id| self.module_ids.get_name(*module_id))
|
||||||
|
.map(|module_name| module_name.as_ref().into())
|
||||||
|
.collect(),
|
||||||
|
region,
|
||||||
|
module_exists: true,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +146,7 @@ impl<'a> Env<'a> {
|
||||||
.map(|string| string.as_ref().into())
|
.map(|string| string.as_ref().into())
|
||||||
.collect(),
|
.collect(),
|
||||||
region,
|
region,
|
||||||
|
module_exists: false,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,10 +171,37 @@ pub enum RuntimeError {
|
||||||
region: Region,
|
region: Region,
|
||||||
exposed_values: Vec<Lowercase>,
|
exposed_values: Vec<Lowercase>,
|
||||||
},
|
},
|
||||||
|
/// A module was referenced, but hasn't been imported anywhere in the program
|
||||||
|
///
|
||||||
|
/// An example would be:
|
||||||
|
/// ```roc
|
||||||
|
/// app "hello"
|
||||||
|
/// packages { pf: "platform" }
|
||||||
|
/// imports [ pf.Stdout]
|
||||||
|
/// provides [ main ] to pf
|
||||||
|
///
|
||||||
|
/// main : Task.Task {} [] // Task isn't imported!
|
||||||
|
/// main = Stdout.line "I'm a Roc application!"
|
||||||
|
/// ```
|
||||||
ModuleNotImported {
|
ModuleNotImported {
|
||||||
|
/// The name of the module that was referenced
|
||||||
module_name: ModuleName,
|
module_name: ModuleName,
|
||||||
|
/// A list of modules which *have* been imported
|
||||||
imported_modules: MutSet<Box<str>>,
|
imported_modules: MutSet<Box<str>>,
|
||||||
|
/// Where the problem occurred
|
||||||
region: Region,
|
region: Region,
|
||||||
|
/// Whether or not the module exists at all
|
||||||
|
///
|
||||||
|
/// This is used to suggest that the user import the module, as opposed to fix a
|
||||||
|
/// typo in the spelling. For example, if the user typed `Task`, and the platform
|
||||||
|
/// exposes a `Task` module that hasn't been imported, we can sugguest that they
|
||||||
|
/// add the import statement.
|
||||||
|
///
|
||||||
|
/// On the other hand, if the user typed `Tesk`, they might want to check their
|
||||||
|
/// spelling.
|
||||||
|
///
|
||||||
|
/// If unsure, this should be set to `false`
|
||||||
|
module_exists: bool,
|
||||||
},
|
},
|
||||||
InvalidPrecedence(PrecedenceProblem, Region),
|
InvalidPrecedence(PrecedenceProblem, Region),
|
||||||
MalformedIdentifier(Box<str>, roc_parse::ident::BadIdent, Region),
|
MalformedIdentifier(Box<str>, roc_parse::ident::BadIdent, Region),
|
||||||
|
|
|
@ -1015,8 +1015,16 @@ fn pretty_runtime_error<'b>(
|
||||||
module_name,
|
module_name,
|
||||||
imported_modules,
|
imported_modules,
|
||||||
region,
|
region,
|
||||||
|
module_exists,
|
||||||
} => {
|
} => {
|
||||||
doc = module_not_found(alloc, lines, region, &module_name, imported_modules);
|
doc = module_not_found(
|
||||||
|
alloc,
|
||||||
|
lines,
|
||||||
|
region,
|
||||||
|
&module_name,
|
||||||
|
imported_modules,
|
||||||
|
module_exists,
|
||||||
|
);
|
||||||
|
|
||||||
title = MODULE_NOT_IMPORTED;
|
title = MODULE_NOT_IMPORTED;
|
||||||
}
|
}
|
||||||
|
@ -1501,34 +1509,39 @@ fn not_found<'b>(
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a message informing the user that a module was referenced, but not found
|
||||||
|
///
|
||||||
|
/// See [`roc_problem::can::ModuleNotImported`]
|
||||||
fn module_not_found<'b>(
|
fn module_not_found<'b>(
|
||||||
alloc: &'b RocDocAllocator<'b>,
|
alloc: &'b RocDocAllocator<'b>,
|
||||||
lines: &LineInfo,
|
lines: &LineInfo,
|
||||||
region: roc_region::all::Region,
|
region: roc_region::all::Region,
|
||||||
name: &ModuleName,
|
name: &ModuleName,
|
||||||
options: MutSet<Box<str>>,
|
options: MutSet<Box<str>>,
|
||||||
|
module_exists: bool,
|
||||||
) -> RocDocBuilder<'b> {
|
) -> RocDocBuilder<'b> {
|
||||||
|
// If the module exists, sugguest that the user import it
|
||||||
|
let details = if module_exists {
|
||||||
|
// TODO: Maybe give an example of how to do that
|
||||||
|
alloc.reflow("Did you mean to import it?")
|
||||||
|
} else {
|
||||||
|
// If the module might not exist, sugguest that it's a typo
|
||||||
let mut suggestions =
|
let mut suggestions =
|
||||||
suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect());
|
suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect());
|
||||||
suggestions.truncate(4);
|
suggestions.truncate(4);
|
||||||
|
|
||||||
let default_no = alloc.concat(vec![
|
if suggestions.is_empty() {
|
||||||
|
// We don't have any recommended spelling corrections
|
||||||
|
alloc.concat(vec![
|
||||||
alloc.reflow("Is there an "),
|
alloc.reflow("Is there an "),
|
||||||
alloc.keyword("import"),
|
alloc.keyword("import"),
|
||||||
alloc.reflow(" or "),
|
alloc.reflow(" or "),
|
||||||
alloc.keyword("exposing"),
|
alloc.keyword("exposing"),
|
||||||
alloc.reflow(" missing up-top"),
|
alloc.reflow(" missing up-top"),
|
||||||
]);
|
])
|
||||||
|
|
||||||
let default_yes = alloc
|
|
||||||
.reflow("Is there an import missing? Perhaps there is a typo. Did you mean one of these?");
|
|
||||||
|
|
||||||
let to_details = |no_suggestion_details, yes_suggestion_details| {
|
|
||||||
if suggestions.is_empty() {
|
|
||||||
no_suggestion_details
|
|
||||||
} else {
|
} else {
|
||||||
alloc.stack(vec![
|
alloc.stack(vec![
|
||||||
yes_suggestion_details,
|
alloc.reflow("Is there an import missing? Perhaps there is a typo. Did you mean one of these?"),
|
||||||
alloc
|
alloc
|
||||||
.vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string())))
|
.vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string())))
|
||||||
.indent(4),
|
.indent(4),
|
||||||
|
@ -1543,6 +1556,6 @@ fn module_not_found<'b>(
|
||||||
alloc.reflow("` module is not imported:"),
|
alloc.reflow("` module is not imported:"),
|
||||||
]),
|
]),
|
||||||
alloc.region(lines.convert_region(region)),
|
alloc.region(lines.convert_region(region)),
|
||||||
to_details(default_no, default_yes),
|
details,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -8274,4 +8274,35 @@ I need all branches in an `if` to have the same type!
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unimported_modules_reported() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
main : Task.Task {} []
|
||||||
|
main = "whatever man you don't even know my type"
|
||||||
|
main
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── MODULE NOT IMPORTED ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
The `Task` module is not imported:
|
||||||
|
|
||||||
|
1│ main : Task.Task {} []
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Is there an import missing? Perhaps there is a typo. Did you mean one
|
||||||
|
of these?
|
||||||
|
|
||||||
|
Test
|
||||||
|
List
|
||||||
|
Num
|
||||||
|
Set
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue