• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Types, constants, and utility functions used by multiple sub-modules in spec_tools."""
2
3# Copyright (c) 2018-2019 Collabora, Ltd.
4#
5# SPDX-License-Identifier: Apache-2.0
6#
7# Author(s):    Ryan Pavlik <ryan.pavlik@collabora.com>
8
9import platform
10from collections import namedtuple
11from enum import Enum
12from inspect import getframeinfo
13from pathlib import Path
14from sys import stdout
15
16# if we have termcolor and we know our stdout is a TTY,
17# pull it in and use it.
18if hasattr(stdout, 'isatty') and stdout.isatty():
19    try:
20        from termcolor import colored as colored_impl
21        HAVE_COLOR = True
22    except ImportError:
23        HAVE_COLOR = False
24elif platform.system() == 'Windows':
25    try:
26        from termcolor import colored as colored_impl
27        import colorama
28        colorama.init()
29        HAVE_COLOR = True
30    except ImportError:
31        HAVE_COLOR = False
32
33else:
34    HAVE_COLOR = False
35
36
37def colored(s, color=None, attrs=None):
38    """Call termcolor.colored with same arguments if this is a tty and it is available."""
39    if HAVE_COLOR:
40        return colored_impl(s, color, attrs=attrs)
41    return s
42
43
44###
45# Constants used in multiple places.
46AUTO_FIX_STRING = 'Note: Auto-fix available.'
47EXTENSION_CATEGORY = 'extension'
48CATEGORIES_WITH_VALIDITY = set(('protos', 'structs'))
49NON_EXISTENT_MACROS = set(('plink', 'ttext', 'dtext'))
50
51###
52# MessageContext: All the information about where a message relates to.
53MessageContext = namedtuple('MessageContext',
54                            ['filename', 'lineNum', 'line',
55                             'match', 'group'])
56
57
58def getInterestedRange(message_context):
59    """Return a (start, end) pair of character index for the match in a MessageContext."""
60    if not message_context.match:
61        # whole line
62        return (0, len(message_context.line))
63    return (message_context.match.start(), message_context.match.end())
64
65
66def getHighlightedRange(message_context):
67    """Return a (start, end) pair of character index for the highlighted range in a MessageContext."""
68    if message_context.group is not None and message_context.match is not None:
69        return (message_context.match.start(message_context.group),
70                message_context.match.end(message_context.group))
71    # no group (whole match) or no match (whole line)
72    return getInterestedRange(message_context)
73
74
75def toNameAndLine(context, root_path=None):
76    """Convert MessageContext into a simple filename:line string."""
77    my_fn = Path(context.filename)
78    if root_path:
79        my_fn = my_fn.relative_to(root_path)
80    return '{}:{}'.format(str(my_fn), context.lineNum)
81
82
83def generateInclude(dir_traverse, generated_type, category, entity):
84    """Create an include:: directive for generated api or validity from the various pieces."""
85    return f'include::{dir_traverse}{generated_type}/{category}/{entity}.adoc[]'
86
87
88# Data stored per entity (function, struct, enumerant type, enumerant, extension, etc.)
89EntityData = namedtuple(
90    'EntityData', ['entity', 'macro', 'elem', 'filename', 'category', 'directory'])
91
92
93class MessageType(Enum):
94    """Type of a message."""
95
96    WARNING = 1
97    ERROR = 2
98    NOTE = 3
99
100    def __str__(self):
101        """Format a MessageType as a lowercase string."""
102        return str(self.name).lower()
103
104    def formattedWithColon(self):
105        """Format a MessageType as a colored, lowercase string followed by a colon."""
106        if self == MessageType.WARNING:
107            return colored(str(self) + ':', 'magenta', attrs=['bold'])
108        if self == MessageType.ERROR:
109            return colored(str(self) + ':', 'red', attrs=['bold'])
110        return str(self) + ':'
111
112
113class MessageId(Enum):
114    # Disable bogus pylint warnings in this enum
115    # pylint: disable=no-member
116    """Enumerates the varieties of messages that can be generated.
117
118    Control over enabled messages with -Wbla or -Wno_bla is per-MessageId.
119    """
120
121    MISSING_TEXT = 1
122    LEGACY = 2
123    WRONG_MACRO = 3
124    MISSING_MACRO = 4
125    BAD_ENTITY = 5
126    BAD_ENUMERANT = 6
127    BAD_MACRO = 7
128    UNRECOGNIZED_CONTEXT = 8
129    UNKNOWN_MEMBER = 9
130    DUPLICATE_INCLUDE = 10
131    UNKNOWN_INCLUDE = 11
132    API_VALIDITY_ORDER = 12
133    UNDOCUMENTED_MEMBER = 13
134    MEMBER_PNAME_MISSING = 14
135    MISSING_VALIDITY_INCLUDE = 15
136    MISSING_API_INCLUDE = 16
137    MISUSED_TEXT = 17
138    EXTENSION = 18
139    REFPAGE_TAG = 19
140    REFPAGE_MISSING_DESC = 20
141    REFPAGE_XREFS = 21
142    REFPAGE_XREFS_COMMA = 22
143    REFPAGE_TYPE = 23
144    REFPAGE_NAME = 24
145    REFPAGE_BLOCK = 25
146    REFPAGE_MISSING = 26
147    REFPAGE_MISMATCH = 27
148    REFPAGE_UNKNOWN_ATTRIB = 28
149    REFPAGE_SELF_XREF = 29
150    REFPAGE_XREF_DUPE = 30
151    REFPAGE_WHITESPACE = 31
152    REFPAGE_DUPLICATE = 32
153    UNCLOSED_BLOCK = 33
154    MISSING_INCLUDE_PATH_ATTRIBUTE = 34
155
156    def __str__(self):
157        """Format as a lowercase string."""
158        return self.name.lower()
159
160    def enable_arg(self):
161        """Return the corresponding Wbla string to make the 'enable this message' argument."""
162        return 'W{}'.format(self.name.lower())
163
164    def disable_arg(self):
165        """Return the corresponding Wno_bla string to make the 'enable this message' argument."""
166        return 'Wno_{}'.format(self.name.lower())
167
168    def desc(self):
169        """Return a brief description of the MessageId suitable for use in --help."""
170        return _MESSAGE_DESCRIPTIONS[self]
171
172
173_MESSAGE_DESCRIPTIONS = {
174    MessageId.MISSING_TEXT: "a *text: macro is expected but not found",
175    MessageId.LEGACY: "legacy usage of *name: macro when *link: is applicable",
176    MessageId.WRONG_MACRO: "wrong macro used for an entity",
177    MessageId.MISSING_MACRO: "a macro might be missing",
178    MessageId.BAD_ENTITY: "entity not recognized, etc.",
179    MessageId.BAD_ENUMERANT: "unrecognized enumerant value used in ename:",
180    MessageId.BAD_MACRO: "unrecognized macro used",
181    MessageId.UNRECOGNIZED_CONTEXT: "pname used with an unrecognized context",
182    MessageId.UNKNOWN_MEMBER: "pname used but member/argument by that name not found",
183    MessageId.DUPLICATE_INCLUDE: "duplicated include line",
184    MessageId.UNKNOWN_INCLUDE: "include line specified file we wouldn't expect to exists",
185    MessageId.API_VALIDITY_ORDER: "saw API include after validity include",
186    MessageId.UNDOCUMENTED_MEMBER: "saw an apparent struct/function documentation, but missing a member",
187    MessageId.MEMBER_PNAME_MISSING: "pname: missing from a 'scope' operator",
188    MessageId.MISSING_VALIDITY_INCLUDE: "missing validity include",
189    MessageId.MISSING_API_INCLUDE: "missing API include",
190    MessageId.MISUSED_TEXT: "a *text: macro is found but not expected",
191    MessageId.EXTENSION: "an extension name is incorrectly marked",
192    MessageId.REFPAGE_TAG: "a refpage tag is missing an expected field",
193    MessageId.REFPAGE_MISSING_DESC: "a refpage tag has an empty description",
194    MessageId.REFPAGE_XREFS: "an unrecognized entity is mentioned in xrefs of a refpage tag",
195    MessageId.REFPAGE_XREFS_COMMA: "a comma was founds in xrefs of a refpage tag, which is space-delimited",
196    MessageId.REFPAGE_TYPE: "a refpage tag has an incorrect type field",
197    MessageId.REFPAGE_NAME: "a refpage tag has an unrecognized entity name in its refpage field",
198    MessageId.REFPAGE_BLOCK: "a refpage block is not correctly opened or closed.",
199    MessageId.REFPAGE_MISSING: "an API include was found outside of a refpage block.",
200    MessageId.REFPAGE_MISMATCH: "an API or validity include was found in a non-matching refpage block.",
201    MessageId.REFPAGE_UNKNOWN_ATTRIB: "a refpage tag has an unrecognized attribute",
202    MessageId.REFPAGE_SELF_XREF: "a refpage tag has itself in the list of cross-references",
203    MessageId.REFPAGE_XREF_DUPE: "a refpage cross-references list has at least one duplicate",
204    MessageId.REFPAGE_WHITESPACE: "a refpage cross-references list has non-minimal whitespace",
205    MessageId.REFPAGE_DUPLICATE: "a refpage tag has been seen for a single entity for a second time",
206    MessageId.UNCLOSED_BLOCK: "one or more blocks remain unclosed at the end of a file",
207    MessageId.MISSING_INCLUDE_PATH_ATTRIBUTE: "include:: directives must begin with a recognized path attribute macro",
208}
209
210
211class Message(object):
212    """An Error, Warning, or Note with a MessageContext, MessageId, and message text.
213
214    May optionally have a replacement, a see_also array, an auto-fix,
215    and a stack frame where the message was created.
216    """
217
218    def __init__(self, message_id, message_type, message, context,
219                 replacement=None, see_also=None, fix=None, frame=None):
220        """Construct a Message.
221
222        Typically called by MacroCheckerFile.diag().
223        """
224        self.message_id = message_id
225
226        self.message_type = message_type
227
228        if isinstance(message, str):
229            self.message = [message]
230        else:
231            self.message = message
232
233        self.context = context
234        if context is not None and context.match is not None and context.group is not None:
235            if context.group not in context.match.groupdict():
236                raise RuntimeError(
237                    'Group "{}" does not exist in the match'.format(context.group))
238
239        self.replacement = replacement
240
241        self.fix = fix
242
243        if see_also is None:
244            self.see_also = None
245        elif isinstance(see_also, MessageContext):
246            self.see_also = [see_also]
247        else:
248            self.see_also = see_also
249
250        self.script_location = None
251        if frame:
252            try:
253                frameinfo = getframeinfo(frame)
254                self.script_location = "{}:{}".format(
255                    frameinfo.filename, frameinfo.lineno)
256            finally:
257                del frame
258