diff --git a/doc/whatsnew/fragments/10880.internal b/doc/whatsnew/fragments/10880.internal new file mode 100644 index 0000000000..9b7d4becd8 --- /dev/null +++ b/doc/whatsnew/fragments/10880.internal @@ -0,0 +1,3 @@ +``add_message`` now accepts optional ``module`` and ``filepath`` keyword arguments to override the reported message location. These parameters are mutually exclusive with ``node``; passing both raises ``TypeError``. + +Refs #10880 diff --git a/pylint/checkers/base_checker.py b/pylint/checkers/base_checker.py index a6eefca386..891d40835a 100644 --- a/pylint/checkers/base_checker.py +++ b/pylint/checkers/base_checker.py @@ -9,7 +9,7 @@ from collections.abc import Iterable, Sequence from inspect import cleandoc from tokenize import TokenInfo -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, overload from astroid import nodes @@ -139,6 +139,35 @@ def get_full_documentation( result += "\n" return result + @overload + def add_message( + self, + msgid: str, + line: int | None = None, + node: nodes.NodeNG = ..., + args: Any = None, + confidence: Confidence | None = None, + col_offset: int | None = None, + end_lineno: int | None = None, + end_col_offset: int | None = None, + ) -> None: ... + + @overload + def add_message( # pylint: disable=too-many-arguments + self, + msgid: str, + line: int | None = None, + node: None = None, + args: Any = None, + confidence: Confidence | None = None, + col_offset: int | None = None, + end_lineno: int | None = None, + end_col_offset: int | None = None, + module: str | None = None, + filepath: str | None = None, + ) -> None: ... + + # pylint: disable-next=too-many-arguments def add_message( self, msgid: str, @@ -149,9 +178,20 @@ def add_message( col_offset: int | None = None, end_lineno: int | None = None, end_col_offset: int | None = None, + module: str | None = None, + filepath: str | None = None, ) -> None: self.linter.add_message( - msgid, line, node, args, confidence, col_offset, end_lineno, end_col_offset + msgid, + line=line, + node=node, + args=args, + confidence=confidence, + col_offset=col_offset, + end_lineno=end_lineno, + end_col_offset=end_col_offset, + module=module, + filepath=filepath, ) def check_consistency(self) -> None: diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 088e9c15c0..2e2058071f 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -19,7 +19,7 @@ from pathlib import Path from re import Pattern from types import ModuleType -from typing import Any, Protocol +from typing import Any, Protocol, overload import astroid import astroid.builder @@ -1219,6 +1219,7 @@ def _report_evaluation(self, verbose: bool = False) -> int | None: self.reporter.display_reports(sect) return note + # pylint: disable-next=too-many-arguments def _add_one_message( self, message_definition: MessageDefinition, @@ -1229,6 +1230,8 @@ def _add_one_message( col_offset: int | None, end_lineno: int | None, end_col_offset: int | None, + module: str | None = None, + filepath: str | None = None, ) -> None: """After various checks have passed a single Message is passed to the reporter and added to stats. @@ -1282,10 +1285,11 @@ def _add_one_message( msg %= args # get module and object if node is None: - module, obj = self.current_name, "" - abspath = self.current_file + _module = module if module is not None else self.current_name + obj = "" + abspath = filepath if filepath is not None else self.current_file else: - module, obj = utils.get_module_and_frameid(node) + _module, obj = utils.get_module_and_frameid(node) abspath = node.root().file if abspath is not None: path = abspath.replace(self.reporter.path_strip_prefix, "", 1) @@ -1299,7 +1303,7 @@ def _add_one_message( MessageLocationTuple( abspath or "", path, - module or "", + _module or "", obj, line or 1, col_offset or 0, @@ -1311,6 +1315,35 @@ def _add_one_message( ) ) + @overload + def add_message( + self, + msgid: str, + line: int | None = None, + node: nodes.NodeNG = ..., + args: Any | None = None, + confidence: interfaces.Confidence | None = None, + col_offset: int | None = None, + end_lineno: int | None = None, + end_col_offset: int | None = None, + ) -> None: ... + + @overload + def add_message( # pylint: disable=too-many-arguments + self, + msgid: str, + line: int | None = None, + node: None = None, + args: Any | None = None, + confidence: interfaces.Confidence | None = None, + col_offset: int | None = None, + end_lineno: int | None = None, + end_col_offset: int | None = None, + module: str | None = None, + filepath: str | None = None, + ) -> None: ... + + # pylint: disable-next=too-many-arguments def add_message( self, msgid: str, @@ -1321,6 +1354,8 @@ def add_message( col_offset: int | None = None, end_lineno: int | None = None, end_col_offset: int | None = None, + module: str | None = None, + filepath: str | None = None, ) -> None: """Adds a message given by ID or name. @@ -1329,7 +1364,18 @@ def add_message( AST checkers must provide the node argument (but may optionally provide line if the line number is different), raw and token checkers must provide the line argument. + + The ``module`` and ``filepath`` parameters allow overriding the module + name and file path reported in the message location. They cannot be + combined with ``node``; pass either ``node`` **or** + ``module``/``filepath``, not both. """ + if node is not None and (module is not None or filepath is not None): + raise TypeError( + "add_message() does not accept both 'node' and " + "'module'/'filepath'. Pass either 'node' to derive location " + "from the AST, or 'module'/'filepath' to set them explicitly." + ) if confidence is None: confidence = interfaces.UNDEFINED message_definitions = self.msgs_store.get_message_definitions(msgid) @@ -1343,6 +1389,8 @@ def add_message( col_offset, end_lineno, end_col_offset, + module=module, + filepath=filepath, ) def add_ignored_message( diff --git a/pylint/testutils/unittest_linter.py b/pylint/testutils/unittest_linter.py index a19afec568..f68be3d340 100644 --- a/pylint/testutils/unittest_linter.py +++ b/pylint/testutils/unittest_linter.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any, Literal +from typing import Any, Literal, overload from astroid import nodes @@ -28,7 +28,36 @@ def release_messages(self) -> list[MessageTest]: finally: self._messages = [] + @overload def add_message( + self, + msgid: str, + line: int | None = None, + node: nodes.NodeNG = ..., + args: Any = None, + confidence: Confidence | None = None, + col_offset: int | None = None, + end_lineno: int | None = None, + end_col_offset: int | None = None, + ) -> None: ... + + @overload + def add_message( # pylint: disable=too-many-arguments + self, + msgid: str, + line: int | None = None, + node: None = None, + args: Any = None, + confidence: Confidence | None = None, + col_offset: int | None = None, + end_lineno: int | None = None, + end_col_offset: int | None = None, + module: str | None = None, + filepath: str | None = None, + ) -> None: ... + + # pylint: disable-next=too-many-arguments + def add_message( # pylint: disable=unused-argument self, msgid: str, line: int | None = None, @@ -39,6 +68,8 @@ def add_message( col_offset: int | None = None, end_lineno: int | None = None, end_col_offset: int | None = None, + module: str | None = None, + filepath: str | None = None, ) -> None: """Add a MessageTest to the _messages attribute of the linter class.""" # If confidence is None we set it to UNDEFINED as well in PyLinter diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py index c037c3636c..67b5f08034 100644 --- a/tests/lint/unittest_lint.py +++ b/tests/lint/unittest_lint.py @@ -515,6 +515,41 @@ def test_addmessage(linter: PyLinter) -> None: ) +def test_addmessage_module_and_filepath_override(linter: PyLinter) -> None: + """Module and filepath override current_name/current_file when node is None.""" + linter.set_reporter(testutils.GenericTestReporter()) + linter.open() + linter.set_current_module("current_module") + linter.add_message( + "C0301", + line=1, + args=(1, 2), + module="overridden_module", + filepath="/fake/path.py", + ) + assert len(linter.reporter.messages) == 1 + msg = linter.reporter.messages[0] + assert msg.location.module == "overridden_module" + assert msg.location.abspath == "/fake/path.py" + assert msg.location.path == "/fake/path.py" + + +def test_addmessage_node_with_module_filepath_raises(linter: PyLinter) -> None: + """Passing both node and module/filepath raises TypeError.""" + linter.set_reporter(testutils.GenericTestReporter()) + linter.open() + linter.set_current_module("current_module") + module_node = astroid.parse("x = 1") + node = module_node.body[0] + with pytest.raises(TypeError, match="does not accept both"): + linter.add_message( + "C0321", + node=node, + module="overridden_module", + filepath="/fake/path.py", + ) + + def test_addmessage_invalid(linter: PyLinter) -> None: linter.set_reporter(testutils.GenericTestReporter()) linter.open()