WebKitTools/Scripts/webkitpy/style/optparser.py
changeset 0 4f2f89ce4247
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebKitTools/Scripts/webkitpy/style/optparser.py	Fri Sep 17 09:02:29 2010 +0300
@@ -0,0 +1,450 @@
+# 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"):
+        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
+
+    # 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
+
+        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
+
+        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 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)
+
+        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)
+
+        return (paths, options)
+