2010-04-01 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / style / optparser.py
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 getopt
26 import logging
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
36 def _create_usage(default_options):
37     """Return the usage string to display for command help.
38
39     Args:
40       default_options: A DefaultCommandOptionValues instance.
41
42     """
43     usage = """
44 Syntax: %(program_name)s [--filter=-x,+y,...] [--git-commit=<SingleCommit>]
45         [--min-confidence=#] [--output=vs7] [--verbose] [file or directory] ...
46
47   The style guidelines this tries to follow are here:
48     http://webkit.org/coding/coding-style.html
49
50   Every style error is given a confidence score from 1-5, with 5 meaning
51   we are certain of the problem, and 1 meaning it could be a legitimate
52   construct.  This can miss some errors and does not substitute for
53   code review.
54
55   To prevent specific lines from being linted, add a '// NOLINT' comment to the
56   end of the line.
57
58   Linted extensions are .cpp, .c and .h.  Other file types are ignored.
59
60   The file parameter is optional and accepts multiple files.  Leaving
61   out the file parameter applies the check to all files considered changed
62   by your source control management system.
63
64   Flags:
65
66     filter=-x,+y,...
67       A comma-separated list of boolean filter rules used to filter
68       which categories of style guidelines to check.  The script checks
69       a category if the category passes the filter rules, as follows.
70
71       Any webkit category starts out passing.  All filter rules are then
72       evaluated left to right, with later rules taking precedence.  For
73       example, the rule "+foo" passes any category that starts with "foo",
74       and "-foo" fails any such category.  The filter input "-whitespace,
75       +whitespace/braces" fails the category "whitespace/tab" and passes
76       "whitespace/braces".
77
78       Examples: --filter=-whitespace,+whitespace/braces
79                 --filter=-whitespace,-runtime/printf,+runtime/printf_format
80                 --filter=-,+build/include_what_you_use
81
82       Category names appear in error messages in brackets, for example
83       [whitespace/indent].  To see a list of all categories available to
84       %(program_name)s, along with which are enabled by default, pass
85       the empty filter as follows:
86          --filter=
87
88     git-commit=<SingleCommit>
89       Checks the style of everything from the given commit to the local tree.
90
91     min-confidence=#
92       An integer between 1 and 5 inclusive that is the minimum confidence
93       level of style errors to report.  In particular, the value 1 displays
94       all errors.  The default is %(default_min_confidence)s.
95
96     output=vs7
97       The output format, which may be one of
98         emacs : to ease emacs parsing
99         vs7   : compatible with Visual Studio
100       Defaults to "%(default_output_format)s". Other formats are unsupported.
101
102     verbose
103       Logging is verbose if this flag is present.
104
105 Path considerations:
106
107   Certain style-checking behavior depends on the paths relative to
108   the WebKit source root of the files being checked.  For example,
109   certain types of errors may be handled differently for files in
110   WebKit/gtk/webkit/ (e.g. by suppressing "readability/naming" errors
111   for files in this directory).
112
113   Consequently, if the path relative to the source root cannot be
114   determined for a file being checked, then style checking may not
115   work correctly for that file.  This can occur, for example, if no
116   WebKit checkout can be found, or if the source root can be detected,
117   but one of the files being checked lies outside the source tree.
118
119   If a WebKit checkout can be detected and all files being checked
120   are in the source tree, then all paths will automatically be
121   converted to paths relative to the source root prior to checking.
122   This is also useful for display purposes.
123
124   Currently, this command can detect the source root only if the
125   command is run from within a WebKit checkout (i.e. if the current
126   working directory is below the root of a checkout).  In particular,
127   it is not recommended to run this script from a directory outside
128   a checkout.
129
130   Running this script from a top-level WebKit source directory and
131   checking only files in the source tree will ensure that all style
132   checking behaves correctly -- whether or not a checkout can be
133   detected.  This is because all file paths will already be relative
134   to the source root and so will not need to be converted.
135
136 """ % {'program_name': os.path.basename(sys.argv[0]),
137        'default_min_confidence': default_options.min_confidence,
138        'default_output_format': default_options.output_format}
139
140     return usage
141
142
143 # This class should not have knowledge of the flag key names.
144 class DefaultCommandOptionValues(object):
145
146     """Stores the default check-webkit-style command-line options.
147
148     Attributes:
149       output_format: A string that is the default output format.
150       min_confidence: An integer that is the default minimum confidence level.
151
152     """
153
154     def __init__(self, min_confidence, output_format):
155         self.min_confidence = min_confidence
156         self.output_format = output_format
157
158
159 # This class should not have knowledge of the flag key names.
160 class CommandOptionValues(object):
161
162     """Stores the option values passed by the user via the command line.
163
164     Attributes:
165       is_verbose: A boolean value of whether verbose logging is enabled.
166
167       filter_rules: The list of filter rules provided by the user.
168                     These rules are appended to the base rules and
169                     path-specific rules and so take precedence over
170                     the base filter rules, etc.
171
172       git_commit: A string representing the git commit to check.
173                   The default is None.
174
175       min_confidence: An integer between 1 and 5 inclusive that is the
176                       minimum confidence level of style errors to report.
177                       The default is 1, which reports all errors.
178
179       output_format: A string that is the output format.  The supported
180                      output formats are "emacs" which emacs can parse
181                      and "vs7" which Microsoft Visual Studio 7 can parse.
182
183     """
184     def __init__(self,
185                  filter_rules=None,
186                  git_commit=None,
187                  is_verbose=False,
188                  min_confidence=1,
189                  output_format="emacs"):
190         if filter_rules is None:
191             filter_rules = []
192
193         if (min_confidence < 1) or (min_confidence > 5):
194             raise ValueError('Invalid "min_confidence" parameter: value '
195                              "must be an integer between 1 and 5 inclusive. "
196                              'Value given: "%s".' % min_confidence)
197
198         if output_format not in ("emacs", "vs7"):
199             raise ValueError('Invalid "output_format" parameter: '
200                              'value must be "emacs" or "vs7". '
201                              'Value given: "%s".' % output_format)
202
203         self.filter_rules = filter_rules
204         self.git_commit = git_commit
205         self.is_verbose = is_verbose
206         self.min_confidence = min_confidence
207         self.output_format = output_format
208
209     # Useful for unit testing.
210     def __eq__(self, other):
211         """Return whether this instance is equal to another."""
212         if self.filter_rules != other.filter_rules:
213             return False
214         if self.git_commit != other.git_commit:
215             return False
216         if self.is_verbose != other.is_verbose:
217             return False
218         if self.min_confidence != other.min_confidence:
219             return False
220         if self.output_format != other.output_format:
221             return False
222
223         return True
224
225     # Useful for unit testing.
226     def __ne__(self, other):
227         # Python does not automatically deduce this from __eq__().
228         return not self.__eq__(other)
229
230
231 class ArgumentPrinter(object):
232
233     """Supports the printing of check-webkit-style command arguments."""
234
235     def _flag_pair_to_string(self, flag_key, flag_value):
236         return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value }
237
238     def to_flag_string(self, options):
239         """Return a flag string of the given CommandOptionValues instance.
240
241         This method orders the flag values alphabetically by the flag key.
242
243         Args:
244           options: A CommandOptionValues instance.
245
246         """
247         flags = {}
248         flags['min-confidence'] = options.min_confidence
249         flags['output'] = options.output_format
250         # Only include the filter flag if user-provided rules are present.
251         filter_rules = options.filter_rules
252         if filter_rules:
253             flags['filter'] = ",".join(filter_rules)
254         if options.git_commit:
255             flags['git-commit'] = options.git_commit
256
257         flag_string = ''
258         # Alphabetizing lets us unit test this method.
259         for key in sorted(flags.keys()):
260             flag_string += self._flag_pair_to_string(key, flags[key]) + ' '
261
262         return flag_string.strip()
263
264
265 # FIXME: Replace the use of getopt.getopt() with optparse.OptionParser.
266 class ArgumentParser(object):
267
268     # FIXME: Move the documentation of the attributes to the __init__
269     #        docstring after making the attributes internal.
270     """Supports the parsing of check-webkit-style command arguments.
271
272     Attributes:
273       create_usage: A function that accepts a DefaultCommandOptionValues
274                     instance and returns a string of usage instructions.
275                     Defaults to the function that generates the usage
276                     string for check-webkit-style.
277       default_options: A DefaultCommandOptionValues instance that provides
278                        the default values for options not explicitly
279                        provided by the user.
280       stderr_write: A function that takes a string as a parameter and
281                     serves as stderr.write.  Defaults to sys.stderr.write.
282                     This parameter should be specified only for unit tests.
283
284     """
285
286     def __init__(self,
287                  all_categories,
288                  default_options,
289                  base_filter_rules=None,
290                  create_usage=None,
291                  stderr_write=None):
292         """Create an ArgumentParser instance.
293
294         Args:
295           all_categories: The set of all available style categories.
296           default_options: See the corresponding attribute in the class
297                            docstring.
298         Keyword Args:
299           base_filter_rules: The list of filter rules at the beginning of
300                              the list of rules used to check style.  This
301                              list has the least precedence when checking
302                              style and precedes any user-provided rules.
303                              The class uses this parameter only for display
304                              purposes to the user.  Defaults to the empty list.
305           create_usage: See the documentation of the corresponding
306                         attribute in the class docstring.
307           stderr_write: See the documentation of the corresponding
308                         attribute in the class docstring.
309
310         """
311         if base_filter_rules is None:
312             base_filter_rules = []
313         if create_usage is None:
314             create_usage = _create_usage
315         if stderr_write is None:
316             stderr_write = sys.stderr.write
317
318         self._all_categories = all_categories
319         self._base_filter_rules = base_filter_rules
320         # FIXME: Rename these to reflect that they are internal.
321         self.create_usage = create_usage
322         self.default_options = default_options
323         self.stderr_write = stderr_write
324
325     def _exit_with_usage(self, error_message=''):
326         """Exit and print a usage string with an optional error message.
327
328         Args:
329           error_message: A string that is an error message to print.
330
331         """
332         usage = self.create_usage(self.default_options)
333         self.stderr_write(usage)
334         if error_message:
335             sys.exit('\nFATAL ERROR: ' + error_message)
336         else:
337             sys.exit(1)
338
339     def _exit_with_categories(self):
340         """Exit and print the style categories and default filter rules."""
341         self.stderr_write('\nAll categories:\n')
342         for category in sorted(self._all_categories):
343             self.stderr_write('    ' + category + '\n')
344
345         self.stderr_write('\nDefault filter rules**:\n')
346         for filter_rule in sorted(self._base_filter_rules):
347             self.stderr_write('    ' + filter_rule + '\n')
348         self.stderr_write('\n**The command always evaluates the above rules, '
349                           'and before any --filter flag.\n\n')
350
351         sys.exit(0)
352
353     def _parse_filter_flag(self, flag_value):
354         """Parse the --filter flag, and return a list of filter rules.
355
356         Args:
357           flag_value: A string of comma-separated filter rules, for
358                       example "-whitespace,+whitespace/indent".
359
360         """
361         filters = []
362         for uncleaned_filter in flag_value.split(','):
363             filter = uncleaned_filter.strip()
364             if not filter:
365                 continue
366             filters.append(filter)
367         return filters
368
369     def parse(self, args, found_checkout):
370         """Parse the command line arguments to check-webkit-style.
371
372         Args:
373           args: A list of command-line arguments as returned by sys.argv[1:].
374           found_checkout: A boolean value of whether the current working
375                           directory was found to be inside a WebKit checkout.
376
377         Returns:
378           A tuple of (paths, options)
379
380           paths: The list of paths to check.
381           options: A CommandOptionValues instance.
382
383         """
384         is_verbose = False
385         min_confidence = self.default_options.min_confidence
386         output_format = self.default_options.output_format
387
388         # The flags that the CommandOptionValues class supports.
389         flags = ['filter=', 'git-commit=', 'help', 'min-confidence=',
390                  'output=', 'verbose']
391
392         try:
393             (opts, paths) = getopt.getopt(args, '', flags)
394         except getopt.GetoptError, err:
395             # FIXME: Settle on an error handling approach: come up
396             #        with a consistent guideline as to when and whether
397             #        a ValueError should be raised versus calling
398             #        sys.exit when needing to interrupt execution.
399             self._exit_with_usage('Invalid arguments: %s' % err)
400
401         git_commit = None
402         filter_rules = []
403
404         for (opt, val) in opts:
405             # Process --help first (out of alphabetical order).
406             if opt == '--help':
407                 self._exit_with_usage()
408             elif opt == '--filter':
409                 if not val:
410                     self._exit_with_categories()
411                 filter_rules = self._parse_filter_flag(val)
412             elif opt == '--git-commit':
413                 git_commit = val
414             elif opt == '--min-confidence':
415                 min_confidence = val
416             elif opt == '--output':
417                 output_format = val
418             elif opt == "--verbose":
419                 is_verbose = True
420             else:
421                 # We should never get here because getopt.getopt()
422                 # raises an error in this case.
423                 raise ValueError('Invalid option: "%s"' % opt)
424
425         # Check validity of resulting values.
426         if not found_checkout and not paths:
427             _log.error("WebKit checkout not found: You must run this script "
428                        "from inside a WebKit checkout if you are not passing "
429                        "specific paths to check.")
430             sys.exit(1)
431
432         if paths and (git_commit != None):
433             self._exit_with_usage('It is not possible to check files and a '
434                                   'specific commit at the same time.')
435
436         if output_format not in ('emacs', 'vs7'):
437             raise ValueError('Invalid --output value "%s": The only '
438                              'allowed output formats are emacs and vs7.' %
439                              output_format)
440
441         validate_filter_rules(filter_rules, self._all_categories)
442
443         min_confidence = int(min_confidence)
444         if (min_confidence < 1) or (min_confidence > 5):
445             raise ValueError('Invalid --min-confidence value %s: value must '
446                              'be between 1 and 5 inclusive.' % min_confidence)
447
448         options = CommandOptionValues(filter_rules=filter_rules,
449                                       git_commit=git_commit,
450                                       is_verbose=is_verbose,
451                                       min_confidence=min_confidence,
452                                       output_format=output_format)
453
454         return (paths, options)
455