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