Home | History | Annotate | Download | only in style
      1 # Copyright (C) 2009 Google Inc. All rights reserved.
      2 # Copyright (C) 2010 Chris Jerdonek (chris.jerdonek (at] gmail.com)
      3 #
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions are
      6 # met:
      7 #
      8 #     * Redistributions of source code must retain the above copyright
      9 # notice, this list of conditions and the following disclaimer.
     10 #     * Redistributions in binary form must reproduce the above
     11 # copyright notice, this list of conditions and the following disclaimer
     12 # in the documentation and/or other materials provided with the
     13 # distribution.
     14 #     * Neither the name of Google Inc. nor the names of its
     15 # contributors may be used to endorse or promote products derived from
     16 # this software without specific prior written permission.
     17 #
     18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 """Front end of some style-checker modules."""
     31 
     32 import codecs
     33 import getopt
     34 import os.path
     35 import sys
     36 
     37 from .. style_references import parse_patch
     38 from error_handlers import DefaultStyleErrorHandler
     39 from error_handlers import PatchStyleErrorHandler
     40 from filter import validate_filter_rules
     41 from filter import FilterConfiguration
     42 from processors.common import check_no_carriage_return
     43 from processors.common import categories as CommonCategories
     44 from processors.cpp import CppProcessor
     45 from processors.text import TextProcessor
     46 
     47 
     48 # These defaults are used by check-webkit-style.
     49 WEBKIT_DEFAULT_VERBOSITY = 1
     50 WEBKIT_DEFAULT_OUTPUT_FORMAT = 'emacs'
     51 
     52 
     53 # FIXME: For style categories we will never want to have, remove them.
     54 #        For categories for which we want to have similar functionality,
     55 #        modify the implementation and enable them.
     56 #
     57 # Throughout this module, we use "filter rule" rather than "filter"
     58 # for an individual boolean filter flag like "+foo". This allows us to
     59 # reserve "filter" for what one gets by collectively applying all of
     60 # the filter rules.
     61 #
     62 # The _WEBKIT_FILTER_RULES are prepended to any user-specified filter
     63 # rules. Since by default all errors are on, only include rules that
     64 # begin with a - sign.
     65 WEBKIT_DEFAULT_FILTER_RULES = [
     66     '-build/endif_comment',
     67     '-build/include_what_you_use',  # <string> for std::string
     68     '-build/storage_class',  # const static
     69     '-legal/copyright',
     70     '-readability/multiline_comment',
     71     '-readability/braces',  # int foo() {};
     72     '-readability/fn_size',
     73     '-readability/casting',
     74     '-readability/function',
     75     '-runtime/arrays',  # variable length array
     76     '-runtime/casting',
     77     '-runtime/sizeof',
     78     '-runtime/explicit',  # explicit
     79     '-runtime/virtual',  # virtual dtor
     80     '-runtime/printf',
     81     '-runtime/threadsafe_fn',
     82     '-runtime/rtti',
     83     '-whitespace/blank_line',
     84     '-whitespace/end_of_line',
     85     '-whitespace/labels',
     86     ]
     87 
     88 
     89 # FIXME: Change the second value of each tuple from a tuple to a list,
     90 #        and alter the filter code so it accepts lists instead.  (The
     91 #        filter code will need to convert incoming values from a list
     92 #        to a tuple prior to caching).  This will make this
     93 #        configuration setting a bit simpler since tuples have an
     94 #        unusual syntax case.
     95 #
     96 # The path-specific filter rules.
     97 #
     98 # This list is order sensitive.  Only the first path substring match
     99 # is used.  See the FilterConfiguration documentation in filter.py
    100 # for more information on this list.
    101 _PATH_RULES_SPECIFIER = [
    102     # Files in these directories are consumers of the WebKit
    103     # API and therefore do not follow the same header including
    104     # discipline as WebCore.
    105     (["WebKitTools/WebKitAPITest/",
    106       "WebKit/qt/QGVLauncher/"],
    107      ("-build/include",
    108       "-readability/streams")),
    109     ([# The GTK+ APIs use GTK+ naming style, which includes
    110       # lower-cased, underscore-separated values.
    111       "WebKit/gtk/webkit/",
    112       # There is no clean way to avoid "yy_*" names used by flex.
    113       "WebCore/css/CSSParser.cpp",
    114       # There is no clean way to avoid "xxx_data" methods inside
    115       # Qt's autotests since they are called automatically by the
    116       # QtTest module.
    117       "WebKit/qt/tests/",
    118       "JavaScriptCore/qt/tests"],
    119      ("-readability/naming",)),
    120     # These are test file patterns.
    121     (["_test.cpp",
    122       "_unittest.cpp",
    123       "_regtest.cpp"],
    124      ("-readability/streams",  # Many unit tests use cout.
    125       "-runtime/rtti")),
    126 ]
    127 
    128 
    129 # Some files should be skipped when checking style. For example,
    130 # WebKit maintains some files in Mozilla style on purpose to ease
    131 # future merges.
    132 #
    133 # Include a warning for skipped files that are less obvious.
    134 SKIPPED_FILES_WITH_WARNING = [
    135     # The Qt API and tests do not follow WebKit style.
    136     # They follow Qt style. :)
    137     "gtk2drawing.c", # WebCore/platform/gtk/gtk2drawing.c
    138     "gtk2drawing.h", # WebCore/platform/gtk/gtk2drawing.h
    139     "JavaScriptCore/qt/api/",
    140     "WebKit/gtk/tests/",
    141     "WebKit/qt/Api/",
    142     "WebKit/qt/tests/",
    143     ]
    144 
    145 
    146 # Don't include a warning for skipped files that are more common
    147 # and more obvious.
    148 SKIPPED_FILES_WITHOUT_WARNING = [
    149     "LayoutTests/"
    150     ]
    151 
    152 
    153 # The maximum number of errors to report per file, per category.
    154 # If a category is not a key, then it has no maximum.
    155 MAX_REPORTS_PER_CATEGORY = {
    156     "whitespace/carriage_return": 1
    157 }
    158 
    159 
    160 def style_categories():
    161     """Return the set of all categories used by check-webkit-style."""
    162     # Take the union across all processors.
    163     return CommonCategories.union(CppProcessor.categories)
    164 
    165 
    166 def webkit_argument_defaults():
    167     """Return the DefaultArguments instance for use by check-webkit-style."""
    168     return ArgumentDefaults(WEBKIT_DEFAULT_OUTPUT_FORMAT,
    169                             WEBKIT_DEFAULT_VERBOSITY,
    170                             WEBKIT_DEFAULT_FILTER_RULES)
    171 
    172 
    173 def _create_usage(defaults):
    174     """Return the usage string to display for command help.
    175 
    176     Args:
    177       defaults: An ArgumentDefaults instance.
    178 
    179     """
    180     usage = """
    181 Syntax: %(program_name)s [--verbose=#] [--git-commit=<SingleCommit>] [--output=vs7]
    182         [--filter=-x,+y,...] [file] ...
    183 
    184   The style guidelines this tries to follow are here:
    185     http://webkit.org/coding/coding-style.html
    186 
    187   Every style error is given a confidence score from 1-5, with 5 meaning
    188   we are certain of the problem, and 1 meaning it could be a legitimate
    189   construct.  This can miss some errors and does not substitute for
    190   code review.
    191 
    192   To prevent specific lines from being linted, add a '// NOLINT' comment to the
    193   end of the line.
    194 
    195   Linted extensions are .cpp, .c and .h.  Other file types are ignored.
    196 
    197   The file parameter is optional and accepts multiple files.  Leaving
    198   out the file parameter applies the check to all files considered changed
    199   by your source control management system.
    200 
    201   Flags:
    202 
    203     verbose=#
    204       A number 1-5 that restricts output to errors with a confidence
    205       score at or above this value. In particular, the value 1 displays
    206       all errors. The default is %(default_verbosity)s.
    207 
    208     git-commit=<SingleCommit>
    209       Checks the style of everything from the given commit to the local tree.
    210 
    211     output=vs7
    212       The output format, which may be one of
    213         emacs : to ease emacs parsing
    214         vs7   : compatible with Visual Studio
    215       Defaults to "%(default_output_format)s". Other formats are unsupported.
    216 
    217     filter=-x,+y,...
    218       A comma-separated list of boolean filter rules used to filter
    219       which categories of style guidelines to check.  The script checks
    220       a category if the category passes the filter rules, as follows.
    221 
    222       Any webkit category starts out passing.  All filter rules are then
    223       evaluated left to right, with later rules taking precedence.  For
    224       example, the rule "+foo" passes any category that starts with "foo",
    225       and "-foo" fails any such category.  The filter input "-whitespace,
    226       +whitespace/braces" fails the category "whitespace/tab" and passes
    227       "whitespace/braces".
    228 
    229       Examples: --filter=-whitespace,+whitespace/braces
    230                 --filter=-whitespace,-runtime/printf,+runtime/printf_format
    231                 --filter=-,+build/include_what_you_use
    232 
    233       Category names appear in error messages in brackets, for example
    234       [whitespace/indent].  To see a list of all categories available to
    235       %(program_name)s, along with which are enabled by default, pass
    236       the empty filter as follows:
    237          --filter=
    238 """ % {'program_name': os.path.basename(sys.argv[0]),
    239        'default_verbosity': defaults.verbosity,
    240        'default_output_format': defaults.output_format}
    241 
    242     return usage
    243 
    244 
    245 # FIXME: Eliminate support for "extra_flag_values".
    246 #
    247 # FIXME: Remove everything from ProcessorOptions except for the
    248 #        information that can be passed via the command line, and
    249 #        rename to something like CheckWebKitStyleOptions.  This
    250 #        includes, but is not limited to, removing the
    251 #        max_reports_per_error attribute and the is_reportable()
    252 #        method.  See also the FIXME below to create a new class
    253 #        called something like CheckerConfiguration.
    254 #
    255 # This class should not have knowledge of the flag key names.
    256 class ProcessorOptions(object):
    257 
    258     """A container to store options passed via the command line.
    259 
    260     Attributes:
    261       extra_flag_values: A string-string dictionary of all flag key-value
    262                          pairs that are not otherwise represented by this
    263                          class.  The default is the empty dictionary.
    264 
    265       filter_configuration: A FilterConfiguration instance.  The default
    266                             is the "empty" filter configuration, which
    267                             means that all errors should be checked.
    268 
    269       git_commit: A string representing the git commit to check.
    270                   The default is None.
    271 
    272       max_reports_per_error: The maximum number of errors to report
    273                              per file, per category.
    274 
    275       output_format: A string that is the output format.  The supported
    276                      output formats are "emacs" which emacs can parse
    277                      and "vs7" which Microsoft Visual Studio 7 can parse.
    278 
    279       verbosity: An integer between 1-5 inclusive that restricts output
    280                  to errors with a confidence score at or above this value.
    281                  The default is 1, which reports all errors.
    282 
    283     """
    284     def __init__(self,
    285                  extra_flag_values=None,
    286                  filter_configuration = None,
    287                  git_commit=None,
    288                  max_reports_per_category=None,
    289                  output_format="emacs",
    290                  verbosity=1):
    291         if extra_flag_values is None:
    292             extra_flag_values = {}
    293         if filter_configuration is None:
    294             filter_configuration = FilterConfiguration()
    295         if max_reports_per_category is None:
    296             max_reports_per_category = {}
    297 
    298         if output_format not in ("emacs", "vs7"):
    299             raise ValueError('Invalid "output_format" parameter: '
    300                              'value must be "emacs" or "vs7". '
    301                              'Value given: "%s".' % output_format)
    302 
    303         if (verbosity < 1) or (verbosity > 5):
    304             raise ValueError('Invalid "verbosity" parameter: '
    305                              "value must be an integer between 1-5 inclusive. "
    306                              'Value given: "%s".' % verbosity)
    307 
    308         self.extra_flag_values = extra_flag_values
    309         self.filter_configuration = filter_configuration
    310         self.git_commit = git_commit
    311         self.max_reports_per_category = max_reports_per_category
    312         self.output_format = output_format
    313         self.verbosity = verbosity
    314 
    315     # Useful for unit testing.
    316     def __eq__(self, other):
    317         """Return whether this ProcessorOptions instance is equal to another."""
    318         if self.extra_flag_values != other.extra_flag_values:
    319             return False
    320         if self.filter_configuration != other.filter_configuration:
    321             return False
    322         if self.git_commit != other.git_commit:
    323             return False
    324         if self.max_reports_per_category != other.max_reports_per_category:
    325             return False
    326         if self.output_format != other.output_format:
    327             return False
    328         if self.verbosity != other.verbosity:
    329             return False
    330 
    331         return True
    332 
    333     # Useful for unit testing.
    334     def __ne__(self, other):
    335         # Python does not automatically deduce this from __eq__().
    336         return not self.__eq__(other)
    337 
    338     def is_reportable(self, category, confidence_in_error, path):
    339         """Return whether an error is reportable.
    340 
    341         An error is reportable if the confidence in the error
    342         is at least the current verbosity level, and if the current
    343         filter says that the category should be checked for the
    344         given path.
    345 
    346         Args:
    347           category: A string that is a style category.
    348           confidence_in_error: An integer between 1 and 5, inclusive, that
    349                                represents the application's confidence in
    350                                the error. A higher number signifies greater
    351                                confidence.
    352           path: The path of the file being checked
    353 
    354         """
    355         if confidence_in_error < self.verbosity:
    356             return False
    357 
    358         return self.filter_configuration.should_check(category, path)
    359 
    360 
    361 # This class should not have knowledge of the flag key names.
    362 class ArgumentDefaults(object):
    363 
    364     """A container to store default argument values.
    365 
    366     Attributes:
    367       output_format: A string that is the default output format.
    368       verbosity: An integer that is the default verbosity level.
    369       base_filter_rules: A list of strings that are boolean filter rules
    370                          to prepend to any user-specified rules.
    371 
    372     """
    373 
    374     def __init__(self, default_output_format, default_verbosity,
    375                  default_base_filter_rules):
    376         self.output_format = default_output_format
    377         self.verbosity = default_verbosity
    378         self.base_filter_rules = default_base_filter_rules
    379 
    380 
    381 class ArgumentPrinter(object):
    382 
    383     """Supports the printing of check-webkit-style command arguments."""
    384 
    385     def _flag_pair_to_string(self, flag_key, flag_value):
    386         return '--%(key)s=%(val)s' % {'key': flag_key, 'val': flag_value }
    387 
    388     def to_flag_string(self, options):
    389         """Return a flag string yielding the given ProcessorOptions instance.
    390 
    391         This method orders the flag values alphabetically by the flag key.
    392 
    393         Args:
    394           options: A ProcessorOptions instance.
    395 
    396         """
    397         flags = options.extra_flag_values.copy()
    398 
    399         flags['output'] = options.output_format
    400         flags['verbose'] = options.verbosity
    401         # Only include the filter flag if user-provided rules are present.
    402         user_rules = options.filter_configuration.user_rules
    403         if user_rules:
    404             flags['filter'] = ",".join(user_rules)
    405         if options.git_commit:
    406             flags['git-commit'] = options.git_commit
    407 
    408         flag_string = ''
    409         # Alphabetizing lets us unit test this method.
    410         for key in sorted(flags.keys()):
    411             flag_string += self._flag_pair_to_string(key, flags[key]) + ' '
    412 
    413         return flag_string.strip()
    414 
    415 
    416 class ArgumentParser(object):
    417 
    418     """Supports the parsing of check-webkit-style command arguments.
    419 
    420     Attributes:
    421       defaults: An ArgumentDefaults instance.
    422       create_usage: A function that accepts an ArgumentDefaults instance
    423                     and returns a string of usage instructions.
    424                     This defaults to the function used to generate the
    425                     usage string for check-webkit-style.
    426       doc_print: A function that accepts a string parameter and that is
    427                  called to display help messages. This defaults to
    428                  sys.stderr.write().
    429 
    430     """
    431 
    432     def __init__(self, argument_defaults, create_usage=None, doc_print=None):
    433         if create_usage is None:
    434             create_usage = _create_usage
    435         if doc_print is None:
    436             doc_print = sys.stderr.write
    437 
    438         self.defaults = argument_defaults
    439         self.create_usage = create_usage
    440         self.doc_print = doc_print
    441 
    442     def _exit_with_usage(self, error_message=''):
    443         """Exit and print a usage string with an optional error message.
    444 
    445         Args:
    446           error_message: A string that is an error message to print.
    447 
    448         """
    449         usage = self.create_usage(self.defaults)
    450         self.doc_print(usage)
    451         if error_message:
    452             sys.exit('\nFATAL ERROR: ' + error_message)
    453         else:
    454             sys.exit(1)
    455 
    456     def _exit_with_categories(self):
    457         """Exit and print the style categories and default filter rules."""
    458         self.doc_print('\nAll categories:\n')
    459         categories = style_categories()
    460         for category in sorted(categories):
    461             self.doc_print('    ' + category + '\n')
    462 
    463         self.doc_print('\nDefault filter rules**:\n')
    464         for filter_rule in sorted(self.defaults.base_filter_rules):
    465             self.doc_print('    ' + filter_rule + '\n')
    466         self.doc_print('\n**The command always evaluates the above rules, '
    467                        'and before any --filter flag.\n\n')
    468 
    469         sys.exit(0)
    470 
    471     def _parse_filter_flag(self, flag_value):
    472         """Parse the --filter flag, and return a list of filter rules.
    473 
    474         Args:
    475           flag_value: A string of comma-separated filter rules, for
    476                       example "-whitespace,+whitespace/indent".
    477 
    478         """
    479         filters = []
    480         for uncleaned_filter in flag_value.split(','):
    481             filter = uncleaned_filter.strip()
    482             if not filter:
    483                 continue
    484             filters.append(filter)
    485         return filters
    486 
    487     def parse(self, args, extra_flags=None):
    488         """Parse the command line arguments to check-webkit-style.
    489 
    490         Args:
    491           args: A list of command-line arguments as returned by sys.argv[1:].
    492           extra_flags: A list of flags whose values we want to extract, but
    493                        are not supported by the ProcessorOptions class.
    494                        An example flag "new_flag=". This defaults to the
    495                        empty list.
    496 
    497         Returns:
    498           A tuple of (filenames, options)
    499 
    500           filenames: The list of filenames to check.
    501           options: A ProcessorOptions instance.
    502 
    503         """
    504         if extra_flags is None:
    505             extra_flags = []
    506 
    507         output_format = self.defaults.output_format
    508         verbosity = self.defaults.verbosity
    509         base_rules = self.defaults.base_filter_rules
    510 
    511         # The flags already supported by the ProcessorOptions class.
    512         flags = ['help', 'output=', 'verbose=', 'filter=', 'git-commit=']
    513 
    514         for extra_flag in extra_flags:
    515             if extra_flag in flags:
    516                 raise ValueError('Flag \'%(extra_flag)s is duplicated '
    517                                  'or already supported.' %
    518                                  {'extra_flag': extra_flag})
    519             flags.append(extra_flag)
    520 
    521         try:
    522             (opts, filenames) = getopt.getopt(args, '', flags)
    523         except getopt.GetoptError:
    524             # FIXME: Settle on an error handling approach: come up
    525             #        with a consistent guideline as to when and whether
    526             #        a ValueError should be raised versus calling
    527             #        sys.exit when needing to interrupt execution.
    528             self._exit_with_usage('Invalid arguments.')
    529 
    530         extra_flag_values = {}
    531         git_commit = None
    532         user_rules = []
    533 
    534         for (opt, val) in opts:
    535             if opt == '--help':
    536                 self._exit_with_usage()
    537             elif opt == '--output':
    538                 output_format = val
    539             elif opt == '--verbose':
    540                 verbosity = val
    541             elif opt == '--git-commit':
    542                 git_commit = val
    543             elif opt == '--filter':
    544                 if not val:
    545                     self._exit_with_categories()
    546                 # Prepend the defaults.
    547                 user_rules = self._parse_filter_flag(val)
    548             else:
    549                 extra_flag_values[opt] = val
    550 
    551         # Check validity of resulting values.
    552         if filenames and (git_commit != None):
    553             self._exit_with_usage('It is not possible to check files and a '
    554                                   'specific commit at the same time.')
    555 
    556         if output_format not in ('emacs', 'vs7'):
    557             raise ValueError('Invalid --output value "%s": The only '
    558                              'allowed output formats are emacs and vs7.' %
    559                              output_format)
    560 
    561         all_categories = style_categories()
    562         validate_filter_rules(user_rules, all_categories)
    563 
    564         verbosity = int(verbosity)
    565         if (verbosity < 1) or (verbosity > 5):
    566             raise ValueError('Invalid --verbose value %s: value must '
    567                              'be between 1-5.' % verbosity)
    568 
    569         filter_configuration = FilterConfiguration(base_rules=base_rules,
    570                                    path_specific=_PATH_RULES_SPECIFIER,
    571                                    user_rules=user_rules)
    572 
    573         options = ProcessorOptions(extra_flag_values=extra_flag_values,
    574                       filter_configuration=filter_configuration,
    575                       git_commit=git_commit,
    576                       max_reports_per_category=MAX_REPORTS_PER_CATEGORY,
    577                       output_format=output_format,
    578                       verbosity=verbosity)
    579 
    580         return (filenames, options)
    581 
    582 
    583 # Enum-like idiom
    584 class FileType:
    585 
    586     NONE = 1
    587     # Alphabetize remaining types
    588     CPP = 2
    589     TEXT = 3
    590 
    591 
    592 class ProcessorDispatcher(object):
    593 
    594     """Supports determining whether and how to check style, based on path."""
    595 
    596     cpp_file_extensions = (
    597         'c',
    598         'cpp',
    599         'h',
    600         )
    601 
    602     text_file_extensions = (
    603         'css',
    604         'html',
    605         'idl',
    606         'js',
    607         'mm',
    608         'php',
    609         'pm',
    610         'py',
    611         'txt',
    612         )
    613 
    614     def _file_extension(self, file_path):
    615         """Return the file extension without the leading dot."""
    616         return os.path.splitext(file_path)[1].lstrip(".")
    617 
    618     def should_skip_with_warning(self, file_path):
    619         """Return whether the given file should be skipped with a warning."""
    620         for skipped_file in SKIPPED_FILES_WITH_WARNING:
    621             if file_path.find(skipped_file) >= 0:
    622                 return True
    623         return False
    624 
    625     def should_skip_without_warning(self, file_path):
    626         """Return whether the given file should be skipped without a warning."""
    627         for skipped_file in SKIPPED_FILES_WITHOUT_WARNING:
    628             if file_path.find(skipped_file) >= 0:
    629                 return True
    630         return False
    631 
    632     def _file_type(self, file_path):
    633         """Return the file type corresponding to the given file."""
    634         file_extension = self._file_extension(file_path)
    635 
    636         if (file_extension in self.cpp_file_extensions) or (file_path == '-'):
    637             # FIXME: Do something about the comment below and the issue it
    638             #        raises since cpp_style already relies on the extension.
    639             #
    640             # Treat stdin as C++. Since the extension is unknown when
    641             # reading from stdin, cpp_style tests should not rely on
    642             # the extension.
    643             return FileType.CPP
    644         elif ("ChangeLog" in file_path
    645               or "WebKitTools/Scripts/" in file_path
    646               or file_extension in self.text_file_extensions):
    647             return FileType.TEXT
    648         else:
    649             return FileType.NONE
    650 
    651     def _create_processor(self, file_type, file_path, handle_style_error, verbosity):
    652         """Instantiate and return a style processor based on file type."""
    653         if file_type == FileType.NONE:
    654             processor = None
    655         elif file_type == FileType.CPP:
    656             file_extension = self._file_extension(file_path)
    657             processor = CppProcessor(file_path, file_extension, handle_style_error, verbosity)
    658         elif file_type == FileType.TEXT:
    659             processor = TextProcessor(file_path, handle_style_error)
    660         else:
    661             raise ValueError('Invalid file type "%(file_type)s": the only valid file types '
    662                              "are %(NONE)s, %(CPP)s, and %(TEXT)s."
    663                              % {"file_type": file_type,
    664                                 "NONE": FileType.NONE,
    665                                 "CPP": FileType.CPP,
    666                                 "TEXT": FileType.TEXT})
    667 
    668         return processor
    669 
    670     def dispatch_processor(self, file_path, handle_style_error, verbosity):
    671         """Instantiate and return a style processor based on file path."""
    672         file_type = self._file_type(file_path)
    673 
    674         processor = self._create_processor(file_type,
    675                                            file_path,
    676                                            handle_style_error,
    677                                            verbosity)
    678         return processor
    679 
    680 
    681 # FIXME: When creating the new CheckWebKitStyleOptions class as
    682 #        described in a FIXME above, add a new class here called
    683 #        something like CheckerConfiguration.  The class should contain
    684 #        attributes for options needed to process a file.  This includes
    685 #        a subset of the CheckWebKitStyleOptions attributes, a
    686 #        FilterConfiguration attribute, an stderr_write attribute, a
    687 #        max_reports_per_category attribute, etc.  It can also include
    688 #        the is_reportable() method.  The StyleChecker should accept
    689 #        an instance of this class rather than a ProcessorOptions
    690 #        instance.
    691 
    692 
    693 class StyleChecker(object):
    694 
    695     """Supports checking style in files and patches.
    696 
    697        Attributes:
    698          error_count: An integer that is the total number of reported
    699                       errors for the lifetime of this StyleChecker
    700                       instance.
    701          options: A ProcessorOptions instance that controls the behavior
    702                   of style checking.
    703 
    704     """
    705 
    706     def __init__(self, options, stderr_write=None):
    707         """Create a StyleChecker instance.
    708 
    709         Args:
    710           options: See options attribute.
    711           stderr_write: A function that takes a string as a parameter
    712                         and that is called when a style error occurs.
    713                         Defaults to sys.stderr.write. This should be
    714                         used only for unit tests.
    715 
    716         """
    717         if stderr_write is None:
    718             stderr_write = sys.stderr.write
    719 
    720         self._stderr_write = stderr_write
    721         self.error_count = 0
    722         self.options = options
    723 
    724     def _increment_error_count(self):
    725         """Increment the total count of reported errors."""
    726         self.error_count += 1
    727 
    728     def _process_file(self, processor, file_path, handle_style_error):
    729         """Process the file using the given processor."""
    730         try:
    731             # Support the UNIX convention of using "-" for stdin.  Note that
    732             # we are not opening the file with universal newline support
    733             # (which codecs doesn't support anyway), so the resulting lines do
    734             # contain trailing '\r' characters if we are reading a file that
    735             # has CRLF endings.
    736             # If after the split a trailing '\r' is present, it is removed
    737             # below. If it is not expected to be present (i.e. os.linesep !=
    738             # '\r\n' as in Windows), a warning is issued below if this file
    739             # is processed.
    740             if file_path == '-':
    741                 file = codecs.StreamReaderWriter(sys.stdin,
    742                                                  codecs.getreader('utf8'),
    743                                                  codecs.getwriter('utf8'),
    744                                                  'replace')
    745             else:
    746                 file = codecs.open(file_path, 'r', 'utf8', 'replace')
    747 
    748             contents = file.read()
    749 
    750         except IOError:
    751             self._stderr_write("Skipping input '%s': Can't open for reading\n" % file_path)
    752             return
    753 
    754         lines = contents.split("\n")
    755 
    756         for line_number in range(len(lines)):
    757             # FIXME: We should probably use the SVN "eol-style" property
    758             #        or a white list to decide whether or not to do
    759             #        the carriage-return check. Originally, we did the
    760             #        check only if (os.linesep != '\r\n').
    761             #
    762             # FIXME: As a minor optimization, we can have
    763             #        check_no_carriage_return() return whether
    764             #        the line ends with "\r".
    765             check_no_carriage_return(lines[line_number], line_number,
    766                                      handle_style_error)
    767             if lines[line_number].endswith("\r"):
    768                 lines[line_number] = lines[line_number].rstrip("\r")
    769 
    770         processor.process(lines)
    771 
    772     def check_file(self, file_path, handle_style_error=None, process_file=None):
    773         """Check style in the given file.
    774 
    775         Args:
    776           file_path: A string that is the path of the file to process.
    777           handle_style_error: The function to call when a style error
    778                               occurs. This parameter is meant for internal
    779                               use within this class. Defaults to a
    780                               DefaultStyleErrorHandler instance.
    781           process_file: The function to call to process the file. This
    782                         parameter should be used only for unit tests.
    783                         Defaults to the file processing method of this class.
    784 
    785         """
    786         if handle_style_error is None:
    787             handle_style_error = DefaultStyleErrorHandler(file_path,
    788                                      self.options,
    789                                      self._increment_error_count,
    790                                      self._stderr_write)
    791         if process_file is None:
    792             process_file = self._process_file
    793 
    794         dispatcher = ProcessorDispatcher()
    795 
    796         if dispatcher.should_skip_without_warning(file_path):
    797             return
    798         if dispatcher.should_skip_with_warning(file_path):
    799             self._stderr_write('Ignoring "%s": this file is exempt from the '
    800                                "style guide.\n" % file_path)
    801             return
    802 
    803         verbosity = self.options.verbosity
    804         processor = dispatcher.dispatch_processor(file_path,
    805                                                   handle_style_error,
    806                                                   verbosity)
    807         if processor is None:
    808             return
    809 
    810         process_file(processor, file_path, handle_style_error)
    811 
    812     def check_patch(self, patch_string):
    813         """Check style in the given patch.
    814 
    815         Args:
    816           patch_string: A string that is a patch string.
    817 
    818         """
    819         patch_files = parse_patch(patch_string)
    820         for file_path, diff in patch_files.iteritems():
    821             style_error_handler = PatchStyleErrorHandler(diff,
    822                                       file_path,
    823                                       self.options,
    824                                       self._increment_error_count,
    825                                       self._stderr_write)
    826 
    827             self.check_file(file_path, style_error_handler)
    828 
    829