diff --git a/ast/src/lang/env.rs b/ast/src/lang/env.rs index 3f9e17e3d5..1b04f417d2 100644 --- a/ast/src/lang/env.rs +++ b/ast/src/lang/env.rs @@ -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, }), } } diff --git a/compiler/can/src/annotation.rs b/compiler/can/src/annotation.rs index 8d3febe818..bcbb7aceed 100644 --- a/compiler/can/src/annotation.rs +++ b/compiler/can/src/annotation.rs @@ -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)) } } } diff --git a/compiler/can/src/env.rs b/compiler/can/src/env.rs index 46f627d7d9..e1216c42eb 100644 --- a/compiler/can/src/env.rs +++ b/compiler/can/src/env.rs @@ -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, }), } } diff --git a/compiler/problem/src/can.rs b/compiler/problem/src/can.rs index 55a26469b0..1adac511cb 100644 --- a/compiler/problem/src/can.rs +++ b/compiler/problem/src/can.rs @@ -171,10 +171,37 @@ pub enum RuntimeError { region: Region, exposed_values: Vec, }, + /// 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>, + /// Where the problem occurred 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, roc_parse::ident::BadIdent, Region), diff --git a/reporting/src/error/canonicalize.rs b/reporting/src/error/canonicalize.rs index 3400a52dbf..4b492db4fe 100644 --- a/reporting/src/error/canonicalize.rs +++ b/reporting/src/error/canonicalize.rs @@ -1015,8 +1015,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; } @@ -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>( alloc: &'b RocDocAllocator<'b>, lines: &LineInfo, region: roc_region::all::Region, name: &ModuleName, options: MutSet>, + module_exists: bool, ) -> RocDocBuilder<'b> { - let mut suggestions = - suggest::sort(name.as_str(), options.iter().map(|v| v.as_ref()).collect()); - suggestions.truncate(4); + // 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![ - 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 + // 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 { 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), @@ -1543,6 +1556,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, ]) } diff --git a/reporting/tests/test_reporting.rs b/reporting/tests/test_reporting.rs index 356a123373..c16690ec0d 100644 --- a/reporting/tests/test_reporting.rs +++ b/reporting/tests/test_reporting.rs @@ -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 + "# + ), + ) + } }