WebKitTools/Scripts/webkitpy/style/optparser.py
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     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 
       
    25 import logging
       
    26 from optparse import OptionParser
       
    27 import os.path
       
    28 import sys
       
    29 
       
    30 from 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 
       
    37 Overview:
       
    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 
       
    46 Style 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 
       
    55 Filters:
       
    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 
       
    70 Paths:
       
    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.
       
   105 class 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.
       
   121 class 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                  is_verbose=False,
       
   149                  min_confidence=1,
       
   150                  output_format="emacs"):
       
   151         if filter_rules is None:
       
   152             filter_rules = []
       
   153 
       
   154         if (min_confidence < 1) or (min_confidence > 5):
       
   155             raise ValueError('Invalid "min_confidence" parameter: value '
       
   156                              "must be an integer between 1 and 5 inclusive. "
       
   157                              'Value given: "%s".' % min_confidence)
       
   158 
       
   159         if output_format not in ("emacs", "vs7"):
       
   160             raise ValueError('Invalid "output_format" parameter: '
       
   161                              'value must be "emacs" or "vs7". '
       
   162                              'Value given: "%s".' % output_format)
       
   163 
       
   164         self.filter_rules = filter_rules
       
   165         self.git_commit = git_commit
       
   166         self.is_verbose = is_verbose
       
   167         self.min_confidence = min_confidence
       
   168         self.output_format = output_format
       
   169 
       
   170     # Useful for unit testing.
       
   171     def __eq__(self, other):
       
   172         """Return whether this instance is equal to another."""
       
   173         if self.filter_rules != other.filter_rules:
       
   174             return False
       
   175         if self.git_commit != other.git_commit:
       
   176             return False
       
   177         if self.is_verbose != other.is_verbose:
       
   178             return False
       
   179         if self.min_confidence != other.min_confidence:
       
   180             return False
       
   181         if self.output_format != other.output_format:
       
   182             return False
       
   183 
       
   184         return True
       
   185 
       
   186     # Useful for unit testing.
       
   187     def __ne__(self, other):
       
   188         # Python does not automatically deduce this from __eq__().
       
   189         return not self.__eq__(other)
       
   190 
       
   191 
       
   192 class ArgumentPrinter(object):
       
   193 
       
   194     """Supports the printing of check-webkit-style command arguments."""
       
   195 
       
   196     def _flag_pair_to_string(self, flag_key, flag_value):
       
   197         return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value }
       
   198 
       
   199     def to_flag_string(self, options):
       
   200         """Return a flag string of the given CommandOptionValues instance.
       
   201 
       
   202         This method orders the flag values alphabetically by the flag key.
       
   203 
       
   204         Args:
       
   205           options: A CommandOptionValues instance.
       
   206 
       
   207         """
       
   208         flags = {}
       
   209         flags['min-confidence'] = options.min_confidence
       
   210         flags['output'] = options.output_format
       
   211         # Only include the filter flag if user-provided rules are present.
       
   212         filter_rules = options.filter_rules
       
   213         if filter_rules:
       
   214             flags['filter'] = ",".join(filter_rules)
       
   215         if options.git_commit:
       
   216             flags['git-commit'] = options.git_commit
       
   217 
       
   218         flag_string = ''
       
   219         # Alphabetizing lets us unit test this method.
       
   220         for key in sorted(flags.keys()):
       
   221             flag_string += self._flag_pair_to_string(key, flags[key]) + ' '
       
   222 
       
   223         return flag_string.strip()
       
   224 
       
   225 
       
   226 class ArgumentParser(object):
       
   227 
       
   228     # FIXME: Move the documentation of the attributes to the __init__
       
   229     #        docstring after making the attributes internal.
       
   230     """Supports the parsing of check-webkit-style command arguments.
       
   231 
       
   232     Attributes:
       
   233       create_usage: A function that accepts a DefaultCommandOptionValues
       
   234                     instance and returns a string of usage instructions.
       
   235                     Defaults to the function that generates the usage
       
   236                     string for check-webkit-style.
       
   237       default_options: A DefaultCommandOptionValues instance that provides
       
   238                        the default values for options not explicitly
       
   239                        provided by the user.
       
   240       stderr_write: A function that takes a string as a parameter and
       
   241                     serves as stderr.write.  Defaults to sys.stderr.write.
       
   242                     This parameter should be specified only for unit tests.
       
   243 
       
   244     """
       
   245 
       
   246     def __init__(self,
       
   247                  all_categories,
       
   248                  default_options,
       
   249                  base_filter_rules=None,
       
   250                  mock_stderr=None,
       
   251                  usage=None):
       
   252         """Create an ArgumentParser instance.
       
   253 
       
   254         Args:
       
   255           all_categories: The set of all available style categories.
       
   256           default_options: See the corresponding attribute in the class
       
   257                            docstring.
       
   258         Keyword Args:
       
   259           base_filter_rules: The list of filter rules at the beginning of
       
   260                              the list of rules used to check style.  This
       
   261                              list has the least precedence when checking
       
   262                              style and precedes any user-provided rules.
       
   263                              The class uses this parameter only for display
       
   264                              purposes to the user.  Defaults to the empty list.
       
   265           create_usage: See the documentation of the corresponding
       
   266                         attribute in the class docstring.
       
   267           stderr_write: See the documentation of the corresponding
       
   268                         attribute in the class docstring.
       
   269 
       
   270         """
       
   271         if base_filter_rules is None:
       
   272             base_filter_rules = []
       
   273         stderr = sys.stderr if mock_stderr is None else mock_stderr
       
   274         if usage is None:
       
   275             usage = _USAGE
       
   276 
       
   277         self._all_categories = all_categories
       
   278         self._base_filter_rules = base_filter_rules
       
   279 
       
   280         # FIXME: Rename these to reflect that they are internal.
       
   281         self.default_options = default_options
       
   282         self.stderr_write = stderr.write
       
   283 
       
   284         self._parser = self._create_option_parser(stderr=stderr,
       
   285             usage=usage,
       
   286             default_min_confidence=self.default_options.min_confidence,
       
   287             default_output_format=self.default_options.output_format)
       
   288 
       
   289     def _create_option_parser(self, stderr, usage,
       
   290                               default_min_confidence, default_output_format):
       
   291         # Since the epilog string is short, it is not necessary to replace
       
   292         # the epilog string with a mock epilog string when testing.
       
   293         # For this reason, we use _EPILOG directly rather than passing it
       
   294         # as an argument like we do for the usage string.
       
   295         parser = OptionParser(usage=usage, epilog=_EPILOG)
       
   296 
       
   297         filter_help = ('set a filter to control what categories of style '
       
   298                        'errors to report.  Specify a filter using a comma-'
       
   299                        'delimited list of boolean filter rules, for example '
       
   300                        '"--filter -whitespace,+whitespace/braces".  To display '
       
   301                        'all categories and which are enabled by default, pass '
       
   302                        """no value (e.g. '-f ""' or '--filter=').""")
       
   303         parser.add_option("-f", "--filter-rules", metavar="RULES",
       
   304                           dest="filter_value", help=filter_help)
       
   305 
       
   306         git_commit_help = ("check all changes in the given commit. "
       
   307                            "Use 'commit_id..' to check all changes after commmit_id")
       
   308         parser.add_option("-g", "--git-diff", "--git-commit",
       
   309                           metavar="COMMIT", dest="git_commit", help=git_commit_help,)
       
   310 
       
   311         min_confidence_help = ("set the minimum confidence of style errors "
       
   312                                "to report.  Can be an integer 1-5, with 1 "
       
   313                                "displaying all errors.  Defaults to %default.")
       
   314         parser.add_option("-m", "--min-confidence", metavar="INT",
       
   315                           type="int", dest="min_confidence",
       
   316                           default=default_min_confidence,
       
   317                           help=min_confidence_help)
       
   318 
       
   319         output_format_help = ('set the output format, which can be "emacs" '
       
   320                               'or "vs7" (for Visual Studio).  '
       
   321                               'Defaults to "%default".')
       
   322         parser.add_option("-o", "--output-format", metavar="FORMAT",
       
   323                           choices=["emacs", "vs7"],
       
   324                           dest="output_format", default=default_output_format,
       
   325                           help=output_format_help)
       
   326 
       
   327         verbose_help = "enable verbose logging."
       
   328         parser.add_option("-v", "--verbose", dest="is_verbose", default=False,
       
   329                           action="store_true", help=verbose_help)
       
   330 
       
   331         # Override OptionParser's error() method so that option help will
       
   332         # also display when an error occurs.  Normally, just the usage
       
   333         # string displays and not option help.
       
   334         parser.error = self._parse_error
       
   335 
       
   336         # Override OptionParser's print_help() method so that help output
       
   337         # does not render to the screen while running unit tests.
       
   338         print_help = parser.print_help
       
   339         parser.print_help = lambda: print_help(file=stderr)
       
   340 
       
   341         return parser
       
   342 
       
   343     def _parse_error(self, error_message):
       
   344         """Print the help string and an error message, and exit."""
       
   345         # The method format_help() includes both the usage string and
       
   346         # the flag options.
       
   347         help = self._parser.format_help()
       
   348         # Separate help from the error message with a single blank line.
       
   349         self.stderr_write(help + "\n")
       
   350         if error_message:
       
   351             _log.error(error_message)
       
   352 
       
   353         # Since we are using this method to replace/override the Python
       
   354         # module optparse's OptionParser.error() method, we match its
       
   355         # behavior and exit with status code 2.
       
   356         #
       
   357         # As additional background, Python documentation says--
       
   358         #
       
   359         # "Unix programs generally use 2 for command line syntax errors
       
   360         #  and 1 for all other kind of errors."
       
   361         #
       
   362         # (from http://docs.python.org/library/sys.html#sys.exit )
       
   363         sys.exit(2)
       
   364 
       
   365     def _exit_with_categories(self):
       
   366         """Exit and print the style categories and default filter rules."""
       
   367         self.stderr_write('\nAll categories:\n')
       
   368         for category in sorted(self._all_categories):
       
   369             self.stderr_write('    ' + category + '\n')
       
   370 
       
   371         self.stderr_write('\nDefault filter rules**:\n')
       
   372         for filter_rule in sorted(self._base_filter_rules):
       
   373             self.stderr_write('    ' + filter_rule + '\n')
       
   374         self.stderr_write('\n**The command always evaluates the above rules, '
       
   375                           'and before any --filter flag.\n\n')
       
   376 
       
   377         sys.exit(0)
       
   378 
       
   379     def _parse_filter_flag(self, flag_value):
       
   380         """Parse the --filter flag, and return a list of filter rules.
       
   381 
       
   382         Args:
       
   383           flag_value: A string of comma-separated filter rules, for
       
   384                       example "-whitespace,+whitespace/indent".
       
   385 
       
   386         """
       
   387         filters = []
       
   388         for uncleaned_filter in flag_value.split(','):
       
   389             filter = uncleaned_filter.strip()
       
   390             if not filter:
       
   391                 continue
       
   392             filters.append(filter)
       
   393         return filters
       
   394 
       
   395     def parse(self, args):
       
   396         """Parse the command line arguments to check-webkit-style.
       
   397 
       
   398         Args:
       
   399           args: A list of command-line arguments as returned by sys.argv[1:].
       
   400 
       
   401         Returns:
       
   402           A tuple of (paths, options)
       
   403 
       
   404           paths: The list of paths to check.
       
   405           options: A CommandOptionValues instance.
       
   406 
       
   407         """
       
   408         (options, paths) = self._parser.parse_args(args=args)
       
   409 
       
   410         filter_value = options.filter_value
       
   411         git_commit = options.git_commit
       
   412         is_verbose = options.is_verbose
       
   413         min_confidence = options.min_confidence
       
   414         output_format = options.output_format
       
   415 
       
   416         if filter_value is not None and not filter_value:
       
   417             # Then the user explicitly passed no filter, for
       
   418             # example "-f ''" or "--filter=".
       
   419             self._exit_with_categories()
       
   420 
       
   421         # Validate user-provided values.
       
   422 
       
   423         if paths and git_commit:
       
   424             self._parse_error('You cannot provide both paths and a git '
       
   425                               'commit at the same time.')
       
   426 
       
   427         min_confidence = int(min_confidence)
       
   428         if (min_confidence < 1) or (min_confidence > 5):
       
   429             self._parse_error('option --min-confidence: invalid integer: '
       
   430                               '%s: value must be between 1 and 5'
       
   431                               % min_confidence)
       
   432 
       
   433         if filter_value:
       
   434             filter_rules = self._parse_filter_flag(filter_value)
       
   435         else:
       
   436             filter_rules = []
       
   437 
       
   438         try:
       
   439             validate_filter_rules(filter_rules, self._all_categories)
       
   440         except ValueError, err:
       
   441             self._parse_error(err)
       
   442 
       
   443         options = CommandOptionValues(filter_rules=filter_rules,
       
   444                                       git_commit=git_commit,
       
   445                                       is_verbose=is_verbose,
       
   446                                       min_confidence=min_confidence,
       
   447                                       output_format=output_format)
       
   448 
       
   449         return (paths, options)
       
   450