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