diff --git a/crates/cairo-lang-semantic/src/diagnostic.rs b/crates/cairo-lang-semantic/src/diagnostic.rs index 8d0c83f0d01..07046ce1dc2 100644 --- a/crates/cairo-lang-semantic/src/diagnostic.rs +++ b/crates/cairo-lang-semantic/src/diagnostic.rs @@ -795,21 +795,42 @@ impl DiagnosticEntry for SemanticDiagnostic { "`#[inline(always)]` is not allowed for functions with impl generic parameters." .into() } - SemanticDiagnosticKind::CannotCallMethod { ty, method_name, inference_errors } => { - if inference_errors.is_empty() { - format!( - "Method `{}` not found on type `{}`. Did you import the correct trait and \ - impl?", - method_name, - ty.format(db) - ) - } else { - format!( + SemanticDiagnosticKind::CannotCallMethod { + ty, + method_name, + inference_errors, + relevant_traits, + } => { + if !inference_errors.is_empty() { + return format!( "Method `{}` could not be called on type `{}`.\n{}", method_name, ty.format(db), inference_errors.format(db) - ) + ); + } + match relevant_traits { + Some(relevant_traits) if !relevant_traits.is_empty() => { + let suggestions = relevant_traits + .iter() + .map(|trait_path| format!("`{trait_path}`")) + .collect::>() + .join(", "); + + format!( + "Method `{}` not found on type `{}`. Consider importing one of the \ + following traits: {}.", + method_name, + ty.format(db), + suggestions + ) + } + _ => format!( + "Method `{}` not found on type `{}`. Did you import the correct trait and \ + impl?", + method_name, + ty.format(db) + ), } } SemanticDiagnosticKind::TailExpressionNotAllowedInLoop => { @@ -1212,6 +1233,7 @@ pub enum SemanticDiagnosticKind { ty: semantic::TypeId, method_name: SmolStr, inference_errors: TraitInferenceErrors, + relevant_traits: Option>, }, NoSuchStructMember { struct_id: StructId, diff --git a/crates/cairo-lang-semantic/src/diagnostic_test_data/not_found b/crates/cairo-lang-semantic/src/diagnostic_test_data/not_found index 5ce5f2ec966..c70377a4716 100644 --- a/crates/cairo-lang-semantic/src/diagnostic_test_data/not_found +++ b/crates/cairo-lang-semantic/src/diagnostic_test_data/not_found @@ -1,7 +1,7 @@ //! > Test PathNotFound. //! > test_runner_name -test_expr_diagnostics +test_expr_diagnostics(expect_diagnostics: true) //! > expr_code { @@ -30,7 +30,7 @@ error: Function not found. //! > Test trying to access a function from a module whose file is missing. //! > test_runner_name -test_expr_diagnostics +test_expr_diagnostics(expect_diagnostics: true) //! > expr_code module_does_not_exist::bar() @@ -51,7 +51,7 @@ mod module_does_not_exist; //! > Test missing implicit in implicit_precedence //! > test_runner_name -test_expr_diagnostics +test_expr_diagnostics(expect_diagnostics: true) //! > expr_code {} @@ -67,3 +67,46 @@ error: Type not found. --> lib.cairo:1:23 #[implicit_precedence(MissingBuiltin1, MissingBuiltin2)] ^^^^^^^^^^^^^^^ + +//! > ========================================================================== + +//! > Test suggesting import for missing method. + +//! > test_runner_name +test_expr_diagnostics(expect_diagnostics: true) + +//! > expr_code +{ +// TODO: Ensure the diagnostic matches the expected output- add the tests' crate to the db. + let x: a_struct = A {a: 0}; + x.foo() +} + +//! > module_code +struct A { + a: felt252, +} +mod some_module { + use super::A; + pub trait Trt1 { + fn foo(self: A) -> felt252; + } + impl Imp1 of Trt1 { + fn foo(self: A) -> felt252 { + 0 + } + } +} + +//! > function_body + +//! > expected_diagnostics +error: Type not found. + --> lib.cairo:18:12 + let x: a_struct = A {a: 0}; + ^^^^^^^^ + +error[E0002]: Method `foo` not found on type ``. Did you import the correct trait and impl? + --> lib.cairo:19:7 + x.foo() + ^^^ diff --git a/crates/cairo-lang-semantic/src/expr/compute.rs b/crates/cairo-lang-semantic/src/expr/compute.rs index 2dd5623cfff..8961fa22383 100644 --- a/crates/cairo-lang-semantic/src/expr/compute.rs +++ b/crates/cairo-lang-semantic/src/expr/compute.rs @@ -34,13 +34,12 @@ use cairo_lang_utils::ordered_hash_map::{Entry, OrderedHashMap}; use cairo_lang_utils::ordered_hash_set::OrderedHashSet; use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; use cairo_lang_utils::unordered_hash_set::UnorderedHashSet; -use cairo_lang_utils::{LookupIntern, OptionHelper, extract_matches, try_extract_matches}; +use cairo_lang_utils::{Intern, LookupIntern, OptionHelper, extract_matches, try_extract_matches}; use itertools::{Itertools, chain, zip_eq}; use num_bigint::BigInt; use num_traits::ToPrimitive; use salsa::InternKey; use smol_str::SmolStr; -use utils::Intern; use super::inference::canonic::ResultNoErrEx; use super::inference::conform::InferenceConform; @@ -62,6 +61,8 @@ use crate::diagnostic::{ ElementKind, MultiArmExprKind, NotFoundItemType, SemanticDiagnostics, SemanticDiagnosticsBuilder, TraitInferenceErrors, UnsupportedOutsideOfFunctionFeatureName, }; +use crate::expr::inference::solver::SolutionSet; +use crate::expr::inference::{ImplVarTraitItemMappings, InferenceId}; use crate::items::constant::{ConstValue, resolve_const_expr_and_evaluate, validate_const_expr}; use crate::items::enm::SemanticEnumEx; use crate::items::feature_kind::extract_item_feature_config; @@ -71,6 +72,7 @@ use crate::items::modifiers::compute_mutability; use crate::items::us::get_use_path_segments; use crate::items::visibility; use crate::literals::try_extract_minus_literal; +use crate::lsp_helpers::TypeFilter; use crate::resolve::{ EnrichedMembers, EnrichedTypeMemberAccess, ResolvedConcreteItem, ResolvedGenericItem, Resolver, }; @@ -2778,6 +2780,12 @@ fn method_call_expr( } } + // Extracting the possible traits that should be imported, in order to use the method. + let method_name = + expr.path(ctx.db.upcast()).node.clone().get_text_without_trivia(ctx.db.upcast()); + let ty = ctx.reduce_ty(lexpr.ty()); + let relevant_traits = + find_relevant_traits(ctx.db, ty, &method_name.into(), ctx, lexpr.stable_ptr().untyped()); let (function_id, actual_trait_id, fixed_lexpr, mutability) = compute_method_function_call_data( ctx, @@ -2787,7 +2795,12 @@ fn method_call_expr( path.stable_ptr().untyped(), generic_args_syntax, |ty, method_name, inference_errors| { - Some(CannotCallMethod { ty, method_name, inference_errors }) + Some(CannotCallMethod { + ty, + method_name, + inference_errors, + relevant_traits: relevant_traits.clone(), + }) }, |_, trait_function_id0, trait_function_id1| { Some(AmbiguousTrait { trait_function_id0, trait_function_id1 }) @@ -3801,3 +3814,77 @@ fn function_parameter_types( let param_types = signature.params.into_iter().map(|param| param.ty); Ok(param_types) } + +/// Finds traits which contain a method matching the given name and type. +/// This function checks for visible traits in the specified module file and filters +/// methods based on their association with the given type and method name. +fn find_relevant_traits( + db: &dyn SemanticGroup, + ty: semantic::TypeId, + method_name: &SmolStr, + ctx: &mut ComputationContext<'_>, + stable_ptr: SyntaxStablePtrId, +) -> Option> { + let module_file_id = ctx.resolver.module_file_id; + let visible_traits = db.visible_traits_from_module(module_file_id)?; + let relevant_methods = find_methods_for_type(db, &mut ctx.resolver, ty, stable_ptr)?; + let mut suggestions = vec![]; + + // Check if the relevant methods belong to visible traits. + for method in relevant_methods { + if method.name(db.upcast()) == *method_name { + if let Some(trait_path) = visible_traits.get(&method.trait_id(db.upcast())).cloned() { + suggestions.push(trait_path); + } + } + } + Some(suggestions) +} + +/// Finds all methods that can be called on a type. +pub fn find_methods_for_type( + db: &dyn SemanticGroup, + resolver: &mut Resolver<'_>, + ty: crate::TypeId, + stable_ptr: SyntaxStablePtrId, +) -> Option> { + let type_filter = match ty.head(db) { + Some(head) => TypeFilter::TypeHead(head), + None => TypeFilter::NoFilter, + }; + + let mut relevant_methods = Vec::new(); + // Find methods on type. + // TODO(spapini): Look only in current crate dependencies. + for crate_id in db.crates() { + let methods = db.methods_in_crate(crate_id, type_filter.clone()); + for trait_function in methods.iter().copied() { + let clone_data = + &mut resolver.inference().clone_with_inference_id(db, InferenceId::NoContext); + let mut inference = clone_data.inference(db); + let lookup_context = resolver.impl_lookup_context(); + // Check if trait function signature's first param can fit our expr type. + let (concrete_trait_id, _) = inference.infer_concrete_trait_by_self( + trait_function, + ty, + &lookup_context, + Some(stable_ptr), + |_| {}, + )?; + + inference.solve().ok(); + if !matches!( + inference.trait_solution_set( + concrete_trait_id, + ImplVarTraitItemMappings::default(), + lookup_context + ), + Ok(SolutionSet::Unique(_) | SolutionSet::Ambiguous(_)) + ) { + continue; + } + relevant_methods.push(trait_function); + } + } + Some(relevant_methods) +}