Skip to content
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

feat(LSP): code action to import trait in a method call #7066

Merged
merged 1 commit into from
Jan 14, 2025
Merged
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
3 changes: 3 additions & 0 deletions tooling/lsp/src/requests/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
mod fill_struct_fields;
mod implement_missing_members;
mod import_or_qualify;
mod import_trait;
mod remove_bang_from_call;
mod remove_unused_import;
mod tests;
Expand Down Expand Up @@ -177,7 +178,7 @@

CodeAction {
title,
kind: Some(CodeActionKind::QUICKFIX),

Check warning on line 181 in tooling/lsp/src/requests/code_action.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (QUICKFIX)
diagnostics: None,
edit: Some(workspace_edit),
command: None,
Expand Down Expand Up @@ -285,6 +286,8 @@
self.remove_bang_from_call(method_call.method_name.span());
}

self.import_trait_in_method_call(method_call);

true
}
}
252 changes: 252 additions & 0 deletions tooling/lsp/src/requests/code_action/import_trait.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
use noirc_errors::Location;
use noirc_frontend::{
ast::MethodCallExpression,
hir::{def_map::ModuleDefId, resolution::visibility::trait_member_is_visible},
hir_def::traits::Trait,
node_interner::ReferenceId,
};

use crate::{
modules::relative_module_full_path,
use_segment_positions::{
use_completion_item_additional_text_edits, UseCompletionItemAdditionTextEditsRequest,
},
};

use super::CodeActionFinder;

impl<'a> CodeActionFinder<'a> {
pub(super) fn import_trait_in_method_call(&mut self, method_call: &MethodCallExpression) {
// First see if the method name already points to a function.
let name_location = Location::new(method_call.method_name.span(), self.file);
if let Some(ReferenceId::Function(func_id)) = self.interner.find_referenced(name_location) {
// If yes, it could be that the compiler is issuing a warning because there's
// only one possible trait that the method could be coming from, but it's not imported
let func_meta = self.interner.function_meta(&func_id);
let Some(trait_impl_id) = func_meta.trait_impl else {
return;
};

let trait_impl = self.interner.get_trait_implementation(trait_impl_id);
let trait_id = trait_impl.borrow().trait_id;
let trait_ = self.interner.get_trait(trait_id);

// Check if the trait is currently imported. If so, no need to suggest anything
let module_data =
&self.def_maps[&self.module_id.krate].modules()[self.module_id.local_id.0];
if !module_data.scope().find_name(&trait_.name).is_none() {
return;
}

self.push_import_trait_code_action(trait_);
return;
}

// Find out the type of the object
let object_location = Location::new(method_call.object.span, self.file);
let Some(typ) = self.interner.type_at_location(object_location) else {
return;
};

for (func_id, trait_id) in
self.interner.lookup_trait_methods(&typ, &method_call.method_name.0.contents, true)
{
let visibility = self.interner.function_modifiers(&func_id).visibility;
if !trait_member_is_visible(trait_id, visibility, self.module_id, self.def_maps) {
continue;
}

let trait_ = self.interner.get_trait(trait_id);
self.push_import_trait_code_action(trait_);
}
}

fn push_import_trait_code_action(&mut self, trait_: &Trait) {
let trait_id = trait_.id;

let module_def_id = ModuleDefId::TraitId(trait_id);
let current_module_parent_id = self.module_id.parent(self.def_maps);
let Some(module_full_path) = relative_module_full_path(
module_def_id,
self.module_id,
current_module_parent_id,
self.interner,
) else {
return;
};
let full_path = format!("{}::{}", module_full_path, trait_.name);

let title = format!("Import {}", full_path);

let text_edits = use_completion_item_additional_text_edits(
UseCompletionItemAdditionTextEditsRequest {
full_path: &full_path,
files: self.files,
file: self.file,
lines: &self.lines,
nesting: self.nesting,
auto_import_line: self.auto_import_line,
},
&self.use_segment_positions,
);

let code_action = self.new_quick_fix_multiple_edits(title, text_edits);
self.code_actions.push(code_action);
}
}

#[cfg(test)]
mod tests {
use tokio::test;

use crate::requests::code_action::tests::assert_code_action;

#[test]
async fn test_import_trait_in_method_call_when_one_option_but_not_in_scope() {
let title = "Import moo::Foo";

let src = r#"mod moo {
pub trait Foo {
fn foobar(self);
}

impl Foo for Field {
fn foobar(self) {}
}
}

fn main() {
let x: Field = 1;
x.foo>|<bar();
}"#;

let expected = r#"use moo::Foo;

mod moo {
pub trait Foo {
fn foobar(self);
}

impl Foo for Field {
fn foobar(self) {}
}
}

fn main() {
let x: Field = 1;
x.foobar();
}"#;

assert_code_action(title, src, expected).await;
}

#[test]
async fn test_import_trait_in_method_call_when_multiple_options_1() {
let title = "Import moo::Foo";

let src = r#"mod moo {
pub trait Foo {
fn foobar(self);
}

impl Foo for Field {
fn foobar(self) {}
}

pub trait Bar {
fn foobar(self);
}

impl Bar for Field {
fn foobar(self) {}
}
}

fn main() {
let x: Field = 1;
x.foo>|<bar();
}"#;

let expected = r#"use moo::Foo;

mod moo {
pub trait Foo {
fn foobar(self);
}

impl Foo for Field {
fn foobar(self) {}
}

pub trait Bar {
fn foobar(self);
}

impl Bar for Field {
fn foobar(self) {}
}
}

fn main() {
let x: Field = 1;
x.foobar();
}"#;

assert_code_action(title, src, expected).await;
}

#[test]
async fn test_import_trait_in_method_call_when_multiple_options_2() {
let title = "Import moo::Bar";

let src = r#"mod moo {
pub trait Foo {
fn foobar(self);
}

impl Foo for Field {
fn foobar(self) {}
}

pub trait Bar {
fn foobar(self);
}

impl Bar for Field {
fn foobar(self) {}
}
}

fn main() {
let x: Field = 1;
x.foo>|<bar();
}"#;

let expected = r#"use moo::Bar;

mod moo {
pub trait Foo {
fn foobar(self);
}

impl Foo for Field {
fn foobar(self) {}
}

pub trait Bar {
fn foobar(self);
}

impl Bar for Field {
fn foobar(self) {}
}
}

fn main() {
let x: Field = 1;
x.foobar();
}"#;

assert_code_action(title, src, expected).await;
}
}
2 changes: 1 addition & 1 deletion tooling/lsp/src/requests/code_action/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async fn get_code_action(src: &str) -> CodeActionResponse {
)
.await
.expect("Could not execute on_code_action_request")
.unwrap()
.expect("Expected to get a CodeActionResponse, got None")
}

pub(crate) async fn assert_code_action(title: &str, src: &str, expected: &str) {
Expand Down
Loading