1# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org) 2# 3# Redistribution and use in source and binary forms, with or without 4# modification, are permitted provided that the following conditions 5# are met: 6# 1. Redistributions of source code must retain the above copyright 7# notice, this list of conditions and the following disclaimer. 8# 2. Redistributions in binary form must reproduce the above copyright 9# notice, this list of conditions and the following disclaimer in the 10# documentation and/or other materials provided with the distribution. 11# 12# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY 13# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY 16# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 23"""Supports the parsing of command-line options for check-webkit-style.""" 24 25import logging 26from optparse import OptionParser 27import os.path 28import sys 29 30from filter import validate_filter_rules 31# This module should not import anything from checker.py. 32 33_log = logging.getLogger(__name__) 34 35_USAGE = """usage: %prog [--help] [options] [path1] [path2] ... 36 37Overview: 38 Check coding style according to WebKit style guidelines: 39 40 http://webkit.org/coding/coding-style.html 41 42 Path arguments can be files and directories. If neither a git commit nor 43 paths are passed, then all changes in your source control working directory 44 are checked. 45 46Style errors: 47 This script assigns to every style error a confidence score from 1-5 and 48 a category name. A confidence score of 5 means the error is certainly 49 a problem, and 1 means it could be fine. 50 51 Category names appear in error messages in brackets, for example 52 [whitespace/indent]. See the options section below for an option that 53 displays all available categories and which are reported by default. 54 55Filters: 56 Use filters to configure what errors to report. Filters are specified using 57 a comma-separated list of boolean filter rules. The script reports errors 58 in a category if the category passes the filter, as described below. 59 60 All categories start out passing. Boolean filter rules are then evaluated 61 from left to right, with later rules taking precedence. For example, the 62 rule "+foo" passes any category that starts with "foo", and "-foo" fails 63 any such category. The filter input "-whitespace,+whitespace/braces" fails 64 the category "whitespace/tab" and passes "whitespace/braces". 65 66 Examples: --filter=-whitespace,+whitespace/braces 67 --filter=-whitespace,-runtime/printf,+runtime/printf_format 68 --filter=-,+build/include_what_you_use 69 70Paths: 71 Certain style-checking behavior depends on the paths relative to 72 the WebKit source root of the files being checked. For example, 73 certain types of errors may be handled differently for files in 74 WebKit/gtk/webkit/ (e.g. by suppressing "readability/naming" errors 75 for files in this directory). 76 77 Consequently, if the path relative to the source root cannot be 78 determined for a file being checked, then style checking may not 79 work correctly for that file. This can occur, for example, if no 80 WebKit checkout can be found, or if the source root can be detected, 81 but one of the files being checked lies outside the source tree. 82 83 If a WebKit checkout can be detected and all files being checked 84 are in the source tree, then all paths will automatically be 85 converted to paths relative to the source root prior to checking. 86 This is also useful for display purposes. 87 88 Currently, this command can detect the source root only if the 89 command is run from within a WebKit checkout (i.e. if the current 90 working directory is below the root of a checkout). In particular, 91 it is not recommended to run this script from a directory outside 92 a checkout. 93 94 Running this script from a top-level WebKit source directory and 95 checking only files in the source tree will ensure that all style 96 checking behaves correctly -- whether or not a checkout can be 97 detected. This is because all file paths will already be relative 98 to the source root and so will not need to be converted.""" 99 100_EPILOG = ("This script can miss errors and does not substitute for " 101 "code review.") 102 103 104# This class should not have knowledge of the flag key names. 105class DefaultCommandOptionValues(object): 106 107 """Stores the default check-webkit-style command-line options. 108 109 Attributes: 110 output_format: A string that is the default output format. 111 min_confidence: An integer that is the default minimum confidence level. 112 113 """ 114 115 def __init__(self, min_confidence, output_format): 116 self.min_confidence = min_confidence 117 self.output_format = output_format 118 119 120# This class should not have knowledge of the flag key names. 121class CommandOptionValues(object): 122 123 """Stores the option values passed by the user via the command line. 124 125 Attributes: 126 is_verbose: A boolean value of whether verbose logging is enabled. 127 128 filter_rules: The list of filter rules provided by the user. 129 These rules are appended to the base rules and 130 path-specific rules and so take precedence over 131 the base filter rules, etc. 132 133 git_commit: A string representing the git commit to check. 134 The default is None. 135 136 min_confidence: An integer between 1 and 5 inclusive that is the 137 minimum confidence level of style errors to report. 138 The default is 1, which reports all errors. 139 140 output_format: A string that is the output format. The supported 141 output formats are "emacs" which emacs can parse 142 and "vs7" which Microsoft Visual Studio 7 can parse. 143 144 """ 145 def __init__(self, 146 filter_rules=None, 147 git_commit=None, 148 diff_files=None, 149 is_verbose=False, 150 min_confidence=1, 151 output_format="emacs"): 152 if filter_rules is None: 153 filter_rules = [] 154 155 if (min_confidence < 1) or (min_confidence > 5): 156 raise ValueError('Invalid "min_confidence" parameter: value ' 157 "must be an integer between 1 and 5 inclusive. " 158 'Value given: "%s".' % min_confidence) 159 160 if output_format not in ("emacs", "vs7"): 161 raise ValueError('Invalid "output_format" parameter: ' 162 'value must be "emacs" or "vs7". ' 163 'Value given: "%s".' % output_format) 164 165 self.filter_rules = filter_rules 166 self.git_commit = git_commit 167 self.diff_files = diff_files 168 self.is_verbose = is_verbose 169 self.min_confidence = min_confidence 170 self.output_format = output_format 171 172 # Useful for unit testing. 173 def __eq__(self, other): 174 """Return whether this instance is equal to another.""" 175 if self.filter_rules != other.filter_rules: 176 return False 177 if self.git_commit != other.git_commit: 178 return False 179 if self.diff_files != other.diff_files: 180 return False 181 if self.is_verbose != other.is_verbose: 182 return False 183 if self.min_confidence != other.min_confidence: 184 return False 185 if self.output_format != other.output_format: 186 return False 187 188 return True 189 190 # Useful for unit testing. 191 def __ne__(self, other): 192 # Python does not automatically deduce this from __eq__(). 193 return not self.__eq__(other) 194 195 196class ArgumentPrinter(object): 197 198 """Supports the printing of check-webkit-style command arguments.""" 199 200 def _flag_pair_to_string(self, flag_key, flag_value): 201 return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value } 202 203 def to_flag_string(self, options): 204 """Return a flag string of the given CommandOptionValues instance. 205 206 This method orders the flag values alphabetically by the flag key. 207 208 Args: 209 options: A CommandOptionValues instance. 210 211 """ 212 flags = {} 213 flags['min-confidence'] = options.min_confidence 214 flags['output'] = options.output_format 215 # Only include the filter flag if user-provided rules are present. 216 filter_rules = options.filter_rules 217 if filter_rules: 218 flags['filter'] = ",".join(filter_rules) 219 if options.git_commit: 220 flags['git-commit'] = options.git_commit 221 if options.diff_files: 222 flags['diff_files'] = options.diff_files 223 224 flag_string = '' 225 # Alphabetizing lets us unit test this method. 226 for key in sorted(flags.keys()): 227 flag_string += self._flag_pair_to_string(key, flags[key]) + ' ' 228 229 return flag_string.strip() 230 231 232class ArgumentParser(object): 233 234 # FIXME: Move the documentation of the attributes to the __init__ 235 # docstring after making the attributes internal. 236 """Supports the parsing of check-webkit-style command arguments. 237 238 Attributes: 239 create_usage: A function that accepts a DefaultCommandOptionValues 240 instance and returns a string of usage instructions. 241 Defaults to the function that generates the usage 242 string for check-webkit-style. 243 default_options: A DefaultCommandOptionValues instance that provides 244 the default values for options not explicitly 245 provided by the user. 246 stderr_write: A function that takes a string as a parameter and 247 serves as stderr.write. Defaults to sys.stderr.write. 248 This parameter should be specified only for unit tests. 249 250 """ 251 252 def __init__(self, 253 all_categories, 254 default_options, 255 base_filter_rules=None, 256 mock_stderr=None, 257 usage=None): 258 """Create an ArgumentParser instance. 259 260 Args: 261 all_categories: The set of all available style categories. 262 default_options: See the corresponding attribute in the class 263 docstring. 264 Keyword Args: 265 base_filter_rules: The list of filter rules at the beginning of 266 the list of rules used to check style. This 267 list has the least precedence when checking 268 style and precedes any user-provided rules. 269 The class uses this parameter only for display 270 purposes to the user. Defaults to the empty list. 271 create_usage: See the documentation of the corresponding 272 attribute in the class docstring. 273 stderr_write: See the documentation of the corresponding 274 attribute in the class docstring. 275 276 """ 277 if base_filter_rules is None: 278 base_filter_rules = [] 279 stderr = sys.stderr if mock_stderr is None else mock_stderr 280 if usage is None: 281 usage = _USAGE 282 283 self._all_categories = all_categories 284 self._base_filter_rules = base_filter_rules 285 286 # FIXME: Rename these to reflect that they are internal. 287 self.default_options = default_options 288 self.stderr_write = stderr.write 289 290 self._parser = self._create_option_parser(stderr=stderr, 291 usage=usage, 292 default_min_confidence=self.default_options.min_confidence, 293 default_output_format=self.default_options.output_format) 294 295 def _create_option_parser(self, stderr, usage, 296 default_min_confidence, default_output_format): 297 # Since the epilog string is short, it is not necessary to replace 298 # the epilog string with a mock epilog string when testing. 299 # For this reason, we use _EPILOG directly rather than passing it 300 # as an argument like we do for the usage string. 301 parser = OptionParser(usage=usage, epilog=_EPILOG) 302 303 filter_help = ('set a filter to control what categories of style ' 304 'errors to report. Specify a filter using a comma-' 305 'delimited list of boolean filter rules, for example ' 306 '"--filter -whitespace,+whitespace/braces". To display ' 307 'all categories and which are enabled by default, pass ' 308 """no value (e.g. '-f ""' or '--filter=').""") 309 parser.add_option("-f", "--filter-rules", metavar="RULES", 310 dest="filter_value", help=filter_help) 311 312 git_commit_help = ("check all changes in the given commit. " 313 "Use 'commit_id..' to check all changes after commmit_id") 314 parser.add_option("-g", "--git-diff", "--git-commit", 315 metavar="COMMIT", dest="git_commit", help=git_commit_help,) 316 317 diff_files_help = "diff the files passed on the command line rather than checking the style of every line" 318 parser.add_option("--diff-files", action="store_true", dest="diff_files", default=False, help=diff_files_help) 319 320 min_confidence_help = ("set the minimum confidence of style errors " 321 "to report. Can be an integer 1-5, with 1 " 322 "displaying all errors. Defaults to %default.") 323 parser.add_option("-m", "--min-confidence", metavar="INT", 324 type="int", dest="min_confidence", 325 default=default_min_confidence, 326 help=min_confidence_help) 327 328 output_format_help = ('set the output format, which can be "emacs" ' 329 'or "vs7" (for Visual Studio). ' 330 'Defaults to "%default".') 331 parser.add_option("-o", "--output-format", metavar="FORMAT", 332 choices=["emacs", "vs7"], 333 dest="output_format", default=default_output_format, 334 help=output_format_help) 335 336 verbose_help = "enable verbose logging." 337 parser.add_option("-v", "--verbose", dest="is_verbose", default=False, 338 action="store_true", help=verbose_help) 339 340 # Override OptionParser's error() method so that option help will 341 # also display when an error occurs. Normally, just the usage 342 # string displays and not option help. 343 parser.error = self._parse_error 344 345 # Override OptionParser's print_help() method so that help output 346 # does not render to the screen while running unit tests. 347 print_help = parser.print_help 348 parser.print_help = lambda: print_help(file=stderr) 349 350 return parser 351 352 def _parse_error(self, error_message): 353 """Print the help string and an error message, and exit.""" 354 # The method format_help() includes both the usage string and 355 # the flag options. 356 help = self._parser.format_help() 357 # Separate help from the error message with a single blank line. 358 self.stderr_write(help + "\n") 359 if error_message: 360 _log.error(error_message) 361 362 # Since we are using this method to replace/override the Python 363 # module optparse's OptionParser.error() method, we match its 364 # behavior and exit with status code 2. 365 # 366 # As additional background, Python documentation says-- 367 # 368 # "Unix programs generally use 2 for command line syntax errors 369 # and 1 for all other kind of errors." 370 # 371 # (from http://docs.python.org/library/sys.html#sys.exit ) 372 sys.exit(2) 373 374 def _exit_with_categories(self): 375 """Exit and print the style categories and default filter rules.""" 376 self.stderr_write('\nAll categories:\n') 377 for category in sorted(self._all_categories): 378 self.stderr_write(' ' + category + '\n') 379 380 self.stderr_write('\nDefault filter rules**:\n') 381 for filter_rule in sorted(self._base_filter_rules): 382 self.stderr_write(' ' + filter_rule + '\n') 383 self.stderr_write('\n**The command always evaluates the above rules, ' 384 'and before any --filter flag.\n\n') 385 386 sys.exit(0) 387 388 def _parse_filter_flag(self, flag_value): 389 """Parse the --filter flag, and return a list of filter rules. 390 391 Args: 392 flag_value: A string of comma-separated filter rules, for 393 example "-whitespace,+whitespace/indent". 394 395 """ 396 filters = [] 397 for uncleaned_filter in flag_value.split(','): 398 filter = uncleaned_filter.strip() 399 if not filter: 400 continue 401 filters.append(filter) 402 return filters 403 404 def parse(self, args): 405 """Parse the command line arguments to check-webkit-style. 406 407 Args: 408 args: A list of command-line arguments as returned by sys.argv[1:]. 409 410 Returns: 411 A tuple of (paths, options) 412 413 paths: The list of paths to check. 414 options: A CommandOptionValues instance. 415 416 """ 417 (options, paths) = self._parser.parse_args(args=args) 418 419 filter_value = options.filter_value 420 git_commit = options.git_commit 421 diff_files = options.diff_files 422 is_verbose = options.is_verbose 423 min_confidence = options.min_confidence 424 output_format = options.output_format 425 426 if filter_value is not None and not filter_value: 427 # Then the user explicitly passed no filter, for 428 # example "-f ''" or "--filter=". 429 self._exit_with_categories() 430 431 # Validate user-provided values. 432 433 min_confidence = int(min_confidence) 434 if (min_confidence < 1) or (min_confidence > 5): 435 self._parse_error('option --min-confidence: invalid integer: ' 436 '%s: value must be between 1 and 5' 437 % min_confidence) 438 439 if filter_value: 440 filter_rules = self._parse_filter_flag(filter_value) 441 else: 442 filter_rules = [] 443 444 try: 445 validate_filter_rules(filter_rules, self._all_categories) 446 except ValueError, err: 447 self._parse_error(err) 448 449 options = CommandOptionValues(filter_rules=filter_rules, 450 git_commit=git_commit, 451 diff_files=diff_files, 452 is_verbose=is_verbose, 453 min_confidence=min_confidence, 454 output_format=output_format) 455 456 return (paths, options) 457 458