summaryrefslogtreecommitdiff
path: root/pylint/testutils/output_line.py
blob: 7465fce9d11c63fcf1152933c4a6261a48e3e54b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# 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 collections.abc import Sequence
from typing import Any, NamedTuple, 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 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) -> 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(",")
        # noinspection PyBroadException
        # pylint: disable = too-many-try-statements
        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,
                    stacklevel=2,
                )
                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,
                    stacklevel=2,
                )
                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:  # pylint: disable=broad-except
            warnings.warn(
                "Expected 'msg-symbolic-name:42:27:MyClass.my_function:The message:"
                f"CONFIDENCE' but we got '{':'.join(row)}'. Try updating the expected"
                f" output with:\npython tests/test_functional.py {UPDATE_OPTION}",
                UserWarning,
            )
            return cls("", 0, 0, None, None, "", "", "")

    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)