diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index e174010fe8..6d1c6c0c03 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -44,7 +44,10 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `object.` context. pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec { let ty = node.value.inferred_type(self); - crate::types::all_members(self.db, ty).into_iter().collect() + crate::types::all_members(self.db, ty) + .iter() + .cloned() + .collect() } /// Returns completions for symbols available in the scope containing the diff --git a/crates/ty_python_semantic/src/types/all_members.rs b/crates/ty_python_semantic/src/types/all_members.rs index 6bcdb2d847..e5a67f6564 100644 --- a/crates/ty_python_semantic/src/types/all_members.rs +++ b/crates/ty_python_semantic/src/types/all_members.rs @@ -200,6 +200,16 @@ impl AllMembers { /// List all members of a given type: anything that would be valid when accessed /// as an attribute on an object of the given type. -pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet { - AllMembers::of(db, ty).members +pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> &'db FxHashSet { + /// This inner function is a Salsa query because [`AllMembers::extend_with_instance_members`] + /// calls [`semantic_index`] of another file, introducing a cross-file dependency. + /// + /// The unused argument is necessary or Salsa won't let us add the `#[salsa::tracked]` + /// attribute. + #[salsa::tracked(returns(ref))] + fn all_members_impl<'db>(db: &'db dyn Db, ty: Type<'db>, _: ()) -> FxHashSet { + AllMembers::of(db, ty).members + } + + all_members_impl(db, ty, ()) } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index d34a12898c..6358f627dc 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -669,10 +669,10 @@ impl<'db> Bindings<'db> { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(TupleType::from_elements( db, - all_members::all_members(db, *ty) - .into_iter() + all_members(db, *ty) + .iter() .sorted() - .map(|member| Type::string_literal(db, &member)), + .map(|member| Type::string_literal(db, member)), )); } } diff --git a/crates/ty_python_semantic/src/types/diagnostic/levenshtein.rs b/crates/ty_python_semantic/src/types/diagnostic/levenshtein.rs index 11fc59674e..4bd4a2c8c8 100644 --- a/crates/ty_python_semantic/src/types/diagnostic/levenshtein.rs +++ b/crates/ty_python_semantic/src/types/diagnostic/levenshtein.rs @@ -10,7 +10,6 @@ use crate::Db; use crate::types::{Type, all_members}; use indexmap::IndexSet; -use ruff_python_ast::name::Name; /// Given a type and an unresolved member name, find the best suggestion for a member name /// that is similar to the unresolved member name. @@ -22,9 +21,11 @@ pub(crate) fn find_best_suggestion_for_unresolved_member<'db>( obj: Type<'db>, unresolved_member: &str, hide_underscored_suggestions: HideUnderscoredSuggestions, -) -> Option { +) -> Option<&'db str> { find_best_suggestion( - all_members(db, obj), + all_members(db, obj) + .iter() + .map(ruff_python_ast::name::Name::as_str), unresolved_member, hide_underscored_suggestions, ) @@ -45,14 +46,14 @@ impl HideUnderscoredSuggestions { } } -fn find_best_suggestion( +fn find_best_suggestion<'db, O, I>( options: O, unresolved_member: &str, hide_underscored_suggestions: HideUnderscoredSuggestions, -) -> Option +) -> Option<&'db str> where O: IntoIterator, - I: ExactSizeIterator, + I: ExactSizeIterator, { if unresolved_member.is_empty() { return None; @@ -76,9 +77,9 @@ where // def bar(self): // print(self.attribute) # error: unresolved attribute `attribute`; did you mean `attribute`? // ``` - let options = options.filter(|name| name != unresolved_member); + let options = options.filter(|name| *name != unresolved_member); - let mut options: IndexSet = + let mut options: IndexSet<&'db str> = if hide_underscored_suggestions.is_no() || unresolved_member.starts_with('_') { options.collect() } else { @@ -88,7 +89,10 @@ where find_best_suggestion_impl(options, unresolved_member) } -fn find_best_suggestion_impl(options: IndexSet, unresolved_member: &str) -> Option { +fn find_best_suggestion_impl<'db>( + options: IndexSet<&'db str>, + unresolved_member: &str, +) -> Option<&'db str> { let mut best_suggestion = None; for member in options { @@ -101,7 +105,7 @@ fn find_best_suggestion_impl(options: IndexSet, unresolved_member: &str) - } } - let current_distance = levenshtein_distance(unresolved_member, &member, max_distance); + let current_distance = levenshtein_distance(unresolved_member, member, max_distance); if current_distance > max_distance { continue; } @@ -250,27 +254,23 @@ mod tests { /// for the typo `bluch` is what we'd expect. /// /// This test is ported from - #[test_case(&["noise", "more_noise", "a", "bc", "bluchin"], "bluchin"; "test for additional characters")] - #[test_case(&["noise", "more_noise", "a", "bc", "blech"], "blech"; "test for substituted characters")] - #[test_case(&["noise", "more_noise", "a", "bc", "blch"], "blch"; "test for eliminated characters")] - #[test_case(&["blach", "bluc"], "blach"; "substitutions are preferred over eliminations")] - #[test_case(&["blach", "bluchi"], "blach"; "substitutions are preferred over additions")] - #[test_case(&["blucha", "bluc"], "bluc"; "eliminations are preferred over additions")] - #[test_case(&["Luch", "fluch", "BLuch"], "BLuch"; "case changes are preferred over substitutions")] - fn test_good_suggestions(candidate_list: &[&str], expected_suggestion: &str) { - let candidates: Vec = candidate_list.iter().copied().map(Name::from).collect(); - let suggestion = find_best_suggestion(candidates, "bluch", HideUnderscoredSuggestions::No); - assert_eq!(suggestion.as_deref(), Some(expected_suggestion)); + #[test_case(["noise", "more_noise", "a", "bc", "bluchin"], "bluchin"; "test for additional characters")] + #[test_case(["noise", "more_noise", "a", "bc", "blech"], "blech"; "test for substituted characters")] + #[test_case(["noise", "more_noise", "a", "bc", "blch"], "blch"; "test for eliminated characters")] + #[test_case(["blach", "bluc"], "blach"; "substitutions are preferred over eliminations")] + #[test_case(["blach", "bluchi"], "blach"; "substitutions are preferred over additions")] + #[test_case(["blucha", "bluc"], "bluc"; "eliminations are preferred over additions")] + #[test_case(["Luch", "fluch", "BLuch"], "BLuch"; "case changes are preferred over substitutions")] + fn test_good_suggestions(candidate_list: [&str; T], expected_suggestion: &str) { + let suggestion = + find_best_suggestion(candidate_list, "bluch", HideUnderscoredSuggestions::No); + assert_eq!(suggestion, Some(expected_suggestion)); } /// Test ported from #[test] fn underscored_names_not_suggested_if_hide_policy_set_to_yes() { - let suggestion = find_best_suggestion( - [Name::from("_bluch")], - "bluch", - HideUnderscoredSuggestions::Yes, - ); + let suggestion = find_best_suggestion(["bluch"], "bluch", HideUnderscoredSuggestions::Yes); if let Some(suggestion) = suggestion { panic!( "Expected no suggestions for `bluch` due to `HideUnderscoredSuggestions::Yes` but `{suggestion}` was suggested" @@ -284,21 +284,16 @@ mod tests { fn underscored_names_are_suggested_if_hide_policy_set_to_yes_when_typo_is_underscored( typo: &str, ) { - let suggestion = find_best_suggestion( - [Name::from("_bluch")], - typo, - HideUnderscoredSuggestions::Yes, - ); - assert_eq!(suggestion.as_deref(), Some("_bluch")); + let suggestion = find_best_suggestion(["_bluch"], typo, HideUnderscoredSuggestions::Yes); + assert_eq!(suggestion, Some("_bluch")); } /// Test ported from #[test_case("_luch")] #[test_case("_bluch")] fn non_underscored_names_always_suggested_even_if_typo_underscored(typo: &str) { - let suggestion = - find_best_suggestion([Name::from("bluch")], typo, HideUnderscoredSuggestions::Yes); - assert_eq!(suggestion.as_deref(), Some("bluch")); + let suggestion = find_best_suggestion(["bluch"], typo, HideUnderscoredSuggestions::Yes); + assert_eq!(suggestion, Some("bluch")); } /// This asserts that we do not offer silly suggestions for very small names. @@ -308,7 +303,7 @@ mod tests { #[test_case("m")] #[test_case("py")] fn test_bad_suggestions_do_not_trigger_for_small_names(typo: &str) { - let candidates = ["vvv", "mom", "w", "id", "pytho"].map(Name::from); + let candidates = ["vvv", "mom", "w", "id", "pytho"]; let suggestion = find_best_suggestion(candidates, typo, HideUnderscoredSuggestions::No); if let Some(suggestion) = suggestion { panic!("Expected no suggestions for `{typo}` but `{suggestion}` was suggested"); @@ -320,7 +315,7 @@ mod tests { fn test_no_suggestion_for_very_different_attribute() { assert_eq!( find_best_suggestion( - [Name::from("blech")], + ["blech"], "somethingverywrong", HideUnderscoredSuggestions::No ),