Skip to content

Made compiler show a hint to import unqualified types/values #4602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions compiler-core/src/analyse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
)
.collect();
let typed_parameters = environment
.get_type_constructor(&None, &name)
.get_type_constructor(&None, &name, Some(parameters.len()))
.expect("Could not find preregistered type constructor")
.parameters
.clone();
Expand Down Expand Up @@ -1601,7 +1601,7 @@ fn analyse_type_alias(t: UntypedTypeAlias, environment: &mut Environment<'_>) ->
// analysis aims to be fault tolerant to get the best possible feedback for
// the programmer in the language server, so the analyser gets here even
// though there was previously errors.
let type_ = match environment.get_type_constructor(&None, &alias) {
let type_ = match environment.get_type_constructor(&None, &alias, Some(args.len())) {
Ok(constructor) => constructor.type_.clone(),
Err(_) => environment.new_generic_var(),
};
Expand Down
12 changes: 9 additions & 3 deletions compiler-core/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![allow(clippy::unwrap_used, clippy::expect_used)]
use crate::ast::Layer;
use crate::build::{Origin, Outcome, Runtime, Target};
use crate::diagnostic::{Diagnostic, ExtraLabel, Label, Location};
use crate::strings::{to_snake_case, to_upper_camel_case};
Expand Down Expand Up @@ -2339,6 +2340,7 @@ Note: If the same type variable is used for multiple fields, all those fields ne
location,
name,
hint,
suggestions
} => {
let label_text = match hint {
UnknownTypeHint::AlternativeTypes(types) => did_you_mean(name, types),
Expand All @@ -2363,7 +2365,10 @@ but no type in scope with that name."
Diagnostic {
title: "Unknown type".into(),
text,
hint: None,
hint: match label_text {
None => suggestions.first().map(|suggestion| suggestion.suggest_unqualified_import(name, Layer::Type)),
Some(_) => None
},
level: Level::Error,
location: Some(Location {
label: Label {
Expand All @@ -2382,6 +2387,7 @@ but no type in scope with that name."
variables,
name,
type_with_name_in_scope,
suggestions
} => {
let text = if *type_with_name_in_scope {
wrap_format!("`{name}` is a type, it cannot be used as a value.")
Expand All @@ -2397,7 +2403,7 @@ but no type in scope with that name."
Diagnostic {
title: "Unknown variable".into(),
text,
hint: None,
hint: suggestions.first().map(|suggestion| suggestion.suggest_unqualified_import(name, Layer::Value)),
level: Level::Error,
location: Some(Location {
label: Label {
Expand Down Expand Up @@ -2452,7 +2458,7 @@ Private types can only be used within the module that defines them.",
} => Diagnostic {
title: "Unknown module".into(),
text: format!("No module has been found with the name `{name}`."),
hint: suggestions.first().map(|suggestion| suggestion.suggestion(name)),
hint: suggestions.first().map(|suggestion| suggestion.suggest_import(name)),
level: Level::Error,
location: Some(Location {
label: Label {
Expand Down
2 changes: 1 addition & 1 deletion compiler-core/src/exhaustiveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ impl Variable {
..
} => {
let constructors = ConstructorSpecialiser::specialise_constructors(
env.get_constructors_for_type(module, name)
env.get_constructors_for_type(module, name, Some(args.len()))
.expect("Custom type variants must exist"),
args.as_slice(),
);
Expand Down
2 changes: 1 addition & 1 deletion compiler-core/src/exhaustiveness/missing_patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ impl<'a, 'env> MissingPatternsGenerator<'a, 'env> {

let name = self
.environment
.get_constructors_for_type(&module, &name)
.get_constructors_for_type(&module, &name, Some(fields.len()))
.expect("Custom type constructor must have custom type kind")
.variants
.get(*index)
Expand Down
142 changes: 140 additions & 2 deletions compiler-core/src/type_/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use pubgrub::range::Range;

use crate::{
analyse::TargetSupport,
ast::{PIPE_VARIABLE, Publicity},
ast::{Layer, PIPE_VARIABLE, Publicity},
build::Target,
error::edit_distance,
reference::{EntityKind, ReferenceTracker},
Expand Down Expand Up @@ -374,6 +374,7 @@ impl Environment<'_> {
&mut self,
module: &Option<(EcoString, SrcSpan)>,
name: &EcoString,
arity: Option<usize>,
) -> Result<&TypeConstructor, UnknownTypeConstructorError> {
match module {
None => self
Expand All @@ -382,6 +383,7 @@ impl Environment<'_> {
.ok_or_else(|| UnknownTypeConstructorError::Type {
name: name.clone(),
hint: self.unknown_type_hint(name),
suggestions: self.suggest_unqualified_modules(name, Layer::Type, arity),
}),

Some((module_name, _)) => {
Expand Down Expand Up @@ -419,6 +421,7 @@ impl Environment<'_> {
&self,
module: &EcoString,
name: &EcoString,
arity: Option<usize>,
) -> Result<&TypeVariantConstructors, UnknownTypeConstructorError> {
let module = if module.is_empty() || *module == self.current_module {
None
Expand All @@ -430,6 +433,7 @@ impl Environment<'_> {
UnknownTypeConstructorError::Type {
name: name.clone(),
hint: self.unknown_type_hint(name),
suggestions: self.suggest_unqualified_modules(name, Layer::Type, arity),
}
}),

Expand Down Expand Up @@ -458,6 +462,7 @@ impl Environment<'_> {
&mut self,
module: Option<&EcoString>,
name: &EcoString,
arity: Option<usize>,
) -> Result<&ValueConstructor, UnknownValueConstructorError> {
match module {
None => self.scope.get(name).ok_or_else(|| {
Expand All @@ -466,6 +471,7 @@ impl Environment<'_> {
name: name.clone(),
variables: self.local_value_names(),
type_with_name_in_scope,
suggestions: self.suggest_unqualified_modules(name, Layer::Value, arity),
}
}),

Expand Down Expand Up @@ -495,8 +501,9 @@ impl Environment<'_> {
&self,
module: &EcoString,
name: &EcoString,
arity: Option<usize>,
) -> Vec<&EcoString> {
self.get_constructors_for_type(module, name)
self.get_constructors_for_type(module, name, arity)
.iter()
.flat_map(|c| &c.variants)
.filter_map(|variant| {
Expand Down Expand Up @@ -750,6 +757,137 @@ impl Environment<'_> {
.collect()
}

/// Suggest modules to import or use unqualified
pub fn suggest_unqualified_modules(
&self,
name: &EcoString,
layer: Layer,
arity: Option<usize>,
) -> Vec<ModuleSuggestion> {
// Don't suggest unqualified imports for values which aren't record constructors.
if layer.is_value() && name.chars().next().is_none_or(|c| c.is_lowercase()) {
return vec![];
}

let suggestions = match layer {
Layer::Type => self.suggest_modules_for_type(name, arity),
Layer::Value => self.suggest_modules_for_value(name, arity),
};

// Sort options based on if its already imported and on lexicographical order.
suggestions.into_iter().sorted().collect()
}

fn suggest_modules_for_type(
&self,
name: &EcoString,
arity: Option<usize>,
) -> Vec<ModuleSuggestion> {
let mut imported_modules = HashSet::new();

let mut suggestions = self
.imported_modules
.iter()
.filter_map(|(_, (_, module_info))| {
let Some(type_) = module_info.get_public_type(name) else {
return None;
};
// If we couldn't find the arity of the type, consider all modules.
let Some(arity) = arity else {
// Should be impossible to exist already
let _ = imported_modules.insert(module_info.name.clone());
return Some(ModuleSuggestion::Imported(module_info.name.clone()));
};
// Make sure the arities match.
if type_.parameters.len() == arity {
// Should be impossible to exist already
let _ = imported_modules.insert(module_info.name.clone());
Some(ModuleSuggestion::Imported(module_info.name.clone()))
} else {
None
}
})
.collect_vec();

suggestions.extend(self.importable_modules.iter().filter_map(
|(importable, module_info)| {
if imported_modules.contains(importable) {
return None;
}
let Some(type_) = module_info.get_public_type(name) else {
return None;
};
// If we couldn't find the arity of the type, consider all modules.
let Some(arity) = arity else {
return Some(ModuleSuggestion::Importable(importable.clone()));
};
// Make sure the arities match.
if type_.parameters.len() == arity {
Some(ModuleSuggestion::Importable(importable.clone()))
} else {
None
}
},
));

suggestions
}

fn suggest_modules_for_value(
&self,
name: &EcoString,
arity: Option<usize>,
) -> Vec<ModuleSuggestion> {
let mut imported_modules = HashSet::new();

let mut suggestions = self
.imported_modules
.iter()
.filter_map(|(_, (_, module_info))| {
let Some(value) = module_info.get_public_value(name) else {
return None;
};
// If we couldn't find the arity of the value, consider all modules.
if arity == None {
// Should be impossible to exist already
let _ = imported_modules.insert(module_info.name.clone());
return Some(ModuleSuggestion::Imported(module_info.name.clone()));
}
// Make sure the arities match.
if value.type_.fn_arity() == arity {
// Should be impossible to exist already
let _ = imported_modules.insert(module_info.name.clone());
Some(ModuleSuggestion::Imported(module_info.name.clone()))
} else {
None
}
})
.collect_vec();

suggestions.extend(self.importable_modules.iter().filter_map(
|(importable, module_info)| {
if imported_modules.contains(importable) {
return None;
}
let Some(value) = module_info.get_public_value(name) else {
return None;
};
// If we couldn't find the arity of the value, consider all modules.
if arity == None {
return Some(ModuleSuggestion::Importable(module_info.name.clone()));
}
// Make sure the arities match.
if value.type_.fn_arity() == arity {
Some(ModuleSuggestion::Importable(module_info.name.clone()))
} else {
None
}
},
));

suggestions
}

/// Suggest modules to import or use, for an unknown module
pub fn suggest_modules(&self, module: &str, imported: Imported) -> Vec<ModuleSuggestion> {
let mut suggestions = self
Expand Down
32 changes: 30 additions & 2 deletions compiler-core/src/type_/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ pub enum ModuleSuggestion {
}

impl ModuleSuggestion {
pub fn suggestion(&self, module: &str) -> String {
pub fn suggest_import(&self, module: &str) -> String {
match self {
ModuleSuggestion::Importable(name) => {
// Add a little extra information if the names don't match
Expand All @@ -134,6 +134,23 @@ impl ModuleSuggestion {
}
}

pub fn suggest_unqualified_import(&self, name: &str, layer: Layer) -> String {
match self {
ModuleSuggestion::Importable(module) => match layer {
Layer::Type => {
format!("Did you mean to import the `{name}` type from the `{module}` module?")
}
Layer::Value => {
format!("Did you mean to import the `{name}` value from the `{module}` module?")
}
},

ModuleSuggestion::Imported(module) => {
format!("Did you mean to update the import of `{module}`?")
}
}
}

pub fn last_name_component(&self) -> &str {
match self {
ModuleSuggestion::Imported(name) | ModuleSuggestion::Importable(name) => {
Expand Down Expand Up @@ -168,12 +185,14 @@ pub enum Error {
name: EcoString,
variables: Vec<EcoString>,
type_with_name_in_scope: bool,
suggestions: Vec<ModuleSuggestion>,
},

UnknownType {
location: SrcSpan,
name: EcoString,
hint: UnknownTypeHint,
suggestions: Vec<ModuleSuggestion>,
},

UnknownModule {
Expand Down Expand Up @@ -1225,6 +1244,7 @@ pub enum UnknownValueConstructorError {
name: EcoString,
variables: Vec<EcoString>,
type_with_name_in_scope: bool,
suggestions: Vec<ModuleSuggestion>,
},

Module {
Expand All @@ -1250,11 +1270,13 @@ pub fn convert_get_value_constructor_error(
name,
variables,
type_with_name_in_scope,
suggestions,
} => Error::UnknownVariable {
location,
name,
variables,
type_with_name_in_scope,
suggestions,
},

UnknownValueConstructorError::Module { name, suggestions } => Error::UnknownModule {
Expand Down Expand Up @@ -1308,6 +1330,7 @@ pub enum UnknownTypeConstructorError {
Type {
name: EcoString,
hint: UnknownTypeHint,
suggestions: Vec<ModuleSuggestion>,
},

Module {
Expand All @@ -1329,10 +1352,15 @@ pub fn convert_get_type_constructor_error(
module_location: Option<SrcSpan>,
) -> Error {
match e {
UnknownTypeConstructorError::Type { name, hint } => Error::UnknownType {
UnknownTypeConstructorError::Type {
name,
hint,
suggestions,
} => Error::UnknownType {
location: *location,
name,
hint,
suggestions,
},

UnknownTypeConstructorError::Module { name, suggestions } => Error::UnknownModule {
Expand Down
Loading