• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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