🐛️ Handle unimported modules properly

helpful error, not panic!

Closes #2422
This commit is contained in:
Emi Simpson 2022-02-24 20:37:16 -05:00
parent b5b26eabc9
commit 4d10c22442
No known key found for this signature in database
GPG key ID: A12F2C2FFDC3D847
7 changed files with 93 additions and 32 deletions

View file

@ -160,12 +160,17 @@ impl<'a> Env<'a> {
})
}
},
None => {
panic!(
"Module {} exists, but is not recorded in dep_idents",
module_name
)
}
None => Err(RuntimeError::ModuleNotImported {
module_name,
imported_modules: self
.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())
.collect(),
region,
module_exists: false,
}),
}
}

View file

@ -130,7 +130,9 @@ fn make_apply_symbol(
// it was imported but it doesn't expose this ident.
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))
}
}
}

View file

@ -124,12 +124,17 @@ impl<'a> Env<'a> {
})
}
},
None => {
panic!(
"Module {} exists, but is not recorded in dep_idents",
module_name
)
}
None => Err(RuntimeError::ModuleNotImported {
module_name,
imported_modules: self
.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())
.collect(),
region,
module_exists: false,
}),
}
}

View file

@ -3227,6 +3227,8 @@ fn canonicalize_and_constrain<'a>(
module_timing.canonicalize = canonicalize_end.duration_since(canonicalize_start).unwrap();
use roc_can::module::ModuleOutput;
use std::process;
match canonicalized {
Ok(module_output) => {
// Generate documentation information

View file

@ -8629,6 +8629,11 @@ fn match_on_lambda_set<'a>(
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) => {
let function_symbol = lambda_set.set[0].0;

View file

@ -161,10 +161,37 @@ pub enum RuntimeError {
region: Region,
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 {
/// The name of the module that was referenced
module_name: ModuleName,
/// A list of modules which *have* been imported
imported_modules: MutSet<Box<str>>,
/// Where the problem occured
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),
MalformedIdentifier(Box<str>, roc_parse::ident::BadIdent, Region),

View file

@ -999,8 +999,16 @@ fn pretty_runtime_error<'b>(
module_name,
imported_modules,
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;
}
@ -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>(
alloc: &'b RocDocAllocator<'b>,
lines: &LineInfo,
region: roc_region::all::Region,
name: &ModuleName,
options: MutSet<Box<str>>,
module_exists: bool,
) -> 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 =
suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect());
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.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() {
no_suggestion_details
])
} else {
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
.vcat(suggestions.into_iter().map(|v| alloc.string(v.to_string())))
.indent(4),
@ -1456,6 +1469,6 @@ fn module_not_found<'b>(
alloc.reflow("` module is not imported:"),
]),
alloc.region(lines.convert_region(region)),
to_details(default_no, default_yes),
details,
])
}