blob: 4b638c05556a2986bd221b578e5c2914becc169b [file] [log] [blame]
# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Supports the parsing of command-line options for check-webkit-style."""
import logging
from optparse import OptionParser
import os.path
import sys
from filter import validate_filter_rules
# This module should not import anything from checker.py.
_log = logging.getLogger(__name__)
_USAGE = """usage: %prog [--help] [options] [path1] [path2] ...
Overview:
Check coding style according to WebKit style guidelines:
http://webkit.org/coding/coding-style.html
Path arguments can be files and directories. If neither a git commit nor
paths are passed, then all changes in your source control working directory
are checked.
Style errors:
This script assigns to every style error a confidence score from 1-5 and
a category name. A confidence score of 5 means the error is certainly
a problem, and 1 means it could be fine.
Category names appear in error messages in brackets, for example
[whitespace/indent]. See the options section below for an option that
displays all available categories and which are reported by default.
Filters:
Use filters to configure what errors to report. Filters are specified using
a comma-separated list of boolean filter rules. The script reports errors
in a category if the category passes the filter, as described below.
All categories start out passing. Boolean filter rules are then evaluated
from left to right, with later rules taking precedence. For example, the
rule "+foo" passes any category that starts with "foo", and "-foo" fails
any such category. The filter input "-whitespace,+whitespace/braces" fails
the category "whitespace/tab" and passes "whitespace/braces".
Examples: --filter=-whitespace,+whitespace/braces
--filter=-whitespace,-runtime/printf,+runtime/printf_format
--filter=-,+build/include_what_you_use
Paths:
Certain style-checking behavior depends on the paths relative to
the WebKit source root of the files being checked. For example,
certain types of errors may be handled differently for files in
WebKit/gtk/webkit/ (e.g. by suppressing "readability/naming" errors
for files in this directory).
Consequently, if the path relative to the source root cannot be
determined for a file being checked, then style checking may not
work correctly for that file. This can occur, for example, if no
WebKit checkout can be found, or if the source root can be detected,
but one of the files being checked lies outside the source tree.
If a WebKit checkout can be detected and all files being checked
are in the source tree, then all paths will automatically be
converted to paths relative to the source root prior to checking.
This is also useful for display purposes.
Currently, this command can detect the source root only if the
command is run from within a WebKit checkout (i.e. if the current
working directory is below the root of a checkout). In particular,
it is not recommended to run this script from a directory outside
a checkout.
Running this script from a top-level WebKit source directory and
checking only files in the source tree will ensure that all style
checking behaves correctly -- whether or not a checkout can be
detected. This is because all file paths will already be relative
to the source root and so will not need to be converted."""
_EPILOG = ("This script can miss errors and does not substitute for "
"code review.")
# This class should not have knowledge of the flag key names.
class DefaultCommandOptionValues(object):
"""Stores the default check-webkit-style command-line options.
Attributes:
output_format: A string that is the default output format.
min_confidence: An integer that is the default minimum confidence level.
"""
def __init__(self, min_confidence, output_format):
self.min_confidence = min_confidence
self.output_format = output_format
# This class should not have knowledge of the flag key names.
class CommandOptionValues(object):
"""Stores the option values passed by the user via the command line.
Attributes:
is_verbose: A boolean value of whether verbose logging is enabled.
filter_rules: The list of filter rules provided by the user.
These rules are appended to the base rules and
path-specific rules and so take precedence over
the base filter rules, etc.
git_commit: A string representing the git commit to check.
The default is None.
min_confidence: An integer between 1 and 5 inclusive that is the
minimum confidence level of style errors to report.
The default is 1, which reports all errors.
output_format: A string that is the output format. The supported
output formats are "emacs" which emacs can parse
and "vs7" which Microsoft Visual Studio 7 can parse.
"""
def __init__(self,
filter_rules=None,
git_commit=None,
diff_files=None,
is_verbose=False,
min_confidence=1,
output_format="emacs"):
if filter_rules is None:
filter_rules = []
if (min_confidence < 1) or (min_confidence > 5):
raise ValueError('Invalid "min_confidence" parameter: value '
"must be an integer between 1 and 5 inclusive. "
'Value given: "%s".' % min_confidence)
if output_format not in ("emacs", "vs7"):
raise ValueError('Invalid "output_format" parameter: '
'value must be "emacs" or "vs7". '
'Value given: "%s".' % output_format)
self.filter_rules = filter_rules
self.git_commit = git_commit
self.diff_files = diff_files
self.is_verbose = is_verbose
self.min_confidence = min_confidence
self.output_format = output_format
# Useful for unit testing.
def __eq__(self, other):
"""Return whether this instance is equal to another."""
if self.filter_rules != other.filter_rules:
return False
if self.git_commit != other.git_commit:
return False
if self.diff_files != other.diff_files:
return False
if self.is_verbose != other.is_verbose:
return False
if self.min_confidence != other.min_confidence:
return False
if self.output_format != other.output_format:
return False
return True
# Useful for unit testing.
def __ne__(self, other):
# Python does not automatically deduce this from __eq__().
return not self.__eq__(other)
class ArgumentPrinter(object):
"""Supports the printing of check-webkit-style command arguments."""
def _flag_pair_to_string(self, flag_key, flag_value):
return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value }
def to_flag_string(self, options):
"""Return a flag string of the given CommandOptionValues instance.
This method orders the flag values alphabetically by the flag key.
Args:
options: A CommandOptionValues instance.
"""
flags = {}
flags['min-confidence'] = options.min_confidence
flags['output'] = options.output_format
# Only include the filter flag if user-provided rules are present.
filter_rules = options.filter_rules
if filter_rules:
flags['filter'] = ",".join(filter_rules)
if options.git_commit:
flags['git-commit'] = options.git_commit
if options.diff_files:
flags['diff_files'] = options.diff_files
flag_string = ''
# Alphabetizing lets us unit test this method.
for key in sorted(flags.keys()):
flag_string += self._flag_pair_to_string(key, flags[key]) + ' '
return flag_string.strip()
class ArgumentParser(object):
# FIXME: Move the documentation of the attributes to the __init__
# docstring after making the attributes internal.
"""Supports the parsing of check-webkit-style command arguments.
Attributes:
create_usage: A function that accepts a DefaultCommandOptionValues
instance and returns a string of usage instructions.
Defaults to the function that generates the usage
string for check-webkit-style.
default_options: A DefaultCommandOptionValues instance that provides
the default values for options not explicitly
provided by the user.
stderr_write: A function that takes a string as a parameter and
serves as stderr.write. Defaults to sys.stderr.write.
This parameter should be specified only for unit tests.
"""
def __init__(self,
all_categories,
default_options,
base_filter_rules=None,
mock_stderr=None,
usage=None):
"""Create an ArgumentParser instance.
Args:
all_categories: The set of all available style categories.
default_options: See the corresponding attribute in the class
docstring.
Keyword Args:
base_filter_rules: The list of filter rules at the beginning of
the list of rules used to check style. This
list has the least precedence when checking
style and precedes any user-provided rules.
The class uses this parameter only for display
purposes to the user. Defaults to the empty list.
create_usage: See the documentation of the corresponding
attribute in the class docstring.
stderr_write: See the documentation of the corresponding
attribute in the class docstring.
"""
if base_filter_rules is None:
base_filter_rules = []
stderr = sys.stderr if mock_stderr is None else mock_stderr
if usage is None:
usage = _USAGE
self._all_categories = all_categories
self._base_filter_rules = base_filter_rules
# FIXME: Rename these to reflect that they are internal.
self.default_options = default_options
self.stderr_write = stderr.write
self._parser = self._create_option_parser(stderr=stderr,
usage=usage,
default_min_confidence=self.default_options.min_confidence,
default_output_format=self.default_options.output_format)
def _create_option_parser(self, stderr, usage,
default_min_confidence, default_output_format):
# Since the epilog string is short, it is not necessary to replace
# the epilog string with a mock epilog string when testing.
# For this reason, we use _EPILOG directly rather than passing it
# as an argument like we do for the usage string.
parser = OptionParser(usage=usage, epilog=_EPILOG)
filter_help = ('set a filter to control what categories of style '
'errors to report. Specify a filter using a comma-'
'delimited list of boolean filter rules, for example '
'"--filter -whitespace,+whitespace/braces". To display '
'all categories and which are enabled by default, pass '
"""no value (e.g. '-f ""' or '--filter=').""")
parser.add_option("-f", "--filter-rules", metavar="RULES",
dest="filter_value", help=filter_help)
git_commit_help = ("check all changes in the given commit. "
"Use 'commit_id..' to check all changes after commmit_id")
parser.add_option("-g", "--git-diff", "--git-commit",
metavar="COMMIT", dest="git_commit", help=git_commit_help,)
diff_files_help = "diff the files passed on the command line rather than checking the style of every line"
parser.add_option("--diff-files", action="store_true", dest="diff_files", default=False, help=diff_files_help)
min_confidence_help = ("set the minimum confidence of style errors "
"to report. Can be an integer 1-5, with 1 "
"displaying all errors. Defaults to %default.")
parser.add_option("-m", "--min-confidence", metavar="INT",
type="int", dest="min_confidence",
default=default_min_confidence,
help=min_confidence_help)
output_format_help = ('set the output format, which can be "emacs" '
'or "vs7" (for Visual Studio). '
'Defaults to "%default".')
parser.add_option("-o", "--output-format", metavar="FORMAT",
choices=["emacs", "vs7"],
dest="output_format", default=default_output_format,
help=output_format_help)
verbose_help = "enable verbose logging."
parser.add_option("-v", "--verbose", dest="is_verbose", default=False,
action="store_true", help=verbose_help)
# Override OptionParser's error() method so that option help will
# also display when an error occurs. Normally, just the usage
# string displays and not option help.
parser.error = self._parse_error
# Override OptionParser's print_help() method so that help output
# does not render to the screen while running unit tests.
print_help = parser.print_help
parser.print_help = lambda file=stderr: print_help(file=file)
return parser
def _parse_error(self, error_message):
"""Print the help string and an error message, and exit."""
# The method format_help() includes both the usage string and
# the flag options.
help = self._parser.format_help()
# Separate help from the error message with a single blank line.
self.stderr_write(help + "\n")
if error_message:
_log.error(error_message)
# Since we are using this method to replace/override the Python
# module optparse's OptionParser.error() method, we match its
# behavior and exit with status code 2.
#
# As additional background, Python documentation says--
#
# "Unix programs generally use 2 for command line syntax errors
# and 1 for all other kind of errors."
#
# (from http://docs.python.org/library/sys.html#sys.exit )
sys.exit(2)
def _exit_with_categories(self):
"""Exit and print the style categories and default filter rules."""
self.stderr_write('\nAll categories:\n')
for category in sorted(self._all_categories):
self.stderr_write(' ' + category + '\n')
self.stderr_write('\nDefault filter rules**:\n')
for filter_rule in sorted(self._base_filter_rules):
self.stderr_write(' ' + filter_rule + '\n')
self.stderr_write('\n**The command always evaluates the above rules, '
'and before any --filter flag.\n\n')
sys.exit(0)
def _parse_filter_flag(self, flag_value):
"""Parse the --filter flag, and return a list of filter rules.
Args:
flag_value: A string of comma-separated filter rules, for
example "-whitespace,+whitespace/indent".
"""
filters = []
for uncleaned_filter in flag_value.split(','):
filter = uncleaned_filter.strip()
if not filter:
continue
filters.append(filter)
return filters
def parse(self, args):
"""Parse the command line arguments to check-webkit-style.
Args:
args: A list of command-line arguments as returned by sys.argv[1:].
Returns:
A tuple of (paths, options)
paths: The list of paths to check.
options: A CommandOptionValues instance.
"""
(options, paths) = self._parser.parse_args(args=args)
filter_value = options.filter_value
git_commit = options.git_commit
diff_files = options.diff_files
is_verbose = options.is_verbose
min_confidence = options.min_confidence
output_format = options.output_format
if filter_value is not None and not filter_value:
# Then the user explicitly passed no filter, for
# example "-f ''" or "--filter=".
self._exit_with_categories()
# Validate user-provided values.
min_confidence = int(min_confidence)
if (min_confidence < 1) or (min_confidence > 5):
self._parse_error('option --min-confidence: invalid integer: '
'%s: value must be between 1 and 5'
% min_confidence)
if filter_value:
filter_rules = self._parse_filter_flag(filter_value)
else:
filter_rules = []
try:
validate_filter_rules(filter_rules, self._all_categories)
except ValueError, err:
self._parse_error(err)
options = CommandOptionValues(filter_rules=filter_rules,
git_commit=git_commit,
diff_files=diff_files,
is_verbose=is_verbose,
min_confidence=min_confidence,
output_format=output_format)
return (paths, options)