• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import os
6import re
7import sys
8import warnings
9
10from py_vulcanize import strip_js_comments
11
12from catapult_build import parse_html
13
14
15class JSChecker(object):
16
17  def __init__(self, input_api, output_api, file_filter=None):
18    self.input_api = input_api
19    self.output_api = output_api
20    if file_filter:
21      self.file_filter = file_filter
22    else:
23      self.file_filter = lambda x: True
24
25  def RegexCheck(self, line_number, line, regex, message):
26    """Searches for |regex| in |line| to check for a style violation.
27
28    The |regex| must have exactly one capturing group so that the relevant
29    part of |line| can be highlighted. If more groups are needed, use
30    "(?:...)" to make a non-capturing group. Sample message:
31
32    Returns a message like the one below if the regex matches.
33       line 6: Use var instead of const.
34           const foo = bar();
35           ^^^^^
36    """
37    match = re.search(regex, line)
38    if match:
39      assert len(match.groups()) == 1
40      start = match.start(1)
41      length = match.end(1) - start
42      return '  line %d: %s\n%s\n%s' % (
43          line_number,
44          message,
45          line,
46          _ErrorHighlight(start, length))
47    return ''
48
49  def ConstCheck(self, i, line):
50    """Checks for use of the 'const' keyword."""
51    if re.search(r'\*\s+@const', line):
52      # Probably a JsDoc line.
53      return ''
54
55    return self.RegexCheck(
56        i, line, r'(?:^|\s|\()(const)\s', 'Use var instead of const.')
57
58  def RunChecks(self):
59    """Checks for violations of the Chromium JavaScript style guide.
60
61    See:
62    http://chromium.org/developers/web-development-style-guide#TOC-JavaScript
63    """
64    old_path = sys.path
65    old_filters = warnings.filters
66
67    try:
68      base_path = os.path.abspath(os.path.join(
69          os.path.dirname(__file__), '..'))
70      closure_linter_path = os.path.join(
71          base_path, 'third_party', 'closure_linter')
72      gflags_path = os.path.join(
73          base_path, 'third_party', 'python_gflags')
74      sys.path.insert(0, closure_linter_path)
75      sys.path.insert(0, gflags_path)
76
77      warnings.filterwarnings('ignore', category=DeprecationWarning)
78
79      from closure_linter import runner, errors
80      from closure_linter.common import errorhandler
81
82    finally:
83      sys.path = old_path
84      warnings.filters = old_filters
85
86    class ErrorHandlerImpl(errorhandler.ErrorHandler):
87      """Filters out errors that don't apply to Chromium JavaScript code."""
88
89      def __init__(self):
90        super(ErrorHandlerImpl, self).__init__()
91        self._errors = []
92        self._filename = None
93
94      def HandleFile(self, filename, _):
95        self._filename = filename
96
97      def HandleError(self, error):
98        if self._Valid(error):
99          error.filename = self._filename
100          self._errors.append(error)
101
102      def GetErrors(self):
103        return self._errors
104
105      def HasErrors(self):
106        return bool(self._errors)
107
108      def _Valid(self, error):
109        """Checks whether an error is valid.
110
111        Most errors are valid, with a few exceptions which are listed here.
112        """
113        if re.search('</?(include|if)', error.token.line):
114          return False  # GRIT statement.
115
116        if (error.code == errors.MISSING_SEMICOLON and
117            error.token.string == 'of'):
118          return False  # ES6 for...of statement.
119
120        return error.code not in [
121            errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
122            errors.MISSING_JSDOC_TAG_THIS,
123            errors.MISSING_MEMBER_DOCUMENTATION,
124        ]
125
126    results = []
127
128    affected_files = self.input_api.AffectedFiles(
129        file_filter=self.file_filter,
130        include_deletes=False)
131
132    def ShouldCheck(f):
133      if f.LocalPath().endswith('.js'):
134        return True
135      if f.LocalPath().endswith('.html'):
136        return True
137      return False
138
139    affected_js_files = filter(ShouldCheck, affected_files)
140    for f in affected_js_files:
141      error_lines = []
142
143      contents = list(f.NewContents())
144      error_lines += CheckStrictMode(
145          '\n'.join(contents),
146          is_html_file=f.LocalPath().endswith('.html'))
147
148      for i, line in enumerate(contents, start=1):
149        error_lines += filter(None, [self.ConstCheck(i, line)])
150
151      # Use closure_linter to check for several different errors.
152      import gflags as flags
153      flags.FLAGS.strict = True
154      error_handler = ErrorHandlerImpl()
155      runner.Run(f.AbsoluteLocalPath(), error_handler)
156
157      for error in error_handler.GetErrors():
158        highlight = _ErrorHighlight(
159            error.token.start_index, error.token.length)
160        error_msg = '  line %d: E%04d: %s\n%s\n%s' % (
161            error.token.line_number,
162            error.code,
163            error.message,
164            error.token.line.rstrip(),
165            highlight)
166        error_lines.append(error_msg)
167
168      if error_lines:
169        error_lines = [
170            'Found JavaScript style violations in %s:' %
171            f.LocalPath()] + error_lines
172        results.append(
173            _MakeErrorOrWarning(self.output_api, '\n'.join(error_lines)))
174
175    return results
176
177
178def _ErrorHighlight(start, length):
179  """Produces a row of '^'s to underline part of a string."""
180  return start * ' ' + length * '^'
181
182
183def _MakeErrorOrWarning(output_api, error_text):
184  return output_api.PresubmitError(error_text)
185
186
187def CheckStrictMode(contents, is_html_file=False):
188  statements_to_check = []
189  if is_html_file:
190    statements_to_check.extend(_FirstStatementsInScriptElements(contents))
191  else:
192    statements_to_check.append(_FirstStatement(contents))
193  error_lines = []
194  for s in statements_to_check:
195    if s != "'use strict'":
196      error_lines.append('Expected "\'use strict\'" as first statement, '
197                         'but found "%s" instead.' % s)
198  return error_lines
199
200
201def _FirstStatementsInScriptElements(contents):
202  """Returns a list of first statements found in each <script> element."""
203  soup = parse_html.BeautifulSoup(contents)
204  script_elements = soup.find_all('script', src=None)
205  return [_FirstStatement(e.get_text()) for e in script_elements]
206
207
208def _FirstStatement(contents):
209  """Extracts the first statement in some JS source code."""
210  stripped_contents = strip_js_comments.StripJSComments(contents).strip()
211  matches = re.match('^(.*?);', stripped_contents, re.DOTALL)
212  if not matches:
213    return ''
214  return matches.group(1).strip()
215
216
217def RunChecks(input_api, output_api, excluded_paths=None):
218
219  def ShouldCheck(affected_file):
220    if not excluded_paths:
221      return True
222    path = affected_file.LocalPath()
223    return not any(re.match(pattern, path) for pattern in excluded_paths)
224
225  return JSChecker(input_api, output_api, file_filter=ShouldCheck).RunChecks()
226