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