dc088e90a3eff1fa6fe909244e45f0b527ff0ae6
[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 # Copyright (C) 2013-2017, 2019 Apple Inc. All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are
8 # met:
9 #
10 #     * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 #     * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following disclaimer
14 # in the documentation and/or other materials provided with the
15 # distribution.
16 #     * Neither the name of Google Inc. nor the names of its
17 # contributors may be used to endorse or promote products derived from
18 # this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 """Front end of some style-checker modules."""
33
34 import logging
35 import os.path
36 import re
37
38 from checkers.common import categories as CommonCategories
39 from checkers.common import CarriageReturnChecker
40 from checkers.contributors import ContributorsChecker
41 from checkers.changelog import ChangeLogChecker
42 from checkers.cpp import CppChecker
43 from checkers.cmake import CMakeChecker
44 from checkers.featuredefines import FeatureDefinesChecker
45 from checkers.js import JSChecker
46 from checkers.jsonchecker import JSONChecker
47 from checkers.jsonchecker import JSONContributorsChecker
48 from checkers.jsonchecker import JSONFeaturesChecker
49 from checkers.jsonchecker import JSONCSSPropertiesChecker
50 from checkers.jstest import JSTestChecker
51 from checkers.messagesin import MessagesInChecker
52 from checkers.png import PNGChecker
53 from checkers.python import PythonChecker, Python3Checker
54 from checkers.sdkvariant import SDKVariantChecker
55 from checkers.test_expectations import TestExpectationsChecker
56 from checkers.text import TextChecker
57 from checkers.watchlist import WatchListChecker
58 from checkers.xcodeproj import XcodeProjectFileChecker
59 from checkers.xml import XMLChecker
60 from error_handlers import DefaultStyleErrorHandler
61 from filter import FilterConfiguration
62 from optparser import ArgumentParser
63 from optparser import DefaultCommandOptionValues
64 from webkitpy.common.host import Host
65 from webkitpy.common.system.logutils import configure_logging as _configure_logging
66 from webkitpy.port.config import apple_additions
67
68
69 _log = logging.getLogger(__name__)
70
71
72 # These are default option values for the command-line option parser.
73 _DEFAULT_MIN_CONFIDENCE = 1
74 _DEFAULT_OUTPUT_FORMAT = 'emacs'
75
76
77 # FIXME: For style categories we will never want to have, remove them.
78 #        For categories for which we want to have similar functionality,
79 #        modify the implementation and enable them.
80 #
81 # Throughout this module, we use "filter rule" rather than "filter"
82 # for an individual boolean filter flag like "+foo".  This allows us to
83 # reserve "filter" for what one gets by collectively applying all of
84 # the filter rules.
85 #
86 # The base filter rules are the filter rules that begin the list of
87 # filter rules used to check style.  For example, these rules precede
88 # any user-specified filter rules.  Since by default all categories are
89 # checked, this list should normally include only rules that begin
90 # with a "-" sign.
91 _BASE_FILTER_RULES = [
92     '-build/endif_comment',
93     '-build/include_what_you_use',  # <string> for std::string
94     '-build/storage_class',  # const static
95     '-readability/multiline_comment',
96     '-readability/braces',  # int foo() {};
97     '-readability/fn_size',
98     '-readability/casting',
99     '-readability/function',
100     '-runtime/arrays',  # variable length array
101     '-runtime/casting',
102     '-runtime/sizeof',
103     '-runtime/explicit',  # explicit
104     '-runtime/virtual',  # virtual dtor
105     '-runtime/printf',
106     '-runtime/threadsafe_fn',
107     '-runtime/rtti',
108     '-whitespace/blank_line',
109     '-whitespace/end_of_line',
110     # List Python pep8 categories last.
111     #
112     # Because much of WebKit's Python code base does not abide by the
113     # PEP8 79 character limit, we ignore the 79-character-limit category
114     # pep8/E501 for now.
115     #
116     # FIXME: Consider bringing WebKit's Python code base into conformance
117     #        with the 79 character limit, or some higher limit that is
118     #        agreeable to the WebKit project.
119     '-pep8/E501',
120     '-pycodestyle/E501',
121
122     # FIXME: Move the pylint rules from the pylintrc to here. This will
123     # also require us to re-work lint-webkitpy to produce the equivalent output.
124     ]
125
126
127 # The path-specific filter rules.
128 #
129 # This list is order sensitive.  Only the first path substring match
130 # is used.  See the FilterConfiguration documentation in filter.py
131 # for more information on this list.
132 #
133 # Each string appearing in this nested list should have at least
134 # one associated unit test assertion.  These assertions are located,
135 # for example, in the test_path_rules_specifier() unit test method of
136 # checker_unittest.py.
137 _PATH_RULES_SPECIFIER = [
138     # Files in these directories are consumers of the WebKit
139     # API and therefore do not follow the same header including
140     # discipline as WebCore.
141
142     ([  # TestNetscapePlugIn has no config.h and uses funny names like
143       # NPP_SetWindow.
144       os.path.join('Tools', 'DumpRenderTree', 'TestNetscapePlugIn')],
145      ["-build/include",
146       "-readability/naming"]),
147     ([  # Ignore use of RetainPtr<NSObject *> for tests that ensure its compatibility with ReteainPtr<NSObject>.
148       os.path.join('Tools', 'TestWebKitAPI', 'Tests', 'WTF', 'ns', 'RetainPtr.mm')],
149      ["-runtime/retainptr"]),
150     ([  # There is no clean way to avoid "yy_*" names used by flex.
151       os.path.join('Source', 'WebCore', 'css', 'CSSParser.cpp'),
152       # TestWebKitAPI uses funny macros like EXPECT_WK_STREQ.
153       os.path.join('Tools', 'TestWebKitAPI')],
154      ["-readability/naming"]),
155
156     ([
157       # The GTK+ and WPE APIs use upper case, underscore separated, words in
158       # certain types of enums (e.g. signals, properties).
159       os.path.join('Source', 'JavaScriptCore', 'API', 'glib'),
160       os.path.join('Source', 'WebKit', 'Shared', 'API', 'glib'),
161       os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'glib'),
162       os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'gtk'),
163       os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'wpe'),
164       os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'glib'),
165       os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'gtk'),
166       os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'wpe')],
167      ["-readability/enum_casing"]),
168
169     ([
170       # To use GStreamer GL without conflicts of GL symbols,
171       # we should include gst/gl/gl.h before including OpenGL[ES]Shims
172       os.path.join('Source', 'WebCore', 'platform', 'graphics', 'gstreamer', 'MediaPlayerPrivateGStreamerBase.cpp')],
173      ["-build/include_order"]),
174
175     ([
176       # Header files in ForwardingHeaders have no header guards or
177       # exceptional header guards (e.g., WebCore_FWD_Debugger_h).
178       os.path.join(os.path.sep, 'ForwardingHeaders')],
179      ["-build/header_guard"]),
180     ([
181       # Assembler has lots of opcodes that use underscores, so
182       # we don't check for underscores in that directory.
183       os.path.join('Source', 'JavaScriptCore', 'assembler'),
184       os.path.join('Source', 'JavaScriptCore', 'jit', 'JIT')],
185      ["-readability/naming/underscores"]),
186     ([  # JITStubs has an usual syntax which causes false alarms for a few checks.
187       os.path.join('JavaScriptCore', 'jit', 'JITStubs.cpp')],
188      ["-readability/parameter_name",
189       "-whitespace/parens"]),
190
191     # WebKit rules:
192     # WebKit and certain directories have idiosyncracies.
193     ([  # NPAPI has function names with underscores.
194       os.path.join('Source', 'WebKit', 'WebProcess', 'Plugins', 'Netscape')],
195      ["-readability/naming"]),
196     ([
197       # The WebKit C API has names with underscores and whitespace-aligned
198       # struct members. Also, we allow unnecessary parameter names in
199       # WebKit APIs because we're matching CF's header style.
200       # Additionally, we use word which starts with non-capital letter 'k'
201       # for types of enums.
202       os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'C'),
203       os.path.join('Source', 'WebKit', 'Shared', 'API', 'c'),
204       os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'c')],
205      ["-readability/enum_casing",
206       "-readability/naming",
207       "-readability/parameter_name",
208       "-whitespace/declaration"]),
209     ([
210       # These files define GObjects, which implies some definitions of
211       # variables and functions containing underscores.
212       os.path.join('Source', 'WebCore', 'platform', 'graphics', 'gstreamer', 'VideoSinkGStreamer.cpp'),
213       os.path.join('Source', 'WebCore', 'platform', 'graphics', 'gstreamer', 'WebKitWebSourceGStreamer.cpp'),
214       os.path.join('Source', 'WebCore', 'platform', 'graphics', 'gstreamer', 'mse', 'WebKitMediaSourceGStreamer.cpp'),
215       os.path.join('Source', 'WebCore', 'platform', 'audio', 'gstreamer', 'WebKitWebAudioSourceGStreamer.cpp'),
216       os.path.join('Source', 'WebCore', 'platform', 'mediastream', 'gstreamer', 'GStreamerMediaStreamSource.h'),
217       os.path.join('Source', 'WebCore', 'platform', 'mediastream', 'gstreamer', 'GStreamerMediaStreamSource.cpp'),
218       os.path.join('Source', 'WebCore', 'platform', 'network', 'soup', 'ProxyResolverSoup.cpp'),
219       os.path.join('Source', 'WebCore', 'platform', 'network', 'soup', 'ProxyResolverSoup.h')],
220      ["-readability/naming",
221       "-readability/enum_casing"]),
222
223     # For third-party code, keep only the following checks--
224     #
225     #   No tabs: to avoid having to set the SVN allow-tabs property.
226     #   No trailing white space: since this is easy to correct.
227     #   No carriage-return line endings: since this is easy to correct.
228     #
229     ([os.path.join('webkitpy', 'thirdparty'),
230       os.path.join('Source', 'ThirdParty', 'ANGLE'),
231       os.path.join('Source', 'ThirdParty', 'libwebrtc'),
232       os.path.join('Source', 'ThirdParty', 'openvr'),
233       os.path.join('Source', 'ThirdParty', 'xdgmime'),
234       os.path.join('Tools', 'WebGPUAPIStructure')],
235      ["-",
236       "+pep8/W191",  # Tabs
237       "+pep8/W291",  # Trailing white space
238       "+whitespace/carriage_return"]),
239
240     ([  # Source/JavaScriptCore/disassembler/udis86/ is generated code.
241       os.path.join('Source', 'JavaScriptCore', 'disassembler', 'udis86')],
242      ["-readability/naming/underscores",
243       "-whitespace/declaration",
244       "-whitespace/indent"]),
245
246     ([  # Files following GStreamer coding style (for a simpler upstreaming process for example)
247       os.path.join('Source', 'WebCore', 'platform', 'mediastream', 'libwebrtc', 'GStreamerVideoEncoder.cpp'),
248       os.path.join('Source', 'WebCore', 'platform', 'mediastream', 'libwebrtc', 'GStreamerVideoEncoder.h'),
249      ],
250      ["-whitespace/indent",
251       "-whitespace/declaration",
252       "-whitespace/parens",
253       "-readability/null",
254       "-whitespace/braces",
255       "-readability/naming/underscores",
256       "-readability/enum_casing",
257      ]),
258
259     ([  # Files following using WebRTC optionnal type
260       os.path.join('Source', 'WebCore', 'platform', 'mediastream', 'libwebrtc', 'GStreamerVideoDecoderFactory.cpp'),
261      ],
262      ["-runtime/wtf_optional",
263      ]),
264
265     ([
266       # There is no way to avoid the symbols __jit_debug_register_code
267       # and __jit_debug_descriptor when integrating with gdb.
268       os.path.join('Source', 'JavaScriptCore', 'jit', 'GDBInterface.cpp')],
269      ["-readability/naming"]),
270
271     ([  # On some systems the trailing CR is causing parser failure.
272       os.path.join('Source', 'JavaScriptCore', 'parser', 'Keywords.table')],
273      ["+whitespace/carriage_return"]),
274
275     ([  # DataDetectorsCoreSPI.h declares enum bitfields as CFIndex.
276       os.path.join('Source', 'WebCore', 'PAL', 'pal', 'spi', 'cocoa', 'DataDetectorsCoreSPI.h')],
277      ["-runtime/enum_bitfields"]),
278
279     ([
280       # PassKitSPI.h imports "PassKit.h" at two lines depending on the build configuration,
281       # which causes a false positive error.
282       os.path.join('Source', 'WebCore', 'PAL', 'pal', 'spi', 'cocoa', 'PassKitSPI.h')],
283      ["-build/include"]),
284
285     ([  # libwebrtc source and some SPI headers have identifier names with underscores.
286       os.path.join('Source', 'ThirdParty', 'libwebrtc', 'Source', 'webrtc'),
287       os.path.join('Source', 'WebCore', 'PAL', 'pal', 'spi')],
288      ["-readability/naming/underscores"]),
289 ]
290
291
292 _CPP_FILE_EXTENSIONS = [
293     'c',
294     'cpp',
295     'h',
296     'mm',
297     'm',
298     ]
299
300 _JS_FILE_EXTENSION = 'js'
301
302 _JSON_FILE_EXTENSION = 'json'
303
304 _PYTHON_FILE_EXTENSION = 'py'
305
306 _TEXT_FILE_EXTENSIONS = [
307     'ac',
308     'cc',
309     'cgi',
310     'css',
311     'exp',
312     'flex',
313     'gyp',
314     'gypi',
315     'html',
316     'idl',
317     'in',
318     'php',
319     'pl',
320     'pm',
321     'pri',
322     'pro',
323     'rb',
324     'sh',
325     'table',
326     'txt',
327     'wm',
328     'xhtml',
329     'y',
330     ]
331
332 _XCODEPROJ_FILE_EXTENSION = 'pbxproj'
333
334 _XML_FILE_EXTENSIONS = [
335     'vcproj',
336     'vsprops',
337     ]
338
339 _PNG_FILE_EXTENSION = 'png'
340
341 _CMAKE_FILE_EXTENSION = 'cmake'
342
343 # Files that are never skipped by name.
344 #
345 # Do not skip these files, even when they appear in
346 # _SKIPPED_FILES_WITH_WARNING or _SKIPPED_FILES_WITHOUT_WARNING.
347 _NEVER_SKIPPED_JS_FILES = [
348     'js-test-pre.js',
349     'js-test-post.js',
350     'js-test-post-async.js',
351     'standalone-pre.js',
352 ]
353
354 _NEVER_SKIPPED_FILES = _NEVER_SKIPPED_JS_FILES + [
355     'TestExpectations',
356 ]
357
358 # Files to skip that are less obvious.
359 #
360 # Some files should be skipped when checking style. For example,
361 # WebKit maintains some files in Mozilla style on purpose to ease
362 # future merges.
363 _SKIPPED_FILES_WITH_WARNING = [
364     os.path.join('Tools', 'TestWebKitAPI', 'Tests', 'WebKitGtk'),
365
366     # WebKit*.h and JSC*.h files in, except those ending in Private.h are API headers, which do not follow WebKit coding style.
367     re.compile(re.escape(os.path.join('Source', 'JavaScriptCore', 'API', 'glib') + os.path.sep) + r'JSC(?!.*Private\.h).*\.h$'),
368     re.compile(re.escape(os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'gtk') + os.path.sep) + r'WebKit(?!.*Private\.h).*\.h$'),
369     re.compile(re.escape(os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'wpe') + os.path.sep) + r'WebKit(?!.*Private\.h).*\.h$'),
370     re.compile(re.escape(os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'gtk') + os.path.sep) + r'WebKit(?!.*Private\.h).*\.h$'),
371     re.compile(re.escape(os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'wpe') + os.path.sep) + r'WebKit(?!.*Private\.h).*\.h$'),
372     re.compile(re.escape(os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'wpe', 'DOM') + os.path.sep) + r'WebKit(?!.*Private\.h).*\.h$'),
373
374     # GObject DOM bindings copied from generated code using different coding style.
375     os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'gtk', 'DOM'),
376
377     os.path.join('Source', 'JavaScriptCore', 'API', 'glib', 'jsc.h'),
378     os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'gtk', 'webkit2.h'),
379     os.path.join('Source', 'WebKit', 'UIProcess', 'API', 'wpe', 'webkit.h'),
380     os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'gtk', 'webkit-web-extension.h'),
381     os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'wpe', 'webkit-web-extension.h'),
382     os.path.join('Source', 'WebKit', 'WebProcess', 'InjectedBundle', 'API', 'wpe', 'DOM', 'webkitdom.h')]
383
384 # Files to skip that are more common or obvious.
385 #
386 # This list should be in addition to files with FileType.NONE.  Files
387 # with FileType.NONE are automatically skipped without warning.
388 _SKIPPED_FILES_WITHOUT_WARNING = [
389     "LayoutTests" + os.path.sep,
390
391     "WebDriverTests" + os.path.sep,
392
393     # Files generated by the bindings script should not be checked for style.
394     os.path.join('Source', 'WebCore', 'bindings', 'scripts', 'test'),
395
396     # ICU headers are imported.
397     os.path.join('Source', 'JavaScriptCore', 'icu'),
398     os.path.join('Source', 'WebCore', 'icu'),
399     os.path.join('Source', 'WebKitLegacy', 'mac', 'icu'),
400     os.path.join('Source', 'WTF', 'icu'),
401     ]
402
403 # Extensions of files which are allowed to contain carriage returns.
404 _CARRIAGE_RETURN_ALLOWED_FILE_EXTENSIONS = [
405     'png',
406     'vcproj',
407     'vsprops',
408     ]
409
410 # The maximum number of errors to report per file, per category.
411 # If a category is not a key, then it has no maximum.
412 _MAX_REPORTS_PER_CATEGORY = {
413     "whitespace/carriage_return": 1
414 }
415
416
417 def _all_categories():
418     """Return the set of all categories used by check-webkit-style."""
419     # Take the union across all checkers.
420     categories = CommonCategories.union(CppChecker.categories)
421     categories = categories.union(JSChecker.categories)
422     categories = categories.union(JSONChecker.categories)
423     categories = categories.union(JSTestChecker.categories)
424     categories = categories.union(TestExpectationsChecker.categories)
425     categories = categories.union(ChangeLogChecker.categories)
426     categories = categories.union(PNGChecker.categories)
427     categories = categories.union(FeatureDefinesChecker.categories)
428     categories = categories.union(SDKVariantChecker.categories)
429
430     # FIXME: Consider adding all of the pep8 categories.  Since they
431     #        are not too meaningful for documentation purposes, for
432     #        now we add only the categories needed for the unit tests
433     #        (which validate the consistency of the configuration
434     #        settings against the known categories, etc).
435     categories = categories.union(["pep8/W191", "pep8/W291", "pep8/E501", "pycodestyle/E501"])
436
437     if apple_additions():
438         categories = categories.union(apple_additions().all_categories())
439
440     return categories
441
442
443 def _check_webkit_style_defaults():
444     """Return the default command-line options for check-webkit-style."""
445     return DefaultCommandOptionValues(min_confidence=_DEFAULT_MIN_CONFIDENCE,
446                                       output_format=_DEFAULT_OUTPUT_FORMAT)
447
448
449 # This function assists in optparser not having to import from checker.
450 def check_webkit_style_parser():
451     all_categories = _all_categories()
452     default_options = _check_webkit_style_defaults()
453     return ArgumentParser(all_categories=all_categories,
454                           base_filter_rules=_BASE_FILTER_RULES,
455                           default_options=default_options)
456
457
458 def check_webkit_style_configuration(options):
459     """Return a StyleProcessorConfiguration instance for check-webkit-style.
460
461     Args:
462       options: A CommandOptionValues instance.
463
464     """
465     filter_configuration = FilterConfiguration(
466                                base_rules=_BASE_FILTER_RULES,
467                                path_specific=_PATH_RULES_SPECIFIER,
468                                user_rules=options.filter_rules)
469
470     return StyleProcessorConfiguration(filter_configuration=filter_configuration,
471                max_reports_per_category=_MAX_REPORTS_PER_CATEGORY,
472                min_confidence=options.min_confidence,
473                output_format=options.output_format,
474                commit_queue=options.commit_queue)
475
476
477 def _create_log_handlers(stream):
478     """Create and return a default list of logging.Handler instances.
479
480     Format WARNING messages and above to display the logging level, and
481     messages strictly below WARNING not to display it.
482
483     Args:
484       stream: See the configure_logging() docstring.
485
486     """
487     # Handles logging.WARNING and above.
488     error_handler = logging.StreamHandler(stream)
489     error_handler.setLevel(logging.WARNING)
490     formatter = logging.Formatter("%(levelname)s: %(message)s")
491     error_handler.setFormatter(formatter)
492
493     # Create a logging.Filter instance that only accepts messages
494     # below WARNING (i.e. filters out anything WARNING or above).
495     non_error_filter = logging.Filter()
496     # The filter method accepts a logging.LogRecord instance.
497     non_error_filter.filter = lambda record: record.levelno < logging.WARNING
498
499     non_error_handler = logging.StreamHandler(stream)
500     non_error_handler.addFilter(non_error_filter)
501     formatter = logging.Formatter("%(message)s")
502     non_error_handler.setFormatter(formatter)
503
504     return [error_handler, non_error_handler]
505
506
507 def _create_debug_log_handlers(stream):
508     """Create and return a list of logging.Handler instances for debugging.
509
510     Args:
511       stream: See the configure_logging() docstring.
512
513     """
514     handler = logging.StreamHandler(stream)
515     formatter = logging.Formatter("%(name)s: %(levelname)-8s %(message)s")
516     handler.setFormatter(formatter)
517
518     return [handler]
519
520
521 def configure_logging(stream, logger=None, is_verbose=False):
522     """Configure logging, and return the list of handlers added.
523
524     Returns:
525       A list of references to the logging handlers added to the root
526       logger.  This allows the caller to later remove the handlers
527       using logger.removeHandler.  This is useful primarily during unit
528       testing where the caller may want to configure logging temporarily
529       and then undo the configuring.
530
531     Args:
532       stream: A file-like object to which to log.  The stream must
533               define an "encoding" data attribute, or else logging
534               raises an error.
535       logger: A logging.logger instance to configure.  This parameter
536               should be used only in unit tests.  Defaults to the
537               root logger.
538       is_verbose: A boolean value of whether logging should be verbose.
539
540     """
541     # If the stream does not define an "encoding" data attribute, the
542     # logging module can throw an error like the following:
543     #
544     # Traceback (most recent call last):
545     #   File "/System/Library/Frameworks/Python.framework/Versions/2.6/...
546     #         lib/python2.6/logging/__init__.py", line 761, in emit
547     #     self.stream.write(fs % msg.encode(self.stream.encoding))
548     # LookupError: unknown encoding: unknown
549     if logger is None:
550         logger = logging.getLogger()
551
552     if is_verbose:
553         logging_level = logging.DEBUG
554         handlers = _create_debug_log_handlers(stream)
555     else:
556         logging_level = logging.INFO
557         handlers = _create_log_handlers(stream)
558
559     handlers = _configure_logging(logging_level=logging_level, logger=logger,
560                                   handlers=handlers)
561
562     return handlers
563
564
565 # Enum-like idiom
566 class FileType:
567
568     NONE = 0  # FileType.NONE evaluates to False.
569     # Alphabetize remaining types
570     CHANGELOG = 1
571     CPP = 2
572     JS = 3
573     JSON = 4
574     PNG = 5
575     PYTHON = 6
576     TEXT = 7
577     WATCHLIST = 8
578     XML = 9
579     XCODEPROJ = 10
580     CMAKE = 11
581     FEATUREDEFINES = 12
582     SDKVARIANT = 13
583
584
585 class CheckerDispatcher(object):
586
587     """Supports determining whether and how to check style, based on path."""
588
589     def _file_extension(self, file_path):
590         """Return the file extension without the leading dot."""
591         return os.path.splitext(file_path)[1].lstrip(".")
592
593     def _should_skip_file_path(self, file_path, skip_array_entry):
594         match = re.search("\s*png$", file_path)
595         if match:
596             return False
597         if isinstance(skip_array_entry, str):
598             if file_path.find(skip_array_entry) >= 0:
599                 return True
600         elif skip_array_entry.match(file_path):
601                 return True
602         return False
603
604     def should_skip_with_warning(self, file_path):
605         """Return whether the given file should be skipped with a warning."""
606         for skipped_file in _SKIPPED_FILES_WITH_WARNING:
607             if self._should_skip_file_path(file_path, skipped_file):
608                 return True
609         return False
610
611     def should_skip_without_warning(self, file_path):
612         """Return whether the given file should be skipped without a warning."""
613         if not self._file_type(file_path):  # FileType.NONE.
614             return True
615         # Since "LayoutTests" is in _SKIPPED_FILES_WITHOUT_WARNING, make
616         # an exception to prevent files like "LayoutTests/ChangeLog" and
617         # "LayoutTests/ChangeLog-2009-06-16" from being skipped.
618         # Files like 'TestExpectations' are also should not be skipped.
619         #
620         # FIXME: Figure out a good way to avoid having to add special logic
621         #        for this special case.
622         basename = os.path.basename(file_path)
623         if basename.startswith('ChangeLog'):
624             return False
625         elif basename in _NEVER_SKIPPED_FILES:
626             return False
627         for skipped_file in _SKIPPED_FILES_WITHOUT_WARNING:
628             if self._should_skip_file_path(file_path, skipped_file):
629                 return True
630         return False
631
632     def should_check_and_strip_carriage_returns(self, file_path):
633         return self._file_extension(file_path) not in _CARRIAGE_RETURN_ALLOWED_FILE_EXTENSIONS
634
635     def _file_type(self, file_path):
636         """Return the file type corresponding to the given file."""
637         file_extension = self._file_extension(file_path)
638
639         if (file_extension in _CPP_FILE_EXTENSIONS) or (file_path == '-'):
640             # FIXME: Do something about the comment below and the issue it
641             #        raises since cpp_style already relies on the extension.
642             #
643             # Treat stdin as C++. Since the extension is unknown when
644             # reading from stdin, cpp_style tests should not rely on
645             # the extension.
646             return FileType.CPP
647         elif file_extension == _JS_FILE_EXTENSION:
648             return FileType.JS
649         elif file_extension == _JSON_FILE_EXTENSION:
650             return FileType.JSON
651         elif file_extension == _PYTHON_FILE_EXTENSION:
652             return FileType.PYTHON
653         elif file_extension in _XML_FILE_EXTENSIONS:
654             return FileType.XML
655         elif os.path.basename(file_path).startswith('ChangeLog'):
656             return FileType.CHANGELOG
657         elif os.path.basename(file_path) == 'watchlist':
658             return FileType.WATCHLIST
659         elif file_extension == _XCODEPROJ_FILE_EXTENSION:
660             return FileType.XCODEPROJ
661         elif file_extension == _PNG_FILE_EXTENSION:
662             return FileType.PNG
663         elif ((file_extension == _CMAKE_FILE_EXTENSION) or os.path.basename(file_path) == 'CMakeLists.txt'):
664             return FileType.CMAKE
665         elif ((not file_extension and os.path.join("Tools", "Scripts") in file_path) or
666               file_extension in _TEXT_FILE_EXTENSIONS or os.path.basename(file_path) == 'TestExpectations'):
667             return FileType.TEXT
668         elif os.path.basename(file_path) == "FeatureDefines.xcconfig":
669             return FileType.FEATUREDEFINES
670         elif os.path.basename(file_path) == "SDKVariant.xcconfig":
671             return FileType.SDKVARIANT
672         else:
673             return FileType.NONE
674
675     def _create_checker(self, file_type, file_path, handle_style_error,
676                         min_confidence, commit_queue):
677         """Instantiate and return a style checker based on file type."""
678         if file_type == FileType.NONE:
679             checker = None
680         elif file_type == FileType.CHANGELOG:
681             should_line_be_checked = None
682             if handle_style_error:
683                 should_line_be_checked = handle_style_error.should_line_be_checked
684             checker = ChangeLogChecker(file_path, handle_style_error, should_line_be_checked)
685         elif file_type == FileType.CPP:
686             file_extension = self._file_extension(file_path)
687             checker = CppChecker(file_path, file_extension,
688                                  handle_style_error, min_confidence)
689         elif file_type == FileType.JS:
690             basename = os.path.basename(file_path)
691             # Do not attempt to check non-Inspector or 3rd-party JavaScript files as JS.
692             if os.path.join('WebInspectorUI', 'UserInterface') in file_path and (not 'External' in file_path):
693                 checker = JSChecker(file_path, handle_style_error)
694             elif basename in _NEVER_SKIPPED_JS_FILES:
695                 checker = JSTestChecker(file_path, handle_style_error)
696             else:
697                 checker = TextChecker(file_path, handle_style_error)
698         elif file_type == FileType.JSON:
699             basename = os.path.basename(file_path)
700             if basename == 'contributors.json':
701                 if commit_queue:
702                     checker = JSONContributorsChecker(file_path, handle_style_error)
703                 else:
704                     checker = ContributorsChecker(file_path, handle_style_error)
705             elif basename == 'features.json':
706                 checker = JSONFeaturesChecker(file_path, handle_style_error)
707             elif basename == 'CSSProperties.json':
708                 checker = JSONCSSPropertiesChecker(file_path, handle_style_error)
709             else:
710                 checker = JSONChecker(file_path, handle_style_error)
711         elif file_type == FileType.PYTHON:
712             python3_paths = ['Tools/resultsdbpy']
713             for partial in python3_paths:
714                 if file_path.startswith(partial):
715                     return Python3Checker(file_path, handle_style_error)
716             if apple_additions():
717                 checker = apple_additions().python_checker(file_path, handle_style_error)
718             else:
719                 checker = PythonChecker(file_path, handle_style_error)
720         elif file_type == FileType.XML:
721             checker = XMLChecker(file_path, handle_style_error)
722         elif file_type == FileType.XCODEPROJ:
723             checker = XcodeProjectFileChecker(file_path, handle_style_error)
724         elif file_type == FileType.PNG:
725             checker = PNGChecker(file_path, handle_style_error)
726         elif file_type == FileType.CMAKE:
727             checker = CMakeChecker(file_path, handle_style_error)
728         elif file_type == FileType.TEXT:
729             basename = os.path.basename(file_path)
730             if basename == 'TestExpectations':
731                 checker = TestExpectationsChecker(file_path, handle_style_error)
732             elif file_path.endswith('.messages.in'):
733                 checker = MessagesInChecker(file_path, handle_style_error)
734             else:
735                 checker = TextChecker(file_path, handle_style_error)
736         elif file_type == FileType.WATCHLIST:
737             checker = WatchListChecker(file_path, handle_style_error)
738         elif file_type == FileType.FEATUREDEFINES:
739             checker = FeatureDefinesChecker(file_path, handle_style_error)
740         elif file_type == FileType.SDKVARIANT:
741             checker = SDKVariantChecker(file_path, handle_style_error)
742         else:
743             raise ValueError('Invalid file type "%(file_type)s": the only valid file types '
744                              "are %(NONE)s, %(CPP)s, and %(TEXT)s."
745                              % {"file_type": file_type,
746                                 "NONE": FileType.NONE,
747                                 "CPP": FileType.CPP,
748                                 "TEXT": FileType.TEXT})
749
750         return checker
751
752     def dispatch(self, file_path, handle_style_error, min_confidence, commit_queue):
753         """Instantiate and return a style checker based on file path."""
754         file_type = self._file_type(file_path)
755
756         checker = self._create_checker(file_type,
757                                        file_path,
758                                        handle_style_error,
759                                        min_confidence,
760                                        commit_queue)
761         return checker
762
763
764 class StyleProcessorConfiguration(object):
765
766     """Stores configuration values for the StyleProcessor class.
767
768     Attributes:
769       min_confidence: An integer between 1 and 5 inclusive that is the
770                       minimum confidence level of style errors to report.
771
772       max_reports_per_category: The maximum number of errors to report
773                                 per category, per file.
774
775     """
776
777     def __init__(self,
778                  filter_configuration,
779                  max_reports_per_category,
780                  min_confidence,
781                  output_format,
782                  commit_queue):
783         """Create a StyleProcessorConfiguration instance.
784
785         Args:
786           filter_configuration: A FilterConfiguration instance.  The default
787                                 is the "empty" filter configuration, which
788                                 means that all errors should be checked.
789
790           max_reports_per_category: The maximum number of errors to report
791                                     per category, per file.
792
793           min_confidence: An integer between 1 and 5 inclusive that is the
794                           minimum confidence level of style errors to report.
795                           The default is 1, which reports all style errors.
796
797           output_format: A string that is the output format.  The supported
798                          output formats are "emacs" which emacs can parse
799                          and "vs7" which Microsoft Visual Studio 7 can parse.
800
801           commit_queue: A bool indicating whether the style check is performed
802                         by the commit queue or not.
803
804         """
805         self._filter_configuration = filter_configuration
806         self._output_format = output_format
807
808         self.max_reports_per_category = max_reports_per_category
809         self.min_confidence = min_confidence
810         self.commit_queue = commit_queue
811
812     def is_reportable(self, category, confidence_in_error, file_path):
813         """Return whether an error is reportable.
814
815         An error is reportable if both the confidence in the error is
816         at least the minimum confidence level and the current filter
817         says the category should be checked for the given path.
818
819         Args:
820           category: A string that is a style category.
821           confidence_in_error: An integer between 1 and 5 inclusive that is
822                                the application's confidence in the error.
823                                A higher number means greater confidence.
824           file_path: The path of the file being checked
825
826         """
827         if confidence_in_error < self.min_confidence:
828             return False
829
830         return self._filter_configuration.should_check(category, file_path)
831
832     def write_style_error(self,
833                           category,
834                           confidence_in_error,
835                           file_path,
836                           line_number,
837                           message):
838         """Write a style error to the configured stderr."""
839         if self._output_format == 'vs7':
840             format_string = "%s(%s):  %s  [%s] [%d]"
841         else:
842             format_string = "%s:%s:  %s  [%s] [%d]"
843
844         _log.error(format_string % (file_path,
845                                            line_number,
846                                            message,
847                                            category,
848                                            confidence_in_error))
849
850
851 class ProcessorBase(object):
852
853     """The base class for processors of lists of lines."""
854
855     def should_process(self, file_path):
856         """Return whether the file at file_path should be processed.
857
858         The TextFileReader class calls this method prior to reading in
859         the lines of a file.  Use this method, for example, to prevent
860         the style checker from reading binary files into memory.
861
862         """
863         raise NotImplementedError('Subclasses should implement.')
864
865     def process(self, lines, file_path, **kwargs):
866         """Process lines of text read from a file.
867
868         Args:
869           lines: A list of lines of text to process.
870           file_path: The path from which the lines were read.
871           **kwargs: This argument signifies that the process() method of
872                     subclasses of ProcessorBase may support additional
873                     keyword arguments.
874                         For example, a style checker's check() method
875                     may support a "reportable_lines" parameter that represents
876                     the line numbers of the lines for which style errors
877                     should be reported.
878
879         """
880         raise NotImplementedError('Subclasses should implement.')
881
882     def do_association_check(self, files, cwd, host=Host()):
883         """It may be the case that changes in a file cause style errors to
884         occur elsewhere in the modified file or in other files (most notably,
885         this is the case for the test expectations linter). The association check
886         is designed to take find issues which require parsing of files outside
887         those provided to the processor.
888
889         Args:
890             files: A dictionary of file names to lists of lines modified in the provided
891             file.  Files which have been removed will also be in the dictionary, they
892             will map to 'None'
893             cwd: Current working directory, assumed to be the root of the scm repository
894             host: Current host for testing
895         """
896         raise NotImplementedError('Subclasses should implement.')
897
898
899 class StyleProcessor(ProcessorBase):
900
901     """A ProcessorBase for checking style.
902
903     Attributes:
904       error_count: An integer that is the total number of reported
905                    errors for the lifetime of this instance.
906
907     """
908
909     def __init__(self, configuration, mock_dispatcher=None,
910                  mock_increment_error_count=None,
911                  mock_carriage_checker_class=None):
912         """Create an instance.
913
914         Args:
915           configuration: A StyleProcessorConfiguration instance.
916           mock_dispatcher: A mock CheckerDispatcher instance.  This
917                            parameter is for unit testing.  Defaults to a
918                            CheckerDispatcher instance.
919           mock_increment_error_count: A mock error-count incrementer.
920           mock_carriage_checker_class: A mock class for checking and
921                                        transforming carriage returns.
922                                        This parameter is for unit testing.
923                                        Defaults to CarriageReturnChecker.
924
925         """
926         if mock_dispatcher is None:
927             dispatcher = CheckerDispatcher()
928         else:
929             dispatcher = mock_dispatcher
930
931         if mock_increment_error_count is None:
932             # The following blank line is present to avoid flagging by pep8.py.
933
934             def increment_error_count():
935                 """Increment the total count of reported errors."""
936                 self.error_count += 1
937         else:
938             increment_error_count = mock_increment_error_count
939
940         if mock_carriage_checker_class is None:
941             # This needs to be a class rather than an instance since the
942             # process() method instantiates one using parameters.
943             carriage_checker_class = CarriageReturnChecker
944         else:
945             carriage_checker_class = mock_carriage_checker_class
946
947         self.error_count = 0
948
949         self._carriage_checker_class = carriage_checker_class
950         self._configuration = configuration
951         self._dispatcher = dispatcher
952         self._increment_error_count = increment_error_count
953
954     def should_process(self, file_path):
955         """Return whether the file should be checked for style."""
956         if self._dispatcher.should_skip_without_warning(file_path):
957             return False
958         if self._dispatcher.should_skip_with_warning(file_path):
959             _log.warn('File exempt from style guide. Skipping: "%s"'
960                       % file_path)
961             return False
962         return True
963
964     def process(self, lines, file_path, line_numbers=None):
965         """Check the given lines for style.
966
967         Arguments:
968           lines: A list of all lines in the file to check.
969           file_path: The path of the file to process.  If possible, the path
970                      should be relative to the source root.  Otherwise,
971                      path-specific logic may not behave as expected.
972           line_numbers: A list of line numbers of the lines for which
973                         style errors should be reported, or None if errors
974                         for all lines should be reported.  When not None, this
975                         list normally contains the line numbers corresponding
976                         to the modified lines of a patch.
977
978         """
979         _log.debug("Checking style: " + file_path)
980
981         style_error_handler = DefaultStyleErrorHandler(
982             configuration=self._configuration,
983             file_path=file_path,
984             increment_error_count=self._increment_error_count,
985             line_numbers=line_numbers)
986
987         carriage_checker = self._carriage_checker_class(style_error_handler)
988
989         # Check for and remove trailing carriage returns ("\r").
990         if self._dispatcher.should_check_and_strip_carriage_returns(file_path):
991             lines = carriage_checker.check(lines)
992
993         min_confidence = self._configuration.min_confidence
994         checker = self._dispatcher.dispatch(file_path,
995                                             style_error_handler,
996                                             min_confidence,
997                                             self._configuration.commit_queue)
998
999         if checker is None:
1000             raise AssertionError("File should not be checked: '%s'" % file_path)
1001
1002         _log.debug("Using class: " + checker.__class__.__name__)
1003
1004         checker.check(lines)
1005
1006     def do_association_check(self, files, cwd, host=Host()):
1007         _log.debug("Running TestExpectations linter")
1008         TestExpectationsChecker.lint_test_expectations(files, self._configuration, cwd, self._increment_error_count, host=host)