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