Home | History | Annotate | Download | only in style
      1 # Copyright (C) 2010 Chris Jerdonek (cjerdonek (at] 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                  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 
    196 class 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 
    232 class 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