WebKitTools/Scripts/webkitpy/style/checker.py
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     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 sys
       
    36 
       
    37 from checkers.common import categories as CommonCategories
       
    38 from checkers.common import CarriageReturnChecker
       
    39 from checkers.cpp import CppChecker
       
    40 from checkers.python import PythonChecker
       
    41 from checkers.text import TextChecker
       
    42 from error_handlers import DefaultStyleErrorHandler
       
    43 from filter import FilterConfiguration
       
    44 from optparser import ArgumentParser
       
    45 from optparser import DefaultCommandOptionValues
       
    46 from webkitpy.style_references import configure_logging as _configure_logging
       
    47 
       
    48 _log = logging.getLogger("webkitpy.style.checker")
       
    49 
       
    50 # These are default option values for the command-line option parser.
       
    51 _DEFAULT_MIN_CONFIDENCE = 1
       
    52 _DEFAULT_OUTPUT_FORMAT = 'emacs'
       
    53 
       
    54 
       
    55 # FIXME: For style categories we will never want to have, remove them.
       
    56 #        For categories for which we want to have similar functionality,
       
    57 #        modify the implementation and enable them.
       
    58 #
       
    59 # Throughout this module, we use "filter rule" rather than "filter"
       
    60 # for an individual boolean filter flag like "+foo".  This allows us to
       
    61 # reserve "filter" for what one gets by collectively applying all of
       
    62 # the filter rules.
       
    63 #
       
    64 # The base filter rules are the filter rules that begin the list of
       
    65 # filter rules used to check style.  For example, these rules precede
       
    66 # any user-specified filter rules.  Since by default all categories are
       
    67 # checked, this list should normally include only rules that begin
       
    68 # with a "-" sign.
       
    69 _BASE_FILTER_RULES = [
       
    70     '-build/endif_comment',
       
    71     '-build/include_what_you_use',  # <string> for std::string
       
    72     '-build/storage_class',  # const static
       
    73     '-legal/copyright',
       
    74     '-readability/multiline_comment',
       
    75     '-readability/braces',  # int foo() {};
       
    76     '-readability/fn_size',
       
    77     '-readability/casting',
       
    78     '-readability/function',
       
    79     '-runtime/arrays',  # variable length array
       
    80     '-runtime/casting',
       
    81     '-runtime/sizeof',
       
    82     '-runtime/explicit',  # explicit
       
    83     '-runtime/virtual',  # virtual dtor
       
    84     '-runtime/printf',
       
    85     '-runtime/threadsafe_fn',
       
    86     '-runtime/rtti',
       
    87     '-whitespace/blank_line',
       
    88     '-whitespace/end_of_line',
       
    89     '-whitespace/labels',
       
    90     # List Python pep8 categories last.
       
    91     #
       
    92     # Because much of WebKit's Python code base does not abide by the
       
    93     # PEP8 79 character limit, we ignore the 79-character-limit category
       
    94     # pep8/E501 for now.
       
    95     #
       
    96     # FIXME: Consider bringing WebKit's Python code base into conformance
       
    97     #        with the 79 character limit, or some higher limit that is
       
    98     #        agreeable to the WebKit project.
       
    99     '-pep8/E501',
       
   100     ]
       
   101 
       
   102 
       
   103 # The path-specific filter rules.
       
   104 #
       
   105 # This list is order sensitive.  Only the first path substring match
       
   106 # is used.  See the FilterConfiguration documentation in filter.py
       
   107 # for more information on this list.
       
   108 #
       
   109 # Each string appearing in this nested list should have at least
       
   110 # one associated unit test assertion.  These assertions are located,
       
   111 # for example, in the test_path_rules_specifier() unit test method of
       
   112 # checker_unittest.py.
       
   113 _PATH_RULES_SPECIFIER = [
       
   114     # Files in these directories are consumers of the WebKit
       
   115     # API and therefore do not follow the same header including
       
   116     # discipline as WebCore.
       
   117     (["WebKitTools/WebKitAPITest/",
       
   118       "WebKit/qt/QGVLauncher/"],
       
   119      ["-build/include",
       
   120       "-readability/streams"]),
       
   121     ([# The EFL APIs use EFL naming style, which includes
       
   122       # both lower-cased and camel-cased, underscore-sparated
       
   123       # values.
       
   124       "WebKit/efl/ewk/",
       
   125       # There is no clean way to avoid "yy_*" names used by flex.
       
   126       "WebCore/css/CSSParser.cpp",
       
   127       # There is no clean way to avoid "xxx_data" methods inside
       
   128       # Qt's autotests since they are called automatically by the
       
   129       # QtTest module.
       
   130       "WebKit/qt/tests/",
       
   131       "JavaScriptCore/qt/tests"],
       
   132      ["-readability/naming"]),
       
   133     ([# The GTK+ APIs use GTK+ naming style, which includes
       
   134       # lower-cased, underscore-separated values.
       
   135       # Also, GTK+ allows the use of NULL.
       
   136       "WebKit/gtk/webkit/",
       
   137       "WebKitTools/DumpRenderTree/gtk/"],
       
   138      ["-readability/naming",
       
   139       "-readability/null"]),
       
   140     ([# Header files in ForwardingHeaders have no header guards or
       
   141       # exceptional header guards (e.g., WebCore_FWD_Debugger_h).
       
   142       "/ForwardingHeaders/"],
       
   143      ["-build/header_guard"]),
       
   144 
       
   145     # For third-party Python code, keep only the following checks--
       
   146     #
       
   147     #   No tabs: to avoid having to set the SVN allow-tabs property.
       
   148     #   No trailing white space: since this is easy to correct.
       
   149     #   No carriage-return line endings: since this is easy to correct.
       
   150     #
       
   151     (["webkitpy/thirdparty/"],
       
   152      ["-",
       
   153       "+pep8/W191",  # Tabs
       
   154       "+pep8/W291",  # Trailing white space
       
   155       "+whitespace/carriage_return"]),
       
   156 ]
       
   157 
       
   158 
       
   159 _CPP_FILE_EXTENSIONS = [
       
   160     'c',
       
   161     'cpp',
       
   162     'h',
       
   163     ]
       
   164 
       
   165 _PYTHON_FILE_EXTENSION = 'py'
       
   166 
       
   167 # FIXME: Include 'vcproj' files as text files after creating a mechanism
       
   168 #        for exempting them from the carriage-return checker (since they
       
   169 #        are Windows-only files).
       
   170 _TEXT_FILE_EXTENSIONS = [
       
   171     'ac',
       
   172     'cc',
       
   173     'cgi',
       
   174     'css',
       
   175     'exp',
       
   176     'flex',
       
   177     'gyp',
       
   178     'gypi',
       
   179     'html',
       
   180     'idl',
       
   181     'in',
       
   182     'js',
       
   183     'mm',
       
   184     'php',
       
   185     'pl',
       
   186     'pm',
       
   187     'pri',
       
   188     'pro',
       
   189     'rb',
       
   190     'sh',
       
   191     'txt',
       
   192 #   'vcproj',  # See FIXME above.
       
   193     'wm',
       
   194     'xhtml',
       
   195     'y',
       
   196     ]
       
   197 
       
   198 
       
   199 # Files to skip that are less obvious.
       
   200 #
       
   201 # Some files should be skipped when checking style. For example,
       
   202 # WebKit maintains some files in Mozilla style on purpose to ease
       
   203 # future merges.
       
   204 _SKIPPED_FILES_WITH_WARNING = [
       
   205     # The Qt API and tests do not follow WebKit style.
       
   206     # They follow Qt style. :)
       
   207     "gtk2drawing.c", # WebCore/platform/gtk/gtk2drawing.c
       
   208     "gtkdrawing.h", # WebCore/platform/gtk/gtkdrawing.h
       
   209     "JavaScriptCore/qt/api/",
       
   210     "WebKit/gtk/tests/",
       
   211     "WebKit/qt/Api/",
       
   212     "WebKit/qt/tests/",
       
   213     "WebKit/qt/examples/",
       
   214     ]
       
   215 
       
   216 
       
   217 # Files to skip that are more common or obvious.
       
   218 #
       
   219 # This list should be in addition to files with FileType.NONE.  Files
       
   220 # with FileType.NONE are automatically skipped without warning.
       
   221 _SKIPPED_FILES_WITHOUT_WARNING = [
       
   222     "LayoutTests/",
       
   223     ]
       
   224 
       
   225 
       
   226 # The maximum number of errors to report per file, per category.
       
   227 # If a category is not a key, then it has no maximum.
       
   228 _MAX_REPORTS_PER_CATEGORY = {
       
   229     "whitespace/carriage_return": 1
       
   230 }
       
   231 
       
   232 
       
   233 def _all_categories():
       
   234     """Return the set of all categories used by check-webkit-style."""
       
   235     # Take the union across all checkers.
       
   236     categories = CommonCategories.union(CppChecker.categories)
       
   237 
       
   238     # FIXME: Consider adding all of the pep8 categories.  Since they
       
   239     #        are not too meaningful for documentation purposes, for
       
   240     #        now we add only the categories needed for the unit tests
       
   241     #        (which validate the consistency of the configuration
       
   242     #        settings against the known categories, etc).
       
   243     categories = categories.union(["pep8/W191", "pep8/W291", "pep8/E501"])
       
   244 
       
   245     return categories
       
   246 
       
   247 
       
   248 def _check_webkit_style_defaults():
       
   249     """Return the default command-line options for check-webkit-style."""
       
   250     return DefaultCommandOptionValues(min_confidence=_DEFAULT_MIN_CONFIDENCE,
       
   251                                       output_format=_DEFAULT_OUTPUT_FORMAT)
       
   252 
       
   253 
       
   254 # This function assists in optparser not having to import from checker.
       
   255 def check_webkit_style_parser():
       
   256     all_categories = _all_categories()
       
   257     default_options = _check_webkit_style_defaults()
       
   258     return ArgumentParser(all_categories=all_categories,
       
   259                           base_filter_rules=_BASE_FILTER_RULES,
       
   260                           default_options=default_options)
       
   261 
       
   262 
       
   263 def check_webkit_style_configuration(options):
       
   264     """Return a StyleProcessorConfiguration instance for check-webkit-style.
       
   265 
       
   266     Args:
       
   267       options: A CommandOptionValues instance.
       
   268 
       
   269     """
       
   270     filter_configuration = FilterConfiguration(
       
   271                                base_rules=_BASE_FILTER_RULES,
       
   272                                path_specific=_PATH_RULES_SPECIFIER,
       
   273                                user_rules=options.filter_rules)
       
   274 
       
   275     return StyleProcessorConfiguration(filter_configuration=filter_configuration,
       
   276                max_reports_per_category=_MAX_REPORTS_PER_CATEGORY,
       
   277                min_confidence=options.min_confidence,
       
   278                output_format=options.output_format,
       
   279                stderr_write=sys.stderr.write)
       
   280 
       
   281 
       
   282 def _create_log_handlers(stream):
       
   283     """Create and return a default list of logging.Handler instances.
       
   284 
       
   285     Format WARNING messages and above to display the logging level, and
       
   286     messages strictly below WARNING not to display it.
       
   287 
       
   288     Args:
       
   289       stream: See the configure_logging() docstring.
       
   290 
       
   291     """
       
   292     # Handles logging.WARNING and above.
       
   293     error_handler = logging.StreamHandler(stream)
       
   294     error_handler.setLevel(logging.WARNING)
       
   295     formatter = logging.Formatter("%(levelname)s: %(message)s")
       
   296     error_handler.setFormatter(formatter)
       
   297 
       
   298     # Create a logging.Filter instance that only accepts messages
       
   299     # below WARNING (i.e. filters out anything WARNING or above).
       
   300     non_error_filter = logging.Filter()
       
   301     # The filter method accepts a logging.LogRecord instance.
       
   302     non_error_filter.filter = lambda record: record.levelno < logging.WARNING
       
   303 
       
   304     non_error_handler = logging.StreamHandler(stream)
       
   305     non_error_handler.addFilter(non_error_filter)
       
   306     formatter = logging.Formatter("%(message)s")
       
   307     non_error_handler.setFormatter(formatter)
       
   308 
       
   309     return [error_handler, non_error_handler]
       
   310 
       
   311 
       
   312 def _create_debug_log_handlers(stream):
       
   313     """Create and return a list of logging.Handler instances for debugging.
       
   314 
       
   315     Args:
       
   316       stream: See the configure_logging() docstring.
       
   317 
       
   318     """
       
   319     handler = logging.StreamHandler(stream)
       
   320     formatter = logging.Formatter("%(name)s: %(levelname)-8s %(message)s")
       
   321     handler.setFormatter(formatter)
       
   322 
       
   323     return [handler]
       
   324 
       
   325 
       
   326 def configure_logging(stream, logger=None, is_verbose=False):
       
   327     """Configure logging, and return the list of handlers added.
       
   328 
       
   329     Returns:
       
   330       A list of references to the logging handlers added to the root
       
   331       logger.  This allows the caller to later remove the handlers
       
   332       using logger.removeHandler.  This is useful primarily during unit
       
   333       testing where the caller may want to configure logging temporarily
       
   334       and then undo the configuring.
       
   335 
       
   336     Args:
       
   337       stream: A file-like object to which to log.  The stream must
       
   338               define an "encoding" data attribute, or else logging
       
   339               raises an error.
       
   340       logger: A logging.logger instance to configure.  This parameter
       
   341               should be used only in unit tests.  Defaults to the
       
   342               root logger.
       
   343       is_verbose: A boolean value of whether logging should be verbose.
       
   344 
       
   345     """
       
   346     # If the stream does not define an "encoding" data attribute, the
       
   347     # logging module can throw an error like the following:
       
   348     #
       
   349     # Traceback (most recent call last):
       
   350     #   File "/System/Library/Frameworks/Python.framework/Versions/2.6/...
       
   351     #         lib/python2.6/logging/__init__.py", line 761, in emit
       
   352     #     self.stream.write(fs % msg.encode(self.stream.encoding))
       
   353     # LookupError: unknown encoding: unknown
       
   354     if logger is None:
       
   355         logger = logging.getLogger()
       
   356 
       
   357     if is_verbose:
       
   358         logging_level = logging.DEBUG
       
   359         handlers = _create_debug_log_handlers(stream)
       
   360     else:
       
   361         logging_level = logging.INFO
       
   362         handlers = _create_log_handlers(stream)
       
   363 
       
   364     handlers = _configure_logging(logging_level=logging_level, logger=logger,
       
   365                                   handlers=handlers)
       
   366 
       
   367     return handlers
       
   368 
       
   369 
       
   370 # Enum-like idiom
       
   371 class FileType:
       
   372 
       
   373     NONE = 0  # FileType.NONE evaluates to False.
       
   374     # Alphabetize remaining types
       
   375     CPP = 1
       
   376     PYTHON = 2
       
   377     TEXT = 3
       
   378 
       
   379 
       
   380 class CheckerDispatcher(object):
       
   381 
       
   382     """Supports determining whether and how to check style, based on path."""
       
   383 
       
   384     def _file_extension(self, file_path):
       
   385         """Return the file extension without the leading dot."""
       
   386         return os.path.splitext(file_path)[1].lstrip(".")
       
   387 
       
   388     def should_skip_with_warning(self, file_path):
       
   389         """Return whether the given file should be skipped with a warning."""
       
   390         for skipped_file in _SKIPPED_FILES_WITH_WARNING:
       
   391             if file_path.find(skipped_file) >= 0:
       
   392                 return True
       
   393         return False
       
   394 
       
   395     def should_skip_without_warning(self, file_path):
       
   396         """Return whether the given file should be skipped without a warning."""
       
   397         if not self._file_type(file_path):  # FileType.NONE.
       
   398             return True
       
   399         # Since "LayoutTests" is in _SKIPPED_FILES_WITHOUT_WARNING, make
       
   400         # an exception to prevent files like "LayoutTests/ChangeLog" and
       
   401         # "LayoutTests/ChangeLog-2009-06-16" from being skipped.
       
   402         #
       
   403         # FIXME: Figure out a good way to avoid having to add special logic
       
   404         #        for this special case.
       
   405         if os.path.basename(file_path).startswith('ChangeLog'):
       
   406             return False
       
   407         for skipped_file in _SKIPPED_FILES_WITHOUT_WARNING:
       
   408             if file_path.find(skipped_file) >= 0:
       
   409                 return True
       
   410         return False
       
   411 
       
   412     def _file_type(self, file_path):
       
   413         """Return the file type corresponding to the given file."""
       
   414         file_extension = self._file_extension(file_path)
       
   415 
       
   416         if (file_extension in _CPP_FILE_EXTENSIONS) or (file_path == '-'):
       
   417             # FIXME: Do something about the comment below and the issue it
       
   418             #        raises since cpp_style already relies on the extension.
       
   419             #
       
   420             # Treat stdin as C++. Since the extension is unknown when
       
   421             # reading from stdin, cpp_style tests should not rely on
       
   422             # the extension.
       
   423             return FileType.CPP
       
   424         elif file_extension == _PYTHON_FILE_EXTENSION:
       
   425             return FileType.PYTHON
       
   426         elif (os.path.basename(file_path).startswith('ChangeLog') or
       
   427               (not file_extension and "WebKitTools/Scripts/" in file_path) or
       
   428               file_extension in _TEXT_FILE_EXTENSIONS):
       
   429             return FileType.TEXT
       
   430         else:
       
   431             return FileType.NONE
       
   432 
       
   433     def _create_checker(self, file_type, file_path, handle_style_error,
       
   434                         min_confidence):
       
   435         """Instantiate and return a style checker based on file type."""
       
   436         if file_type == FileType.NONE:
       
   437             checker = None
       
   438         elif file_type == FileType.CPP:
       
   439             file_extension = self._file_extension(file_path)
       
   440             checker = CppChecker(file_path, file_extension,
       
   441                                  handle_style_error, min_confidence)
       
   442         elif file_type == FileType.PYTHON:
       
   443             checker = PythonChecker(file_path, handle_style_error)
       
   444         elif file_type == FileType.TEXT:
       
   445             checker = TextChecker(file_path, handle_style_error)
       
   446         else:
       
   447             raise ValueError('Invalid file type "%(file_type)s": the only valid file types '
       
   448                              "are %(NONE)s, %(CPP)s, and %(TEXT)s."
       
   449                              % {"file_type": file_type,
       
   450                                 "NONE": FileType.NONE,
       
   451                                 "CPP": FileType.CPP,
       
   452                                 "TEXT": FileType.TEXT})
       
   453 
       
   454         return checker
       
   455 
       
   456     def dispatch(self, file_path, handle_style_error, min_confidence):
       
   457         """Instantiate and return a style checker based on file path."""
       
   458         file_type = self._file_type(file_path)
       
   459 
       
   460         checker = self._create_checker(file_type,
       
   461                                        file_path,
       
   462                                        handle_style_error,
       
   463                                        min_confidence)
       
   464         return checker
       
   465 
       
   466 
       
   467 # FIXME: Remove the stderr_write attribute from this class and replace
       
   468 #        its use with calls to a logging module logger.
       
   469 class StyleProcessorConfiguration(object):
       
   470 
       
   471     """Stores configuration values for the StyleProcessor class.
       
   472 
       
   473     Attributes:
       
   474       min_confidence: An integer between 1 and 5 inclusive that is the
       
   475                       minimum confidence level of style errors to report.
       
   476 
       
   477       max_reports_per_category: The maximum number of errors to report
       
   478                                 per category, per file.
       
   479 
       
   480       stderr_write: A function that takes a string as a parameter and
       
   481                     serves as stderr.write.
       
   482 
       
   483     """
       
   484 
       
   485     def __init__(self,
       
   486                  filter_configuration,
       
   487                  max_reports_per_category,
       
   488                  min_confidence,
       
   489                  output_format,
       
   490                  stderr_write):
       
   491         """Create a StyleProcessorConfiguration instance.
       
   492 
       
   493         Args:
       
   494           filter_configuration: A FilterConfiguration instance.  The default
       
   495                                 is the "empty" filter configuration, which
       
   496                                 means that all errors should be checked.
       
   497 
       
   498           max_reports_per_category: The maximum number of errors to report
       
   499                                     per category, per file.
       
   500 
       
   501           min_confidence: An integer between 1 and 5 inclusive that is the
       
   502                           minimum confidence level of style errors to report.
       
   503                           The default is 1, which reports all style errors.
       
   504 
       
   505           output_format: A string that is the output format.  The supported
       
   506                          output formats are "emacs" which emacs can parse
       
   507                          and "vs7" which Microsoft Visual Studio 7 can parse.
       
   508 
       
   509           stderr_write: A function that takes a string as a parameter and
       
   510                         serves as stderr.write.
       
   511 
       
   512         """
       
   513         self._filter_configuration = filter_configuration
       
   514         self._output_format = output_format
       
   515 
       
   516         self.max_reports_per_category = max_reports_per_category
       
   517         self.min_confidence = min_confidence
       
   518         self.stderr_write = stderr_write
       
   519 
       
   520     def is_reportable(self, category, confidence_in_error, file_path):
       
   521         """Return whether an error is reportable.
       
   522 
       
   523         An error is reportable if both the confidence in the error is
       
   524         at least the minimum confidence level and the current filter
       
   525         says the category should be checked for the given path.
       
   526 
       
   527         Args:
       
   528           category: A string that is a style category.
       
   529           confidence_in_error: An integer between 1 and 5 inclusive that is
       
   530                                the application's confidence in the error.
       
   531                                A higher number means greater confidence.
       
   532           file_path: The path of the file being checked
       
   533 
       
   534         """
       
   535         if confidence_in_error < self.min_confidence:
       
   536             return False
       
   537 
       
   538         return self._filter_configuration.should_check(category, file_path)
       
   539 
       
   540     def write_style_error(self,
       
   541                           category,
       
   542                           confidence_in_error,
       
   543                           file_path,
       
   544                           line_number,
       
   545                           message):
       
   546         """Write a style error to the configured stderr."""
       
   547         if self._output_format == 'vs7':
       
   548             format_string = "%s(%s):  %s  [%s] [%d]\n"
       
   549         else:
       
   550             format_string = "%s:%s:  %s  [%s] [%d]\n"
       
   551 
       
   552         self.stderr_write(format_string % (file_path,
       
   553                                            line_number,
       
   554                                            message,
       
   555                                            category,
       
   556                                            confidence_in_error))
       
   557 
       
   558 
       
   559 class ProcessorBase(object):
       
   560 
       
   561     """The base class for processors of lists of lines."""
       
   562 
       
   563     def should_process(self, file_path):
       
   564         """Return whether the file at file_path should be processed.
       
   565 
       
   566         The TextFileReader class calls this method prior to reading in
       
   567         the lines of a file.  Use this method, for example, to prevent
       
   568         the style checker from reading binary files into memory.
       
   569 
       
   570         """
       
   571         raise NotImplementedError('Subclasses should implement.')
       
   572 
       
   573     def process(self, lines, file_path, **kwargs):
       
   574         """Process lines of text read from a file.
       
   575 
       
   576         Args:
       
   577           lines: A list of lines of text to process.
       
   578           file_path: The path from which the lines were read.
       
   579           **kwargs: This argument signifies that the process() method of
       
   580                     subclasses of ProcessorBase may support additional
       
   581                     keyword arguments.
       
   582                         For example, a style checker's check() method
       
   583                     may support a "reportable_lines" parameter that represents
       
   584                     the line numbers of the lines for which style errors
       
   585                     should be reported.
       
   586 
       
   587         """
       
   588         raise NotImplementedError('Subclasses should implement.')
       
   589 
       
   590 
       
   591 class StyleProcessor(ProcessorBase):
       
   592 
       
   593     """A ProcessorBase for checking style.
       
   594 
       
   595     Attributes:
       
   596       error_count: An integer that is the total number of reported
       
   597                    errors for the lifetime of this instance.
       
   598 
       
   599     """
       
   600 
       
   601     def __init__(self, configuration, mock_dispatcher=None,
       
   602                  mock_increment_error_count=None,
       
   603                  mock_carriage_checker_class=None):
       
   604         """Create an instance.
       
   605 
       
   606         Args:
       
   607           configuration: A StyleProcessorConfiguration instance.
       
   608           mock_dispatcher: A mock CheckerDispatcher instance.  This
       
   609                            parameter is for unit testing.  Defaults to a
       
   610                            CheckerDispatcher instance.
       
   611           mock_increment_error_count: A mock error-count incrementer.
       
   612           mock_carriage_checker_class: A mock class for checking and
       
   613                                        transforming carriage returns.
       
   614                                        This parameter is for unit testing.
       
   615                                        Defaults to CarriageReturnChecker.
       
   616 
       
   617         """
       
   618         if mock_dispatcher is None:
       
   619             dispatcher = CheckerDispatcher()
       
   620         else:
       
   621             dispatcher = mock_dispatcher
       
   622 
       
   623         if mock_increment_error_count is None:
       
   624             # The following blank line is present to avoid flagging by pep8.py.
       
   625 
       
   626             def increment_error_count():
       
   627                 """Increment the total count of reported errors."""
       
   628                 self.error_count += 1
       
   629         else:
       
   630             increment_error_count = mock_increment_error_count
       
   631 
       
   632         if mock_carriage_checker_class is None:
       
   633             # This needs to be a class rather than an instance since the
       
   634             # process() method instantiates one using parameters.
       
   635             carriage_checker_class = CarriageReturnChecker
       
   636         else:
       
   637             carriage_checker_class = mock_carriage_checker_class
       
   638 
       
   639         self.error_count = 0
       
   640 
       
   641         self._carriage_checker_class = carriage_checker_class
       
   642         self._configuration = configuration
       
   643         self._dispatcher = dispatcher
       
   644         self._increment_error_count = increment_error_count
       
   645 
       
   646     def should_process(self, file_path):
       
   647         """Return whether the file should be checked for style."""
       
   648         if self._dispatcher.should_skip_without_warning(file_path):
       
   649             return False
       
   650         if self._dispatcher.should_skip_with_warning(file_path):
       
   651             _log.warn('File exempt from style guide. Skipping: "%s"'
       
   652                       % file_path)
       
   653             return False
       
   654         return True
       
   655 
       
   656     def process(self, lines, file_path, line_numbers=None):
       
   657         """Check the given lines for style.
       
   658 
       
   659         Arguments:
       
   660           lines: A list of all lines in the file to check.
       
   661           file_path: The path of the file to process.  If possible, the path
       
   662                      should be relative to the source root.  Otherwise,
       
   663                      path-specific logic may not behave as expected.
       
   664           line_numbers: A list of line numbers of the lines for which
       
   665                         style errors should be reported, or None if errors
       
   666                         for all lines should be reported.  When not None, this
       
   667                         list normally contains the line numbers corresponding
       
   668                         to the modified lines of a patch.
       
   669 
       
   670         """
       
   671         _log.debug("Checking style: " + file_path)
       
   672 
       
   673         style_error_handler = DefaultStyleErrorHandler(
       
   674             configuration=self._configuration,
       
   675             file_path=file_path,
       
   676             increment_error_count=self._increment_error_count,
       
   677             line_numbers=line_numbers)
       
   678 
       
   679         carriage_checker = self._carriage_checker_class(style_error_handler)
       
   680 
       
   681         # FIXME: We should probably use the SVN "eol-style" property
       
   682         #        or a white list to decide whether or not to do
       
   683         #        the carriage-return check. Originally, we did the
       
   684         #        check only if (os.linesep != '\r\n').
       
   685         #
       
   686         # Check for and remove trailing carriage returns ("\r").
       
   687         lines = carriage_checker.check(lines)
       
   688 
       
   689         min_confidence = self._configuration.min_confidence
       
   690         checker = self._dispatcher.dispatch(file_path,
       
   691                                                       style_error_handler,
       
   692                                                       min_confidence)
       
   693 
       
   694         if checker is None:
       
   695             raise AssertionError("File should not be checked: '%s'" % file_path)
       
   696 
       
   697         _log.debug("Using class: " + checker.__class__.__name__)
       
   698 
       
   699         checker.check(lines)