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