blob: 2cd4f69c0442769440ffe859ad4407f4c1a0a45e [file] [log] [blame]
"""Provides a re-usable command-line interface to a MacroChecker."""
# Copyright (c) 2018-2019 Collabora, Ltd.
#
# SPDX-License-Identifier: Apache-2.0
#
# Author(s): Ryan Pavlik <ryan.pavlik@collabora.com>
import argparse
import logging
import re
from pathlib import Path
from .shared import MessageId
def checkerMain(default_enabled_messages, make_macro_checker,
all_docs, available_messages=None):
"""Perform the bulk of the work for a command-line interface to a MacroChecker.
Arguments:
default_enabled_messages -- The MessageId values that should be enabled by default.
make_macro_checker -- A function that can be called with a set of enabled MessageId to create a
properly-configured MacroChecker.
all_docs -- A list of all spec documentation files.
available_messages -- a list of all MessageId values that can be generated for this project.
Defaults to every value. (e.g. some projects don't have MessageId.LEGACY)
"""
enabled_messages = set(default_enabled_messages)
if not available_messages:
available_messages = list(MessageId)
disable_args = []
enable_args = []
parser = argparse.ArgumentParser()
parser.add_argument(
"--scriptlocation",
help="Append the script location generated a message to the output.",
action="store_true")
parser.add_argument(
"--verbose",
"-v",
help="Output 'info'-level development logging messages.",
action="store_true")
parser.add_argument(
"--debug",
"-d",
help="Output 'debug'-level development logging messages (more verbose than -v).",
action="store_true")
parser.add_argument(
"-Werror",
"--warning_error",
help="Make warnings act as errors, exiting with non-zero error code",
action="store_true")
parser.add_argument(
"--include_warn",
help="List all expected but unseen include files, not just those that are referenced.",
action='store_true')
parser.add_argument(
"-Wmissing_refpages",
help="List all entities with expected but unseen ref page blocks. NOT included in -Wall!",
action='store_true')
parser.add_argument(
"--include_error",
help="Make expected but unseen include files cause exiting with non-zero error code",
action='store_true')
parser.add_argument(
"--broken_error",
help="Make missing include/anchor for linked-to entities cause exiting with non-zero error code. Weaker version of --include_error.",
action='store_true')
parser.add_argument(
"--dump_entities",
help="Just dump the parsed entity data to entities.json and exit.",
action='store_true')
parser.add_argument(
"--html",
help="Output messages to the named HTML file instead of stdout.")
parser.add_argument(
"file",
help="Only check the indicated file(s). By default, all chapters and extensions are checked.",
nargs="*")
parser.add_argument(
"--ignore_count",
type=int,
help="Ignore up to the given number of errors without exiting with a non-zero error code.")
parser.add_argument("-Wall",
help="Enable all warning categories.",
action='store_true')
for message_id in MessageId:
enable_arg = message_id.enable_arg()
enable_args.append((message_id, enable_arg))
disable_arg = message_id.disable_arg()
disable_args.append((message_id, disable_arg))
if message_id in enabled_messages:
parser.add_argument('-' + disable_arg, action="store_true",
help="Disable message category {}: {}".format(str(message_id), message_id.desc()))
# Don't show the enable flag in help since it's enabled by default
parser.add_argument('-' + enable_arg, action="store_true",
help=argparse.SUPPRESS)
else:
parser.add_argument('-' + enable_arg, action="store_true",
help="Enable message category {}: {}".format(str(message_id), message_id.desc()))
# Don't show the disable flag in help since it's disabled by
# default
parser.add_argument('-' + disable_arg, action="store_true",
help=argparse.SUPPRESS)
args = parser.parse_args()
arg_dict = vars(args)
for message_id, arg in enable_args:
if args.Wall or (arg in arg_dict and arg_dict[arg]):
enabled_messages.add(message_id)
for message_id, arg in disable_args:
if arg in arg_dict and arg_dict[arg]:
enabled_messages.discard(message_id)
if args.verbose:
logging.basicConfig(level='INFO')
if args.debug:
logging.basicConfig(level='DEBUG')
checker = make_macro_checker(enabled_messages)
if args.dump_entities:
with open('entities.json', 'w', encoding='utf-8') as f:
f.write(checker.getEntityJson())
exit(0)
if args.file:
files = (str(Path(f).resolve()) for f in args.file)
else:
files = all_docs
for fn in files:
checker.processFile(fn)
if args.html:
from .html_printer import HTMLPrinter
printer = HTMLPrinter(args.html)
else:
from .console_printer import ConsolePrinter
printer = ConsolePrinter()
if args.scriptlocation:
printer.show_script_location = True
if args.file:
printer.output("Only checked specified files.")
for f in args.file:
printer.output(f)
else:
printer.output("Checked all chapters and extensions.")
if args.warning_error:
numErrors = checker.numDiagnostics()
else:
numErrors = checker.numErrors()
check_includes = args.include_warn
check_broken = not args.file
if args.file and check_includes:
print('Note: forcing --include_warn off because only checking supplied files.')
check_includes = False
printer.outputResults(checker, broken_links=(not args.file),
missing_includes=check_includes)
if check_broken:
numErrors += len(checker.getBrokenLinks())
if args.file and args.include_error:
print('Note: forcing --include_error off because only checking supplied files.')
args.include_error = False
if args.include_error:
numErrors += len(checker.getMissingUnreferencedApiIncludes())
check_missing_refpages = args.Wmissing_refpages
if args.file and check_missing_refpages:
print('Note: forcing -Wmissing_refpages off because only checking supplied files.')
check_missing_refpages = False
if check_missing_refpages:
missing = checker.getMissingRefPages()
if missing:
printer.output("Expected, but did not find, ref page blocks for the following {} entities: {}".format(
len(missing),
', '.join(missing)
))
if args.warning_error:
numErrors += len(missing)
printer.close()
if args.broken_error and not args.file:
numErrors += len(checker.getBrokenLinks())
if checker.hasFixes():
fixFn = 'applyfixes.sh'
print('Saving shell script to apply fixes as {}'.format(fixFn))
with open(fixFn, 'w', encoding='utf-8') as f:
f.write('#!/bin/sh -e\n')
for fileChecker in checker.files:
wroteComment = False
for msg in fileChecker.messages:
if msg.fix is not None:
if not wroteComment:
f.write('\n# {}\n'.format(fileChecker.filename))
wroteComment = True
search, replace = msg.fix
f.write(
r"sed -i -r 's~\b{}\b~{}~g' {}".format(
re.escape(search),
replace,
fileChecker.filename))
f.write('\n')
print('Total number of errors with this run: {}'.format(numErrors))
if args.ignore_count:
if numErrors > args.ignore_count:
# Exit with non-zero error code so that we "fail" CI, etc.
print('Exceeded specified limit of {}, so exiting with error'.format(
args.ignore_count))
exit(1)
else:
print('At or below specified limit of {}, so exiting with success'.format(
args.ignore_count))
exit(0)
if numErrors:
# Exit with non-zero error code so that we "fail" CI, etc.
print('Exiting with error')
exit(1)
else:
print('Exiting with success')
exit(0)