diff --git a/doc/whatsnew/fragments/10277.other b/doc/whatsnew/fragments/10277.other new file mode 100644 index 0000000000..a7a1bfb0b2 --- /dev/null +++ b/doc/whatsnew/fragments/10277.other @@ -0,0 +1,4 @@ +The algorithm used for ``no-member`` suggestions is now more efficient and cut the +calculation when the distance score is already above the threshold. + +Refs #10277 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index bc7ddfc2a4..51d56ea699 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -150,8 +150,12 @@ def _(node: nodes.ClassDef | bases.Instance) -> Iterable[str]: return itertools.chain(values, other_values) -def _string_distance(seq1: str, seq2: str) -> int: - seq2_length = len(seq2) +def _string_distance(seq1: str, seq2: str, seq1_length: int, seq2_length: int) -> int: + if not seq1_length: + return seq2_length + + if not seq2_length: + return seq1_length row = [*list(range(1, seq2_length + 1)), 0] for seq1_index, seq1_char in enumerate(seq1): @@ -182,11 +186,20 @@ def _similar_names( possible_names: list[tuple[str, int]] = [] names = _node_names(owner) + attr_str = attrname or "" + attr_len = len(attr_str) + for name in names: if name == attrname: continue - distance = _string_distance(attrname or "", name) + name_len = len(name) + + min_distance = abs(attr_len - name_len) + if min_distance > distance_threshold: + continue + + distance = _string_distance(attr_str, name, attr_len, name_len) if distance <= distance_threshold: possible_names.append((name, distance)) diff --git a/tests/checkers/unittest_typecheck.py b/tests/checkers/unittest_typecheck.py index c944b863f3..d3fd5a34c0 100644 --- a/tests/checkers/unittest_typecheck.py +++ b/tests/checkers/unittest_typecheck.py @@ -221,3 +221,47 @@ def decorated(): ) ): self.checker.visit_subscript(subscript) + + +class TestTypeCheckerStringDistance: + """Tests for the _string_distance helper in pylint.checkers.typecheck.""" + + def test_string_distance_identical_strings(self) -> None: + seq1 = "hi" + seq2 = "hi" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 0 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 0 + + def test_string_distance_empty_string(self) -> None: + seq1 = "" + seq2 = "hi" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 2 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 2 + + def test_string_distance_edit_distance_one_character(self) -> None: + seq1 = "hi" + seq2 = "he" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 1 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 1 + + def test_string_distance_edit_distance_multiple_similar_characters(self) -> None: + seq1 = "hello" + seq2 = "yelps" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 3 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 3 + + def test_string_distance_edit_distance_all_dissimilar_characters(self) -> None: + seq1 = "yellow" + seq2 = "orange" + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 6 + + seq1, seq2 = seq2, seq1 + assert typecheck._string_distance(seq1, seq2, len(seq1), len(seq2)) == 6