From a43a797e34fe9e43d58ccf7fa736e39dca19a500 Mon Sep 17 00:00:00 2001 From: correctmost <134317971+correctmost@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:49:17 -0400 Subject: [PATCH 1/3] Speed up the generation of no-member suggestions Previously, edit distances were calculated for strings that had length differences greater than the maximum suggestion threshold. --- pylint/checkers/typecheck.py | 22 ++++++++++++-- tests/checkers/unittest_typecheck.py | 44 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index edef5188b4..6ff8044ff3 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): @@ -179,14 +183,26 @@ def _similar_names( The similar names are searched given a distance metric and only a given number of choices will be returned. """ + if max_choices <= 0: + return [] + 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 From c73bc96b5e38e2230fedea9ef034582261b8013b Mon Sep 17 00:00:00 2001 From: correctmost <134317971+correctmost@users.noreply.github.com> Date: Sat, 15 Mar 2025 18:03:27 -0400 Subject: [PATCH 2/3] Remove handling of missing-member-hint-distance = 0 --- pylint/checkers/typecheck.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index 6ff8044ff3..84ce54a629 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -183,9 +183,6 @@ def _similar_names( The similar names are searched given a distance metric and only a given number of choices will be returned. """ - if max_choices <= 0: - return [] - possible_names: list[tuple[str, int]] = [] names = _node_names(owner) From ab593930c7ffe9b01f586f9c0fc5480da3309c4a Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 25 Mar 2025 13:27:18 +0100 Subject: [PATCH 3/3] Add fragment --- doc/whatsnew/fragments/10277.other | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/whatsnew/fragments/10277.other 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