main.py
1 """Provides a re-usable command-line interface to a MacroChecker.""" 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 20 import argparse 21 import logging 22 import re 23 from pathlib import Path 24 25 from .shared import MessageId 26 27 28 def checkerMain(default_enabled_messages, make_macro_checker, 29 all_docs, available_messages=None): 30 """Perform the bulk of the work for a command-line interface to a MacroChecker. 31 32 Arguments: 33 default_enabled_messages -- The MessageId values that should be enabled by default. 34 make_macro_checker -- A function that can be called with a set of enabled MessageId to create a 35 properly-configured MacroChecker. 36 all_docs -- A list of all spec documentation files. 37 available_messages -- a list of all MessageId values that can be generated for this project. 38 Defaults to every value. (e.g. some projects don't have MessageId.LEGACY) 39 """ 40 enabled_messages = set(default_enabled_messages) 41 if not available_messages: 42 available_messages = list(MessageId) 43 44 disable_args = [] 45 enable_args = [] 46 47 parser = argparse.ArgumentParser() 48 parser.add_argument( 49 "--scriptlocation", 50 help="Append the script location generated a message to the output.", 51 action="store_true") 52 parser.add_argument( 53 "--verbose", 54 "-v", 55 help="Output 'info'-level development logging messages.", 56 action="store_true") 57 parser.add_argument( 58 "--debug", 59 "-d", 60 help="Output 'debug'-level development logging messages (more verbose than -v).", 61 action="store_true") 62 parser.add_argument( 63 "-Werror", 64 "--warning_error", 65 help="Make warnings act as errors, exiting with non-zero error code", 66 action="store_true") 67 parser.add_argument( 68 "--include_warn", 69 help="List all expected but unseen include files, not just those that are referenced.", 70 action='store_true') 71 parser.add_argument( 72 "-Wmissing_refpages", 73 help="List all entities with expected but unseen ref page blocks. NOT included in -Wall!", 74 action='store_true') 75 parser.add_argument( 76 "--include_error", 77 help="Make expected but unseen include files cause exiting with non-zero error code", 78 action='store_true') 79 parser.add_argument( 80 "--broken_error", 81 help="Make missing include/anchor for linked-to entities cause exiting with non-zero error code. Weaker version of --include_error.", 82 action='store_true') 83 parser.add_argument( 84 "--dump_entities", 85 help="Just dump the parsed entity data to entities.json and exit.", 86 action='store_true') 87 parser.add_argument( 88 "--html", 89 help="Output messages to the named HTML file instead of stdout.") 90 parser.add_argument( 91 "file", 92 help="Only check the indicated file(s). By default, all chapters and extensions are checked.", 93 nargs="*") 94 parser.add_argument( 95 "--ignore_count", 96 type=int, 97 help="Ignore up to the given number of errors without exiting with a non-zero error code.") 98 parser.add_argument("-Wall", 99 help="Enable all warning categories.", 100 action='store_true') 101 102 for message_id in MessageId: 103 enable_arg = message_id.enable_arg() 104 enable_args.append((message_id, enable_arg)) 105 106 disable_arg = message_id.disable_arg() 107 disable_args.append((message_id, disable_arg)) 108 if message_id in enabled_messages: 109 parser.add_argument('-' + disable_arg, action="store_true", 110 help="Disable message category {}: {}".format(str(message_id), message_id.desc())) 111 # Don't show the enable flag in help since it's enabled by default 112 parser.add_argument('-' + enable_arg, action="store_true", 113 help=argparse.SUPPRESS) 114 else: 115 parser.add_argument('-' + enable_arg, action="store_true", 116 help="Enable message category {}: {}".format(str(message_id), message_id.desc())) 117 # Don't show the disable flag in help since it's disabled by 118 # default 119 parser.add_argument('-' + disable_arg, action="store_true", 120 help=argparse.SUPPRESS) 121 122 args = parser.parse_args() 123 124 arg_dict = vars(args) 125 for message_id, arg in enable_args: 126 if args.Wall or (arg in arg_dict and arg_dict[arg]): 127 enabled_messages.add(message_id) 128 129 for message_id, arg in disable_args: 130 if arg in arg_dict and arg_dict[arg]: 131 enabled_messages.discard(message_id) 132 133 if args.verbose: 134 logging.basicConfig(level='INFO') 135 136 if args.debug: 137 logging.basicConfig(level='DEBUG') 138 139 checker = make_macro_checker(enabled_messages) 140 141 if args.dump_entities: 142 with open('entities.json', 'w', encoding='utf-8') as f: 143 f.write(checker.getEntityJson()) 144 exit(0) 145 146 if args.file: 147 files = (str(Path(f).resolve()) for f in args.file) 148 else: 149 files = all_docs 150 151 for fn in files: 152 checker.processFile(fn) 153 154 if args.html: 155 from .html_printer import HTMLPrinter 156 printer = HTMLPrinter(args.html) 157 else: 158 from .console_printer import ConsolePrinter 159 printer = ConsolePrinter() 160 161 if args.scriptlocation: 162 printer.show_script_location = True 163 164 if args.file: 165 printer.output("Only checked specified files.") 166 for f in args.file: 167 printer.output(f) 168 else: 169 printer.output("Checked all chapters and extensions.") 170 171 if args.warning_error: 172 numErrors = checker.numDiagnostics() 173 else: 174 numErrors = checker.numErrors() 175 176 check_includes = args.include_warn 177 check_broken = not args.file 178 179 if args.file and check_includes: 180 print('Note: forcing --include_warn off because only checking supplied files.') 181 check_includes = False 182 183 printer.outputResults(checker, broken_links=(not args.file), 184 missing_includes=check_includes) 185 186 if check_broken: 187 numErrors += len(checker.getBrokenLinks()) 188 189 if args.file and args.include_error: 190 print('Note: forcing --include_error off because only checking supplied files.') 191 args.include_error = False 192 if args.include_error: 193 numErrors += len(checker.getMissingUnreferencedApiIncludes()) 194 195 check_missing_refpages = args.Wmissing_refpages 196 if args.file and check_missing_refpages: 197 print('Note: forcing -Wmissing_refpages off because only checking supplied files.') 198 check_missing_refpages = False 199 200 if check_missing_refpages: 201 missing = checker.getMissingRefPages() 202 if missing: 203 printer.output("Expected, but did not find, ref page blocks for the following {} entities: {}".format( 204 len(missing), 205 ', '.join(missing) 206 )) 207 if args.warning_error: 208 numErrors += len(missing) 209 210 printer.close() 211 212 if args.broken_error and not args.file: 213 numErrors += len(checker.getBrokenLinks()) 214 215 if checker.hasFixes(): 216 fixFn = 'applyfixes.sh' 217 print('Saving shell script to apply fixes as {}'.format(fixFn)) 218 with open(fixFn, 'w', encoding='utf-8') as f: 219 f.write('#!/bin/sh -e\n') 220 for fileChecker in checker.files: 221 wroteComment = False 222 for msg in fileChecker.messages: 223 if msg.fix is not None: 224 if not wroteComment: 225 f.write('\n# {}\n'.format(fileChecker.filename)) 226 wroteComment = True 227 search, replace = msg.fix 228 f.write( 229 r"sed -i -r 's~\b{}\b~{}~g' {}".format( 230 re.escape(search), 231 replace, 232 fileChecker.filename)) 233 f.write('\n') 234 235 print('Total number of errors with this run: {}'.format(numErrors)) 236 237 if args.ignore_count: 238 if numErrors > args.ignore_count: 239 # Exit with non-zero error code so that we "fail" CI, etc. 240 print('Exceeded specified limit of {}, so exiting with error'.format( 241 args.ignore_count)) 242 exit(1) 243 else: 244 print('At or below specified limit of {}, so exiting with success'.format( 245 args.ignore_count)) 246 exit(0) 247 248 if numErrors: 249 # Exit with non-zero error code so that we "fail" CI, etc. 250 print('Exiting with error') 251 exit(1) 252 else: 253 print('Exiting with success') 254 exit(0)