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
7
8from tracing import tracing_project
9
10
11class JSChecker(object):
12
13  def __init__(self, input_api, file_filter=None):
14    self.input_api = input_api
15    if file_filter:
16      self.file_filter = file_filter
17    else:
18      self.file_filter = lambda x: True
19
20  def RegexCheck(self, line_number, line, regex, message):
21    """Searches for |regex| in |line| to check for a particular style
22       violation, returning a message like the one below if the regex matches.
23       The |regex| must have exactly one capturing group so that the relevant
24       part of |line| can be highlighted. If more groups are needed, use
25       "(?:...)" to make a non-capturing group. Sample message:
26
27       line 6: Use var instead of const.
28           const foo = bar();
29           ^^^^^
30    """
31    match = re.search(regex, line)
32    if match:
33      assert len(match.groups()) == 1
34      start = match.start(1)
35      length = match.end(1) - start
36      return '  line %d: %s\n%s\n%s' % (
37          line_number,
38          message,
39          line,
40          self.error_highlight(start, length))
41    return ''
42
43  def ConstCheck(self, i, line):
44    """Check for use of the 'const' keyword."""
45    if re.search(r'\*\s+@const', line):
46      # Probably a JsDoc line
47      return ''
48
49    return self.RegexCheck(i, line, r'(?:^|\s|\()(const)\s',
50                           'Use var instead of const.')
51
52  def error_highlight(self, start, length):
53    """Takes a start position and a length, and produces a row of '^'s to
54       highlight the corresponding part of a string.
55    """
56    return start * ' ' + length * '^'
57
58  def _makeErrorOrWarning(self, error_text, filename):
59    return error_text
60
61  def RunChecks(self):
62    """Check for violations of the Chromium JavaScript style guide. See
63       http://chromium.org/developers/web-development-style-guide#TOC-JavaScript
64    """
65
66    import sys
67    import warnings
68    old_path = sys.path
69    old_filters = warnings.filters
70
71    try:
72      base_path = os.path.abspath(os.path.join(
73          os.path.dirname(__file__), '..', '..'))
74      closure_linter_path = os.path.join(
75          base_path, 'tracing', 'third_party', 'closure_linter')
76      gflags_path = os.path.join(
77          base_path, 'tracing', 'third_party', 'python_gflags')
78      sys.path.insert(0, closure_linter_path)
79      sys.path.insert(0, gflags_path)
80
81      warnings.filterwarnings('ignore', category=DeprecationWarning)
82
83      from closure_linter import checker, errors
84      from closure_linter.common import errorhandler
85
86    finally:
87      sys.path = old_path
88      warnings.filters = old_filters
89
90    class ErrorHandlerImpl(errorhandler.ErrorHandler):
91      """Filters out errors that don't apply to Chromium JavaScript code."""
92
93      def __init__(self):
94        self._errors = []
95
96      def HandleFile(self, filename, first_token):
97        self._filename = filename
98
99      def HandleError(self, error):
100        if (self._valid(error)):
101          error.filename = self._filename
102          self._errors.append(error)
103
104      def GetErrors(self):
105        return self._errors
106
107      def HasErrors(self):
108        return bool(self._errors)
109
110      def _valid(self, error):
111        """Check whether an error is valid. Most errors are valid, with a few
112           exceptions which are listed here.
113        """
114
115        is_grit_statement = bool(
116            re.search("</?(include|if)", error.token.line))
117
118        return not is_grit_statement and error.code not in [
119            errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
120            errors.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER,
121            errors.MISSING_JSDOC_TAG_THIS,
122        ]
123
124    results = []
125
126    try:
127      affected_files = self.input_api.AffectedFiles(
128          file_filter=self.file_filter,
129          include_deletes=False)
130    except:
131      affected_files = []
132
133    def ShouldCheck(f):
134      if tracing_project.TracingProject.IsIgnoredFile(f):
135        return False
136      if f.LocalPath().endswith('.js'):
137        return True
138      if f.LocalPath().endswith('.html'):
139        return True
140      return False
141
142    affected_js_files = filter(ShouldCheck, affected_files)
143    for f in affected_js_files:
144      error_lines = []
145
146      for i, line in enumerate(f.NewContents(), start=1):
147        error_lines += filter(None, [
148            self.ConstCheck(i, line),
149        ])
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      js_checker = checker.JavaScriptStyleChecker(error_handler)
156      js_checker.Check(f.AbsoluteLocalPath())
157
158      for error in error_handler.GetErrors():
159        highlight = self.error_highlight(
160            error.token.start_index, error.token.length)
161        error_msg = '  line %d: E%04d: %s\n%s\n%s' % (
162            error.token.line_number,
163            error.code,
164            error.message,
165            error.token.line.rstrip(),
166            highlight)
167        error_lines.append(error_msg)
168
169      if error_lines:
170        error_lines = [
171            'Found JavaScript style violations in %s:' %
172            f.LocalPath()] + error_lines
173        results.append(self._makeErrorOrWarning(
174            '\n'.join(error_lines), f.LocalPath()))
175
176    return results
177
178
179def RunChecks(input_api):
180  return JSChecker(input_api).RunChecks()
181