diff options
author | Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> | 2022-05-07 21:23:26 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-07 21:23:26 +0200 |
commit | ccd87b41819153ffe33f19d9e7db499aba19bbca (patch) | |
tree | d7382509efbd1c641b377dcc66f59904f5419e3c | |
parent | d13af82135e12b0ccc7d928a011b00821907e969 (diff) | |
download | pylint-git-ccd87b41819153ffe33f19d9e7db499aba19bbca.tar.gz |
Add an exception for `IndexError` inside `uninferable_final_decorator` (#6532)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
Co-authored-by: Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
-rw-r--r-- | ChangeLog | 3 | ||||
-rw-r--r-- | doc/whatsnew/2.13.rst | 4 | ||||
-rw-r--r-- | pylint/checkers/utils.py | 30 | ||||
-rw-r--r-- | tests/functional/r/regression/regression_6531_crash_index_error.py | 30 |
4 files changed, 55 insertions, 12 deletions
@@ -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 895d5d369..b7a3eeff4 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 ed263fc4e..3f267e33c 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 000000000..6cdc96617 --- /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 |