add Salsa caching

This commit is contained in:
Alex Waygood 2025-06-17 22:17:33 +01:00
parent 79f3c8cf67
commit 592f30f626
4 changed files with 51 additions and 43 deletions

View file

@ -44,7 +44,10 @@ impl<'db> SemanticModel<'db> {
/// Returns completions for symbols available in a `object.<CURSOR>` context.
pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec<Name> {
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

View file

@ -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<Name> {
AllMembers::of(db, ty).members
pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> &'db FxHashSet<Name> {
/// 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<Name> {
AllMembers::of(db, ty).members
}
all_members_impl(db, ty, ())
}

View file

@ -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)),
));
}
}

View file

@ -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<Name> {
) -> 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<O, I>(
fn find_best_suggestion<'db, O, I>(
options: O,
unresolved_member: &str,
hide_underscored_suggestions: HideUnderscoredSuggestions,
) -> Option<Name>
) -> Option<&'db str>
where
O: IntoIterator<IntoIter = I>,
I: ExactSizeIterator<Item = Name>,
I: ExactSizeIterator<Item = &'db str>,
{
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<Name> =
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<Name>, unresolved_member: &str) -> Option<Name> {
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<Name>, 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 <https://github.com/python/cpython/blob/6eb6c5dbfb528bd07d77b60fd71fd05d81d45c41/Lib/test/test_traceback.py#L4037-L4078>
#[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<Name> = 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<const T: usize>(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 <https://github.com/python/cpython/blob/6eb6c5dbfb528bd07d77b60fd71fd05d81d45c41/Lib/test/test_traceback.py#L4080-L4099>
#[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 <https://github.com/python/cpython/blob/6eb6c5dbfb528bd07d77b60fd71fd05d81d45c41/Lib/test/test_traceback.py#L4080-L4099>
#[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
),