248 lines
9.0 KiB
Python
248 lines
9.0 KiB
Python
|
"""Provides a re-usable command-line interface to a MacroChecker."""
|
||
|
|
||
|
# Copyright (c) 2018-2019 Collabora, Ltd.
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
#
|
||
|
# 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(
|
||
|
"--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.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)
|