286 lines
11 KiB
Python
286 lines
11 KiB
Python
|
"""Types, constants, and utility functions used by multiple sub-modules in spec_tools."""
|
||
|
|
||
|
# Copyright (c) 2018-2019 Collabora, Ltd.
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
#
|
||
|
# Author(s): Ryan Pavlik <ryan.pavlik@collabora.com>
|
||
|
|
||
|
from collections import namedtuple
|
||
|
from enum import Enum
|
||
|
from inspect import currentframe, getframeinfo
|
||
|
from pathlib import Path
|
||
|
from sys import stdout
|
||
|
|
||
|
# if we have termcolor and we know our stdout is a TTY,
|
||
|
# pull it in and use it.
|
||
|
if hasattr(stdout, 'isatty') and stdout.isatty():
|
||
|
try:
|
||
|
from termcolor import colored as colored_impl
|
||
|
HAVE_COLOR = True
|
||
|
except ImportError:
|
||
|
HAVE_COLOR = False
|
||
|
else:
|
||
|
HAVE_COLOR = False
|
||
|
|
||
|
|
||
|
def colored(s, color=None, attrs=None):
|
||
|
"""Call termcolor.colored with same arguments if this is a tty and it is available."""
|
||
|
if HAVE_COLOR:
|
||
|
return colored_impl(s, color, attrs=attrs)
|
||
|
return s
|
||
|
|
||
|
|
||
|
###
|
||
|
# Constants used in multiple places.
|
||
|
AUTO_FIX_STRING = 'Note: Auto-fix available.'
|
||
|
EXTENSION_CATEGORY = 'extension'
|
||
|
CATEGORIES_WITH_VALIDITY = set(['protos', 'structs'])
|
||
|
NON_EXISTENT_MACROS = set(['plink', 'ttext', 'dtext'])
|
||
|
|
||
|
###
|
||
|
# MessageContext: All the information about where a message relates to.
|
||
|
MessageContext = namedtuple('MessageContext',
|
||
|
['filename', 'lineNum', 'line',
|
||
|
'match', 'group'])
|
||
|
|
||
|
|
||
|
def getInterestedRange(message_context):
|
||
|
"""Return a (start, end) pair of character index for the match in a MessageContext."""
|
||
|
if not message_context.match:
|
||
|
# whole line
|
||
|
return (0, len(message_context.line))
|
||
|
return (message_context.match.start(), message_context.match.end())
|
||
|
|
||
|
|
||
|
def getHighlightedRange(message_context):
|
||
|
"""Return a (start, end) pair of character index for the highlighted range in a MessageContext."""
|
||
|
if message_context.group is not None and message_context.match is not None:
|
||
|
return (message_context.match.start(message_context.group),
|
||
|
message_context.match.end(message_context.group))
|
||
|
# no group (whole match) or no match (whole line)
|
||
|
return getInterestedRange(message_context)
|
||
|
|
||
|
|
||
|
def toNameAndLine(context, root_path=None):
|
||
|
"""Convert MessageContext into a simple filename:line string."""
|
||
|
my_fn = Path(context.filename)
|
||
|
if root_path:
|
||
|
my_fn = my_fn.relative_to(root_path)
|
||
|
return '{}:{}'.format(str(my_fn), context.lineNum)
|
||
|
|
||
|
|
||
|
def generateInclude(dir_traverse, generated_type, category, entity):
|
||
|
"""Create an include:: directive for geneated api or validity from the various pieces."""
|
||
|
return 'include::{directory_traverse}{generated_type}/{category}/{entity_name}.txt[]'.format(
|
||
|
directory_traverse=dir_traverse,
|
||
|
generated_type=generated_type,
|
||
|
category=category,
|
||
|
entity_name=entity)
|
||
|
|
||
|
|
||
|
# Data stored per entity (function, struct, enumerant type, enumerant, extension, etc.)
|
||
|
EntityData = namedtuple(
|
||
|
'EntityData', ['entity', 'macro', 'elem', 'filename', 'category', 'directory'])
|
||
|
|
||
|
|
||
|
class MessageType(Enum):
|
||
|
"""Type of a message."""
|
||
|
|
||
|
WARNING = 1
|
||
|
ERROR = 2
|
||
|
NOTE = 3
|
||
|
|
||
|
def __str__(self):
|
||
|
"""Format a MessageType as a lowercase string."""
|
||
|
return str(self.name).lower()
|
||
|
|
||
|
def formattedWithColon(self):
|
||
|
"""Format a MessageType as a colored, lowercase string followed by a colon."""
|
||
|
if self == MessageType.WARNING:
|
||
|
return colored(str(self) + ':', 'magenta', attrs=['bold'])
|
||
|
if self == MessageType.ERROR:
|
||
|
return colored(str(self) + ':', 'red', attrs=['bold'])
|
||
|
return str(self) + ':'
|
||
|
|
||
|
|
||
|
class MessageId(Enum):
|
||
|
"""Enumerates the varieties of messages that can be generated.
|
||
|
|
||
|
Control over enabled messages with -Wbla or -Wno_bla is per-MessageId.
|
||
|
"""
|
||
|
|
||
|
MISSING_TEXT = 1
|
||
|
LEGACY = 2
|
||
|
WRONG_MACRO = 3
|
||
|
MISSING_MACRO = 4
|
||
|
BAD_ENTITY = 5
|
||
|
BAD_ENUMERANT = 6
|
||
|
BAD_MACRO = 7
|
||
|
UNRECOGNIZED_CONTEXT = 8
|
||
|
UNKNOWN_MEMBER = 9
|
||
|
DUPLICATE_INCLUDE = 10
|
||
|
UNKNOWN_INCLUDE = 11
|
||
|
API_VALIDITY_ORDER = 12
|
||
|
UNDOCUMENTED_MEMBER = 13
|
||
|
MEMBER_PNAME_MISSING = 14
|
||
|
MISSING_VALIDITY_INCLUDE = 15
|
||
|
MISSING_API_INCLUDE = 16
|
||
|
MISUSED_TEXT = 17
|
||
|
EXTENSION = 18
|
||
|
REFPAGE_TAG = 19
|
||
|
REFPAGE_MISSING_DESC = 20
|
||
|
REFPAGE_XREFS = 21
|
||
|
REFPAGE_XREFS_COMMA = 22
|
||
|
REFPAGE_TYPE = 23
|
||
|
REFPAGE_NAME = 24
|
||
|
REFPAGE_BLOCK = 25
|
||
|
REFPAGE_MISSING = 26
|
||
|
REFPAGE_MISMATCH = 27
|
||
|
REFPAGE_UNKNOWN_ATTRIB = 28
|
||
|
REFPAGE_SELF_XREF = 29
|
||
|
REFPAGE_XREF_DUPE = 30
|
||
|
REFPAGE_WHITESPACE = 31
|
||
|
REFPAGE_DUPLICATE = 32
|
||
|
UNCLOSED_BLOCK = 33
|
||
|
|
||
|
def __str__(self):
|
||
|
"""Format as a lowercase string."""
|
||
|
return self.name.lower()
|
||
|
|
||
|
def enable_arg(self):
|
||
|
"""Return the corresponding Wbla string to make the 'enable this message' argument."""
|
||
|
return 'W{}'.format(self.name.lower())
|
||
|
|
||
|
def disable_arg(self):
|
||
|
"""Return the corresponding Wno_bla string to make the 'enable this message' argument."""
|
||
|
return 'Wno_{}'.format(self.name.lower())
|
||
|
|
||
|
def desc(self):
|
||
|
"""Return a brief description of the MessageId suitable for use in --help."""
|
||
|
if self == MessageId.MISSING_TEXT:
|
||
|
return "a *text: macro is expected but not found"
|
||
|
elif self == MessageId.LEGACY:
|
||
|
return "legacy usage of *name: macro when *link: is applicable"
|
||
|
elif self == MessageId.WRONG_MACRO:
|
||
|
return "wrong macro used for an entity"
|
||
|
elif self == MessageId.MISSING_MACRO:
|
||
|
return "a macro might be missing"
|
||
|
elif self == MessageId.BAD_ENTITY:
|
||
|
return "entity not recognized, etc."
|
||
|
elif self == MessageId.BAD_ENUMERANT:
|
||
|
return "unrecognized enumerant value used in ename:"
|
||
|
elif self == MessageId.BAD_MACRO:
|
||
|
return "unrecognized macro used"
|
||
|
elif self == MessageId.UNRECOGNIZED_CONTEXT:
|
||
|
return "pname used with an unrecognized context"
|
||
|
elif self == MessageId.UNKNOWN_MEMBER:
|
||
|
return "pname used but member/argument by that name not found"
|
||
|
elif self == MessageId.DUPLICATE_INCLUDE:
|
||
|
return "duplicated include line"
|
||
|
elif self == MessageId.UNKNOWN_INCLUDE:
|
||
|
return "include line specified file we wouldn't expect to exists"
|
||
|
elif self == MessageId.API_VALIDITY_ORDER:
|
||
|
return "saw API include after validity include"
|
||
|
elif self == MessageId.UNDOCUMENTED_MEMBER:
|
||
|
return "saw an apparent struct/function documentation, but missing a member"
|
||
|
elif self == MessageId.MEMBER_PNAME_MISSING:
|
||
|
return "pname: missing from a 'scope' operator"
|
||
|
elif self == MessageId.MISSING_VALIDITY_INCLUDE:
|
||
|
return "missing validity include"
|
||
|
elif self == MessageId.MISSING_API_INCLUDE:
|
||
|
return "missing API include"
|
||
|
elif self == MessageId.MISUSED_TEXT:
|
||
|
return "a *text: macro is found but not expected"
|
||
|
elif self == MessageId.EXTENSION:
|
||
|
return "an extension name is incorrectly marked"
|
||
|
elif self == MessageId.REFPAGE_TAG:
|
||
|
return "a refpage tag is missing an expected field"
|
||
|
elif self == MessageId.REFPAGE_MISSING_DESC:
|
||
|
return "a refpage tag has an empty description"
|
||
|
elif self == MessageId.REFPAGE_XREFS:
|
||
|
return "an unrecognized entity is mentioned in xrefs of a refpage tag"
|
||
|
elif self == MessageId.REFPAGE_XREFS_COMMA:
|
||
|
return "a comma was founds in xrefs of a refpage tag, which is space-delimited"
|
||
|
elif self == MessageId.REFPAGE_TYPE:
|
||
|
return "a refpage tag has an incorrect type field"
|
||
|
elif self == MessageId.REFPAGE_NAME:
|
||
|
return "a refpage tag has an unrecognized entity name in its refpage field"
|
||
|
elif self == MessageId.REFPAGE_BLOCK:
|
||
|
return "a refpage block is not correctly opened or closed."
|
||
|
elif self == MessageId.REFPAGE_MISSING:
|
||
|
return "an API include was found outside of a refpage block."
|
||
|
elif self == MessageId.REFPAGE_MISMATCH:
|
||
|
return "an API or validity include was found in a non-matching refpage block."
|
||
|
elif self == MessageId.REFPAGE_UNKNOWN_ATTRIB:
|
||
|
return "a refpage tag has an unrecognized attribute"
|
||
|
elif self == MessageId.REFPAGE_SELF_XREF:
|
||
|
return "a refpage tag has itself in the list of cross-references"
|
||
|
elif self == MessageId.REFPAGE_XREF_DUPE:
|
||
|
return "a refpage cross-references list has at least one duplicate"
|
||
|
elif self == MessageId.REFPAGE_WHITESPACE:
|
||
|
return "a refpage cross-references list has non-minimal whitespace"
|
||
|
elif self == MessageId.REFPAGE_DUPLICATE:
|
||
|
return "a refpage tag has been seen for a single entity for a second time"
|
||
|
elif self == MessageId.UNCLOSED_BLOCK:
|
||
|
return "one or more blocks remain unclosed at the end of a file"
|
||
|
|
||
|
|
||
|
class Message(object):
|
||
|
"""An Error, Warning, or Note with a MessageContext, MessageId, and message text.
|
||
|
|
||
|
May optionally have a replacement, a see_also array, an auto-fix,
|
||
|
and a stack frame where the message was created.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, message_id, message_type, message, context,
|
||
|
replacement=None, see_also=None, fix=None, frame=None):
|
||
|
"""Construct a Message.
|
||
|
|
||
|
Typically called by MacroCheckerFile.diag().
|
||
|
"""
|
||
|
self.message_id = message_id
|
||
|
|
||
|
self.message_type = message_type
|
||
|
|
||
|
if isinstance(message, str):
|
||
|
self.message = [message]
|
||
|
else:
|
||
|
self.message = message
|
||
|
|
||
|
self.context = context
|
||
|
if context is not None and context.match is not None and context.group is not None:
|
||
|
if context.group not in context.match.groupdict():
|
||
|
raise RuntimeError(
|
||
|
'Group "{}" does not exist in the match'.format(context.group))
|
||
|
|
||
|
self.replacement = replacement
|
||
|
|
||
|
self.fix = fix
|
||
|
|
||
|
if see_also is None:
|
||
|
self.see_also = None
|
||
|
elif isinstance(see_also, MessageContext):
|
||
|
self.see_also = [see_also]
|
||
|
else:
|
||
|
self.see_also = see_also
|
||
|
|
||
|
self.script_location = None
|
||
|
if frame:
|
||
|
try:
|
||
|
frameinfo = getframeinfo(frame)
|
||
|
self.script_location = "{}:{}".format(
|
||
|
frameinfo.filename, frameinfo.lineno)
|
||
|
finally:
|
||
|
del frame
|