mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-28 14:24:45 +00:00
🐛️ Handle unimported modules properly
helpful error, not panic! Closes #2422
This commit is contained in:
parent
b5b26eabc9
commit
4d10c22442
7 changed files with 93 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,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3227,6 +3227,8 @@ fn canonicalize_and_constrain<'a>(
|
||||||
|
|
||||||
module_timing.canonicalize = canonicalize_end.duration_since(canonicalize_start).unwrap();
|
module_timing.canonicalize = canonicalize_end.duration_since(canonicalize_start).unwrap();
|
||||||
|
|
||||||
|
use roc_can::module::ModuleOutput;
|
||||||
|
use std::process;
|
||||||
match canonicalized {
|
match canonicalized {
|
||||||
Ok(module_output) => {
|
Ok(module_output) => {
|
||||||
// Generate documentation information
|
// Generate documentation information
|
||||||
|
|
|
@ -8629,6 +8629,11 @@ fn match_on_lambda_set<'a>(
|
||||||
env.arena.alloc(result),
|
env.arena.alloc(result),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Layout::Struct([]) => {
|
||||||
|
// This is a lambda set with no associated lambdas, often produced as a result
|
||||||
|
// of a runtime error at another point in the code.
|
||||||
|
Stmt::RuntimeError("Cannot have a lambda set with zero variants")
|
||||||
|
}
|
||||||
Layout::Struct(fields) => {
|
Layout::Struct(fields) => {
|
||||||
let function_symbol = lambda_set.set[0].0;
|
let function_symbol = lambda_set.set[0].0;
|
||||||
|
|
||||||
|
|
|
@ -161,10 +161,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 occured
|
||||||
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),
|
||||||
|
|
|
@ -999,8 +999,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;
|
||||||
}
|
}
|
||||||
|
@ -1414,34 +1422,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> {
|
||||||
let mut suggestions =
|
// If the module exists, sugguest that the user import it
|
||||||
suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect());
|
let details = if module_exists {
|
||||||
suggestions.truncate(4);
|
// 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 =
|
||||||
|
suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect());
|
||||||
|
suggestions.truncate(4);
|
||||||
|
|
||||||
let default_no = alloc.concat(vec![
|
|
||||||
alloc.reflow("Is there an "),
|
|
||||||
alloc.keyword("import"),
|
|
||||||
alloc.reflow(" or "),
|
|
||||||
alloc.keyword("exposing"),
|
|
||||||
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() {
|
if suggestions.is_empty() {
|
||||||
no_suggestion_details
|
// We don't have any recommended spelling corrections
|
||||||
|
alloc.concat(vec![
|
||||||
|
alloc.reflow("Is there an "),
|
||||||
|
alloc.keyword("import"),
|
||||||
|
alloc.reflow(" or "),
|
||||||
|
alloc.keyword("exposing"),
|
||||||
|
alloc.reflow(" missing up-top"),
|
||||||
|
])
|
||||||
} 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),
|
||||||
|
@ -1456,6 +1469,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,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue