optparser.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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. """Supports the parsing of command-line options for check-webkit-style."""
  23. import logging
  24. from optparse import OptionParser
  25. import os.path
  26. import sys
  27. from filter import validate_filter_rules
  28. # This module should not import anything from checker.py.
  29. _log = logging.getLogger(__name__)
  30. _USAGE = """usage: %prog [--help] [options] [path1] [path2] ...
  31. Overview:
  32. Check coding style according to WebKit style guidelines:
  33. http://webkit.org/coding/coding-style.html
  34. Path arguments can be files and directories. If neither a git commit nor
  35. paths are passed, then all changes in your source control working directory
  36. are checked.
  37. Style errors:
  38. This script assigns to every style error a confidence score from 1-5 and
  39. a category name. A confidence score of 5 means the error is certainly
  40. a problem, and 1 means it could be fine.
  41. Category names appear in error messages in brackets, for example
  42. [whitespace/indent]. See the options section below for an option that
  43. displays all available categories and which are reported by default.
  44. Filters:
  45. Use filters to configure what errors to report. Filters are specified using
  46. a comma-separated list of boolean filter rules. The script reports errors
  47. in a category if the category passes the filter, as described below.
  48. All categories start out passing. Boolean filter rules are then evaluated
  49. from left to right, with later rules taking precedence. For example, the
  50. rule "+foo" passes any category that starts with "foo", and "-foo" fails
  51. any such category. The filter input "-whitespace,+whitespace/braces" fails
  52. the category "whitespace/tab" and passes "whitespace/braces".
  53. Examples: --filter=-whitespace,+whitespace/braces
  54. --filter=-whitespace,-runtime/printf,+runtime/printf_format
  55. --filter=-,+build/include_what_you_use
  56. Paths:
  57. Certain style-checking behavior depends on the paths relative to
  58. the WebKit source root of the files being checked. For example,
  59. certain types of errors may be handled differently for files in
  60. WebKit/gtk/webkit/ (e.g. by suppressing "readability/naming" errors
  61. for files in this directory).
  62. Consequently, if the path relative to the source root cannot be
  63. determined for a file being checked, then style checking may not
  64. work correctly for that file. This can occur, for example, if no
  65. WebKit checkout can be found, or if the source root can be detected,
  66. but one of the files being checked lies outside the source tree.
  67. If a WebKit checkout can be detected and all files being checked
  68. are in the source tree, then all paths will automatically be
  69. converted to paths relative to the source root prior to checking.
  70. This is also useful for display purposes.
  71. Currently, this command can detect the source root only if the
  72. command is run from within a WebKit checkout (i.e. if the current
  73. working directory is below the root of a checkout). In particular,
  74. it is not recommended to run this script from a directory outside
  75. a checkout.
  76. Running this script from a top-level WebKit source directory and
  77. checking only files in the source tree will ensure that all style
  78. checking behaves correctly -- whether or not a checkout can be
  79. detected. This is because all file paths will already be relative
  80. to the source root and so will not need to be converted."""
  81. _EPILOG = ("This script can miss errors and does not substitute for "
  82. "code review.")
  83. # This class should not have knowledge of the flag key names.
  84. class DefaultCommandOptionValues(object):
  85. """Stores the default check-webkit-style command-line options.
  86. Attributes:
  87. output_format: A string that is the default output format.
  88. min_confidence: An integer that is the default minimum confidence level.
  89. """
  90. def __init__(self, min_confidence, output_format):
  91. self.min_confidence = min_confidence
  92. self.output_format = output_format
  93. # This class should not have knowledge of the flag key names.
  94. class CommandOptionValues(object):
  95. """Stores the option values passed by the user via the command line.
  96. Attributes:
  97. is_verbose: A boolean value of whether verbose logging is enabled.
  98. filter_rules: The list of filter rules provided by the user.
  99. These rules are appended to the base rules and
  100. path-specific rules and so take precedence over
  101. the base filter rules, etc.
  102. git_commit: A string representing the git commit to check.
  103. The default is None.
  104. min_confidence: An integer between 1 and 5 inclusive that is the
  105. minimum confidence level of style errors to report.
  106. The default is 1, which reports all errors.
  107. output_format: A string that is the output format. The supported
  108. output formats are "emacs" which emacs can parse
  109. and "vs7" which Microsoft Visual Studio 7 can parse.
  110. """
  111. def __init__(self,
  112. filter_rules=None,
  113. git_commit=None,
  114. diff_files=None,
  115. is_verbose=False,
  116. min_confidence=1,
  117. output_format="emacs"):
  118. if filter_rules is None:
  119. filter_rules = []
  120. if (min_confidence < 1) or (min_confidence > 5):
  121. raise ValueError('Invalid "min_confidence" parameter: value '
  122. "must be an integer between 1 and 5 inclusive. "
  123. 'Value given: "%s".' % min_confidence)
  124. if output_format not in ("emacs", "vs7"):
  125. raise ValueError('Invalid "output_format" parameter: '
  126. 'value must be "emacs" or "vs7". '
  127. 'Value given: "%s".' % output_format)
  128. self.filter_rules = filter_rules
  129. self.git_commit = git_commit
  130. self.diff_files = diff_files
  131. self.is_verbose = is_verbose
  132. self.min_confidence = min_confidence
  133. self.output_format = output_format
  134. # Useful for unit testing.
  135. def __eq__(self, other):
  136. """Return whether this instance is equal to another."""
  137. if self.filter_rules != other.filter_rules:
  138. return False
  139. if self.git_commit != other.git_commit:
  140. return False
  141. if self.diff_files != other.diff_files:
  142. return False
  143. if self.is_verbose != other.is_verbose:
  144. return False
  145. if self.min_confidence != other.min_confidence:
  146. return False
  147. if self.output_format != other.output_format:
  148. return False
  149. return True
  150. # Useful for unit testing.
  151. def __ne__(self, other):
  152. # Python does not automatically deduce this from __eq__().
  153. return not self.__eq__(other)
  154. class ArgumentPrinter(object):
  155. """Supports the printing of check-webkit-style command arguments."""
  156. def _flag_pair_to_string(self, flag_key, flag_value):
  157. return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value }
  158. def to_flag_string(self, options):
  159. """Return a flag string of the given CommandOptionValues instance.
  160. This method orders the flag values alphabetically by the flag key.
  161. Args:
  162. options: A CommandOptionValues instance.
  163. """
  164. flags = {}
  165. flags['min-confidence'] = options.min_confidence
  166. flags['output'] = options.output_format
  167. # Only include the filter flag if user-provided rules are present.
  168. filter_rules = options.filter_rules
  169. if filter_rules:
  170. flags['filter'] = ",".join(filter_rules)
  171. if options.git_commit:
  172. flags['git-commit'] = options.git_commit
  173. if options.diff_files:
  174. flags['diff_files'] = options.diff_files
  175. flag_string = ''
  176. # Alphabetizing lets us unit test this method.
  177. for key in sorted(flags.keys()):
  178. flag_string += self._flag_pair_to_string(key, flags[key]) + ' '
  179. return flag_string.strip()
  180. class ArgumentParser(object):
  181. # FIXME: Move the documentation of the attributes to the __init__
  182. # docstring after making the attributes internal.
  183. """Supports the parsing of check-webkit-style command arguments.
  184. Attributes:
  185. create_usage: A function that accepts a DefaultCommandOptionValues
  186. instance and returns a string of usage instructions.
  187. Defaults to the function that generates the usage
  188. string for check-webkit-style.
  189. default_options: A DefaultCommandOptionValues instance that provides
  190. the default values for options not explicitly
  191. provided by the user.
  192. stderr_write: A function that takes a string as a parameter and
  193. serves as stderr.write. Defaults to sys.stderr.write.
  194. This parameter should be specified only for unit tests.
  195. """
  196. def __init__(self,
  197. all_categories,
  198. default_options,
  199. base_filter_rules=None,
  200. mock_stderr=None,
  201. usage=None):
  202. """Create an ArgumentParser instance.
  203. Args:
  204. all_categories: The set of all available style categories.
  205. default_options: See the corresponding attribute in the class
  206. docstring.
  207. Keyword Args:
  208. base_filter_rules: The list of filter rules at the beginning of
  209. the list of rules used to check style. This
  210. list has the least precedence when checking
  211. style and precedes any user-provided rules.
  212. The class uses this parameter only for display
  213. purposes to the user. Defaults to the empty list.
  214. create_usage: See the documentation of the corresponding
  215. attribute in the class docstring.
  216. stderr_write: See the documentation of the corresponding
  217. attribute in the class docstring.
  218. """
  219. if base_filter_rules is None:
  220. base_filter_rules = []
  221. stderr = sys.stderr if mock_stderr is None else mock_stderr
  222. if usage is None:
  223. usage = _USAGE
  224. self._all_categories = all_categories
  225. self._base_filter_rules = base_filter_rules
  226. # FIXME: Rename these to reflect that they are internal.
  227. self.default_options = default_options
  228. self.stderr_write = stderr.write
  229. self._parser = self._create_option_parser(stderr=stderr,
  230. usage=usage,
  231. default_min_confidence=self.default_options.min_confidence,
  232. default_output_format=self.default_options.output_format)
  233. def _create_option_parser(self, stderr, usage,
  234. default_min_confidence, default_output_format):
  235. # Since the epilog string is short, it is not necessary to replace
  236. # the epilog string with a mock epilog string when testing.
  237. # For this reason, we use _EPILOG directly rather than passing it
  238. # as an argument like we do for the usage string.
  239. parser = OptionParser(usage=usage, epilog=_EPILOG)
  240. filter_help = ('set a filter to control what categories of style '
  241. 'errors to report. Specify a filter using a comma-'
  242. 'delimited list of boolean filter rules, for example '
  243. '"--filter -whitespace,+whitespace/braces". To display '
  244. 'all categories and which are enabled by default, pass '
  245. """no value (e.g. '-f ""' or '--filter=').""")
  246. parser.add_option("-f", "--filter-rules", metavar="RULES",
  247. dest="filter_value", help=filter_help)
  248. git_commit_help = ("check all changes in the given commit. "
  249. "Use 'commit_id..' to check all changes after commmit_id")
  250. parser.add_option("-g", "--git-diff", "--git-commit",
  251. metavar="COMMIT", dest="git_commit", help=git_commit_help,)
  252. diff_files_help = "diff the files passed on the command line rather than checking the style of every line"
  253. parser.add_option("--diff-files", action="store_true", dest="diff_files", default=False, help=diff_files_help)
  254. min_confidence_help = ("set the minimum confidence of style errors "
  255. "to report. Can be an integer 1-5, with 1 "
  256. "displaying all errors. Defaults to %default.")
  257. parser.add_option("-m", "--min-confidence", metavar="INT",
  258. type="int", dest="min_confidence",
  259. default=default_min_confidence,
  260. help=min_confidence_help)
  261. output_format_help = ('set the output format, which can be "emacs" '
  262. 'or "vs7" (for Visual Studio). '
  263. 'Defaults to "%default".')
  264. parser.add_option("-o", "--output-format", metavar="FORMAT",
  265. choices=["emacs", "vs7"],
  266. dest="output_format", default=default_output_format,
  267. help=output_format_help)
  268. verbose_help = "enable verbose logging."
  269. parser.add_option("-v", "--verbose", dest="is_verbose", default=False,
  270. action="store_true", help=verbose_help)
  271. # Override OptionParser's error() method so that option help will
  272. # also display when an error occurs. Normally, just the usage
  273. # string displays and not option help.
  274. parser.error = self._parse_error
  275. # Override OptionParser's print_help() method so that help output
  276. # does not render to the screen while running unit tests.
  277. print_help = parser.print_help
  278. parser.print_help = lambda file=stderr: print_help(file=file)
  279. return parser
  280. def _parse_error(self, error_message):
  281. """Print the help string and an error message, and exit."""
  282. # The method format_help() includes both the usage string and
  283. # the flag options.
  284. help = self._parser.format_help()
  285. # Separate help from the error message with a single blank line.
  286. self.stderr_write(help + "\n")
  287. if error_message:
  288. _log.error(error_message)
  289. # Since we are using this method to replace/override the Python
  290. # module optparse's OptionParser.error() method, we match its
  291. # behavior and exit with status code 2.
  292. #
  293. # As additional background, Python documentation says--
  294. #
  295. # "Unix programs generally use 2 for command line syntax errors
  296. # and 1 for all other kind of errors."
  297. #
  298. # (from http://docs.python.org/library/sys.html#sys.exit )
  299. sys.exit(2)
  300. def _exit_with_categories(self):
  301. """Exit and print the style categories and default filter rules."""
  302. self.stderr_write('\nAll categories:\n')
  303. for category in sorted(self._all_categories):
  304. self.stderr_write(' ' + category + '\n')
  305. self.stderr_write('\nDefault filter rules**:\n')
  306. for filter_rule in sorted(self._base_filter_rules):
  307. self.stderr_write(' ' + filter_rule + '\n')
  308. self.stderr_write('\n**The command always evaluates the above rules, '
  309. 'and before any --filter flag.\n\n')
  310. sys.exit(0)
  311. def _parse_filter_flag(self, flag_value):
  312. """Parse the --filter flag, and return a list of filter rules.
  313. Args:
  314. flag_value: A string of comma-separated filter rules, for
  315. example "-whitespace,+whitespace/indent".
  316. """
  317. filters = []
  318. for uncleaned_filter in flag_value.split(','):
  319. filter = uncleaned_filter.strip()
  320. if not filter:
  321. continue
  322. filters.append(filter)
  323. return filters
  324. def parse(self, args):
  325. """Parse the command line arguments to check-webkit-style.
  326. Args:
  327. args: A list of command-line arguments as returned by sys.argv[1:].
  328. Returns:
  329. A tuple of (paths, options)
  330. paths: The list of paths to check.
  331. options: A CommandOptionValues instance.
  332. """
  333. (options, paths) = self._parser.parse_args(args=args)
  334. filter_value = options.filter_value
  335. git_commit = options.git_commit
  336. diff_files = options.diff_files
  337. is_verbose = options.is_verbose
  338. min_confidence = options.min_confidence
  339. output_format = options.output_format
  340. if filter_value is not None and not filter_value:
  341. # Then the user explicitly passed no filter, for
  342. # example "-f ''" or "--filter=".
  343. self._exit_with_categories()
  344. # Validate user-provided values.
  345. min_confidence = int(min_confidence)
  346. if (min_confidence < 1) or (min_confidence > 5):
  347. self._parse_error('option --min-confidence: invalid integer: '
  348. '%s: value must be between 1 and 5'
  349. % min_confidence)
  350. if filter_value:
  351. filter_rules = self._parse_filter_flag(filter_value)
  352. else:
  353. filter_rules = []
  354. try:
  355. validate_filter_rules(filter_rules, self._all_categories)
  356. except ValueError, err:
  357. self._parse_error(err)
  358. options = CommandOptionValues(filter_rules=filter_rules,
  359. git_commit=git_commit,
  360. diff_files=diff_files,
  361. is_verbose=is_verbose,
  362. min_confidence=min_confidence,
  363. output_format=output_format)
  364. return (paths, options)