1#!/usr/bin/env python 2# Copyright 2007 The Closure Linter Authors. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS-IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Checks JavaScript files for common style guide violations. 17 18gjslint.py is designed to be used as a PRESUBMIT script to check for javascript 19style guide violations. As of now, it checks for the following violations: 20 21 * Missing and extra spaces 22 * Lines longer than 80 characters 23 * Missing newline at end of file 24 * Missing semicolon after function declaration 25 * Valid JsDoc including parameter matching 26 27Someday it will validate to the best of its ability against the entirety of the 28JavaScript style guide. 29 30This file is a front end that parses arguments and flags. The core of the code 31is in tokenizer.py and checker.py. 32""" 33 34__author__ = ('robbyw@google.com (Robert Walker)', 35 'ajp@google.com (Andy Perelson)', 36 'nnaze@google.com (Nathan Naze)',) 37 38import errno 39import itertools 40import os 41import platform 42import re 43import sys 44import time 45 46import gflags as flags 47 48from closure_linter import errorrecord 49from closure_linter import runner 50from closure_linter.common import erroraccumulator 51from closure_linter.common import simplefileflags as fileflags 52 53# Attempt import of multiprocessing (should be available in Python 2.6 and up). 54try: 55 # pylint: disable=g-import-not-at-top 56 import multiprocessing 57except ImportError: 58 multiprocessing = None 59 60FLAGS = flags.FLAGS 61flags.DEFINE_boolean('unix_mode', False, 62 'Whether to emit warnings in standard unix format.') 63flags.DEFINE_boolean('beep', True, 'Whether to beep when errors are found.') 64flags.DEFINE_boolean('time', False, 'Whether to emit timing statistics.') 65flags.DEFINE_boolean('quiet', False, 'Whether to minimize logged messages. ' 66 'Most useful for per-file linting, such as that performed ' 67 'by the presubmit linter service.') 68flags.DEFINE_boolean('check_html', False, 69 'Whether to check javascript in html files.') 70flags.DEFINE_boolean('summary', False, 71 'Whether to show an error count summary.') 72flags.DEFINE_list('additional_extensions', None, 'List of additional file ' 73 'extensions (not js) that should be treated as ' 74 'JavaScript files.') 75flags.DEFINE_boolean('multiprocess', 76 platform.system() is 'Linux' and bool(multiprocessing), 77 'Whether to attempt parallelized linting using the ' 78 'multiprocessing module. Enabled by default on Linux ' 79 'if the multiprocessing module is present (Python 2.6+). ' 80 'Otherwise disabled by default. ' 81 'Disabling may make debugging easier.') 82flags.ADOPT_module_key_flags(fileflags) 83flags.ADOPT_module_key_flags(runner) 84 85 86GJSLINT_ONLY_FLAGS = ['--unix_mode', '--beep', '--nobeep', '--time', 87 '--check_html', '--summary', '--quiet'] 88 89 90 91def _MultiprocessCheckPaths(paths): 92 """Run _CheckPath over mutltiple processes. 93 94 Tokenization, passes, and checks are expensive operations. Running in a 95 single process, they can only run on one CPU/core. Instead, 96 shard out linting over all CPUs with multiprocessing to parallelize. 97 98 Args: 99 paths: paths to check. 100 101 Yields: 102 errorrecord.ErrorRecords for any found errors. 103 """ 104 105 pool = multiprocessing.Pool() 106 107 path_results = pool.imap(_CheckPath, paths) 108 for results in path_results: 109 for result in results: 110 yield result 111 112 # Force destruct before returning, as this can sometimes raise spurious 113 # "interrupted system call" (EINTR), which we can ignore. 114 try: 115 pool.close() 116 pool.join() 117 del pool 118 except OSError as err: 119 if err.errno is not errno.EINTR: 120 raise err 121 122 123def _CheckPaths(paths): 124 """Run _CheckPath on all paths in one thread. 125 126 Args: 127 paths: paths to check. 128 129 Yields: 130 errorrecord.ErrorRecords for any found errors. 131 """ 132 133 for path in paths: 134 results = _CheckPath(path) 135 for record in results: 136 yield record 137 138 139def _CheckPath(path): 140 """Check a path and return any errors. 141 142 Args: 143 path: paths to check. 144 145 Returns: 146 A list of errorrecord.ErrorRecords for any found errors. 147 """ 148 149 error_handler = erroraccumulator.ErrorAccumulator() 150 runner.Run(path, error_handler) 151 152 make_error_record = lambda err: errorrecord.MakeErrorRecord(path, err) 153 return map(make_error_record, error_handler.GetErrors()) 154 155 156def _GetFilePaths(argv): 157 suffixes = ['.js'] 158 if FLAGS.additional_extensions: 159 suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions] 160 if FLAGS.check_html: 161 suffixes += ['.html', '.htm'] 162 return fileflags.GetFileList(argv, 'JavaScript', suffixes) 163 164 165# Error printing functions 166 167 168def _PrintFileSummary(paths, records): 169 """Print a detailed summary of the number of errors in each file.""" 170 171 paths = list(paths) 172 paths.sort() 173 174 for path in paths: 175 path_errors = [e for e in records if e.path == path] 176 print '%s: %d' % (path, len(path_errors)) 177 178 179def _PrintFileSeparator(path): 180 print '----- FILE : %s -----' % path 181 182 183def _PrintSummary(paths, error_records): 184 """Print a summary of the number of errors and files.""" 185 186 error_count = len(error_records) 187 all_paths = set(paths) 188 all_paths_count = len(all_paths) 189 190 if error_count is 0: 191 print '%d files checked, no errors found.' % all_paths_count 192 193 new_error_count = len([e for e in error_records if e.new_error]) 194 195 error_paths = set([e.path for e in error_records]) 196 error_paths_count = len(error_paths) 197 no_error_paths_count = all_paths_count - error_paths_count 198 199 if (error_count or new_error_count) and not FLAGS.quiet: 200 error_noun = 'error' if error_count == 1 else 'errors' 201 new_error_noun = 'error' if new_error_count == 1 else 'errors' 202 error_file_noun = 'file' if error_paths_count == 1 else 'files' 203 ok_file_noun = 'file' if no_error_paths_count == 1 else 'files' 204 print ('Found %d %s, including %d new %s, in %d %s (%d %s OK).' % 205 (error_count, 206 error_noun, 207 new_error_count, 208 new_error_noun, 209 error_paths_count, 210 error_file_noun, 211 no_error_paths_count, 212 ok_file_noun)) 213 214 215def _PrintErrorRecords(error_records): 216 """Print error records strings in the expected format.""" 217 218 current_path = None 219 for record in error_records: 220 221 if current_path != record.path: 222 current_path = record.path 223 if not FLAGS.unix_mode: 224 _PrintFileSeparator(current_path) 225 226 print record.error_string 227 228 229def _FormatTime(t): 230 """Formats a duration as a human-readable string. 231 232 Args: 233 t: A duration in seconds. 234 235 Returns: 236 A formatted duration string. 237 """ 238 if t < 1: 239 return '%dms' % round(t * 1000) 240 else: 241 return '%.2fs' % t 242 243 244 245 246def main(argv=None): 247 """Main function. 248 249 Args: 250 argv: Sequence of command line arguments. 251 """ 252 if argv is None: 253 argv = flags.FLAGS(sys.argv) 254 255 if FLAGS.time: 256 start_time = time.time() 257 258 # Emacs sets the environment variable INSIDE_EMACS in the subshell. 259 # Request Unix mode as emacs will expect output to be in Unix format 260 # for integration. 261 # See https://www.gnu.org/software/emacs/manual/html_node/emacs/ 262 # Interactive-Shell.html 263 if 'INSIDE_EMACS' in os.environ: 264 FLAGS.unix_mode = True 265 266 suffixes = ['.js'] 267 if FLAGS.additional_extensions: 268 suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions] 269 if FLAGS.check_html: 270 suffixes += ['.html', '.htm'] 271 paths = fileflags.GetFileList(argv, 'JavaScript', suffixes) 272 273 if FLAGS.multiprocess: 274 records_iter = _MultiprocessCheckPaths(paths) 275 else: 276 records_iter = _CheckPaths(paths) 277 278 records_iter, records_iter_copy = itertools.tee(records_iter, 2) 279 _PrintErrorRecords(records_iter_copy) 280 281 error_records = list(records_iter) 282 _PrintSummary(paths, error_records) 283 284 exit_code = 0 285 286 # If there are any errors 287 if error_records: 288 exit_code += 1 289 290 # If there are any new errors 291 if [r for r in error_records if r.new_error]: 292 exit_code += 2 293 294 if exit_code: 295 if FLAGS.summary: 296 _PrintFileSummary(paths, error_records) 297 298 if FLAGS.beep: 299 # Make a beep noise. 300 sys.stdout.write(chr(7)) 301 302 # Write out instructions for using fixjsstyle script to fix some of the 303 # reported errors. 304 fix_args = [] 305 for flag in sys.argv[1:]: 306 for f in GJSLINT_ONLY_FLAGS: 307 if flag.startswith(f): 308 break 309 else: 310 fix_args.append(flag) 311 312 if not FLAGS.quiet: 313 print """ 314Some of the errors reported by GJsLint may be auto-fixable using the script 315fixjsstyle. Please double check any changes it makes and report any bugs. The 316script can be run by executing: 317 318fixjsstyle %s """ % ' '.join(fix_args) 319 320 if FLAGS.time: 321 print 'Done in %s.' % _FormatTime(time.time() - start_time) 322 323 sys.exit(exit_code) 324 325 326if __name__ == '__main__': 327 main() 328