diff --git a/doc/whatsnew/fragments/10510.bugfix b/doc/whatsnew/fragments/10510.bugfix new file mode 100644 index 0000000000..d7653b3cb4 --- /dev/null +++ b/doc/whatsnew/fragments/10510.bugfix @@ -0,0 +1,7 @@ +Fixed crash in 'unnecessary-list-index-lookup' when starting an enumeration using +minus the length of an iterable inside a dict comprehension when the len call was only +made in this dict comprehension, and not elsewhere. Also changed the approach, +to use inference in all cases but the simple ones, so we don't have to fix crashes +one by one for arbitrarily complex expressions in enumerate. + +Closes #10510 diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py index c67c8dd232..5cba122c1d 100644 --- a/pylint/checkers/refactoring/refactoring_checker.py +++ b/pylint/checkers/refactoring/refactoring_checker.py @@ -2452,18 +2452,20 @@ def _enumerate_with_start( return False, confidence def _get_start_value(self, node: nodes.NodeNG) -> tuple[int | None, Confidence]: - if ( - isinstance(node, (nodes.Name, nodes.Call, nodes.Attribute)) - or isinstance(node, nodes.UnaryOp) - and isinstance(node.operand, (nodes.Attribute, nodes.Name)) - ): - inferred = utils.safe_infer(node) - # inferred can be an astroid.base.Instance as in 'enumerate(x, int(y))' or - # not correctly inferred (None) - start_val = inferred.value if isinstance(inferred, nodes.Const) else None - return start_val, INFERENCE - if isinstance(node, nodes.UnaryOp): - return node.operand.value, HIGH + # Most common use cases are a constant integer or minus a constant integer. We + # don't need inference for that. If that's not the case, we assume arbitrary + # complexity and we use inference. if isinstance(node, nodes.Const): return node.value, HIGH - return None, HIGH + if isinstance(node, nodes.UnaryOp) and isinstance(node.operand, nodes.Const): + return node.operand.value, HIGH + inferred = utils.safe_infer(node) + if isinstance(inferred, nodes.Const): + return inferred.value, INFERENCE + # inferred can be an 'astroid.base.Instance' in 'enumerate(x, int(y))', + # for example. We're doing nothing in this case for now, as extracting + # the value is costly. + + # At this point the most likely cases is that the node is uninferable + # But we don't have to check if it's actually uninferable. + return None, INFERENCE diff --git a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py index b9f8b73251..d9a333d314 100644 --- a/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py +++ b/tests/functional/u/unnecessary/unnecessary_list_index_lookup.py @@ -167,3 +167,7 @@ def random_uninferrable_start(pears): for _, _ in enumerate(pears, random.choice([5, 42])): ... + +# Regression test for https://github.com/pylint-dev/pylint/issues/10510 +xs = [1, 2, 3] +test_dict = {j: i for i, j in enumerate(xs, -len(xs))}