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