1"""Provides the BasePrinter base class for MacroChecker/Message output techniques.""" 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 abc import ABC, abstractmethod 10from pathlib import Path 11 12from .macro_checker import MacroChecker 13from .macro_checker_file import MacroCheckerFile 14from .shared import EntityData, Message, MessageContext, MessageType 15 16 17def getColumn(message_context): 18 """Return the (zero-based) column number of the message context. 19 20 If a group is specified: returns the column of the start of the group. 21 If no group, but a match is specified: returns the column of the start of 22 the match. 23 If no match: returns column 0 (whole line). 24 """ 25 if not message_context.match: 26 # whole line 27 return 0 28 if message_context.group is not None: 29 return message_context.match.start(message_context.group) 30 return message_context.match.start() 31 32 33class BasePrinter(ABC): 34 """Base class for a way of outputting results of a checker execution.""" 35 36 def __init__(self): 37 """Constructor.""" 38 self._cwd = None 39 40 def close(self): 41 """Write the tail end of the output and close it, if applicable. 42 43 Override if you want to print a summary or are writing to a file. 44 """ 45 pass 46 47 ### 48 # Output methods: these should all print/output directly. 49 def output(self, obj): 50 """Output any object. 51 52 Delegates to other output* methods, if type known, 53 otherwise uses self.outputFallback(). 54 """ 55 if isinstance(obj, Message): 56 self.outputMessage(obj) 57 elif isinstance(obj, MacroCheckerFile): 58 self.outputCheckerFile(obj) 59 elif isinstance(obj, MacroChecker): 60 self.outputChecker(obj) 61 else: 62 self.outputFallback(self.formatBrief(obj)) 63 64 @abstractmethod 65 def outputResults(self, checker, broken_links=True, 66 missing_includes=False): 67 """Output the full results of a checker run. 68 69 Must be implemented. 70 71 Typically will call self.output() on the MacroChecker, 72 as well as calling self.outputBrokenAndMissing() 73 """ 74 raise NotImplementedError 75 76 @abstractmethod 77 def outputBrokenLinks(self, checker, broken): 78 """Output the collection of broken links. 79 80 `broken` is a dictionary of entity names: usage contexts. 81 82 Must be implemented. 83 84 Called by self.outputBrokenAndMissing() if requested. 85 """ 86 raise NotImplementedError 87 88 @abstractmethod 89 def outputMissingIncludes(self, checker, missing): 90 """Output a table of missing includes. 91 92 `missing` is a iterable entity names. 93 94 Must be implemented. 95 96 Called by self.outputBrokenAndMissing() if requested. 97 """ 98 raise NotImplementedError 99 100 def outputChecker(self, checker): 101 """Output the contents of a MacroChecker object. 102 103 Default implementation calls self.output() on every MacroCheckerFile. 104 """ 105 for f in checker.files: 106 self.output(f) 107 108 def outputCheckerFile(self, fileChecker): 109 """Output the contents of a MacroCheckerFile object. 110 111 Default implementation calls self.output() on every Message. 112 """ 113 for m in fileChecker.messages: 114 self.output(m) 115 116 def outputBrokenAndMissing(self, checker, broken_links=True, 117 missing_includes=False): 118 """Outputs broken links and missing includes, if desired. 119 120 Delegates to self.outputBrokenLinks() (if broken_links==True) 121 and self.outputMissingIncludes() (if missing_includes==True). 122 """ 123 if broken_links: 124 broken = checker.getBrokenLinks() 125 if broken: 126 self.outputBrokenLinks(checker, broken) 127 if missing_includes: 128 missing = checker.getMissingUnreferencedApiIncludes() 129 if missing: 130 self.outputMissingIncludes(checker, missing) 131 132 @abstractmethod 133 def outputMessage(self, msg): 134 """Output a Message. 135 136 Must be implemented. 137 """ 138 raise NotImplementedError 139 140 @abstractmethod 141 def outputFallback(self, msg): 142 """Output some text in a general way. 143 144 Must be implemented. 145 """ 146 raise NotImplementedError 147 148 ### 149 # Format methods: these should all return a string. 150 def formatContext(self, context, _message_type=None): 151 """Format a message context in a verbose way, if applicable. 152 153 May override, default implementation delegates to 154 self.formatContextBrief(). 155 """ 156 return self.formatContextBrief(context) 157 158 def formatContextBrief(self, context, _with_color=True): 159 """Format a message context in a brief way. 160 161 May override, default is relativeFilename:line:column 162 """ 163 return '{}:{}:{}'.format(self.getRelativeFilename(context.filename), 164 context.lineNum, getColumn(context)) 165 166 def formatMessageTypeBrief(self, message_type, _with_color=True): 167 """Format a message type in a brief way. 168 169 May override, default is message_type: 170 """ 171 return '{}:'.format(message_type) 172 173 def formatEntityBrief(self, entity_data, _with_color=True): 174 """Format an entity in a brief way. 175 176 May override, default is macro:entity. 177 """ 178 return '{}:{}'.format(entity_data.macro, entity_data.entity) 179 180 def formatBrief(self, obj, with_color=True): 181 """Format any object in a brief way. 182 183 Delegates to other format*Brief methods, if known, 184 otherwise uses str(). 185 """ 186 if isinstance(obj, MessageContext): 187 return self.formatContextBrief(obj, with_color) 188 if isinstance(obj, MessageType): 189 return self.formatMessageTypeBrief(obj, with_color) 190 if isinstance(obj, EntityData): 191 return self.formatEntityBrief(obj, with_color) 192 return str(obj) 193 194 @property 195 def cwd(self): 196 """Get the current working directory, fully resolved. 197 198 Lazy initialized. 199 """ 200 if not self._cwd: 201 self._cwd = Path('.').resolve() 202 return self._cwd 203 204 ### 205 # Helper function 206 def getRelativeFilename(self, fn): 207 """Return the given filename relative to the current directory, 208 if possible. 209 """ 210 try: 211 return str(Path(fn).relative_to(self.cwd)) 212 except ValueError: 213 return str(Path(fn)) 214