diff --git a/ChangeLog b/ChangeLog index 84c91e9ea6..245910e58b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -317,6 +317,9 @@ What's New in Pylint 2.13.9? ============================ Release date: TBA +* Fix ``IndexError`` crash in ``uninferable_final_decorators`` method. + + Relates to #6531 What's New in Pylint 2.13.8? diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 895d5d3694..b7a3eeff49 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -639,3 +639,7 @@ Other Changes ``open`` Closes #6414 + +* Fix ``IndexError`` crash in ``uninferable_final_decorators`` method. + + Relates to #6531 diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index ed263fc4ee..3f267e33cb 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -847,28 +847,34 @@ def uninferable_final_decorators( """ decorators = [] for decorator in getattr(node, "nodes", []): + import_nodes: tuple[nodes.Import | nodes.ImportFrom] | None = None + + # Get the `Import` node. The decorator is of the form: @module.name if isinstance(decorator, nodes.Attribute): - try: - import_node = decorator.expr.lookup(decorator.expr.name)[1][0] - except AttributeError: - continue + inferred = safe_infer(decorator.expr) + if isinstance(inferred, nodes.Module) and inferred.qname() == "typing": + _, import_nodes = decorator.expr.lookup(decorator.expr.name) + + # Get the `ImportFrom` node. The decorator is of the form: @name elif isinstance(decorator, nodes.Name): - lookup_values = decorator.lookup(decorator.name) - if lookup_values[1]: - import_node = lookup_values[1][0] - else: - continue # pragma: no cover # Covered on Python < 3.8 - else: + _, import_nodes = decorator.lookup(decorator.name) + + # The `final` decorator is expected to be found in the + # import_nodes. Continue if we don't find any `Import` or `ImportFrom` + # nodes for this decorator. + if not import_nodes: continue + import_node = import_nodes[0] if not isinstance(import_node, (astroid.Import, astroid.ImportFrom)): continue import_names = dict(import_node.names) - # from typing import final + # Check if the import is of the form: `from typing import final` is_from_import = ("final" in import_names) and import_node.modname == "typing" - # import typing + + # Check if the import is of the form: `import typing` is_import = ("typing" in import_names) and getattr( decorator, "attrname", None ) == "final" diff --git a/tests/functional/r/regression/regression_6531_crash_index_error.py b/tests/functional/r/regression/regression_6531_crash_index_error.py new file mode 100644 index 0000000000..6cdc96617b --- /dev/null +++ b/tests/functional/r/regression/regression_6531_crash_index_error.py @@ -0,0 +1,30 @@ +"""Regression test for https://github.com/PyCQA/pylint/issues/6531.""" + +# pylint: disable=missing-docstring, redefined-outer-name + +import pytest + + +class Wallet: + def __init__(self): + self.balance = 0 + + def add_cash(self, earned): + self.balance += earned + + def spend_cash(self, spent): + self.balance -= spent + +@pytest.fixture +def my_wallet(): + '''Returns a Wallet instance with a zero balance''' + return Wallet() + +@pytest.mark.parametrize("earned,spent,expected", [ + (30, 10, 20), + (20, 2, 18), +]) +def test_transactions(my_wallet, earned, spent, expected): + my_wallet.add_cash(earned) + my_wallet.spend_cash(spent) + assert my_wallet.balance == expected