193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik# Copyright (c) 2014 The Chromium Authors. All rights reserved.
22da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# Use of this source code is governed by a BSD-style license that can be
32da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# found in the LICENSE file.
4b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikimport os
693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikimport re
72da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
8b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craikfrom tracing import tracing_project
9b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
10b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
112da489cd246702bee5938545b18a6f710ed214bcJamie Gennisclass JSChecker(object):
12b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik
1393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik  def __init__(self, input_api, file_filter=None):
142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    self.input_api = input_api
1593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik    if file_filter:
1693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      self.file_filter = file_filter
1793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik    else:
1893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      self.file_filter = lambda x: True
192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  def RegexCheck(self, line_number, line, regex, message):
212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """Searches for |regex| in |line| to check for a particular style
222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis       violation, returning a message like the one below if the regex matches.
232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis       The |regex| must have exactly one capturing group so that the relevant
242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis       part of |line| can be highlighted. If more groups are needed, use
252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis       "(?:...)" to make a non-capturing group. Sample message:
262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis       line 6: Use var instead of const.
282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis           const foo = bar();
292da489cd246702bee5938545b18a6f710ed214bcJamie Gennis           ^^^^^
302da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """
3193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik    match = re.search(regex, line)
322da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    if match:
332da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      assert len(match.groups()) == 1
342da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      start = match.start(1)
352da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      length = match.end(1) - start
362da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return '  line %d: %s\n%s\n%s' % (
372da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          line_number,
382da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          message,
392da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          line,
402da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          self.error_highlight(start, length))
412da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    return ''
422da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
432da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  def ConstCheck(self, i, line):
442da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """Check for use of the 'const' keyword."""
4593216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik    if re.search(r'\*\s+@const', line):
462da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      # Probably a JsDoc line
472da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      return ''
482da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
492da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    return self.RegexCheck(i, line, r'(?:^|\s|\()(const)\s',
50b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik                           'Use var instead of const.')
512da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
522da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  def error_highlight(self, start, length):
532da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """Takes a start position and a length, and produces a row of '^'s to
542da489cd246702bee5938545b18a6f710ed214bcJamie Gennis       highlight the corresponding part of a string.
552da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """
562da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    return start * ' ' + length * '^'
572da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
582da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  def _makeErrorOrWarning(self, error_text, filename):
5993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik    return error_text
602da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
612da489cd246702bee5938545b18a6f710ed214bcJamie Gennis  def RunChecks(self):
622da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """Check for violations of the Chromium JavaScript style guide. See
632da489cd246702bee5938545b18a6f710ed214bcJamie Gennis       http://chromium.org/developers/web-development-style-guide#TOC-JavaScript
642da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """
652da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
662da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    import sys
672da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    import warnings
682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    old_path = sys.path
692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    old_filters = warnings.filters
702da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
712da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    try:
7293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      base_path = os.path.abspath(os.path.join(
73b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik          os.path.dirname(__file__), '..', '..'))
7493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      closure_linter_path = os.path.join(
75b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik          base_path, 'tracing', 'third_party', 'closure_linter')
7693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      gflags_path = os.path.join(
77b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik          base_path, 'tracing', 'third_party', 'python_gflags')
782da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      sys.path.insert(0, closure_linter_path)
792da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      sys.path.insert(0, gflags_path)
802da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
812da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      warnings.filterwarnings('ignore', category=DeprecationWarning)
822da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
832da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      from closure_linter import checker, errors
842da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      from closure_linter.common import errorhandler
852da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
862da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    finally:
872da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      sys.path = old_path
882da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      warnings.filters = old_filters
892da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
902da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    class ErrorHandlerImpl(errorhandler.ErrorHandler):
912da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      """Filters out errors that don't apply to Chromium JavaScript code."""
922da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
9393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      def __init__(self):
942da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._errors = []
952da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
962da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      def HandleFile(self, filename, first_token):
972da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._filename = filename
982da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
992da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      def HandleError(self, error):
1002da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if (self._valid(error)):
1012da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          error.filename = self._filename
1022da489cd246702bee5938545b18a6f710ed214bcJamie Gennis          self._errors.append(error)
1032da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1042da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      def GetErrors(self):
1052da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return self._errors
1062da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1072da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      def HasErrors(self):
1082da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return bool(self._errors)
1092da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1102da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      def _valid(self, error):
1112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """Check whether an error is valid. Most errors are valid, with a few
1122da489cd246702bee5938545b18a6f710ed214bcJamie Gennis           exceptions which are listed here.
1132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """
1142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        is_grit_statement = bool(
11693216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik            re.search("</?(include|if)", error.token.line))
1172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return not is_grit_statement and error.code not in [
1192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
1202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            errors.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER,
1212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            errors.MISSING_JSDOC_TAG_THIS,
1222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        ]
1232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    results = []
1252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
12666a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    try:
12793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      affected_files = self.input_api.AffectedFiles(
12866a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          file_filter=self.file_filter,
12966a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis          include_deletes=False)
13066a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis    except:
13166a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      affected_files = []
13266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis
13393216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik    def ShouldCheck(f):
134b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik      if tracing_project.TracingProject.IsIgnoredFile(f):
13524385dbd2e4e893fe6ed411b3dbec947d956a599Chris Craik        return False
136b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik      if f.LocalPath().endswith('.js'):
13793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik        return True
138b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik      if f.LocalPath().endswith('.html'):
13993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik        return True
14093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      return False
14193216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik
14293216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik    affected_js_files = filter(ShouldCheck, affected_files)
1432da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    for f in affected_js_files:
1442da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      error_lines = []
1452da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
146b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik      for i, line in enumerate(f.NewContents(), start=1):
1472da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        error_lines += filter(None, [
1482da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            self.ConstCheck(i, line),
1492da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        ])
1502da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1512da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      # Use closure_linter to check for several different errors
15266a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      import gflags as flags
15366a37686207944273ced825e0e8b6b6375f8c3deJamie Gennis      flags.FLAGS.strict = True
15493216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik      error_handler = ErrorHandlerImpl()
1552da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      js_checker = checker.JavaScriptStyleChecker(error_handler)
156b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik      js_checker.Check(f.AbsoluteLocalPath())
1572da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1582da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      for error in error_handler.GetErrors():
1592da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        highlight = self.error_highlight(
1602da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            error.token.start_index, error.token.length)
1612da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        error_msg = '  line %d: E%04d: %s\n%s\n%s' % (
1622da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            error.token.line_number,
1632da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            error.code,
1642da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            error.message,
1652da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            error.token.line.rstrip(),
1662da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            highlight)
1672da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        error_lines.append(error_msg)
1682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis      if error_lines:
1702da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        error_lines = [
1712da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            'Found JavaScript style violations in %s:' %
172b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            f.LocalPath()] + error_lines
1732da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        results.append(self._makeErrorOrWarning(
174b2cbf1594f8d6e4ba32d384cf379f62a74ed7654Chris Craik            '\n'.join(error_lines), f.LocalPath()))
1752da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1762da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    return results
17793216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik
17893216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik
17993216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craikdef RunChecks(input_api):
18093216d0b8afcc23eb8811175ca32338cd09c9dcaChris Craik  return JSChecker(input_api).RunChecks()
181