Skip to content

Add a qualified/unqualified import code actions for unknown values/types #4615

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
219 changes: 218 additions & 1 deletion compiler-core/src/language_server/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ use vec1::{Vec1, vec1};
use super::{
TextEdits,
compiler::LspProjectCompiler,
edits::{add_newlines_after_import, get_import_edit, position_of_first_definition_if_import},
edits::{
add_newlines_after_import, add_unqualified_import, get_import_edit,
get_unqualified_import_edit, position_of_first_definition_if_import,
},
engine::{overlaps, within},
files::FileSystemProxy,
reference::find_variable_references,
Expand Down Expand Up @@ -1011,6 +1014,220 @@ fn suggest_imports(
}
}

pub fn code_action_import_qualified_module_for_type_or_value(
module: &Module,
line_numbers: &LineNumbers,
params: &CodeActionParams,
error: &Option<Error>,
actions: &mut Vec<CodeAction>,
) {
let uri = &params.text_document.uri;
let Some(Error::Type { errors, .. }) = error else {
return;
};

let missing_imports = errors
.into_iter()
.filter_map(|e| match e {
type_::Error::UnknownType {
location,
suggestions,
name,
..
} => suggest_unqualified_imports(*location, suggestions)
.map(|(location, suggestions)| (location, suggestions, name)),
type_::Error::UnknownVariable {
location,
suggestions,
name,
..
} => suggest_unqualified_imports(*location, suggestions)
.map(|(location, suggestions)| (location, suggestions, name)),
_ => None,
})
.collect_vec();

if missing_imports.is_empty() {
return;
}

let first_import_pos = position_of_first_definition_if_import(module, line_numbers);
let first_is_import = first_import_pos.is_some();
let import_location = first_import_pos.unwrap_or_default();

let after_import_newlines =
add_newlines_after_import(import_location, first_is_import, line_numbers, &module.code);

for (location, suggestions, name) in missing_imports {
let range = src_span_to_lsp_range(location, line_numbers);
if !overlaps(params.range, range) {
continue;
}

for suggestion in suggestions {
let (edits, title) = match suggestion {
ModuleSuggestion::Importable(full_name) => (
vec![
get_import_edit(import_location, full_name, &after_import_newlines),
TextEdit {
range: Range {
start: range.start,
end: range.start,
},
new_text: suggestion.last_name_component().to_string() + ".",
},
],
&format!("Import `{full_name}` and reference it"),
),
ModuleSuggestion::Imported(full_name) => {
let mut matching_import = None;

for def in &module.ast.definitions {
if let ast::Definition::Import(import) = def {
if &import.module == full_name {
matching_import = Some(import);
break;
}
}
}

let import = matching_import.expect("Couldn't find matching import");

(
vec![TextEdit {
range: Range {
start: range.start,
end: range.start,
},
new_text: import.used_name().unwrap_or(name.clone()).to_string() + ".",
}],
&format!(
"Qualify and reference `{}`",
import
.used_name()
.unwrap_or(suggestion.last_name_component().into())
),
)
}
};

CodeActionBuilder::new(title)
.kind(CodeActionKind::QUICKFIX)
.changes(uri.clone(), edits)
.preferred(true)
.push_to(actions);
}
}
}

pub fn code_action_import_unqualified_module_for_type_or_value(
module: &Module,
line_numbers: &LineNumbers,
params: &CodeActionParams,
error: &Option<Error>,
actions: &mut Vec<CodeAction>,
) {
let uri = &params.text_document.uri;
let Some(Error::Type { errors, .. }) = error else {
return;
};

let missing_imports = errors
.into_iter()
.filter_map(|e| match e {
type_::Error::UnknownType {
location,
suggestions,
name,
..
} => suggest_unqualified_imports(*location, suggestions)
.map(|(location, suggestions)| (location, suggestions, name, ast::Layer::Type)),
type_::Error::UnknownVariable {
location,
suggestions,
name,
..
} => suggest_unqualified_imports(*location, suggestions)
.map(|(location, suggestions)| (location, suggestions, name, ast::Layer::Value)),
_ => None,
})
.collect_vec();

if missing_imports.is_empty() {
return;
}

let first_import_pos = position_of_first_definition_if_import(module, line_numbers);
let first_is_import = first_import_pos.is_some();
let import_location = first_import_pos.unwrap_or_default();

let after_import_newlines =
add_newlines_after_import(import_location, first_is_import, line_numbers, &module.code);

for (location, suggestions, name, layer) in missing_imports {
let range = src_span_to_lsp_range(location, line_numbers);
if !overlaps(params.range, range) {
continue;
}

for suggestion in suggestions {
let (edit, title) = match suggestion {
ModuleSuggestion::Importable(full_name) => (
get_unqualified_import_edit(
import_location,
full_name,
name,
layer,
&after_import_newlines,
),
&format!("Import `{name}` from `{full_name}`"),
),
ModuleSuggestion::Imported(full_name) => {
let mut matching_import = None;

for def in &module.ast.definitions {
if let ast::Definition::Import(import) = def {
if &import.module == full_name {
matching_import = Some(import);
break;
}
}
}

let import = matching_import.expect("Couldn't find matching import");

(
add_unqualified_import(name, layer, module, import, line_numbers),
&format!(
"Update import of `{}`",
import
.used_name()
.unwrap_or(suggestion.last_name_component().into())
),
)
}
};

CodeActionBuilder::new(title)
.kind(CodeActionKind::QUICKFIX)
.changes(uri.clone(), vec![edit])
.preferred(true)
.push_to(actions);
}
}
}

fn suggest_unqualified_imports(
location: SrcSpan,
suggestions: &[ModuleSuggestion],
) -> Option<(SrcSpan, &[ModuleSuggestion])> {
if suggestions.is_empty() {
None
} else {
Some((location, suggestions))
}
}

pub fn code_action_add_missing_patterns(
module: &Module,
line_numbers: &LineNumbers,
Expand Down
Loading