285 lines
10 KiB
Python
285 lines
10 KiB
Python
"""Defines ConsolePrinter, a BasePrinter subclass for appealing console output."""
|
|
|
|
# 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 sys import stdout
|
|
|
|
from .base_printer import BasePrinter
|
|
from .shared import (colored, getHighlightedRange, getInterestedRange,
|
|
toNameAndLine)
|
|
|
|
try:
|
|
from tabulate import tabulate_impl
|
|
HAVE_TABULATE = True
|
|
except ImportError:
|
|
HAVE_TABULATE = False
|
|
|
|
|
|
def colWidth(collection, columnNum):
|
|
"""Compute the required width of a column in a collection of row-tuples."""
|
|
MIN_PADDING = 5
|
|
return MIN_PADDING + max((len(row[columnNum]) for row in collection))
|
|
|
|
|
|
def alternateTabulate(collection, headers=None):
|
|
"""Minimal re-implementation of the tabulate module."""
|
|
# We need a list, not a generator or anything else.
|
|
if not isinstance(collection, list):
|
|
collection = list(collection)
|
|
|
|
# Empty collection means no table
|
|
if not collection:
|
|
return None
|
|
|
|
if headers is None:
|
|
fullTable = collection
|
|
else:
|
|
underline = ['-' * len(header) for header in headers]
|
|
fullTable = [headers, underline] + collection
|
|
widths = [colWidth(collection, colNum)
|
|
for colNum in range(len(fullTable[0]))]
|
|
widths[-1] = None
|
|
|
|
lines = []
|
|
for row in fullTable:
|
|
fields = []
|
|
for data, width in zip(row, widths):
|
|
if width:
|
|
spaces = ' ' * (width - len(data))
|
|
fields.append(data + spaces)
|
|
else:
|
|
fields.append(data)
|
|
lines.append(''.join(fields))
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def printTabulated(collection, headers=None):
|
|
"""Call either tabulate.tabulate(), or our internal alternateTabulate()."""
|
|
if HAVE_TABULATE:
|
|
tabulated = tabulate_impl(collection, headers=headers)
|
|
else:
|
|
tabulated = alternateTabulate(collection, headers=headers)
|
|
if tabulated:
|
|
print(tabulated)
|
|
|
|
|
|
def printLineSubsetWithHighlighting(
|
|
line, start, end, highlightStart=None, highlightEnd=None, maxLen=120, replacement=None):
|
|
"""Print a (potential subset of a) line, with highlighting/underline and optional replacement.
|
|
|
|
Will print at least the characters line[start:end], and potentially more if possible
|
|
to do so without making the output too wide.
|
|
Will highlight (underline) line[highlightStart:highlightEnd], where the default
|
|
value for highlightStart is simply start, and the default value for highlightEnd is simply end.
|
|
Replacment, if supplied, will be aligned with the highlighted range.
|
|
|
|
Output is intended to look like part of a Clang compile error/warning message.
|
|
"""
|
|
# Fill in missing start/end with start/end of range.
|
|
if highlightStart is None:
|
|
highlightStart = start
|
|
if highlightEnd is None:
|
|
highlightEnd = end
|
|
|
|
# Expand interested range start/end.
|
|
start = min(start, highlightStart)
|
|
end = max(end, highlightEnd)
|
|
|
|
tildeLength = highlightEnd - highlightStart - 1
|
|
caretLoc = highlightStart
|
|
continuation = '[...]'
|
|
|
|
if len(line) > maxLen:
|
|
# Too long
|
|
|
|
# the max is to handle -1 from .find() (which indicates "not found")
|
|
followingSpaceIndex = max(end, line.find(' ', min(len(line), end + 1)))
|
|
|
|
# Maximum length has decreased by at least
|
|
# the length of a single continuation we absolutely need.
|
|
maxLen -= len(continuation)
|
|
|
|
if followingSpaceIndex <= maxLen:
|
|
# We can grab the whole beginning of the line,
|
|
# and not adjust caretLoc
|
|
line = line[:maxLen] + continuation
|
|
|
|
elif (len(line) - followingSpaceIndex) < 5:
|
|
# We need to truncate the beginning,
|
|
# but we're close to the end of line.
|
|
newBeginning = len(line) - maxLen
|
|
|
|
caretLoc += len(continuation)
|
|
caretLoc -= newBeginning
|
|
line = continuation + line[newBeginning:]
|
|
else:
|
|
# Need to truncate the beginning of the string too.
|
|
newEnd = followingSpaceIndex
|
|
|
|
# Now we need two continuations
|
|
# (and to adjust caret to the right accordingly)
|
|
maxLen -= len(continuation)
|
|
caretLoc += len(continuation)
|
|
|
|
newBeginning = newEnd - maxLen
|
|
caretLoc -= newBeginning
|
|
|
|
line = continuation + line[newBeginning:newEnd] + continuation
|
|
|
|
stdout.buffer.write(line.encode('utf-8'))
|
|
print()
|
|
|
|
spaces = ' ' * caretLoc
|
|
tildes = '~' * tildeLength
|
|
print(spaces + colored('^' + tildes, 'green'))
|
|
if replacement is not None:
|
|
print(spaces + colored(replacement, 'green'))
|
|
|
|
|
|
class ConsolePrinter(BasePrinter):
|
|
"""Implementation of BasePrinter for generating diagnostic reports in colored, helpful console output."""
|
|
|
|
def __init__(self):
|
|
self.show_script_location = False
|
|
super().__init__()
|
|
|
|
###
|
|
# Output methods: these all print directly.
|
|
def outputResults(self, checker, broken_links=True,
|
|
missing_includes=False):
|
|
"""Output the full results of a checker run.
|
|
|
|
Includes the diagnostics, broken links (if desired),
|
|
and missing includes (if desired).
|
|
"""
|
|
self.output(checker)
|
|
if broken_links:
|
|
broken = checker.getBrokenLinks()
|
|
if broken:
|
|
self.outputBrokenLinks(checker, broken)
|
|
if missing_includes:
|
|
missing = checker.getMissingUnreferencedApiIncludes()
|
|
if missing:
|
|
self.outputMissingIncludes(checker, missing)
|
|
|
|
def outputBrokenLinks(self, checker, broken):
|
|
"""Output a table of broken links.
|
|
|
|
Called by self.outputBrokenAndMissing() if requested.
|
|
"""
|
|
print('Missing API includes that are referenced by a linking macro: these result in broken links in the spec!')
|
|
|
|
def makeRowOfBroken(entity, uses):
|
|
fn = checker.findEntity(entity).filename
|
|
anchor = '[[{}]]'.format(entity)
|
|
locations = ', '.join((toNameAndLine(context, root_path=checker.root_path)
|
|
for context in uses))
|
|
return (fn, anchor, locations)
|
|
printTabulated((makeRowOfBroken(entity, uses)
|
|
for entity, uses in sorted(broken.items())),
|
|
headers=['Include File', 'Anchor in lieu of include', 'Links to this entity'])
|
|
|
|
def outputMissingIncludes(self, checker, missing):
|
|
"""Output a table of missing includes.
|
|
|
|
Called by self.outputBrokenAndMissing() if requested.
|
|
"""
|
|
missing = list(sorted(missing))
|
|
if not missing:
|
|
# Exit if none
|
|
return
|
|
print(
|
|
'Missing, but unreferenced, API includes/anchors - potentially not-documented entities:')
|
|
|
|
def makeRowOfMissing(entity):
|
|
fn = checker.findEntity(entity).filename
|
|
anchor = '[[{}]]'.format(entity)
|
|
return (fn, anchor)
|
|
printTabulated((makeRowOfMissing(entity) for entity in missing),
|
|
headers=['Include File', 'Anchor in lieu of include'])
|
|
|
|
def outputMessage(self, msg):
|
|
"""Output a Message, with highlighted range and replacement, if appropriate."""
|
|
highlightStart, highlightEnd = getHighlightedRange(msg.context)
|
|
|
|
if '\n' in msg.context.filename:
|
|
# This is a multi-line string "filename".
|
|
# Extra blank line and delimiter line for readability:
|
|
print()
|
|
print('--------------------------------------------------------------------')
|
|
|
|
fileAndLine = colored('{}:'.format(
|
|
self.formatBrief(msg.context)), attrs=['bold'])
|
|
|
|
headingSize = len('{context}: {mtype}: '.format(
|
|
context=self.formatBrief(msg.context),
|
|
mtype=self.formatBrief(msg.message_type, False)))
|
|
indent = ' ' * headingSize
|
|
printedHeading = False
|
|
|
|
lines = msg.message[:]
|
|
if msg.see_also:
|
|
lines.append('See also:')
|
|
lines.extend((' {}'.format(self.formatBrief(see))
|
|
for see in msg.see_also))
|
|
|
|
if msg.fix:
|
|
lines.append('Note: Auto-fix available')
|
|
|
|
for line in msg.message:
|
|
if not printedHeading:
|
|
scriptloc = ''
|
|
if msg.script_location and self.show_script_location:
|
|
scriptloc = ', ' + msg.script_location
|
|
print('{fileLine} {mtype} {msg} (-{arg}{loc})'.format(
|
|
fileLine=fileAndLine, mtype=msg.message_type.formattedWithColon(),
|
|
msg=colored(line, attrs=['bold']), arg=msg.message_id.enable_arg(), loc=scriptloc))
|
|
printedHeading = True
|
|
else:
|
|
print(colored(indent + line, attrs=['bold']))
|
|
|
|
if len(msg.message) > 1:
|
|
# extra blank line after multiline message
|
|
print('')
|
|
|
|
start, end = getInterestedRange(msg.context)
|
|
printLineSubsetWithHighlighting(
|
|
msg.context.line,
|
|
start, end,
|
|
highlightStart, highlightEnd,
|
|
replacement=msg.replacement)
|
|
|
|
def outputFallback(self, obj):
|
|
"""Output by calling print."""
|
|
print(obj)
|
|
|
|
###
|
|
# Format methods: these all return a string.
|
|
def formatFilename(self, fn, _with_color=True):
|
|
"""Format a local filename, as a relative path if possible."""
|
|
return self.getRelativeFilename(fn)
|
|
|
|
def formatMessageTypeBrief(self, message_type, with_color=True):
|
|
"""Format a message type briefly, applying color if desired and possible.
|
|
|
|
Delegates to the superclass if not formatting with color.
|
|
"""
|
|
if with_color:
|
|
return message_type.formattedWithColon()
|
|
return super(ConsolePrinter, self).formatMessageTypeBrief(
|
|
message_type, with_color)
|