|
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 |