| OLD | NEW |
| (Empty) |
| 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 | |
| 18 gjslint.py is designed to be used as a PRESUBMIT script to check for javascript | |
| 19 style 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 | |
| 27 Someday it will validate to the best of its ability against the entirety of the | |
| 28 JavaScript style guide. | |
| 29 | |
| 30 This file is a front end that parses arguments and flags. The core of the code | |
| 31 is 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 | |
| 38 import errno | |
| 39 import itertools | |
| 40 import os | |
| 41 import platform | |
| 42 import re | |
| 43 import sys | |
| 44 import time | |
| 45 | |
| 46 import gflags as flags | |
| 47 | |
| 48 from closure_linter import errorrecord | |
| 49 from closure_linter import runner | |
| 50 from closure_linter.common import erroraccumulator | |
| 51 from closure_linter.common import simplefileflags as fileflags | |
| 52 | |
| 53 # Attempt import of multiprocessing (should be available in Python 2.6 and up). | |
| 54 try: | |
| 55 # pylint: disable=g-import-not-at-top | |
| 56 import multiprocessing | |
| 57 except ImportError: | |
| 58 multiprocessing = None | |
| 59 | |
| 60 FLAGS = flags.FLAGS | |
| 61 flags.DEFINE_boolean('unix_mode', False, | |
| 62 'Whether to emit warnings in standard unix format.') | |
| 63 flags.DEFINE_boolean('beep', True, 'Whether to beep when errors are found.') | |
| 64 flags.DEFINE_boolean('time', False, 'Whether to emit timing statistics.') | |
| 65 flags.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.') | |
| 68 flags.DEFINE_boolean('check_html', False, | |
| 69 'Whether to check javascript in html files.') | |
| 70 flags.DEFINE_boolean('summary', False, | |
| 71 'Whether to show an error count summary.') | |
| 72 flags.DEFINE_list('additional_extensions', None, 'List of additional file ' | |
| 73 'extensions (not js) that should be treated as ' | |
| 74 'JavaScript files.') | |
| 75 flags.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.') | |
| 82 flags.ADOPT_module_key_flags(fileflags) | |
| 83 flags.ADOPT_module_key_flags(runner) | |
| 84 | |
| 85 | |
| 86 GJSLINT_ONLY_FLAGS = ['--unix_mode', '--beep', '--nobeep', '--time', | |
| 87 '--check_html', '--summary', '--quiet'] | |
| 88 | |
| 89 | |
| 90 | |
| 91 def _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 | |
| 123 def _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 | |
| 139 def _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 | |
| 156 def _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 | |
| 168 def _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 | |
| 179 def _PrintFileSeparator(path): | |
| 180 print '----- FILE : %s -----' % path | |
| 181 | |
| 182 | |
| 183 def _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 | |
| 215 def _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 | |
| 229 def _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 | |
| 246 def 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 """ | |
| 314 Some of the errors reported by GJsLint may be auto-fixable using the script | |
| 315 fixjsstyle. Please double check any changes it makes and report any bugs. The | |
| 316 script can be run by executing: | |
| 317 | |
| 318 fixjsstyle %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 | |
| 326 if __name__ == '__main__': | |
| 327 main() | |
| OLD | NEW |