summaryrefslogtreecommitdiff
path: root/pylint/message/message_definition_store.py
blob: fdd1f0b548ffc17509b24adf5d9de81c3d8dc0b8 (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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# -*- coding: utf-8 -*-

# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/master/COPYING

from __future__ import print_function

import collections

from pylint.exceptions import InvalidMessageError, UnknownMessageError


class MessageDefinitionStore:

    """The messages store knows information about every possible message but has
    no particular state during analysis.
    """

    def __init__(self):
        # Primary registry for all active messages (i.e. all messages
        # that can be emitted by pylint for the underlying Python
        # version). It contains the 1:1 mapping from symbolic names
        # to message definition objects.
        # Keys are msg ids, values are a 2-uple with the msg type and the
        # msg itself
        self._messages_definitions = {}
        # Maps alternative names (numeric IDs, deprecated names) to
        # message definitions. May contain several names for each definition
        # object.
        self._alternative_names = {}
        self._msgs_by_category = collections.defaultdict(list)

    @property
    def messages(self) -> list:
        """The list of all active messages."""
        return self._messages_definitions.values()

    def register_messages_from_checker(self, checker):
        """Register all messages from a checker.

        :param BaseChecker checker:
        """
        checker.check_consistency()
        for message in checker.messages:
            self.register_message(message)

    def register_message(self, message):
        """Register a MessageDefinition with consistency in mind.

        :param MessageDefinition message: The message definition being added.
        """
        self._check_id_and_symbol_consistency(message.msgid, message.symbol)
        self._check_symbol(message.msgid, message.symbol)
        self._check_msgid(message.msgid, message.symbol)
        for old_name in message.old_names:
            self._check_symbol(message.msgid, old_name[1])
        self._messages_definitions[message.symbol] = message
        self._register_alternative_name(message, message.msgid, message.symbol)
        for old_id, old_symbol in message.old_names:
            self._register_alternative_name(message, old_id, old_symbol)
        self._msgs_by_category[message.msgid[0]].append(message.msgid)

    def _register_alternative_name(self, msg, msgid, symbol):
        """helper for register_message()"""
        self._check_id_and_symbol_consistency(msgid, symbol)
        self._alternative_names[msgid] = msg
        self._alternative_names[symbol] = msg

    def _check_symbol(self, msgid, symbol):
        """Check that a symbol is not already used. """
        other_message = self._messages_definitions.get(symbol)
        if other_message:
            self._raise_duplicate_msgid(symbol, msgid, other_message.msgid)
        else:
            alternative_msgid = None
        alternative_message = self._alternative_names.get(symbol)
        if alternative_message:
            if alternative_message.symbol == symbol:
                alternative_msgid = alternative_message.msgid
            else:
                for old_msgid, old_symbol in alternative_message.old_names:
                    if old_symbol == symbol:
                        alternative_msgid = old_msgid
                        break
            if msgid != alternative_msgid:
                self._raise_duplicate_msgid(symbol, msgid, alternative_msgid)

    def _check_msgid(self, msgid, symbol):
        for message in self._messages_definitions.values():
            if message.msgid == msgid:
                self._raise_duplicate_symbol(msgid, symbol, message.symbol)

    def _check_id_and_symbol_consistency(self, msgid, symbol):
        try:
            alternative = self._alternative_names[msgid]
        except KeyError:
            alternative = False
        try:
            if not alternative:
                alternative = self._alternative_names[symbol]
        except KeyError:
            # There is no alternative names concerning this msgid/symbol.
            # So nothing to check
            return None
        old_symbolic_name = None
        old_symbolic_id = None
        for alternate_msgid, alternate_symbol in alternative.old_names:
            if alternate_msgid == msgid or alternate_symbol == symbol:
                old_symbolic_id = alternate_msgid
                old_symbolic_name = alternate_symbol
        if symbol not in (alternative.symbol, old_symbolic_name):
            if msgid == old_symbolic_id:
                self._raise_duplicate_symbol(msgid, symbol, old_symbolic_name)
            else:
                self._raise_duplicate_symbol(msgid, symbol, alternative.symbol)
        return None

    @staticmethod
    def _raise_duplicate_symbol(msgid, symbol, other_symbol):
        """Raise an error when a symbol is duplicated.

        :param str msgid: The msgid corresponding to the symbols
        :param str symbol: Offending symbol
        :param str other_symbol: Other offending symbol
        :raises InvalidMessageError:"""
        symbols = [symbol, other_symbol]
        symbols.sort()
        error_message = "Message id '{msgid}' cannot have both ".format(msgid=msgid)
        error_message += "'{other_symbol}' and '{symbol}' as symbolic name.".format(
            other_symbol=symbols[0], symbol=symbols[1]
        )
        raise InvalidMessageError(error_message)

    @staticmethod
    def _raise_duplicate_msgid(symbol, msgid, other_msgid):
        """Raise an error when a msgid is duplicated.

        :param str symbol: The symbol corresponding to the msgids
        :param str msgid: Offending msgid
        :param str other_msgid: Other offending msgid
        :raises InvalidMessageError:"""
        msgids = [msgid, other_msgid]
        msgids.sort()
        error_message = "Message symbol '{symbol}' cannot be used for ".format(
            symbol=symbol
        )
        error_message += "'{other_msgid}' and '{msgid}' at the same time.".format(
            other_msgid=msgids[0], msgid=msgids[1]
        )
        raise InvalidMessageError(error_message)

    def get_message_definitions(self, msgid_or_symbol: str) -> list:
        """Returns the Message object for this message.
        :param str msgid_or_symbol: msgid_or_symbol may be either a numeric or symbolic id.
        :raises UnknownMessageError: if the message id is not defined.
        :rtype: List of MessageDefinition
        :return: A message definition corresponding to msgid_or_symbol
        """
        # Only msgid can have a digit as second letter
        is_msgid = msgid_or_symbol[1:].isdigit()
        if is_msgid:
            msgid_or_symbol = msgid_or_symbol.upper()
        for source in (self._alternative_names, self._messages_definitions):
            try:
                return [source[msgid_or_symbol]]
            except KeyError:
                pass
        error_msg = "No such message id or symbol '{msgid_or_symbol}'.".format(
            msgid_or_symbol=msgid_or_symbol
        )
        raise UnknownMessageError(error_msg)

    def get_msg_display_string(self, msgid):
        """Generates a user-consumable representation of a message. """
        message_definitions = self.get_message_definitions(msgid)
        if len(message_definitions) == 1:
            return repr(message_definitions[0].symbol)
        return repr([md.symbol for md in message_definitions])

    def help_message(self, msgids):
        """Display help messages for the given message identifiers"""
        for msgid in msgids:
            try:
                for message_definition in self.get_message_definitions(msgid):
                    print(message_definition.format_help(checkerref=True))
                    print("")
            except UnknownMessageError as ex:
                print(ex)
                print("")
                continue

    def list_messages(self):
        """Output full messages list documentation in ReST format. """
        messages = sorted(self._messages_definitions.values(), key=lambda m: m.msgid)
        for message in messages:
            if not message.may_be_emitted():
                continue
            print(message.format_help(checkerref=False))
        print("")