summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/style
diff options
context:
space:
mode:
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/style')
-rw-r--r--WebKitTools/Scripts/webkitpy/style/checker.py168
-rwxr-xr-xWebKitTools/Scripts/webkitpy/style/checker_unittest.py80
-rw-r--r--WebKitTools/Scripts/webkitpy/style/error_handlers.py44
-rw-r--r--WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py62
-rw-r--r--WebKitTools/Scripts/webkitpy/style/filter.py97
-rw-r--r--WebKitTools/Scripts/webkitpy/style/filter_unittest.py84
-rw-r--r--WebKitTools/Scripts/webkitpy/style/processors/common.py59
-rw-r--r--WebKitTools/Scripts/webkitpy/style/processors/common_unittest.py82
-rw-r--r--WebKitTools/Scripts/webkitpy/style/unittests.py2
9 files changed, 489 insertions, 189 deletions
diff --git a/WebKitTools/Scripts/webkitpy/style/checker.py b/WebKitTools/Scripts/webkitpy/style/checker.py
index faf954f..dc14ea3 100644
--- a/WebKitTools/Scripts/webkitpy/style/checker.py
+++ b/WebKitTools/Scripts/webkitpy/style/checker.py
@@ -37,6 +37,9 @@ import sys
from .. style_references import parse_patch
from error_handlers import DefaultStyleErrorHandler
from error_handlers import PatchStyleErrorHandler
+from filter import CategoryFilter
+from processors.common import check_no_carriage_return
+from processors.common import categories as CommonCategories
from processors.cpp import CppProcessor
from processors.text import TextProcessor
@@ -106,10 +109,17 @@ SKIPPED_FILES_WITHOUT_WARNING = [
]
+# The maximum number of errors to report per file, per category.
+# If a category is not a key, then it has no maximum.
+MAX_REPORTS_PER_CATEGORY = {
+ "whitespace/carriage_return": 1
+}
+
+
def style_categories():
"""Return the set of all categories used by check-webkit-style."""
- # If other processors had categories, we would take their union here.
- return CppProcessor.categories
+ # Take the union across all processors.
+ return CommonCategories.union(CppProcessor.categories)
def webkit_argument_defaults():
@@ -191,79 +201,6 @@ Syntax: %(program_name)s [--verbose=#] [--git-commit=<SingleCommit>] [--output=v
return usage
-class CategoryFilter(object):
-
- """Filters whether to check style categories."""
-
- def __init__(self, filter_rules=None):
- """Create a category filter.
-
- This method performs argument validation but does not strip
- leading or trailing white space.
-
- Args:
- filter_rules: A list of strings that are filter rules, which
- are strings beginning with the plus or minus
- symbol (+/-). The list should include any
- default filter rules at the beginning.
- Defaults to the empty list.
-
- Raises:
- ValueError: Invalid filter rule if a rule does not start with
- plus ("+") or minus ("-").
-
- """
- if filter_rules is None:
- filter_rules = []
-
- for rule in filter_rules:
- if not (rule.startswith('+') or rule.startswith('-')):
- raise ValueError('Invalid filter rule "%s": every rule '
- 'rule in the --filter flag must start '
- 'with + or -.' % rule)
-
- self._filter_rules = filter_rules
- self._should_check_category = {} # Cached dictionary of category to True/False
-
- def __str__(self):
- return ",".join(self._filter_rules)
-
- # Useful for unit testing.
- def __eq__(self, other):
- """Return whether this CategoryFilter instance is equal to another."""
- return self._filter_rules == other._filter_rules
-
- # Useful for unit testing.
- def __ne__(self, other):
- # Python does not automatically deduce from __eq__().
- return not (self == other)
-
- def should_check(self, category):
- """Return whether the category should be checked.
-
- The rules for determining whether a category should be checked
- are as follows. By default all categories should be checked.
- Then apply the filter rules in order from first to last, with
- later flags taking precedence.
-
- A filter rule applies to a category if the string after the
- leading plus/minus (+/-) matches the beginning of the category
- name. A plus (+) means the category should be checked, while a
- minus (-) means the category should not be checked.
-
- """
- if category in self._should_check_category:
- return self._should_check_category[category]
-
- should_check = True # All categories checked by default.
- for rule in self._filter_rules:
- if not category.startswith(rule[1:]):
- continue
- should_check = rule.startswith('+')
- self._should_check_category[category] = should_check # Update cache.
- return should_check
-
-
# This class should not have knowledge of the flag key names.
class ProcessorOptions(object):
@@ -290,12 +227,19 @@ class ProcessorOptions(object):
"""
- def __init__(self, output_format="emacs", verbosity=1, filter=None,
- git_commit=None, extra_flag_values=None):
- if filter is None:
- filter = CategoryFilter()
+ def __init__(self,
+ output_format="emacs",
+ verbosity=1,
+ filter=None,
+ max_reports_per_category=None,
+ git_commit=None,
+ extra_flag_values=None):
if extra_flag_values is None:
extra_flag_values = {}
+ if filter is None:
+ filter = CategoryFilter()
+ if max_reports_per_category is None:
+ max_reports_per_category = {}
if output_format not in ("emacs", "vs7"):
raise ValueError('Invalid "output_format" parameter: '
@@ -307,24 +251,27 @@ class ProcessorOptions(object):
"value must be an integer between 1-5 inclusive. "
'Value given: "%s".' % verbosity)
- self.output_format = output_format
- self.verbosity = verbosity
+ self.extra_flag_values = extra_flag_values
self.filter = filter
self.git_commit = git_commit
- self.extra_flag_values = extra_flag_values
+ self.max_reports_per_category = max_reports_per_category
+ self.output_format = output_format
+ self.verbosity = verbosity
# Useful for unit testing.
def __eq__(self, other):
"""Return whether this ProcessorOptions instance is equal to another."""
- if self.output_format != other.output_format:
- return False
- if self.verbosity != other.verbosity:
+ if self.extra_flag_values != other.extra_flag_values:
return False
if self.filter != other.filter:
return False
if self.git_commit != other.git_commit:
return False
- if self.extra_flag_values != other.extra_flag_values:
+ if self.max_reports_per_category != other.max_reports_per_category:
+ return False
+ if self.output_format != other.output_format:
+ return False
+ if self.verbosity != other.verbosity:
return False
return True
@@ -568,8 +515,12 @@ class ArgumentParser(object):
filter = CategoryFilter(filter_rules)
- options = ProcessorOptions(output_format, verbosity, filter,
- git_commit, extra_flag_values)
+ options = ProcessorOptions(extra_flag_values=extra_flag_values,
+ filter=filter,
+ git_commit=git_commit,
+ max_reports_per_category=MAX_REPORTS_PER_CATEGORY,
+ output_format=output_format,
+ verbosity=verbosity)
return (filenames, options)
@@ -720,35 +671,36 @@ class StyleChecker(object):
# '\r\n' as in Windows), a warning is issued below if this file
# is processed.
if file_path == '-':
- lines = codecs.StreamReaderWriter(sys.stdin,
- codecs.getreader('utf8'),
- codecs.getwriter('utf8'),
- 'replace').read().split('\n')
+ file = codecs.StreamReaderWriter(sys.stdin,
+ codecs.getreader('utf8'),
+ codecs.getwriter('utf8'),
+ 'replace')
else:
- lines = codecs.open(file_path, 'r', 'utf8', 'replace').read().split('\n')
+ file = codecs.open(file_path, 'r', 'utf8', 'replace')
- carriage_return_found = False
- # Remove trailing '\r'.
- for line_number in range(len(lines)):
- if lines[line_number].endswith('\r'):
- lines[line_number] = lines[line_number].rstrip('\r')
- carriage_return_found = True
+ contents = file.read()
except IOError:
self._stderr_write("Skipping input '%s': Can't open for reading\n" % file_path)
return
- processor.process(lines)
+ lines = contents.split("\n")
- if carriage_return_found and os.linesep != '\r\n':
- # FIXME: Make sure this error also shows up when checking
- # patches, if appropriate.
+ for line_number in range(len(lines)):
+ # FIXME: We should probably use the SVN "eol-style" property
+ # or a white list to decide whether or not to do
+ # the carriage-return check. Originally, we did the
+ # check only if (os.linesep != '\r\n').
#
- # Use 0 for line_number since outputting only one error for
- # potentially several lines.
- handle_style_error(file_path, 0, 'whitespace/newline', 1,
- 'One or more unexpected \\r (^M) found;'
- 'better to use only a \\n')
+ # FIXME: As a minor optimization, we can have
+ # check_no_carriage_return() return whether
+ # the line ends with "\r".
+ check_no_carriage_return(lines[line_number], line_number,
+ handle_style_error)
+ if lines[line_number].endswith("\r"):
+ lines[line_number] = lines[line_number].rstrip("\r")
+
+ processor.process(lines)
def check_file(self, file_path, handle_style_error=None, process_file=None):
"""Check style in the given file.
diff --git a/WebKitTools/Scripts/webkitpy/style/checker_unittest.py b/WebKitTools/Scripts/webkitpy/style/checker_unittest.py
index 4d6b2e7..814bd41 100755
--- a/WebKitTools/Scripts/webkitpy/style/checker_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/style/checker_unittest.py
@@ -37,67 +37,13 @@
import unittest
import checker as style
-from checker import CategoryFilter
from checker import ProcessorDispatcher
from checker import ProcessorOptions
from checker import StyleChecker
+from filter import CategoryFilter
from processors.cpp import CppProcessor
from processors.text import TextProcessor
-class CategoryFilterTest(unittest.TestCase):
-
- """Tests CategoryFilter class."""
-
- def test_init(self):
- """Test __init__ constructor."""
- self.assertRaises(ValueError, CategoryFilter, ["no_prefix"])
- CategoryFilter() # No ValueError: works
- CategoryFilter(["+"]) # No ValueError: works
- CategoryFilter(["-"]) # No ValueError: works
-
- def test_str(self):
- """Test __str__ "to string" operator."""
- filter = CategoryFilter(["+a", "-b"])
- self.assertEquals(str(filter), "+a,-b")
-
- def test_eq(self):
- """Test __eq__ equality function."""
- filter1 = CategoryFilter(["+a", "+b"])
- filter2 = CategoryFilter(["+a", "+b"])
- filter3 = CategoryFilter(["+b", "+a"])
-
- # == calls __eq__.
- self.assertTrue(filter1 == filter2)
- self.assertFalse(filter1 == filter3) # Cannot test with assertNotEqual.
-
- def test_ne(self):
- """Test __ne__ inequality function."""
- # != calls __ne__.
- # By default, __ne__ always returns true on different objects.
- # Thus, just check the distinguishing case to verify that the
- # code defines __ne__.
- self.assertFalse(CategoryFilter() != CategoryFilter())
-
- def test_should_check(self):
- """Test should_check() method."""
- filter = CategoryFilter()
- self.assertTrue(filter.should_check("everything"))
- # Check a second time to exercise cache.
- self.assertTrue(filter.should_check("everything"))
-
- filter = CategoryFilter(["-"])
- self.assertFalse(filter.should_check("anything"))
- # Check a second time to exercise cache.
- self.assertFalse(filter.should_check("anything"))
-
- filter = CategoryFilter(["-", "+ab"])
- self.assertTrue(filter.should_check("abc"))
- self.assertFalse(filter.should_check("a"))
-
- filter = CategoryFilter(["+", "-ab"])
- self.assertFalse(filter.should_check("abc"))
- self.assertTrue(filter.should_check("a"))
-
class ProcessorOptionsTest(unittest.TestCase):
@@ -110,6 +56,7 @@ class ProcessorOptionsTest(unittest.TestCase):
self.assertEquals(options.extra_flag_values, {})
self.assertEquals(options.filter, CategoryFilter())
self.assertEquals(options.git_commit, None)
+ self.assertEquals(options.max_reports_per_category, {})
self.assertEquals(options.output_format, "emacs")
self.assertEquals(options.verbosity, 1)
@@ -126,11 +73,13 @@ class ProcessorOptionsTest(unittest.TestCase):
options = ProcessorOptions(extra_flag_values={"extra_value" : 2},
filter=CategoryFilter(["+"]),
git_commit="commit",
+ max_reports_per_category={"category": 3},
output_format="vs7",
verbosity=3)
self.assertEquals(options.extra_flag_values, {"extra_value" : 2})
self.assertEquals(options.filter, CategoryFilter(["+"]))
self.assertEquals(options.git_commit, "commit")
+ self.assertEquals(options.max_reports_per_category, {"category": 3})
self.assertEquals(options.output_format, "vs7")
self.assertEquals(options.verbosity, 3)
@@ -143,11 +92,14 @@ class ProcessorOptionsTest(unittest.TestCase):
options = ProcessorOptions(extra_flag_values={"extra_value" : 1},
filter=CategoryFilter(["+"]),
git_commit="commit",
+ max_reports_per_category={"category": 3},
output_format="vs7",
verbosity=1)
self.assertFalse(options == ProcessorOptions(extra_flag_values={"extra_value" : 2}))
self.assertFalse(options == ProcessorOptions(filter=CategoryFilter(["-"])))
self.assertFalse(options == ProcessorOptions(git_commit="commit2"))
+ self.assertFalse(options == ProcessorOptions(max_reports_per_category=
+ {"category": 2}))
self.assertFalse(options == ProcessorOptions(output_format="emacs"))
self.assertFalse(options == ProcessorOptions(verbosity=2))
@@ -173,9 +125,9 @@ class ProcessorOptionsTest(unittest.TestCase):
self.assertFalse(options.is_reportable("xyz", 3))
-class WebKitArgumentDefaultsTest(unittest.TestCase):
+class GlobalVariablesTest(unittest.TestCase):
- """Tests validity of default arguments used by check-webkit-style."""
+ """Tests validity of the global variables."""
def defaults(self):
return style.webkit_argument_defaults()
@@ -206,6 +158,13 @@ class WebKitArgumentDefaultsTest(unittest.TestCase):
# on valid arguments elsewhere.
parser.parse([]) # arguments valid: no error or SystemExit
+ def test_max_reports_per_category(self):
+ """Check that MAX_REPORTS_PER_CATEGORY is valid."""
+ categories = style.style_categories()
+ for category in style.MAX_REPORTS_PER_CATEGORY.iterkeys():
+ self.assertTrue(category in categories,
+ 'Key "%s" is not a category' % category)
+
class ArgumentPrinterTest(unittest.TestCase):
@@ -217,8 +176,11 @@ class ArgumentPrinterTest(unittest.TestCase):
filter_rules=[], git_commit=None,
extra_flag_values={}):
filter = CategoryFilter(filter_rules)
- return style.ProcessorOptions(output_format, verbosity, filter,
- git_commit, extra_flag_values)
+ return style.ProcessorOptions(extra_flag_values=extra_flag_values,
+ filter=filter,
+ git_commit=git_commit,
+ output_format=output_format,
+ verbosity=verbosity)
def test_to_flag_string(self):
options = self._create_options('vs7', 5, ['+foo', '-bar'], 'git',
diff --git a/WebKitTools/Scripts/webkitpy/style/error_handlers.py b/WebKitTools/Scripts/webkitpy/style/error_handlers.py
index 54b1d76..31140de 100644
--- a/WebKitTools/Scripts/webkitpy/style/error_handlers.py
+++ b/WebKitTools/Scripts/webkitpy/style/error_handlers.py
@@ -32,7 +32,9 @@ Methods:
Handle the occurrence of a style error.
- Check whether the error is reportable. If so, report the details.
+ Check whether the error is reportable. If so, increment the total
+ error count and report the details. Note that error reporting can
+ be suppressed after reaching a certain number of reports.
Args:
line_number: The integer line number of the line containing the error.
@@ -79,6 +81,28 @@ class DefaultStyleErrorHandler(object):
self._options = options
self._stderr_write = stderr_write
+ # A string to integer dictionary cache of the number of reportable
+ # errors per category passed to this instance.
+ self._category_totals = { }
+
+ def _add_reportable_error(self, category):
+ """Increment the error count and return the new category total."""
+ self._increment_error_count() # Increment the total.
+
+ # Increment the category total.
+ if not category in self._category_totals:
+ self._category_totals[category] = 1
+ else:
+ self._category_totals[category] += 1
+
+ return self._category_totals[category]
+
+ def _max_reports(self, category):
+ """Return the maximum number of errors to report."""
+ if not category in self._options.max_reports_per_category:
+ return None
+ return self._options.max_reports_per_category[category]
+
def __call__(self, line_number, category, confidence, message):
"""Handle the occurrence of a style error.
@@ -88,13 +112,23 @@ class DefaultStyleErrorHandler(object):
if not self._options.is_reportable(category, confidence):
return
- self._increment_error_count()
+ category_total = self._add_reportable_error(category)
+
+ max_reports = self._max_reports(category)
+
+ if (max_reports is not None) and (category_total > max_reports):
+ # Then suppress displaying the error.
+ return
if self._options.output_format == 'vs7':
format_string = "%s(%s): %s [%s] [%d]\n"
else:
format_string = "%s:%s: %s [%s] [%d]\n"
+ if category_total == max_reports:
+ format_string += ("Suppressing further [%s] reports for this "
+ "file.\n" % category)
+
self._stderr_write(format_string % (self._file_path,
line_number,
message,
@@ -130,7 +164,7 @@ class PatchStyleErrorHandler(object):
if not self._line_numbers:
for line in self._diff.lines:
# When deleted line is not set, it means that
- # the line is newly added.
+ # the line is newly added (or modified).
if not line[0]:
self._line_numbers.add(line[1])
@@ -140,9 +174,9 @@ class PatchStyleErrorHandler(object):
"""Handle the occurrence of a style error.
This function does not report errors occurring in lines not
- modified or added.
+ marked as modified or added in the patch.
- Args: see the DefaultStyleErrorHandler.__call__() documentation.
+ See the docstring of this module for more information.
"""
if line_number not in self._get_line_numbers():
diff --git a/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py b/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py
index 6a91ff2..83bdbb9 100644
--- a/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/style/error_handlers_unittest.py
@@ -50,9 +50,6 @@ class DefaultStyleErrorHandlerTest(StyleErrorHandlerTestBase):
_category = "whitespace/tab"
- def _options(self, output_format):
- return ProcessorOptions(verbosity=3, output_format=output_format)
-
def _error_handler(self, options):
file_path = "foo.h"
return DefaultStyleErrorHandler(file_path,
@@ -60,29 +57,28 @@ class DefaultStyleErrorHandlerTest(StyleErrorHandlerTestBase):
self._mock_increment_error_count,
self._mock_stderr_write)
- def _prepare_call(self, output_format="emacs"):
- """Return options after initializing."""
- options = self._options(output_format)
-
- # Test that count is initialized to zero.
+ def _check_initialized(self):
+ """Check that count and error messages are initialized."""
self.assertEquals(0, self._error_count)
self.assertEquals("", self._error_messages)
- return options
-
- def _call_error_handler(self, options, confidence):
- """Handle an error with given confidence."""
- handle_error = self._error_handler(options)
-
+ def _call(self, handle_error, options, confidence):
+ """Handle an error with the given error handler."""
line_number = 100
message = "message"
handle_error(line_number, self._category, confidence, message)
+ def _call_error_handler(self, options, confidence):
+ """Handle an error using a new error handler."""
+ handle_error = self._error_handler(options)
+ self._call(handle_error, options, confidence)
+
def test_call_non_reportable(self):
"""Test __call__() method with a non-reportable error."""
confidence = 1
- options = self._prepare_call()
+ options = ProcessorOptions(verbosity=3)
+ self._check_initialized()
# Confirm the error is not reportable.
self.assertFalse(options.is_reportable(self._category, confidence))
@@ -95,7 +91,8 @@ class DefaultStyleErrorHandlerTest(StyleErrorHandlerTestBase):
def test_call_reportable_emacs(self):
"""Test __call__() method with a reportable error and emacs format."""
confidence = 5
- options = self._prepare_call("emacs")
+ options = ProcessorOptions(verbosity=3, output_format="emacs")
+ self._check_initialized()
self._call_error_handler(options, confidence)
@@ -106,7 +103,8 @@ class DefaultStyleErrorHandlerTest(StyleErrorHandlerTestBase):
def test_call_reportable_vs7(self):
"""Test __call__() method with a reportable error and vs7 format."""
confidence = 5
- options = self._prepare_call("vs7")
+ options = ProcessorOptions(verbosity=3, output_format="vs7")
+ self._check_initialized()
self._call_error_handler(options, confidence)
@@ -114,6 +112,36 @@ class DefaultStyleErrorHandlerTest(StyleErrorHandlerTestBase):
self.assertEquals(self._error_messages,
"foo.h(100): message [whitespace/tab] [5]\n")
+ def test_call_max_reports_per_category(self):
+ """Test error report suppression in __call__() method."""
+ confidence = 5
+ options = ProcessorOptions(verbosity=3,
+ max_reports_per_category={self._category: 2})
+ error_handler = self._error_handler(options)
+
+ self._check_initialized()
+
+ # First call: usual reporting.
+ self._call(error_handler, options, confidence)
+ self.assertEquals(1, self._error_count)
+ self.assertEquals(self._error_messages,
+ "foo.h:100: message [whitespace/tab] [5]\n")
+
+ # Second call: suppression message reported.
+ self._error_messages = ""
+ self._call(error_handler, options, confidence)
+ self.assertEquals(2, self._error_count)
+ self.assertEquals(self._error_messages,
+ "foo.h:100: message [whitespace/tab] [5]\n"
+ "Suppressing further [%s] reports for this file.\n"
+ % self._category)
+
+ # Third call: no report.
+ self._error_messages = ""
+ self._call(error_handler, options, confidence)
+ self.assertEquals(3, self._error_count)
+ self.assertEquals(self._error_messages, "")
+
class PatchStyleErrorHandlerTest(StyleErrorHandlerTestBase):
diff --git a/WebKitTools/Scripts/webkitpy/style/filter.py b/WebKitTools/Scripts/webkitpy/style/filter.py
new file mode 100644
index 0000000..1b41424
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/style/filter.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+"""Contains filter-related code."""
+
+
+class CategoryFilter(object):
+
+ """Filters whether to check style categories."""
+
+ def __init__(self, filter_rules=None):
+ """Create a category filter.
+
+ This method performs argument validation but does not strip
+ leading or trailing white space.
+
+ Args:
+ filter_rules: A list of strings that are filter rules, which
+ are strings beginning with the plus or minus
+ symbol (+/-). The list should include any
+ default filter rules at the beginning.
+ Defaults to the empty list.
+
+ Raises:
+ ValueError: Invalid filter rule if a rule does not start with
+ plus ("+") or minus ("-").
+
+ """
+ if filter_rules is None:
+ filter_rules = []
+
+ for rule in filter_rules:
+ if not (rule.startswith('+') or rule.startswith('-')):
+ raise ValueError('Invalid filter rule "%s": every rule '
+ 'rule in the --filter flag must start '
+ 'with + or -.' % rule)
+
+ self._filter_rules = filter_rules
+ self._should_check_category = {} # Cached dictionary of category to True/False
+
+ def __str__(self):
+ return ",".join(self._filter_rules)
+
+ # Useful for unit testing.
+ def __eq__(self, other):
+ """Return whether this CategoryFilter instance is equal to another."""
+ return self._filter_rules == other._filter_rules
+
+ # Useful for unit testing.
+ def __ne__(self, other):
+ # Python does not automatically deduce from __eq__().
+ return not (self == other)
+
+ def should_check(self, category):
+ """Return whether the category should be checked.
+
+ The rules for determining whether a category should be checked
+ are as follows. By default all categories should be checked.
+ Then apply the filter rules in order from first to last, with
+ later flags taking precedence.
+
+ A filter rule applies to a category if the string after the
+ leading plus/minus (+/-) matches the beginning of the category
+ name. A plus (+) means the category should be checked, while a
+ minus (-) means the category should not be checked.
+
+ """
+ if category in self._should_check_category:
+ return self._should_check_category[category]
+
+ should_check = True # All categories checked by default.
+ for rule in self._filter_rules:
+ if not category.startswith(rule[1:]):
+ continue
+ should_check = rule.startswith('+')
+ self._should_check_category[category] = should_check # Update cache.
+ return should_check
+
diff --git a/WebKitTools/Scripts/webkitpy/style/filter_unittest.py b/WebKitTools/Scripts/webkitpy/style/filter_unittest.py
new file mode 100644
index 0000000..0b12123
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/style/filter_unittest.py
@@ -0,0 +1,84 @@
+# Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
+#
+# 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.
+
+"""Unit tests for filter.py."""
+
+
+import unittest
+
+from filter import CategoryFilter
+
+
+class CategoryFilterTest(unittest.TestCase):
+
+ """Tests CategoryFilter class."""
+
+ def test_init(self):
+ """Test __init__ constructor."""
+ self.assertRaises(ValueError, CategoryFilter, ["no_prefix"])
+ CategoryFilter() # No ValueError: works
+ CategoryFilter(["+"]) # No ValueError: works
+ CategoryFilter(["-"]) # No ValueError: works
+
+ def test_str(self):
+ """Test __str__ "to string" operator."""
+ filter = CategoryFilter(["+a", "-b"])
+ self.assertEquals(str(filter), "+a,-b")
+
+ def test_eq(self):
+ """Test __eq__ equality function."""
+ filter1 = CategoryFilter(["+a", "+b"])
+ filter2 = CategoryFilter(["+a", "+b"])
+ filter3 = CategoryFilter(["+b", "+a"])
+
+ # == calls __eq__.
+ self.assertTrue(filter1 == filter2)
+ self.assertFalse(filter1 == filter3) # Cannot test with assertNotEqual.
+
+ def test_ne(self):
+ """Test __ne__ inequality function."""
+ # != calls __ne__.
+ # By default, __ne__ always returns true on different objects.
+ # Thus, just check the distinguishing case to verify that the
+ # code defines __ne__.
+ self.assertFalse(CategoryFilter() != CategoryFilter())
+
+ def test_should_check(self):
+ """Test should_check() method."""
+ filter = CategoryFilter()
+ self.assertTrue(filter.should_check("everything"))
+ # Check a second time to exercise cache.
+ self.assertTrue(filter.should_check("everything"))
+
+ filter = CategoryFilter(["-"])
+ self.assertFalse(filter.should_check("anything"))
+ # Check a second time to exercise cache.
+ self.assertFalse(filter.should_check("anything"))
+
+ filter = CategoryFilter(["-", "+ab"])
+ self.assertTrue(filter.should_check("abc"))
+ self.assertFalse(filter.should_check("a"))
+
+ filter = CategoryFilter(["+", "-ab"])
+ self.assertFalse(filter.should_check("abc"))
+ self.assertTrue(filter.should_check("a"))
+
diff --git a/WebKitTools/Scripts/webkitpy/style/processors/common.py b/WebKitTools/Scripts/webkitpy/style/processors/common.py
new file mode 100644
index 0000000..dbf4bea
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/style/processors/common.py
@@ -0,0 +1,59 @@
+# 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 style checking not specific to any one processor."""
+
+
+# FIXME: Test this list in the same way that the list of CppProcessor
+# categories is tested, for example by checking that all of its
+# elements appear in the unit tests. This should probably be done
+# after moving the relevant cpp_unittest.ErrorCollector code
+# into a shared location and refactoring appropriately.
+categories = set([
+ "whitespace/carriage_return",
+])
+
+
+def check_no_carriage_return(line, line_number, error):
+ """Check that a line does not end with a carriage return.
+
+ Returns true if the check is successful (i.e. if the line does not
+ end with a carriage return), and false otherwise.
+
+ Args:
+ line: A string that is the line to check.
+ line_number: The line number.
+ error: The function to call with any errors found.
+
+ """
+
+ if line.endswith("\r"):
+ error(line_number,
+ "whitespace/carriage_return",
+ 1,
+ "One or more unexpected \\r (^M) found; "
+ "better to use only a \\n")
+ return False
+
+ return True
+
+
diff --git a/WebKitTools/Scripts/webkitpy/style/processors/common_unittest.py b/WebKitTools/Scripts/webkitpy/style/processors/common_unittest.py
new file mode 100644
index 0000000..9362b65
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/style/processors/common_unittest.py
@@ -0,0 +1,82 @@
+# 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.
+
+"""Unit tests for common.py."""
+
+
+import unittest
+
+from common import check_no_carriage_return
+
+
+# FIXME: The unit tests for the cpp, text, and common processors should
+# share supporting test code. This can include, for example, the
+# mock style error handling code and the code to check that all
+# of a processor's categories are covered by the unit tests.
+# Such shared code can be located in a shared test file, perhaps
+# ilke this one.
+class CarriageReturnTest(unittest.TestCase):
+
+ """Tests check_no_carriage_return()."""
+
+ _category = "whitespace/carriage_return"
+ _confidence = 1
+
+ def setUp(self):
+ self._style_errors = [] # The list of accumulated style errors.
+
+ def _mock_style_error_handler(self, line_number, category, confidence,
+ message):
+ """Append the error information to the list of style errors."""
+ error = (line_number, category, confidence, message)
+ self._style_errors.append(error)
+
+ def assert_carriage_return(self, line, is_error):
+ """Call check_no_carriage_return() and assert the result."""
+ line_number = 100
+ handle_style_error = self._mock_style_error_handler
+
+ check_no_carriage_return(line, line_number, handle_style_error)
+
+ expected_message = ("One or more unexpected \\r (^M) found; "
+ "better to use only a \\n")
+
+ if is_error:
+ expected_errors = [(line_number, self._category, self._confidence,
+ expected_message)]
+ self.assertEquals(self._style_errors, expected_errors)
+ else:
+ self.assertEquals(self._style_errors, [])
+
+ def test_ends_with_carriage(self):
+ self.assert_carriage_return("carriage return\r", is_error=True)
+
+ def test_ends_with_nothing(self):
+ self.assert_carriage_return("no carriage return", is_error=False)
+
+ def test_ends_with_newline(self):
+ self.assert_carriage_return("no carriage return\n", is_error=False)
+
+ def test_ends_with_carriage_newline(self):
+ # Check_no_carriage_return only() checks the final character.
+ self.assert_carriage_return("carriage\r in a string", is_error=False)
+
diff --git a/WebKitTools/Scripts/webkitpy/style/unittests.py b/WebKitTools/Scripts/webkitpy/style/unittests.py
index 11c10e7..f8e3f71 100644
--- a/WebKitTools/Scripts/webkitpy/style/unittests.py
+++ b/WebKitTools/Scripts/webkitpy/style/unittests.py
@@ -37,5 +37,7 @@ import unittest
from checker_unittest import *
from error_handlers_unittest import *
+from filter_unittest import *
+from processors.common_unittest import *
from processors.cpp_unittest import *
from processors.text_unittest import *