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 5 import re 6 7 from node_runner import node_util 8 from py_vulcanize import strip_js_comments 9 10 from catapult_build import parse_html 11 12 13 class JSChecker(object): 14 15 def __init__(self, input_api, output_api, file_filter=None): 16 self.input_api = input_api 17 self.output_api = output_api 18 if file_filter: 19 self.file_filter = file_filter 20 else: 21 self.file_filter = lambda x: True 22 23 def RegexCheck(self, line_number, line, regex, message): 24 """Searches for |regex| in |line| to check for a style violation. 25 26 The |regex| must have exactly one capturing group so that the relevant 27 part of |line| can be highlighted. If more groups are needed, use 28 "(?:...)" to make a non-capturing group. Sample message: 29 30 Returns a message like the one below if the regex matches. 31 line 6: Use var instead of const. 32 const foo = bar(); 33 ^^^^^ 34 """ 35 match = re.search(regex, line) 36 if match: 37 assert len(match.groups()) == 1 38 start = match.start(1) 39 length = match.end(1) - start 40 return ' line %d: %s\n%s\n%s' % ( 41 line_number, 42 message, 43 line, 44 _ErrorHighlight(start, length)) 45 return '' 46 47 def ConstCheck(self, i, line): 48 """Checks for use of the 'const' keyword.""" 49 if re.search(r'\*\s+@const', line): 50 # Probably a JsDoc line. 51 return '' 52 53 return self.RegexCheck( 54 i, line, r'(?:^|\s|\()(const)\s', 'Use var instead of const.') 55 56 def RunChecks(self): 57 """Checks for violations of the Chromium JavaScript style guide. 58 59 See: 60 http://chromium.org/developers/web-development-style-guide#TOC-JavaScript 61 """ 62 results = [] 63 64 affected_files = self.input_api.AffectedFiles( 65 file_filter=self.file_filter, 66 include_deletes=False) 67 68 def ShouldCheck(f): 69 if f.LocalPath().endswith('.js'): 70 return True 71 if f.LocalPath().endswith('.html'): 72 return True 73 return False 74 75 affected_js_files = filter(ShouldCheck, affected_files) 76 error_lines = [] 77 for f in affected_js_files: 78 contents = list(f.NewContents()) 79 error_lines += CheckStrictMode( 80 '\n'.join(contents), 81 is_html_file=f.LocalPath().endswith('.html')) 82 83 for i, line in enumerate(contents, start=1): 84 error_lines += filter(None, [self.ConstCheck(i, line)]) 85 86 if affected_js_files: 87 eslint_output = node_util.RunEslint( 88 [f.AbsoluteLocalPath() for f in affected_js_files]).rstrip() 89 90 if eslint_output: 91 error_lines.append('\neslint found lint errors:') 92 error_lines.append(eslint_output) 93 94 if error_lines: 95 error_lines.insert(0, 'Found JavaScript style violations:') 96 results.append( 97 _MakeErrorOrWarning(self.output_api, '\n'.join(error_lines))) 98 99 return results 100 101 102 def _ErrorHighlight(start, length): 103 """Produces a row of '^'s to underline part of a string.""" 104 return start * ' ' + length * '^' 105 106 107 def _MakeErrorOrWarning(output_api, error_text): 108 return output_api.PresubmitError(error_text) 109 110 111 def CheckStrictMode(contents, is_html_file=False): 112 statements_to_check = [] 113 if is_html_file: 114 statements_to_check.extend(_FirstStatementsInScriptElements(contents)) 115 else: 116 statements_to_check.append(_FirstStatement(contents)) 117 error_lines = [] 118 for s in statements_to_check: 119 if s != "'use strict'": 120 error_lines.append('Expected "\'use strict\'" as first statement, ' 121 'but found "%s" instead.' % s) 122 return error_lines 123 124 125 def _FirstStatementsInScriptElements(contents): 126 """Returns a list of first statements found in each <script> element.""" 127 soup = parse_html.BeautifulSoup(contents) 128 script_elements = soup.find_all('script', src=None) 129 return [_FirstStatement(e.get_text()) for e in script_elements] 130 131 132 def _FirstStatement(contents): 133 """Extracts the first statement in some JS source code.""" 134 stripped_contents = strip_js_comments.StripJSComments(contents).strip() 135 matches = re.match('^(.*?);', stripped_contents, re.DOTALL) 136 if not matches: 137 return '' 138 return matches.group(1).strip() 139 140 141 def RunChecks(input_api, output_api, excluded_paths=None): 142 143 def ShouldCheck(affected_file): 144 if not excluded_paths: 145 return True 146 path = affected_file.LocalPath() 147 return not any(re.match(pattern, path) for pattern in excluded_paths) 148 149 return JSChecker(input_api, output_api, file_filter=ShouldCheck).RunChecks() 150