# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt from __future__ import annotations import warnings from typing import Any, NamedTuple, Sequence, TypeVar from astroid import nodes from pylint.constants import PY38_PLUS from pylint.interfaces import UNDEFINED, Confidence from pylint.message.message import Message from pylint.testutils.constants import UPDATE_OPTION T = TypeVar("T") class MessageTest(NamedTuple): msg_id: str line: int | None = None node: nodes.NodeNG | None = None args: Any | None = None confidence: Confidence | None = UNDEFINED col_offset: int | None = None end_line: int | None = None end_col_offset: int | None = None """Used to test messages produced by pylint. Class name cannot start with Test as pytest doesn't allow constructors in test classes. """ class MalformedOutputLineException(Exception): def __init__( self, row: Sequence[str] | str, exception: Exception, ) -> None: example = "msg-symbolic-name:42:27:MyClass.my_function:The message" other_example = "msg-symbolic-name:7:42::The message" expected = [ "symbol", "line", "column", "end_line", "end_column", "MyClass.myFunction, (or '')", "Message", "confidence", ] reconstructed_row = "" i = 0 try: for i, column in enumerate(row): reconstructed_row += f"\t{expected[i]}='{column}' ?\n" for missing in expected[i + 1 :]: reconstructed_row += f"\t{missing}= Nothing provided !\n" except IndexError: pass raw = ":".join(row) msg = f"""\ {exception} Expected '{example}' or '{other_example}' but we got '{raw}': {reconstructed_row} Try updating it with: 'python tests/test_functional.py {UPDATE_OPTION}'""" super().__init__(msg) class OutputLine(NamedTuple): symbol: str lineno: int column: int end_lineno: int | None end_column: int | None object: str msg: str confidence: str @classmethod def from_msg(cls, msg: Message, check_endline: bool = True) -> OutputLine: """Create an OutputLine from a Pylint Message.""" column = cls._get_column(msg.column) end_line = cls._get_py38_none_value(msg.end_line, check_endline) end_column = cls._get_py38_none_value(msg.end_column, check_endline) return cls( msg.symbol, msg.line, column, end_line, end_column, msg.obj or "", msg.msg.replace("\r\n", "\n"), msg.confidence.name, ) @staticmethod def _get_column(column: str) -> int: """Handle column numbers except for python < 3.8. The ast parser in those versions doesn't return them. """ if not PY38_PLUS: # We check the column only for the new better ast parser introduced in python 3.8 return 0 # pragma: no cover return int(column) @staticmethod def _get_py38_none_value(value: T, check_endline: bool) -> T | None: """Used to make end_line and end_column None as indicated by our version compared to `min_pyver_end_position`. """ if not check_endline: return None # pragma: no cover return value @classmethod def from_csv( cls, row: Sequence[str] | str, check_endline: bool = True ) -> OutputLine: """Create an OutputLine from a comma separated list (the functional tests expected output .txt files). """ if isinstance(row, str): row = row.split(",") try: column = cls._get_column(row[2]) if len(row) == 5: warnings.warn( "In pylint 3.0 functional tests expected output should always include the " "expected confidence level, expected end_line and expected end_column. " "An OutputLine should thus have a length of 8.", DeprecationWarning, ) return cls( row[0], int(row[1]), column, None, None, row[3], row[4], UNDEFINED.name, ) if len(row) == 6: warnings.warn( "In pylint 3.0 functional tests expected output should always include the " "expected end_line and expected end_column. An OutputLine should thus have " "a length of 8.", DeprecationWarning, ) return cls( row[0], int(row[1]), column, None, None, row[3], row[4], row[5] ) if len(row) == 8: end_line = cls._get_py38_none_value(row[3], check_endline) end_column = cls._get_py38_none_value(row[4], check_endline) return cls( row[0], int(row[1]), column, cls._value_to_optional_int(end_line), cls._value_to_optional_int(end_column), row[5], row[6], row[7], ) raise IndexError except Exception as e: raise MalformedOutputLineException(row, e) from e def to_csv(self) -> tuple[str, str, str, str, str, str, str, str]: """Convert an OutputLine to a tuple of string to be written by a csv-writer. """ return ( str(self.symbol), str(self.lineno), str(self.column), str(self.end_lineno), str(self.end_column), str(self.object), str(self.msg), str(self.confidence), ) @staticmethod def _value_to_optional_int(value: str | None) -> int | None: """Checks if a (stringified) value should be None or a Python integer.""" if value == "None" or not value: return None return int(value)