# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org) # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Supports the parsing of command-line options for check-webkit-style.""" import logging from optparse import OptionParser import os.path import sys from filter import validate_filter_rules # This module should not import anything from checker.py. _log = logging.getLogger(__name__) _USAGE = """usage: %prog [--help] [options] [path1] [path2] ... Overview: Check coding style according to WebKit style guidelines: http://webkit.org/coding/coding-style.html Path arguments can be files and directories. If neither a git commit nor paths are passed, then all changes in your source control working directory are checked. Style errors: This script assigns to every style error a confidence score from 1-5 and a category name. A confidence score of 5 means the error is certainly a problem, and 1 means it could be fine. Category names appear in error messages in brackets, for example [whitespace/indent]. See the options section below for an option that displays all available categories and which are reported by default. Filters: Use filters to configure what errors to report. Filters are specified using a comma-separated list of boolean filter rules. The script reports errors in a category if the category passes the filter, as described below. All categories start out passing. Boolean filter rules are then evaluated from left to right, with later rules taking precedence. For example, the rule "+foo" passes any category that starts with "foo", and "-foo" fails any such category. The filter input "-whitespace,+whitespace/braces" fails the category "whitespace/tab" and passes "whitespace/braces". Examples: --filter=-whitespace,+whitespace/braces --filter=-whitespace,-runtime/printf,+runtime/printf_format --filter=-,+build/include_what_you_use Paths: Certain style-checking behavior depends on the paths relative to the WebKit source root of the files being checked. For example, certain types of errors may be handled differently for files in WebKit/gtk/webkit/ (e.g. by suppressing "readability/naming" errors for files in this directory). Consequently, if the path relative to the source root cannot be determined for a file being checked, then style checking may not work correctly for that file. This can occur, for example, if no WebKit checkout can be found, or if the source root can be detected, but one of the files being checked lies outside the source tree. If a WebKit checkout can be detected and all files being checked are in the source tree, then all paths will automatically be converted to paths relative to the source root prior to checking. This is also useful for display purposes. Currently, this command can detect the source root only if the command is run from within a WebKit checkout (i.e. if the current working directory is below the root of a checkout). In particular, it is not recommended to run this script from a directory outside a checkout. Running this script from a top-level WebKit source directory and checking only files in the source tree will ensure that all style checking behaves correctly -- whether or not a checkout can be detected. This is because all file paths will already be relative to the source root and so will not need to be converted.""" _EPILOG = ("This script can miss errors and does not substitute for " "code review.") # This class should not have knowledge of the flag key names. class DefaultCommandOptionValues(object): """Stores the default check-webkit-style command-line options. Attributes: output_format: A string that is the default output format. min_confidence: An integer that is the default minimum confidence level. """ def __init__(self, min_confidence, output_format): self.min_confidence = min_confidence self.output_format = output_format # This class should not have knowledge of the flag key names. class CommandOptionValues(object): """Stores the option values passed by the user via the command line. Attributes: is_verbose: A boolean value of whether verbose logging is enabled. filter_rules: The list of filter rules provided by the user. These rules are appended to the base rules and path-specific rules and so take precedence over the base filter rules, etc. git_commit: A string representing the git commit to check. The default is None. min_confidence: An integer between 1 and 5 inclusive that is the minimum confidence level of style errors to report. The default is 1, which reports all errors. output_format: A string that is the output format. The supported output formats are "emacs" which emacs can parse and "vs7" which Microsoft Visual Studio 7 can parse. """ def __init__(self, filter_rules=None, git_commit=None, is_verbose=False, min_confidence=1, output_format="emacs", squash=False): if filter_rules is None: filter_rules = [] if (min_confidence < 1) or (min_confidence > 5): raise ValueError('Invalid "min_confidence" parameter: value ' "must be an integer between 1 and 5 inclusive. " 'Value given: "%s".' % min_confidence) if output_format not in ("emacs", "vs7"): raise ValueError('Invalid "output_format" parameter: ' 'value must be "emacs" or "vs7". ' 'Value given: "%s".' % output_format) self.filter_rules = filter_rules self.git_commit = git_commit self.is_verbose = is_verbose self.min_confidence = min_confidence self.output_format = output_format self.squash = squash # Useful for unit testing. def __eq__(self, other): """Return whether this instance is equal to another.""" if self.filter_rules != other.filter_rules: return False if self.git_commit != other.git_commit: return False if self.is_verbose != other.is_verbose: return False if self.min_confidence != other.min_confidence: return False if self.output_format != other.output_format: return False if self.squash != other.squash: return False return True # Useful for unit testing. def __ne__(self, other): # Python does not automatically deduce this from __eq__(). return not self.__eq__(other) class ArgumentPrinter(object): """Supports the printing of check-webkit-style command arguments.""" def _flag_pair_to_string(self, flag_key, flag_value): return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value } def to_flag_string(self, options): """Return a flag string of the given CommandOptionValues instance. This method orders the flag values alphabetically by the flag key. Args: options: A CommandOptionValues instance. """ flags = {} flags['min-confidence'] = options.min_confidence flags['output'] = options.output_format # Only include the filter flag if user-provided rules are present. filter_rules = options.filter_rules if filter_rules: flags['filter'] = ",".join(filter_rules) if options.git_commit: flags['git-commit'] = options.git_commit if options.squash: flags['squash'] = options.squash flag_string = '' # Alphabetizing lets us unit test this method. for key in sorted(flags.keys()): flag_string += self._flag_pair_to_string(key, flags[key]) + ' ' return flag_string.strip() class ArgumentParser(object): # FIXME: Move the documentation of the attributes to the __init__ # docstring after making the attributes internal. """Supports the parsing of check-webkit-style command arguments. Attributes: create_usage: A function that accepts a DefaultCommandOptionValues instance and returns a string of usage instructions. Defaults to the function that generates the usage string for check-webkit-style. default_options: A DefaultCommandOptionValues instance that provides the default values for options not explicitly provided by the user. stderr_write: A function that takes a string as a parameter and serves as stderr.write. Defaults to sys.stderr.write. This parameter should be specified only for unit tests. """ def __init__(self, all_categories, default_options, base_filter_rules=None, mock_stderr=None, usage=None): """Create an ArgumentParser instance. Args: all_categories: The set of all available style categories. default_options: See the corresponding attribute in the class docstring. Keyword Args: base_filter_rules: The list of filter rules at the beginning of the list of rules used to check style. This list has the least precedence when checking style and precedes any user-provided rules. The class uses this parameter only for display purposes to the user. Defaults to the empty list. create_usage: See the documentation of the corresponding attribute in the class docstring. stderr_write: See the documentation of the corresponding attribute in the class docstring. """ if base_filter_rules is None: base_filter_rules = [] stderr = sys.stderr if mock_stderr is None else mock_stderr if usage is None: usage = _USAGE self._all_categories = all_categories self._base_filter_rules = base_filter_rules # FIXME: Rename these to reflect that they are internal. self.default_options = default_options self.stderr_write = stderr.write self._parser = self._create_option_parser(stderr=stderr, usage=usage, default_min_confidence=self.default_options.min_confidence, default_output_format=self.default_options.output_format) def _create_option_parser(self, stderr, usage, default_min_confidence, default_output_format): # Since the epilog string is short, it is not necessary to replace # the epilog string with a mock epilog string when testing. # For this reason, we use _EPILOG directly rather than passing it # as an argument like we do for the usage string. parser = OptionParser(usage=usage, epilog=_EPILOG) filter_help = ('set a filter to control what categories of style ' 'errors to report. Specify a filter using a comma-' 'delimited list of boolean filter rules, for example ' '"--filter -whitespace,+whitespace/braces". To display ' 'all categories and which are enabled by default, pass ' """no value (e.g. '-f ""' or '--filter=').""") parser.add_option("-f", "--filter-rules", metavar="RULES", dest="filter_value", help=filter_help) git_commit_help = ("check all changes in the given git commit. " "Use 'commit_id..' to check all changes after commmit_id") parser.add_option("-g", "--git-diff", "--git-commit", metavar="COMMIT", dest="git_commit", help=git_commit_help,) min_confidence_help = ("set the minimum confidence of style errors " "to report. Can be an integer 1-5, with 1 " "displaying all errors. Defaults to %default.") parser.add_option("-m", "--min-confidence", metavar="INT", type="int", dest="min_confidence", default=default_min_confidence, help=min_confidence_help) output_format_help = ('set the output format, which can be "emacs" ' 'or "vs7" (for Visual Studio). ' 'Defaults to "%default".') parser.add_option("-o", "--output-format", metavar="FORMAT", choices=["emacs", "vs7"], dest="output_format", default=default_output_format, help=output_format_help) squash_help = ("All diffs from the remote branch are checked." "If excluded, prompts whether to squash when there are multiple commits.") parser.add_option("-s", "--squash", action="store_true", dest="squash", help=squash_help) squash_help = ("Only working copy diffs are checked." "If excluded, prompts whether to squash when there are multiple commits.") parser.add_option("--no-squash", action="store_false", dest="squash", help=squash_help) verbose_help = "enable verbose logging." parser.add_option("-v", "--verbose", dest="is_verbose", default=False, action="store_true", help=verbose_help) # Override OptionParser's error() method so that option help will # also display when an error occurs. Normally, just the usage # string displays and not option help. parser.error = self._parse_error # Override OptionParser's print_help() method so that help output # does not render to the screen while running unit tests. print_help = parser.print_help parser.print_help = lambda: print_help(file=stderr) return parser def _parse_error(self, error_message): """Print the help string and an error message, and exit.""" # The method format_help() includes both the usage string and # the flag options. help = self._parser.format_help() # Separate help from the error message with a single blank line. self.stderr_write(help + "\n") if error_message: _log.error(error_message) # Since we are using this method to replace/override the Python # module optparse's OptionParser.error() method, we match its # behavior and exit with status code 2. # # As additional background, Python documentation says-- # # "Unix programs generally use 2 for command line syntax errors # and 1 for all other kind of errors." # # (from http://docs.python.org/library/sys.html#sys.exit ) sys.exit(2) def _exit_with_categories(self): """Exit and print the style categories and default filter rules.""" self.stderr_write('\nAll categories:\n') for category in sorted(self._all_categories): self.stderr_write(' ' + category + '\n') self.stderr_write('\nDefault filter rules**:\n') for filter_rule in sorted(self._base_filter_rules): self.stderr_write(' ' + filter_rule + '\n') self.stderr_write('\n**The command always evaluates the above rules, ' 'and before any --filter flag.\n\n') sys.exit(0) def _parse_filter_flag(self, flag_value): """Parse the --filter flag, and return a list of filter rules. Args: flag_value: A string of comma-separated filter rules, for example "-whitespace,+whitespace/indent". """ filters = [] for uncleaned_filter in flag_value.split(','): filter = uncleaned_filter.strip() if not filter: continue filters.append(filter) return filters def parse(self, args): """Parse the command line arguments to check-webkit-style. Args: args: A list of command-line arguments as returned by sys.argv[1:]. Returns: A tuple of (paths, options) paths: The list of paths to check. options: A CommandOptionValues instance. """ (options, paths) = self._parser.parse_args(args=args) filter_value = options.filter_value git_commit = options.git_commit is_verbose = options.is_verbose min_confidence = options.min_confidence output_format = options.output_format if filter_value is not None and not filter_value: # Then the user explicitly passed no filter, for # example "-f ''" or "--filter=". self._exit_with_categories() # Validate user-provided values. if paths and git_commit: self._parse_error('You cannot provide both paths and a git ' 'commit at the same time.') min_confidence = int(min_confidence) if (min_confidence < 1) or (min_confidence > 5): self._parse_error('option --min-confidence: invalid integer: ' '%s: value must be between 1 and 5' % min_confidence) if filter_value: filter_rules = self._parse_filter_flag(filter_value) else: filter_rules = [] try: validate_filter_rules(filter_rules, self._all_categories) except ValueError, err: self._parse_error(err) options = CommandOptionValues(filter_rules=filter_rules, git_commit=git_commit, is_verbose=is_verbose, min_confidence=min_confidence, output_format=output_format, squash=options.squash) return (paths, options)