🐛️ 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 => { 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,
}), }),
} }
} }

View file

@ -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))
} }
} }
} }

View file

@ -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,
}), }),
} }
} }

View file

@ -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

View file

@ -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;

View file

@ -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),

View file

@ -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,
]) ])
} }