• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Defines ConsolePrinter, a BasePrinter subclass for appealing console output."""
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
9from sys import stdout
10
11from .base_printer import BasePrinter
12from .shared import (colored, getHighlightedRange, getInterestedRange,
13                     toNameAndLine)
14
15try:
16    from tabulate import tabulate_impl
17    HAVE_TABULATE = True
18except ImportError:
19    HAVE_TABULATE = False
20
21
22def colWidth(collection, columnNum):
23    """Compute the required width of a column in a collection of row-tuples."""
24    MIN_PADDING = 5
25    return MIN_PADDING + max((len(row[columnNum]) for row in collection))
26
27
28def alternateTabulate(collection, headers=None):
29    """Minimal re-implementation of the tabulate module."""
30    # We need a list, not a generator or anything else.
31    if not isinstance(collection, list):
32        collection = list(collection)
33
34    # Empty collection means no table
35    if not collection:
36        return None
37
38    if headers is None:
39        fullTable = collection
40    else:
41        underline = ['-' * len(header) for header in headers]
42        fullTable = [headers, underline] + collection
43    widths = [colWidth(collection, colNum)
44              for colNum in range(len(fullTable[0]))]
45    widths[-1] = None
46
47    lines = []
48    for row in fullTable:
49        fields = []
50        for data, width in zip(row, widths):
51            if width:
52                spaces = ' ' * (width - len(data))
53                fields.append(data + spaces)
54            else:
55                fields.append(data)
56        lines.append(''.join(fields))
57    return '\n'.join(lines)
58
59
60def printTabulated(collection, headers=None):
61    """Call either tabulate.tabulate(), or our internal alternateTabulate()."""
62    if HAVE_TABULATE:
63        tabulated = tabulate_impl(collection, headers=headers)
64    else:
65        tabulated = alternateTabulate(collection, headers=headers)
66    if tabulated:
67        print(tabulated)
68
69
70def printLineSubsetWithHighlighting(
71        line, start, end, highlightStart=None, highlightEnd=None, maxLen=120, replacement=None):
72    """Print a (potential subset of a) line, with highlighting/underline and optional replacement.
73
74    Will print at least the characters line[start:end], and potentially more if possible
75    to do so without making the output too wide.
76    Will highlight (underline) line[highlightStart:highlightEnd], where the default
77    value for highlightStart is simply start, and the default value for highlightEnd is simply end.
78    Replacment, if supplied, will be aligned with the highlighted range.
79
80    Output is intended to look like part of a Clang compile error/warning message.
81    """
82    # Fill in missing start/end with start/end of range.
83    if highlightStart is None:
84        highlightStart = start
85    if highlightEnd is None:
86        highlightEnd = end
87
88    # Expand interested range start/end.
89    start = min(start, highlightStart)
90    end = max(end, highlightEnd)
91
92    tildeLength = highlightEnd - highlightStart - 1
93    caretLoc = highlightStart
94    continuation = '[...]'
95
96    if len(line) > maxLen:
97        # Too long
98
99        # the max is to handle -1 from .find() (which indicates "not found")
100        followingSpaceIndex = max(end, line.find(' ', min(len(line), end + 1)))
101
102        # Maximum length has decreased by at least
103        # the length of a single continuation we absolutely need.
104        maxLen -= len(continuation)
105
106        if followingSpaceIndex <= maxLen:
107            # We can grab the whole beginning of the line,
108            # and not adjust caretLoc
109            line = line[:maxLen] + continuation
110
111        elif (len(line) - followingSpaceIndex) < 5:
112            # We need to truncate the beginning,
113            # but we're close to the end of line.
114            newBeginning = len(line) - maxLen
115
116            caretLoc += len(continuation)
117            caretLoc -= newBeginning
118            line = continuation + line[newBeginning:]
119        else:
120            # Need to truncate the beginning of the string too.
121            newEnd = followingSpaceIndex
122
123            # Now we need two continuations
124            # (and to adjust caret to the right accordingly)
125            maxLen -= len(continuation)
126            caretLoc += len(continuation)
127
128            newBeginning = newEnd - maxLen
129            caretLoc -= newBeginning
130
131            line = continuation + line[newBeginning:newEnd] + continuation
132
133    stdout.buffer.write(line.encode('utf-8'))
134    print()
135
136    spaces = ' ' * caretLoc
137    tildes = '~' * tildeLength
138    print(spaces + colored('^' + tildes, 'green'))
139    if replacement is not None:
140        print(spaces + colored(replacement, 'green'))
141
142
143class ConsolePrinter(BasePrinter):
144    """Implementation of BasePrinter for generating diagnostic reports in colored, helpful console output."""
145
146    def __init__(self):
147        self.show_script_location = False
148        super().__init__()
149
150    ###
151    # Output methods: these all print directly.
152    def outputResults(self, checker, broken_links=True,
153                      missing_includes=False):
154        """Output the full results of a checker run.
155
156        Includes the diagnostics, broken links (if desired),
157        and missing includes (if desired).
158        """
159        self.output(checker)
160        if broken_links:
161            broken = checker.getBrokenLinks()
162            if broken:
163                self.outputBrokenLinks(checker, broken)
164        if missing_includes:
165            missing = checker.getMissingUnreferencedApiIncludes()
166            if missing:
167                self.outputMissingIncludes(checker, missing)
168
169    def outputBrokenLinks(self, checker, broken):
170        """Output a table of broken links.
171
172        Called by self.outputBrokenAndMissing() if requested.
173        """
174        print('Missing API includes that are referenced by a linking macro: these result in broken links in the spec!')
175
176        def makeRowOfBroken(entity, uses):
177            fn = checker.findEntity(entity).filename
178            anchor = '[[{}]]'.format(entity)
179            locations = ', '.join((toNameAndLine(context, root_path=checker.root_path)
180                                   for context in uses))
181            return (fn, anchor, locations)
182        printTabulated((makeRowOfBroken(entity, uses)
183                        for entity, uses in sorted(broken.items())),
184                       headers=['Include File', 'Anchor in lieu of include', 'Links to this entity'])
185
186    def outputMissingIncludes(self, checker, missing):
187        """Output a table of missing includes.
188
189        Called by self.outputBrokenAndMissing() if requested.
190        """
191        missing = list(sorted(missing))
192        if not missing:
193            # Exit if none
194            return
195        print(
196            'Missing, but unreferenced, API includes/anchors - potentially not-documented entities:')
197
198        def makeRowOfMissing(entity):
199            fn = checker.findEntity(entity).filename
200            anchor = '[[{}]]'.format(entity)
201            return (fn, anchor)
202        printTabulated((makeRowOfMissing(entity) for entity in missing),
203                       headers=['Include File', 'Anchor in lieu of include'])
204
205    def outputMessage(self, msg):
206        """Output a Message, with highlighted range and replacement, if appropriate."""
207        highlightStart, highlightEnd = getHighlightedRange(msg.context)
208
209        if '\n' in msg.context.filename:
210            # This is a multi-line string "filename".
211            # Extra blank line and delimiter line for readability:
212            print()
213            print('--------------------------------------------------------------------')
214
215        fileAndLine = colored('{}:'.format(
216            self.formatBrief(msg.context)), attrs=['bold'])
217
218        headingSize = len('{context}: {mtype}: '.format(
219            context=self.formatBrief(msg.context),
220            mtype=self.formatBrief(msg.message_type, False)))
221        indent = ' ' * headingSize
222        printedHeading = False
223
224        lines = msg.message[:]
225        if msg.see_also:
226            lines.append('See also:')
227            lines.extend(('  {}'.format(self.formatBrief(see))
228                          for see in msg.see_also))
229
230        if msg.fix:
231            lines.append('Note: Auto-fix available')
232
233        for line in msg.message:
234            if not printedHeading:
235                scriptloc = ''
236                if msg.script_location and self.show_script_location:
237                    scriptloc = ', ' + msg.script_location
238                print('{fileLine} {mtype} {msg} (-{arg}{loc})'.format(
239                    fileLine=fileAndLine, mtype=msg.message_type.formattedWithColon(),
240                    msg=colored(line, attrs=['bold']), arg=msg.message_id.enable_arg(), loc=scriptloc))
241                printedHeading = True
242            else:
243                print(colored(indent + line, attrs=['bold']))
244
245        if len(msg.message) > 1:
246            # extra blank line after multiline message
247            print('')
248
249        start, end = getInterestedRange(msg.context)
250        printLineSubsetWithHighlighting(
251            msg.context.line,
252            start, end,
253            highlightStart, highlightEnd,
254            replacement=msg.replacement)
255
256    def outputFallback(self, obj):
257        """Output by calling print."""
258        print(obj)
259
260    ###
261    # Format methods: these all return a string.
262    def formatFilename(self, fn, _with_color=True):
263        """Format a local filename, as a relative path if possible."""
264        return self.getRelativeFilename(fn)
265
266    def formatMessageTypeBrief(self, message_type, with_color=True):
267        """Format a message type briefly, applying color if desired and possible.
268
269        Delegates to the superclass if not formatting with color.
270        """
271        if with_color:
272            return message_type.formattedWithColon()
273        return super(ConsolePrinter, self).formatMessageTypeBrief(
274            message_type, with_color)
275