From c543c23e33a13f5d71008adb6f7bb1f1f2ce21d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sm=C3=B3=C5=82ka?= Date: Mon, 2 Dec 2024 10:52:19 +0100 Subject: [PATCH] Added code action to implement missing trait members --- src/ide/code_actions/fill_trait_members.rs | 278 ++++++++++++++++++ src/ide/code_actions/mod.rs | 4 + tests/e2e/code_actions.rs | 1 + .../code_actions/fill_trait_members.txt | 146 +++++++++ 4 files changed, 429 insertions(+) create mode 100644 src/ide/code_actions/fill_trait_members.rs create mode 100644 tests/test_data/code_actions/fill_trait_members.txt diff --git a/src/ide/code_actions/fill_trait_members.rs b/src/ide/code_actions/fill_trait_members.rs new file mode 100644 index 00000000..b9a36d2e --- /dev/null +++ b/src/ide/code_actions/fill_trait_members.rs @@ -0,0 +1,278 @@ +use std::collections::HashMap; + +use cairo_lang_defs::ids::{NamedLanguageElementId, TraitConstantId, TraitFunctionId}; +use cairo_lang_semantic::db::SemanticGroup; +use cairo_lang_semantic::diagnostic::{NotFoundItemType, SemanticDiagnostics}; +use cairo_lang_semantic::expr::inference::InferenceId; +use cairo_lang_semantic::lookup_item::HasResolverData; +use cairo_lang_semantic::resolve::{ResolvedConcreteItem, Resolver}; +use cairo_lang_semantic::substitution::{ + GenericSubstitution, SemanticRewriter, SubstitutionRewriter, +}; +use cairo_lang_semantic::{ConcreteTraitId, GenericArgumentId, GenericParam, Parameter}; +use cairo_lang_syntax::node::ast::{ImplItem, ItemImpl, MaybeImplBody}; +use cairo_lang_syntax::node::kind::SyntaxKind; +use cairo_lang_syntax::node::{SyntaxNode, Token, TypedSyntaxNode}; +use itertools::{Itertools, chain}; +use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Range, TextEdit, WorkspaceEdit}; + +use crate::lang::db::{AnalysisDatabase, LsSemanticGroup, LsSyntaxGroup}; +use crate::lang::lsp::ToLsp; + +/// Generates a completion adding all trait members that have not yet been specified. +/// Functions are added with empty bodies, consts with placeholder values. +pub fn fill_trait_members( + db: &AnalysisDatabase, + node: SyntaxNode, + params: &CodeActionParams, +) -> Option { + let file = db.find_module_file_containing_node(&node)?.file_id(db).ok()?; + + let item_impl_node = db.first_ancestor_of_kind(node, SyntaxKind::ItemImpl)?; + let item_impl = ItemImpl::from_syntax_node(db, item_impl_node); + + // Do not complete `impl`s without braces. + let MaybeImplBody::Some(impl_body) = item_impl.body(db) else { + return None; + }; + + let specified_impl_items = impl_body.items(db); + + let already_implemented_item_names = specified_impl_items + .elements(db) + .iter() + .filter_map(|item| match item { + ImplItem::Function(item) => Some(item.declaration(db).name(db).token(db).text(db)), + ImplItem::Type(item) => Some(item.name(db).token(db).text(db)), + ImplItem::Constant(item) => Some(item.name(db).token(db).text(db)), + _ => None, // No other items can appear in trait impl. + }) + .collect_vec(); + + let concrete_trait_id = find_concrete_trait_id(db, &item_impl)?; + let trait_id = concrete_trait_id.trait_id(db); + + let mut trait_constants = db.trait_constants(trait_id).ok()?; + let mut trait_types = db.trait_types(trait_id).ok()?; + let mut trait_functions = db.trait_functions(trait_id).ok()?; + + trait_constants.retain(|key, _| !already_implemented_item_names.contains(key)); + trait_types.retain(|key, _| !already_implemented_item_names.contains(key)); + trait_functions.retain(|key, _| !already_implemented_item_names.contains(key)); + + let trait_generics = db.trait_generic_params(trait_id).ok()?; + let specified_generics = concrete_trait_id.generic_args(db); + let substitution = GenericSubstitution::new(&trait_generics, &specified_generics); + let mut type_concretizer = SubstitutionRewriter { db, substitution: &substitution }; + + // Iterators borrowing SubstitutionRewriter mutably need intermediate collection. + let code = chain!( + trait_constants + .values() + .filter_map(|&id| constant_code(db, id, &mut type_concretizer)) + .collect_vec() + .into_iter(), + trait_types.values().map(|id| format!("type {} = ();", id.name(db))), + trait_functions + .values() + .filter_map(|&id| function_code(db, id, &mut type_concretizer)) + .collect_vec() + .into_iter() + ) + .join("\n\n"); + + let impl_body_end_before_right_brace = + specified_impl_items.as_syntax_node().span_end_without_trivia(db); + + let code_insert_position = + impl_body_end_before_right_brace.position_in_file(db, file)?.to_lsp(); + + let edit_start = code_insert_position; + let edit_end = code_insert_position; + + let mut changes = HashMap::new(); + let url = params.text_document.uri.clone(); + let change = TextEdit { range: Range::new(edit_start, edit_end), new_text: code }; + + changes.insert(url, vec![change]); + + let edit = WorkspaceEdit::new(changes); + + Some(CodeAction { + title: String::from("Implement missing members"), + kind: Some(CodeActionKind::QUICKFIX), + edit: Some(edit), + ..Default::default() + }) +} + +/// Obtains semantic model of [`ItemImpl`] and returns a [`ConcreteTraitId`] it refers to. +fn find_concrete_trait_id(db: &AnalysisDatabase, item_impl: &ItemImpl) -> Option { + let lookup_item_id = db.find_lookup_item(&item_impl.as_syntax_node())?; + let resolver_data = lookup_item_id.resolver_data(db).ok()?; + + let mut resolver = Resolver::with_data( + db, + resolver_data.as_ref().clone_with_inference_id(db, InferenceId::NoContext), + ); + + let mut diagnostics = SemanticDiagnostics::default(); + + match resolver.resolve_concrete_path( + &mut diagnostics, + item_impl.trait_path(db).elements(db), + NotFoundItemType::Trait, + ) { + Ok(ResolvedConcreteItem::Trait(id)) => Some(id), + _ => None, + } +} + +/// Generates declaration of a [`TraitConstantId`] containing its name, type +/// and a placeholder value. +/// Generics are substituted with concrete types according to a given [`SubstitutionRewriter`] +fn constant_code( + db: &AnalysisDatabase, + id: TraitConstantId, + type_concretizer: &mut SubstitutionRewriter<'_>, +) -> Option { + let name = id.name(db); + let ty = type_concretizer.rewrite(db.trait_constant_type(id).ok()?).ok()?.format(db); + + Some(format!("const {name}: {ty} = ();")) +} + +/// Generates declaration of a [`TraitFunctionId`] containing its signature with parameters, +/// panic indicator, implicits and default implementation if such exists. +/// Generics are substituted with concrete types according to a given [`SubstitutionRewriter`]. +/// Returns None if function has a default implementation. +fn function_code( + db: &AnalysisDatabase, + id: TraitFunctionId, + type_concretizer: &mut SubstitutionRewriter<'_>, +) -> Option { + // Do not complete functions that have default implementations. + if db.trait_function_body(id).ok()?.is_some() { + return None; + } + + let signature = db.trait_function_signature(id).ok()?; + + let generic_parameters = db.trait_function_generic_params(id).ok()?; + let generic_parameters_bracket = if generic_parameters.is_empty() { + String::new() + } else { + let formatted_parameters = generic_parameters + .into_iter() + .map(|parameter| generic_parameter_code(db, parameter, type_concretizer)) + .collect::>>()? + .join(", "); + + format!("<{formatted_parameters}>") + }; + + let parameters = signature + .params + .iter() + .map(|parameter| function_parameter(db, parameter, type_concretizer)) + .collect::>>()? + .join(", "); + + let name = id.name(db); + let title = Some(format!("fn {name}{generic_parameters_bracket}({parameters})")); + + let return_type = type_concretizer.rewrite(signature.return_type).ok()?; + let return_type = + if return_type.is_unit(db) { None } else { Some(format!("-> {}", return_type.format(db))) }; + + let implicits = match &signature.implicits[..] { + [] => None, + types => Some(format!("implicits({})", types.iter().map(|ty| ty.format(db)).join(", "))), + }; + + let nopanic = if !signature.panicable { Some(String::from("nopanic")) } else { None }; + + let body: Option = Some(String::from("{}")); + + Some([title, return_type, implicits, nopanic, body].into_iter().flatten().join(" ")) +} + +/// Formats [`GenericParam`] to be used as a free-standing type parameter +/// (not belonging to the trait) in a generic function's declaration. +fn generic_parameter_code( + db: &AnalysisDatabase, + parameter: GenericParam, + type_concretizer: &mut SubstitutionRewriter<'_>, +) -> Option { + match parameter { + GenericParam::Const(param) => { + Some(format!("const {}: {}", param.id.format(db), param.ty.format(db))) + } + + GenericParam::Impl(param) => { + let concrete_trait = param.concrete_trait.ok()?; + let trait_name = concrete_trait.name(db); + let trait_generic_arguments = concrete_trait.generic_args(db); + + let generic_arguments_bracket = if trait_generic_arguments.is_empty() { + String::new() + } else { + let formatted_arguments = trait_generic_arguments + .into_iter() + .map(|argument| generic_argument_code(db, argument, type_concretizer)) + .collect::>>()? + .join(", "); + + format!("<{formatted_arguments}>") + }; + + Some(param.id.name(db).map_or_else( + // concrete trait used only as a constraint + || format!("+{trait_name}{generic_arguments_bracket}"), + // concrete trait with explicit impl + |name| format!("impl {name}: {trait_name}{generic_arguments_bracket}"), + )) + } + + other => Some(other.as_arg(db).format(db)), + } +} + +/// Formats [`GenericArgumentId`] as it were used as a generic argument +/// nested in a generic parameter (e.g. `T` in `+Drop`). +fn generic_argument_code( + db: &AnalysisDatabase, + argument: GenericArgumentId, + type_concretizer: &mut SubstitutionRewriter<'_>, +) -> Option { + match argument { + GenericArgumentId::Type(type_id) => { + Some(type_concretizer.rewrite(type_id).ok()?.format(db)) + } + GenericArgumentId::Constant(const_value) => Some(const_value.format(db)), + // Trait constraint shouldn't appear as a generic argument + GenericArgumentId::Impl(_) => None, + // Negative constraints are allowed only in impl statements + GenericArgumentId::NegImpl => None, + } +} + +/// Generates [`Parameter`] declaration containing its name, +/// type and optionally a `ref` or `mut` indicator. +/// Generics are substituted with concrete types according to a given [`SubstitutionRewriter`] +fn function_parameter( + db: &AnalysisDatabase, + parameter: &Parameter, + type_concretizer: &mut SubstitutionRewriter<'_>, +) -> Option { + let prefix = match parameter.mutability { + cairo_lang_semantic::Mutability::Immutable => "", + cairo_lang_semantic::Mutability::Mutable => "mut ", + cairo_lang_semantic::Mutability::Reference => "ref ", + }; + + let name = parameter.id.name(db); + let ty = type_concretizer.rewrite(parameter.ty).ok()?.format(db); + + Some(format!("{prefix}{name}: {ty}")) +} diff --git a/src/ide/code_actions/mod.rs b/src/ide/code_actions/mod.rs index 3d0f0da8..51e2e2a7 100644 --- a/src/ide/code_actions/mod.rs +++ b/src/ide/code_actions/mod.rs @@ -13,6 +13,7 @@ use crate::lang::lsp::{LsProtoGroup, ToCairo}; mod add_missing_trait; mod expand_macro; mod fill_struct_fields; +mod fill_trait_members; mod rename_unused_variable; /// Compute commands for a given text document and range. These commands are typically code fixes to @@ -87,6 +88,9 @@ fn get_code_actions_for_diagnostics( "E0003" => fill_struct_fields::fill_struct_fields(db, node.clone(), params) .map(|result| vec![result]) .unwrap_or_default(), + "E0004" => fill_trait_members::fill_trait_members(db, node.clone(), params) + .map(|result| vec![result]) + .unwrap_or_default(), _ => { debug!("no code actions for diagnostic code: {code}"); vec![] diff --git a/tests/e2e/code_actions.rs b/tests/e2e/code_actions.rs index 823d75b7..a0ba26d1 100644 --- a/tests/e2e/code_actions.rs +++ b/tests/e2e/code_actions.rs @@ -15,6 +15,7 @@ cairo_lang_test_utils::test_file_test!( missing_trait: "missing_trait.txt", macro_expand: "macro_expand.txt", fill_struct_fields: "fill_struct_fields.txt", + fill_trait_members: "fill_trait_members.txt", }, test_quick_fix ); diff --git a/tests/test_data/code_actions/fill_trait_members.txt b/tests/test_data/code_actions/fill_trait_members.txt new file mode 100644 index 00000000..40e5a9bd --- /dev/null +++ b/tests/test_data/code_actions/fill_trait_members.txt @@ -0,0 +1,146 @@ +//! > Test completing missing trait members in impl block. + +//! > test_runner_name +test_quick_fix + +//! > cairo_project.toml +[crate_roots] +hello = "src" + +[config.global] +edition = "2024_07" + +//! > cairo_code +mod some_module { + pub struct SomeStructWithConstParameter {} + + pub trait MyTrait { + const CONCRETE_CONST: u32; + const GENERIC_CONST: T; + + type Type; + + fn foo(t: T, v: U) -> T; + fn bar(t: T) -> U; + fn baz(s: SomeStructWithConstParameter); + + fn generic>(w: W); + + fn with_concrete_impl>(w: W) -> W; + } +} + +mod happy_cases { + use super::some_module::{MyTrait, SomeStructWithConstParameter}; + + impl EmptyImpl of MyTrait { + + } + + impl ImplWithConst of MyTrait { + const CONCRETE_CONST: u32 = 0; + } + + impl ImplWithFoo of MyTrait { + fn foo(t: u32, v: felt252) -> u32 { 0 } + } + + impl ImplWithEverything of MyTrait { + const CONCRETE_CONST: u32 = 0; + const GENERIC_CONST: u32 = 0; + + type Type = u256; + + fn foo(t: u32, v: felt252) -> u32 { 0 } + fn bar(t: u32) -> felt252 { 0 } + fn baz(s: SomeStructWithConstParameter::<12>) {} + + fn generic>(w: W) {} + + fn with_concrete_impl>(w: W) -> W {} + } + + impl ImplWithoutGenericArgs of MyTrait { + + } +} + +mod unhappy_cases { + impl WrongImpl NonExistentTrait { + + } + + use super::some_module::MyTrait; + + impl NoBraces of MyTrait; +} + +//! > Code action #0 + impl EmptyImpl of MyTrait { +Title: Implement missing members +Add new text: "const CONCRETE_CONST: core::integer::u32 = (); + +const GENERIC_CONST: core::integer::u32 = (); + +type Type = (); + +fn foo(t: core::integer::u32, v: core::felt252) -> core::integer::u32 {} + +fn bar(t: core::integer::u32) -> core::felt252 {} + +fn baz(s: hello::some_module::SomeStructWithConstParameter::<1>) {} + +fn generic>(w: W) {} + +fn with_concrete_impl>(w: W) -> W {}" +At: Range { start: Position { line: 23, character: 0 }, end: Position { line: 23, character: 0 } } + +//! > Code action #1 + impl ImplWithConst of MyTrait { +Title: Implement missing members +Add new text: "const GENERIC_CONST: core::integer::u32 = (); + +type Type = (); + +fn foo(t: core::integer::u32, v: core::felt252) -> core::integer::u32 {} + +fn bar(t: core::integer::u32) -> core::felt252 {} + +fn baz(s: hello::some_module::SomeStructWithConstParameter::<10>) {} + +fn generic>(w: W) {} + +fn with_concrete_impl>(w: W) -> W {}" +At: Range { start: Position { line: 27, character: 38 }, end: Position { line: 27, character: 38 } } + +//! > Code action #2 + impl ImplWithFoo of MyTrait { +Title: Implement missing members +Add new text: "const CONCRETE_CONST: core::integer::u32 = (); + +const GENERIC_CONST: core::integer::u32 = (); + +type Type = (); + +fn bar(t: core::integer::u32) -> core::felt252 {} + +fn baz(s: hello::some_module::SomeStructWithConstParameter::<0>) {} + +fn generic>(w: W) {} + +fn with_concrete_impl>(w: W) -> W {}" +At: Range { start: Position { line: 31, character: 47 }, end: Position { line: 31, character: 47 } } + +//! > Code action #3 + impl ImplWithEverything of MyTrait { +Title: Implement missing members +Add new text: "" +At: Range { start: Position { line: 46, character: 73 }, end: Position { line: 46, character: 73 } } + +//! > Code action #4 + impl WrongImpl NonExistentTrait { +No code actions. + +//! > Code action #5 + impl NoBraces of MyTrait; +No code actions.