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