watchlist: Add a stylecheck to do validity checks for the watchlist config.
[WebKit-https.git] / Tools / Scripts / webkitpy / style / checker.py
1 # Copyright (C) 2009 Google Inc. All rights reserved.
2 # Copyright (C) 2010 Chris Jerdonek (chris.jerdonek@gmail.com)
3 # Copyright (C) 2010 ProFUSION embedded systems
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
8 #
9 #     * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 #     * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following disclaimer
13 # in the documentation and/or other materials provided with the
14 # distribution.
15 #     * Neither the name of Google Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31 """Front end of some style-checker modules."""
32
33 import logging
34 import os.path
35 import re
36 import sys
37
38 from checkers.common import categories as CommonCategories
39 from checkers.common import CarriageReturnChecker
40 from checkers.changelog import ChangeLogChecker
41 from checkers.cpp import CppChecker
42 from checkers.python import PythonChecker
43 from checkers.test_expectations import TestExpectationsChecker
44 from checkers.text import TextChecker
45 from checkers.watchlist import WatchListChecker
46 from checkers.xcodeproj import XcodeProjectFileChecker
47 from checkers.xml import XMLChecker
48 from error_handlers import DefaultStyleErrorHandler
49 from filter import FilterConfiguration
50 from optparser import ArgumentParser
51 from optparser import DefaultCommandOptionValues
52 from webkitpy.common.system.logutils import configure_logging as _configure_logging
53
54
55 _log = logging.getLogger(__name__)
56
57
58 # These are default option values for the command-line option parser.
59 _DEFAULT_MIN_CONFIDENCE = 1
60 _DEFAULT_OUTPUT_FORMAT = 'emacs'
61
62
63 # FIXME: For style categories we will never want to have, remove them.
64 #        For categories for which we want to have similar functionality,
65 #        modify the implementation and enable them.
66 #
67 # Throughout this module, we use "filter rule" rather than "filter"
68 # for an individual boolean filter flag like "+foo".  This allows us to
69 # reserve "filter" for what one gets by collectively applying all of
70 # the filter rules.
71 #
72 # The base filter rules are the filter rules that begin the list of
73 # filter rules used to check style.  For example, these rules precede
74 # any user-specified filter rules.  Since by default all categories are
75 # checked, this list should normally include only rules that begin
76 # with a "-" sign.
77 _BASE_FILTER_RULES = [
78     '-build/endif_comment',
79     '-build/include_what_you_use',  # <string> for std::string
80     '-build/storage_class',  # const static
81     '-legal/copyright',
82     '-readability/multiline_comment',
83     '-readability/braces',  # int foo() {};
84     '-readability/fn_size',
85     '-readability/casting',
86     '-readability/function',
87     '-runtime/arrays',  # variable length array
88     '-runtime/casting',
89     '-runtime/sizeof',
90     '-runtime/explicit',  # explicit
91     '-runtime/virtual',  # virtual dtor
92     '-runtime/printf',
93     '-runtime/threadsafe_fn',
94     '-runtime/rtti',
95     '-whitespace/blank_line',
96     '-whitespace/end_of_line',
97     '-whitespace/labels',
98     # List Python pep8 categories last.
99     #
100     # Because much of WebKit's Python code base does not abide by the
101     # PEP8 79 character limit, we ignore the 79-character-limit category
102     # pep8/E501 for now.
103     #
104     # FIXME: Consider bringing WebKit's Python code base into conformance
105     #        with the 79 character limit, or some higher limit that is
106     #        agreeable to the WebKit project.
107     '-pep8/E501',
108     ]
109
110
111 # The path-specific filter rules.
112 #
113 # This list is order sensitive.  Only the first path substring match
114 # is used.  See the FilterConfiguration documentation in filter.py
115 # for more information on this list.
116 #
117 # Each string appearing in this nested list should have at least
118 # one associated unit test assertion.  These assertions are located,
119 # for example, in the test_path_rules_specifier() unit test method of
120 # checker_unittest.py.
121 _PATH_RULES_SPECIFIER = [
122     # Files in these directories are consumers of the WebKit
123     # API and therefore do not follow the same header including
124     # discipline as WebCore.
125
126     ([# TestNetscapePlugIn has no config.h and uses funny names like
127       # NPP_SetWindow.
128       "Tools/DumpRenderTree/TestNetscapePlugIn/",
129       # The API test harnesses have no config.h and use funny macros like
130       # TEST_CLASS_NAME.
131       "Tools/WebKitAPITest/",
132       "Tools/TestWebKitAPI/",
133       "Source/WebKit/qt/tests/qdeclarativewebview"],
134      ["-build/include",
135       "-readability/naming"]),
136     ([# There is no clean way to avoid "yy_*" names used by flex.
137       "Source/WebCore/css/CSSParser.cpp",
138       # Qt code uses '_' in some places (such as private slots
139       # and on test xxx_data methos on tests)
140       "Source/JavaScriptCore/qt/",
141       "Source/WebKit/qt/Api/",
142       "Source/WebKit/qt/tests/",
143       "Source/WebKit/qt/declarative/",
144       "Source/WebKit/qt/examples/"],
145      ["-readability/naming"]),
146      ([# Qt's MiniBrowser has no config.h
147        "Tools/MiniBrowser/qt"],
148       ["-build/include"]),
149
150     ([# The Qt APIs use Qt/QML naming style, which includes
151       # naming parameters in h files.
152       "Source/WebKit2/UIProcess/API/qt"],
153      ["-readability/parameter_name"]),
154
155     ([# The GTK+ APIs use GTK+ naming style, which includes
156       # lower-cased, underscore-separated values, whitespace before
157       # parens for function calls, and always having variable names.
158       # Also, GTK+ allows the use of NULL.
159       "Source/WebCore/bindings/scripts/test/GObject",
160       "Source/WebKit/gtk/webkit/",
161       "Tools/DumpRenderTree/gtk/"],
162      ["-readability/naming",
163       "-readability/parameter_name",
164       "-readability/null",
165       "-whitespace/parens"]),
166     ([# Header files in ForwardingHeaders have no header guards or
167       # exceptional header guards (e.g., WebCore_FWD_Debugger_h).
168       "/ForwardingHeaders/"],
169      ["-build/header_guard"]),
170     ([# assembler has lots of opcodes that use underscores, so
171       # we don't check for underscores in that directory.
172       "/Source/JavaScriptCore/assembler/"],
173      ["-readability/naming"]),
174     ([# JITStubs has an usual syntax which causes false alarms for a few checks.
175       "JavaScriptCore/jit/JITStubs.cpp"],
176      ["-readability/parameter_name",
177       "-whitespace/parens"]),
178
179     ([# The EFL APIs use EFL naming style, which includes
180       # both lower-cased and camel-cased, underscore-sparated
181       # values.
182       "Source/WebKit/efl/ewk/",
183       "Source/WebKit2/UIProcess/API/efl/",
184       "Tools/EWebLauncher/",
185       "Tools/MiniBrowser/efl/"],
186      ["-readability/naming",
187       "-readability/parameter_name",
188       "-whitespace/declaration"]),
189
190     # WebKit2 rules:
191     # WebKit2 and certain directories have idiosyncracies.
192     ([# NPAPI has function names with underscores.
193       "Source/WebKit2/WebProcess/Plugins/Netscape"],
194      ["-readability/naming"]),
195     ([# The WebKit2 C API has names with underscores and whitespace-aligned
196       # struct members. Also, we allow unnecessary parameter names in
197       # WebKit2 APIs because we're matching CF's header style.
198       "Source/WebKit2/UIProcess/API/C/",
199       "Source/WebKit2/Shared/API/c/",
200       "Source/WebKit2/WebProcess/InjectedBundle/API/c/"],
201      ["-readability/naming",
202       "-readability/parameter_name",
203       "-whitespace/declaration"]),
204
205     # For third-party Python code, keep only the following checks--
206     #
207     #   No tabs: to avoid having to set the SVN allow-tabs property.
208     #   No trailing white space: since this is easy to correct.
209     #   No carriage-return line endings: since this is easy to correct.
210     #
211     (["webkitpy/thirdparty/"],
212      ["-",
213       "+pep8/W191",  # Tabs
214       "+pep8/W291",  # Trailing white space
215       "+whitespace/carriage_return"]),
216
217     ([# glu's libtess is third-party code, and doesn't follow WebKit style.
218       "Source/ThirdParty/glu"],
219      ["-readability",
220       "-whitespace",
221       "-build/header_guard",
222       "-build/include_order"]),
223 ]
224
225
226 _CPP_FILE_EXTENSIONS = [
227     'c',
228     'cpp',
229     'h',
230     ]
231
232 _PYTHON_FILE_EXTENSION = 'py'
233
234 _TEXT_FILE_EXTENSIONS = [
235     'ac',
236     'cc',
237     'cgi',
238     'css',
239     'exp',
240     'flex',
241     'gyp',
242     'gypi',
243     'html',
244     'idl',
245     'in',
246     'js',
247     'mm',
248     'php',
249     'pl',
250     'pm',
251     'pri',
252     'pro',
253     'rb',
254     'sh',
255     'txt',
256     'wm',
257     'xhtml',
258     'y',
259     ]
260
261 _XCODEPROJ_FILE_EXTENSION = 'pbxproj'
262
263 _XML_FILE_EXTENSIONS = [
264     'vcproj',
265     'vsprops',
266     ]
267
268 # Files to skip that are less obvious.
269 #
270 # Some files should be skipped when checking style. For example,
271 # WebKit maintains some files in Mozilla style on purpose to ease
272 # future merges.
273 _SKIPPED_FILES_WITH_WARNING = [
274     "Source/WebKit/gtk/tests/",
275     # All WebKit*.h files in Source/WebKit2/UIProcess/API/gtk,
276     # except those ending in ...Private.h are GTK+ API headers,
277     # which differ greatly from WebKit coding style.
278     re.compile(r'Source/WebKit2/UIProcess/API/gtk/WebKit(?!.*Private\.h).*\.h$'),
279     'Source/WebKit2/UIProcess/API/gtk/webkit2.h']
280
281 # Files to skip that are more common or obvious.
282 #
283 # This list should be in addition to files with FileType.NONE.  Files
284 # with FileType.NONE are automatically skipped without warning.
285 _SKIPPED_FILES_WITHOUT_WARNING = [
286     "LayoutTests" + os.path.sep,
287     ]
288
289 # Extensions of files which are allowed to contain carriage returns.
290 _CARRIAGE_RETURN_ALLOWED_FILE_EXTENSIONS = [
291     'vcproj',
292     'vsprops',
293     ]
294
295 # The maximum number of errors to report per file, per category.
296 # If a category is not a key, then it has no maximum.
297 _MAX_REPORTS_PER_CATEGORY = {
298     "whitespace/carriage_return": 1
299 }
300
301
302 def _all_categories():
303     """Return the set of all categories used by check-webkit-style."""
304     # Take the union across all checkers.
305     categories = CommonCategories.union(CppChecker.categories)
306     categories = categories.union(TestExpectationsChecker.categories)
307     categories = categories.union(ChangeLogChecker.categories)
308
309     # FIXME: Consider adding all of the pep8 categories.  Since they
310     #        are not too meaningful for documentation purposes, for
311     #        now we add only the categories needed for the unit tests
312     #        (which validate the consistency of the configuration
313     #        settings against the known categories, etc).
314     categories = categories.union(["pep8/W191", "pep8/W291", "pep8/E501"])
315
316     return categories
317
318
319 def _check_webkit_style_defaults():
320     """Return the default command-line options for check-webkit-style."""
321     return DefaultCommandOptionValues(min_confidence=_DEFAULT_MIN_CONFIDENCE,
322                                       output_format=_DEFAULT_OUTPUT_FORMAT)
323
324
325 # This function assists in optparser not having to import from checker.
326 def check_webkit_style_parser():
327     all_categories = _all_categories()
328     default_options = _check_webkit_style_defaults()
329     return ArgumentParser(all_categories=all_categories,
330                           base_filter_rules=_BASE_FILTER_RULES,
331                           default_options=default_options)
332
333
334 def check_webkit_style_configuration(options):
335     """Return a StyleProcessorConfiguration instance for check-webkit-style.
336
337     Args:
338       options: A CommandOptionValues instance.
339
340     """
341     filter_configuration = FilterConfiguration(
342                                base_rules=_BASE_FILTER_RULES,
343                                path_specific=_PATH_RULES_SPECIFIER,
344                                user_rules=options.filter_rules)
345
346     return StyleProcessorConfiguration(filter_configuration=filter_configuration,
347                max_reports_per_category=_MAX_REPORTS_PER_CATEGORY,
348                min_confidence=options.min_confidence,
349                output_format=options.output_format,
350                stderr_write=sys.stderr.write)
351
352
353 def _create_log_handlers(stream):
354     """Create and return a default list of logging.Handler instances.
355
356     Format WARNING messages and above to display the logging level, and
357     messages strictly below WARNING not to display it.
358
359     Args:
360       stream: See the configure_logging() docstring.
361
362     """
363     # Handles logging.WARNING and above.
364     error_handler = logging.StreamHandler(stream)
365     error_handler.setLevel(logging.WARNING)
366     formatter = logging.Formatter("%(levelname)s: %(message)s")
367     error_handler.setFormatter(formatter)
368
369     # Create a logging.Filter instance that only accepts messages
370     # below WARNING (i.e. filters out anything WARNING or above).
371     non_error_filter = logging.Filter()
372     # The filter method accepts a logging.LogRecord instance.
373     non_error_filter.filter = lambda record: record.levelno < logging.WARNING
374
375     non_error_handler = logging.StreamHandler(stream)
376     non_error_handler.addFilter(non_error_filter)
377     formatter = logging.Formatter("%(message)s")
378     non_error_handler.setFormatter(formatter)
379
380     return [error_handler, non_error_handler]
381
382
383 def _create_debug_log_handlers(stream):
384     """Create and return a list of logging.Handler instances for debugging.
385
386     Args:
387       stream: See the configure_logging() docstring.
388
389     """
390     handler = logging.StreamHandler(stream)
391     formatter = logging.Formatter("%(name)s: %(levelname)-8s %(message)s")
392     handler.setFormatter(formatter)
393
394     return [handler]
395
396
397 def configure_logging(stream, logger=None, is_verbose=False):
398     """Configure logging, and return the list of handlers added.
399
400     Returns:
401       A list of references to the logging handlers added to the root
402       logger.  This allows the caller to later remove the handlers
403       using logger.removeHandler.  This is useful primarily during unit
404       testing where the caller may want to configure logging temporarily
405       and then undo the configuring.
406
407     Args:
408       stream: A file-like object to which to log.  The stream must
409               define an "encoding" data attribute, or else logging
410               raises an error.
411       logger: A logging.logger instance to configure.  This parameter
412               should be used only in unit tests.  Defaults to the
413               root logger.
414       is_verbose: A boolean value of whether logging should be verbose.
415
416     """
417     # If the stream does not define an "encoding" data attribute, the
418     # logging module can throw an error like the following:
419     #
420     # Traceback (most recent call last):
421     #   File "/System/Library/Frameworks/Python.framework/Versions/2.6/...
422     #         lib/python2.6/logging/__init__.py", line 761, in emit
423     #     self.stream.write(fs % msg.encode(self.stream.encoding))
424     # LookupError: unknown encoding: unknown
425     if logger is None:
426         logger = logging.getLogger()
427
428     if is_verbose:
429         logging_level = logging.DEBUG
430         handlers = _create_debug_log_handlers(stream)
431     else:
432         logging_level = logging.INFO
433         handlers = _create_log_handlers(stream)
434
435     handlers = _configure_logging(logging_level=logging_level, logger=logger,
436                                   handlers=handlers)
437
438     return handlers
439
440
441 # Enum-like idiom
442 class FileType:
443
444     NONE = 0  # FileType.NONE evaluates to False.
445     # Alphabetize remaining types
446     CHANGELOG = 1
447     CPP = 2
448     PYTHON = 3
449     TEXT = 4
450     WATCHLIST = 5
451     XML = 6
452     XCODEPROJ = 7
453
454
455 class CheckerDispatcher(object):
456
457     """Supports determining whether and how to check style, based on path."""
458
459     def _file_extension(self, file_path):
460         """Return the file extension without the leading dot."""
461         return os.path.splitext(file_path)[1].lstrip(".")
462
463     def _should_skip_file_path(self, file_path, skip_array_entry):
464         if isinstance(skip_array_entry, str):
465             if file_path.find(skip_array_entry) >= 0:
466                 return True
467         elif skip_array_entry.match(file_path):
468                 return True
469         return False
470
471     def should_skip_with_warning(self, file_path):
472         """Return whether the given file should be skipped with a warning."""
473         for skipped_file in _SKIPPED_FILES_WITH_WARNING:
474             if self._should_skip_file_path(file_path, skipped_file):
475                 return True
476         return False
477
478     def should_skip_without_warning(self, file_path):
479         """Return whether the given file should be skipped without a warning."""
480         if not self._file_type(file_path):  # FileType.NONE.
481             return True
482         # Since "LayoutTests" is in _SKIPPED_FILES_WITHOUT_WARNING, make
483         # an exception to prevent files like "LayoutTests/ChangeLog" and
484         # "LayoutTests/ChangeLog-2009-06-16" from being skipped.
485         # Files like 'test_expectations.txt' and 'drt_expectations.txt'
486         # are also should not be skipped.
487         #
488         # FIXME: Figure out a good way to avoid having to add special logic
489         #        for this special case.
490         basename = os.path.basename(file_path)
491         if basename.startswith('ChangeLog'):
492             return False
493         elif basename == 'test_expectations.txt' or basename == 'drt_expectations.txt':
494             return False
495         for skipped_file in _SKIPPED_FILES_WITHOUT_WARNING:
496             if self._should_skip_file_path(file_path, skipped_file):
497                 return True
498         return False
499
500     def should_check_and_strip_carriage_returns(self, file_path):
501         return self._file_extension(file_path) not in _CARRIAGE_RETURN_ALLOWED_FILE_EXTENSIONS
502
503     def _file_type(self, file_path):
504         """Return the file type corresponding to the given file."""
505         file_extension = self._file_extension(file_path)
506
507         if (file_extension in _CPP_FILE_EXTENSIONS) or (file_path == '-'):
508             # FIXME: Do something about the comment below and the issue it
509             #        raises since cpp_style already relies on the extension.
510             #
511             # Treat stdin as C++. Since the extension is unknown when
512             # reading from stdin, cpp_style tests should not rely on
513             # the extension.
514             return FileType.CPP
515         elif file_extension == _PYTHON_FILE_EXTENSION:
516             return FileType.PYTHON
517         elif file_extension in _XML_FILE_EXTENSIONS:
518             return FileType.XML
519         elif os.path.basename(file_path).startswith('ChangeLog'):
520             return FileType.CHANGELOG
521         elif os.path.basename(file_path) == 'watchlist':
522             return FileType.WATCHLIST
523         elif file_extension == _XCODEPROJ_FILE_EXTENSION:
524             return FileType.XCODEPROJ
525         elif ((not file_extension and os.path.join("Tools", "Scripts") in file_path) or
526               file_extension in _TEXT_FILE_EXTENSIONS):
527             return FileType.TEXT
528         else:
529             return FileType.NONE
530
531     def _create_checker(self, file_type, file_path, handle_style_error,
532                         min_confidence):
533         """Instantiate and return a style checker based on file type."""
534         if file_type == FileType.NONE:
535             checker = None
536         elif file_type == FileType.CHANGELOG:
537             should_line_be_checked = None
538             if handle_style_error:
539                 should_line_be_checked = handle_style_error.should_line_be_checked
540             checker = ChangeLogChecker(file_path, handle_style_error, should_line_be_checked)
541         elif file_type == FileType.CPP:
542             file_extension = self._file_extension(file_path)
543             checker = CppChecker(file_path, file_extension,
544                                  handle_style_error, min_confidence)
545         elif file_type == FileType.PYTHON:
546             checker = PythonChecker(file_path, handle_style_error)
547         elif file_type == FileType.XML:
548             checker = XMLChecker(file_path, handle_style_error)
549         elif file_type == FileType.XCODEPROJ:
550             checker = XcodeProjectFileChecker(file_path, handle_style_error)
551         elif file_type == FileType.TEXT:
552             basename = os.path.basename(file_path)
553             if basename == 'test_expectations.txt' or basename == 'drt_expectations.txt':
554                 checker = TestExpectationsChecker(file_path, handle_style_error)
555             else:
556                 checker = TextChecker(file_path, handle_style_error)
557         elif file_type == FileType.WATCHLIST:
558             checker = WatchListChecker(file_path, handle_style_error)
559         else:
560             raise ValueError('Invalid file type "%(file_type)s": the only valid file types '
561                              "are %(NONE)s, %(CPP)s, and %(TEXT)s."
562                              % {"file_type": file_type,
563                                 "NONE": FileType.NONE,
564                                 "CPP": FileType.CPP,
565                                 "TEXT": FileType.TEXT})
566
567         return checker
568
569     def dispatch(self, file_path, handle_style_error, min_confidence):
570         """Instantiate and return a style checker based on file path."""
571         file_type = self._file_type(file_path)
572
573         checker = self._create_checker(file_type,
574                                        file_path,
575                                        handle_style_error,
576                                        min_confidence)
577         return checker
578
579
580 # FIXME: Remove the stderr_write attribute from this class and replace
581 #        its use with calls to a logging module logger.
582 class StyleProcessorConfiguration(object):
583
584     """Stores configuration values for the StyleProcessor class.
585
586     Attributes:
587       min_confidence: An integer between 1 and 5 inclusive that is the
588                       minimum confidence level of style errors to report.
589
590       max_reports_per_category: The maximum number of errors to report
591                                 per category, per file.
592
593       stderr_write: A function that takes a string as a parameter and
594                     serves as stderr.write.
595
596     """
597
598     def __init__(self,
599                  filter_configuration,
600                  max_reports_per_category,
601                  min_confidence,
602                  output_format,
603                  stderr_write):
604         """Create a StyleProcessorConfiguration instance.
605
606         Args:
607           filter_configuration: A FilterConfiguration instance.  The default
608                                 is the "empty" filter configuration, which
609                                 means that all errors should be checked.
610
611           max_reports_per_category: The maximum number of errors to report
612                                     per category, per file.
613
614           min_confidence: An integer between 1 and 5 inclusive that is the
615                           minimum confidence level of style errors to report.
616                           The default is 1, which reports all style errors.
617
618           output_format: A string that is the output format.  The supported
619                          output formats are "emacs" which emacs can parse
620                          and "vs7" which Microsoft Visual Studio 7 can parse.
621
622           stderr_write: A function that takes a string as a parameter and
623                         serves as stderr.write.
624
625         """
626         self._filter_configuration = filter_configuration
627         self._output_format = output_format
628
629         self.max_reports_per_category = max_reports_per_category
630         self.min_confidence = min_confidence
631         self.stderr_write = stderr_write
632
633     def is_reportable(self, category, confidence_in_error, file_path):
634         """Return whether an error is reportable.
635
636         An error is reportable if both the confidence in the error is
637         at least the minimum confidence level and the current filter
638         says the category should be checked for the given path.
639
640         Args:
641           category: A string that is a style category.
642           confidence_in_error: An integer between 1 and 5 inclusive that is
643                                the application's confidence in the error.
644                                A higher number means greater confidence.
645           file_path: The path of the file being checked
646
647         """
648         if confidence_in_error < self.min_confidence:
649             return False
650
651         return self._filter_configuration.should_check(category, file_path)
652
653     def write_style_error(self,
654                           category,
655                           confidence_in_error,
656                           file_path,
657                           line_number,
658                           message):
659         """Write a style error to the configured stderr."""
660         if self._output_format == 'vs7':
661             format_string = "%s(%s):  %s  [%s] [%d]\n"
662         else:
663             format_string = "%s:%s:  %s  [%s] [%d]\n"
664
665         self.stderr_write(format_string % (file_path,
666                                            line_number,
667                                            message,
668                                            category,
669                                            confidence_in_error))
670
671
672 class ProcessorBase(object):
673
674     """The base class for processors of lists of lines."""
675
676     def should_process(self, file_path):
677         """Return whether the file at file_path should be processed.
678
679         The TextFileReader class calls this method prior to reading in
680         the lines of a file.  Use this method, for example, to prevent
681         the style checker from reading binary files into memory.
682
683         """
684         raise NotImplementedError('Subclasses should implement.')
685
686     def process(self, lines, file_path, **kwargs):
687         """Process lines of text read from a file.
688
689         Args:
690           lines: A list of lines of text to process.
691           file_path: The path from which the lines were read.
692           **kwargs: This argument signifies that the process() method of
693                     subclasses of ProcessorBase may support additional
694                     keyword arguments.
695                         For example, a style checker's check() method
696                     may support a "reportable_lines" parameter that represents
697                     the line numbers of the lines for which style errors
698                     should be reported.
699
700         """
701         raise NotImplementedError('Subclasses should implement.')
702
703
704 class StyleProcessor(ProcessorBase):
705
706     """A ProcessorBase for checking style.
707
708     Attributes:
709       error_count: An integer that is the total number of reported
710                    errors for the lifetime of this instance.
711
712     """
713
714     def __init__(self, configuration, mock_dispatcher=None,
715                  mock_increment_error_count=None,
716                  mock_carriage_checker_class=None):
717         """Create an instance.
718
719         Args:
720           configuration: A StyleProcessorConfiguration instance.
721           mock_dispatcher: A mock CheckerDispatcher instance.  This
722                            parameter is for unit testing.  Defaults to a
723                            CheckerDispatcher instance.
724           mock_increment_error_count: A mock error-count incrementer.
725           mock_carriage_checker_class: A mock class for checking and
726                                        transforming carriage returns.
727                                        This parameter is for unit testing.
728                                        Defaults to CarriageReturnChecker.
729
730         """
731         if mock_dispatcher is None:
732             dispatcher = CheckerDispatcher()
733         else:
734             dispatcher = mock_dispatcher
735
736         if mock_increment_error_count is None:
737             # The following blank line is present to avoid flagging by pep8.py.
738
739             def increment_error_count():
740                 """Increment the total count of reported errors."""
741                 self.error_count += 1
742         else:
743             increment_error_count = mock_increment_error_count
744
745         if mock_carriage_checker_class is None:
746             # This needs to be a class rather than an instance since the
747             # process() method instantiates one using parameters.
748             carriage_checker_class = CarriageReturnChecker
749         else:
750             carriage_checker_class = mock_carriage_checker_class
751
752         self.error_count = 0
753
754         self._carriage_checker_class = carriage_checker_class
755         self._configuration = configuration
756         self._dispatcher = dispatcher
757         self._increment_error_count = increment_error_count
758
759     def should_process(self, file_path):
760         """Return whether the file should be checked for style."""
761         if self._dispatcher.should_skip_without_warning(file_path):
762             return False
763         if self._dispatcher.should_skip_with_warning(file_path):
764             _log.warn('File exempt from style guide. Skipping: "%s"'
765                       % file_path)
766             return False
767         return True
768
769     def process(self, lines, file_path, line_numbers=None):
770         """Check the given lines for style.
771
772         Arguments:
773           lines: A list of all lines in the file to check.
774           file_path: The path of the file to process.  If possible, the path
775                      should be relative to the source root.  Otherwise,
776                      path-specific logic may not behave as expected.
777           line_numbers: A list of line numbers of the lines for which
778                         style errors should be reported, or None if errors
779                         for all lines should be reported.  When not None, this
780                         list normally contains the line numbers corresponding
781                         to the modified lines of a patch.
782
783         """
784         _log.debug("Checking style: " + file_path)
785
786         style_error_handler = DefaultStyleErrorHandler(
787             configuration=self._configuration,
788             file_path=file_path,
789             increment_error_count=self._increment_error_count,
790             line_numbers=line_numbers)
791
792         carriage_checker = self._carriage_checker_class(style_error_handler)
793
794         # Check for and remove trailing carriage returns ("\r").
795         if self._dispatcher.should_check_and_strip_carriage_returns(file_path):
796             lines = carriage_checker.check(lines)
797
798         min_confidence = self._configuration.min_confidence
799         checker = self._dispatcher.dispatch(file_path,
800                                             style_error_handler,
801                                             min_confidence)
802
803         if checker is None:
804             raise AssertionError("File should not be checked: '%s'" % file_path)
805
806         _log.debug("Using class: " + checker.__class__.__name__)
807
808         checker.check(lines)