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