3 # Copyright 2007 The Closure Linter Authors. All Rights Reserved.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS-IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Main class responsible for automatically fixing simple style violations."""
19 # Allow non-Google copyright
20 # pylint: disable=g-bad-file-header
22 __author__ = 'robbyw@google.com (Robert Walker)'
26 import gflags as flags
27 from closure_linter import errors
28 from closure_linter import javascriptstatetracker
29 from closure_linter import javascripttokens
30 from closure_linter import requireprovidesorter
31 from closure_linter import tokenutil
32 from closure_linter.common import errorhandler
35 Token = javascripttokens.JavaScriptToken
36 Type = javascripttokens.JavaScriptTokenType
38 END_OF_FLAG_TYPE = re.compile(r'(}?\s*)$')
40 # Regex to represent common mistake inverting author name and email as
41 # @author User Name (user@company)
42 INVERTED_AUTHOR_SPEC = re.compile(r'(?P<leading_whitespace>\s*)'
44 r'(?P<whitespace_after_name>\s+)'
46 r'(?P<email>[^\s]+@[^)\s]+)'
48 r'(?P<trailing_characters>.*)')
51 flags.DEFINE_boolean('disable_indentation_fixing', False,
52 'Whether to disable automatic fixing of indentation.')
55 class ErrorFixer(errorhandler.ErrorHandler):
56 """Object that fixes simple style errors."""
58 def __init__(self, external_file=None):
59 """Initialize the error fixer.
62 external_file: If included, all output will be directed to this file
63 instead of overwriting the files the errors are found in.
65 errorhandler.ErrorHandler.__init__(self)
67 self._file_name = None
68 self._file_token = None
69 self._external_file = external_file
71 def HandleFile(self, filename, first_token):
72 """Notifies this ErrorPrinter that subsequent errors are in filename.
75 filename: The name of the file about to be checked.
76 first_token: The first token in the file.
78 self._file_name = filename
79 self._file_is_html = filename.endswith('.html') or filename.endswith('.htm')
80 self._file_token = first_token
81 self._file_fix_count = 0
82 self._file_changed_lines = set()
84 def _AddFix(self, tokens):
85 """Adds the fix to the internal count.
88 tokens: The token or sequence of tokens changed to fix an error.
90 self._file_fix_count += 1
91 if hasattr(tokens, 'line_number'):
92 self._file_changed_lines.add(tokens.line_number)
95 self._file_changed_lines.add(token.line_number)
97 def HandleError(self, error):
98 """Attempts to fix the error.
101 error: The error object
106 if code == errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL:
107 iterator = token.attached_object.type_start_token
108 if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace():
109 iterator = iterator.next
111 leading_space = len(iterator.string) - len(iterator.string.lstrip())
112 iterator.string = '%s?%s' % (' ' * leading_space,
113 iterator.string.lstrip())
115 # Cover the no outer brace case where the end token is part of the type.
116 while iterator and iterator != token.attached_object.type_end_token.next:
117 iterator.string = iterator.string.replace(
118 'null|', '').replace('|null', '')
119 iterator = iterator.next
121 # Create a new flag object with updated type info.
122 token.attached_object = javascriptstatetracker.JsDocFlag(token)
125 elif code == errors.JSDOC_MISSING_OPTIONAL_TYPE:
126 iterator = token.attached_object.type_end_token
127 if iterator.type == Type.DOC_END_BRACE or iterator.string.isspace():
128 iterator = iterator.previous
130 ending_space = len(iterator.string) - len(iterator.string.rstrip())
131 iterator.string = '%s=%s' % (iterator.string.rstrip(),
134 # Create a new flag object with updated type info.
135 token.attached_object = javascriptstatetracker.JsDocFlag(token)
138 elif code == errors.JSDOC_MISSING_VAR_ARGS_TYPE:
139 iterator = token.attached_object.type_start_token
140 if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace():
141 iterator = iterator.next
143 starting_space = len(iterator.string) - len(iterator.string.lstrip())
144 iterator.string = '%s...%s' % (' ' * starting_space,
145 iterator.string.lstrip())
147 # Create a new flag object with updated type info.
148 token.attached_object = javascriptstatetracker.JsDocFlag(token)
151 elif code in (errors.MISSING_SEMICOLON_AFTER_FUNCTION,
152 errors.MISSING_SEMICOLON):
153 semicolon_token = Token(';', Type.SEMICOLON, token.line,
155 tokenutil.InsertTokenAfter(semicolon_token, token)
156 token.metadata.is_implied_semicolon = False
157 semicolon_token.metadata.is_implied_semicolon = False
160 elif code in (errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
161 errors.REDUNDANT_SEMICOLON,
162 errors.COMMA_AT_END_OF_LITERAL):
163 self._DeleteToken(token)
166 elif code == errors.INVALID_JSDOC_TAG:
167 if token.string == '@returns':
168 token.string = '@return'
171 elif code == errors.FILE_MISSING_NEWLINE:
172 # This error is fixed implicitly by the way we restore the file
175 elif code == errors.MISSING_SPACE:
177 token.string = error.fix_data
180 if error.position.IsAtBeginning():
181 tokenutil.InsertSpaceTokenAfter(token.previous)
182 elif error.position.IsAtEnd(token.string):
183 tokenutil.InsertSpaceTokenAfter(token)
185 token.string = error.position.Set(token.string, ' ')
188 elif code == errors.EXTRA_SPACE:
190 token.string = error.position.Set(token.string, '')
193 elif code == errors.MISSING_LINE:
194 if error.position.IsAtBeginning():
195 tokenutil.InsertBlankLineAfter(token.previous)
197 tokenutil.InsertBlankLineAfter(token)
200 elif code == errors.EXTRA_LINE:
201 self._DeleteToken(token)
204 elif code == errors.WRONG_BLANK_LINE_COUNT:
205 if not token.previous:
206 # TODO(user): Add an insertBefore method to tokenutil.
209 num_lines = error.fix_data
210 should_delete = False
216 for unused_i in xrange(1, num_lines + 1):
218 # TODO(user): DeleteToken should update line numbers.
219 self._DeleteToken(token.previous)
221 tokenutil.InsertBlankLineAfter(token.previous)
224 elif code == errors.UNNECESSARY_DOUBLE_QUOTED_STRING:
225 end_quote = tokenutil.Search(token, Type.DOUBLE_QUOTE_STRING_END)
227 single_quote_start = Token(
228 "'", Type.SINGLE_QUOTE_STRING_START, token.line, token.line_number)
229 single_quote_end = Token(
230 "'", Type.SINGLE_QUOTE_STRING_START, end_quote.line,
233 tokenutil.InsertTokenAfter(single_quote_start, token)
234 tokenutil.InsertTokenAfter(single_quote_end, end_quote)
235 self._DeleteToken(token)
236 self._DeleteToken(end_quote)
237 self._AddFix([token, end_quote])
239 elif code == errors.MISSING_BRACES_AROUND_TYPE:
241 start_token = token.attached_object.type_start_token
243 if start_token.type != Type.DOC_START_BRACE:
245 len(start_token.string) - len(start_token.string.lstrip()))
247 start_token = tokenutil.SplitToken(start_token, leading_space)
248 # Fix case where start and end token were the same.
249 if token.attached_object.type_end_token == start_token.previous:
250 token.attached_object.type_end_token = start_token
252 new_token = Token('{', Type.DOC_START_BRACE, start_token.line,
253 start_token.line_number)
254 tokenutil.InsertTokenAfter(new_token, start_token.previous)
255 token.attached_object.type_start_token = new_token
256 fixed_tokens.append(new_token)
258 end_token = token.attached_object.type_end_token
259 if end_token.type != Type.DOC_END_BRACE:
260 # If the start token was a brace, the end token will be a
261 # FLAG_ENDING_TYPE token, if there wasn't a starting brace then
262 # the end token is the last token of the actual type.
263 last_type = end_token
265 last_type = end_token.previous
267 while last_type.string.isspace():
268 last_type = last_type.previous
270 # If there was no starting brace then a lone end brace wouldn't have
271 # been type end token. Now that we've added any missing start brace,
272 # see if the last effective type token was an end brace.
273 if last_type.type != Type.DOC_END_BRACE:
274 trailing_space = (len(last_type.string) -
275 len(last_type.string.rstrip()))
277 tokenutil.SplitToken(last_type,
278 len(last_type.string) - trailing_space)
280 new_token = Token('}', Type.DOC_END_BRACE, last_type.line,
281 last_type.line_number)
282 tokenutil.InsertTokenAfter(new_token, last_type)
283 token.attached_object.type_end_token = new_token
284 fixed_tokens.append(new_token)
286 self._AddFix(fixed_tokens)
288 elif code == errors.GOOG_REQUIRES_NOT_ALPHABETIZED:
289 require_start_token = error.fix_data
290 sorter = requireprovidesorter.RequireProvideSorter()
291 sorter.FixRequires(require_start_token)
293 self._AddFix(require_start_token)
295 elif code == errors.GOOG_PROVIDES_NOT_ALPHABETIZED:
296 provide_start_token = error.fix_data
297 sorter = requireprovidesorter.RequireProvideSorter()
298 sorter.FixProvides(provide_start_token)
300 self._AddFix(provide_start_token)
302 elif code == errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC:
303 if token.previous.string == '{' and token.next.string == '}':
304 self._DeleteToken(token.previous)
305 self._DeleteToken(token.next)
306 self._AddFix([token])
308 elif code == errors.INVALID_AUTHOR_TAG_DESCRIPTION:
309 match = INVERTED_AUTHOR_SPEC.match(token.string)
311 token.string = '%s%s%s(%s)%s' % (match.group('leading_whitespace'),
312 match.group('email'),
313 match.group('whitespace_after_name'),
315 match.group('trailing_characters'))
318 elif (code == errors.WRONG_INDENTATION and
319 not FLAGS.disable_indentation_fixing):
320 token = tokenutil.GetFirstTokenInSameLine(token)
321 actual = error.position.start
322 expected = error.position.length
324 # Cases where first token is param but with leading spaces.
325 if (len(token.string.lstrip()) == len(token.string) - actual and
326 token.string.lstrip()):
327 token.string = token.string.lstrip()
330 if token.type in (Type.WHITESPACE, Type.PARAMETERS) and actual != 0:
331 token.string = token.string.lstrip() + (' ' * expected)
332 self._AddFix([token])
334 # We need to add indentation.
335 new_token = Token(' ' * expected, Type.WHITESPACE,
336 token.line, token.line_number)
337 # Note that we'll never need to add indentation at the first line,
338 # since it will always not be indented. Therefore it's safe to assume
339 # token.previous exists.
340 tokenutil.InsertTokenAfter(new_token, token.previous)
341 self._AddFix([token])
343 elif code in [errors.MALFORMED_END_OF_SCOPE_COMMENT,
344 errors.MISSING_END_OF_SCOPE_COMMENT]:
345 # Only fix cases where }); is found with no trailing content on the line
346 # other than a comment. Value of 'token' is set to } for this error.
347 if (token.type == Type.END_BLOCK and
348 token.next.type == Type.END_PAREN and
349 token.next.next.type == Type.SEMICOLON):
350 current_token = token.next.next.next
352 while current_token and current_token.line_number == token.line_number:
353 if current_token.IsAnyType(Type.WHITESPACE,
354 Type.START_SINGLE_LINE_COMMENT,
356 removed_tokens.append(current_token)
357 current_token = current_token.next
362 self._DeleteTokens(removed_tokens[0], len(removed_tokens))
364 whitespace_token = Token(' ', Type.WHITESPACE, token.line,
366 start_comment_token = Token('//', Type.START_SINGLE_LINE_COMMENT,
367 token.line, token.line_number)
368 comment_token = Token(' goog.scope', Type.COMMENT, token.line,
370 insertion_tokens = [whitespace_token, start_comment_token,
373 tokenutil.InsertTokensAfter(insertion_tokens, token.next.next)
374 self._AddFix(removed_tokens + insertion_tokens)
376 elif code in [errors.EXTRA_GOOG_PROVIDE, errors.EXTRA_GOOG_REQUIRE]:
377 tokens_in_line = tokenutil.GetAllTokensInSameLine(token)
378 self._DeleteTokens(tokens_in_line[0], len(tokens_in_line))
379 self._AddFix(tokens_in_line)
381 elif code in [errors.MISSING_GOOG_PROVIDE, errors.MISSING_GOOG_REQUIRE]:
382 is_provide = code == errors.MISSING_GOOG_PROVIDE
383 is_require = code == errors.MISSING_GOOG_REQUIRE
385 missing_namespaces = error.fix_data[0]
386 need_blank_line = error.fix_data[1]
388 if need_blank_line is None:
389 # TODO(user): This happens when there are no existing
390 # goog.provide or goog.require statements to position new statements
391 # relative to. Consider handling this case with a heuristic.
394 insert_location = token.previous
396 # If inserting a missing require with no existing requires, insert a
398 if need_blank_line and is_require:
399 tokenutil.InsertBlankLineAfter(insert_location)
400 insert_location = insert_location.next
402 for missing_namespace in missing_namespaces:
403 new_tokens = self._GetNewRequireOrProvideTokens(
404 is_provide, missing_namespace, insert_location.line_number + 1)
405 tokenutil.InsertLineAfter(insert_location, new_tokens)
406 insert_location = new_tokens[-1]
407 self._AddFix(new_tokens)
409 # If inserting a missing provide with no existing provides, insert a
411 if need_blank_line and is_provide:
412 tokenutil.InsertBlankLineAfter(insert_location)
414 def _GetNewRequireOrProvideTokens(self, is_provide, namespace, line_number):
415 """Returns a list of tokens to create a goog.require/provide statement.
418 is_provide: True if getting tokens for a provide, False for require.
419 namespace: The required or provided namespaces to get tokens for.
420 line_number: The line number the new require or provide statement will be
424 Tokens to create a new goog.require or goog.provide statement.
426 string = 'goog.require'
428 string = 'goog.provide'
429 line_text = string + '(\'' + namespace + '\');\n'
431 Token(string, Type.IDENTIFIER, line_text, line_number),
432 Token('(', Type.START_PAREN, line_text, line_number),
433 Token('\'', Type.SINGLE_QUOTE_STRING_START, line_text, line_number),
434 Token(namespace, Type.STRING_TEXT, line_text, line_number),
435 Token('\'', Type.SINGLE_QUOTE_STRING_END, line_text, line_number),
436 Token(')', Type.END_PAREN, line_text, line_number),
437 Token(';', Type.SEMICOLON, line_text, line_number)
440 def _DeleteToken(self, token):
441 """Deletes the specified token from the linked list of tokens.
443 Updates instance variables pointing to tokens such as _file_token if
444 they reference the deleted token.
447 token: The token to delete.
449 if token == self._file_token:
450 self._file_token = token.next
452 tokenutil.DeleteToken(token)
454 def _DeleteTokens(self, token, token_count):
455 """Deletes the given number of tokens starting with the given token.
457 Updates instance variables pointing to tokens such as _file_token if
458 they reference the deleted token.
461 token: The first token to delete.
462 token_count: The total number of tokens to delete.
464 if token == self._file_token:
465 for unused_i in xrange(token_count):
466 self._file_token = self._file_token.next
468 tokenutil.DeleteTokens(token, token_count)
470 def FinishFile(self):
471 """Called when the current file has finished style checking.
473 Used to go back and fix any errors in the file. It currently supports both
474 js and html files. For js files it does a simple dump of all tokens, but in
475 order to support html file, we need to merge the original file with the new
476 token set back together. This works because the tokenized html file is the
477 original html file with all non js lines kept but blanked out with one blank
478 line token per line of html.
480 if self._file_fix_count:
481 # Get the original file content for html.
482 if self._file_is_html:
483 f = open(self._file_name, 'r')
484 original_lines = f.readlines()
487 f = self._external_file
489 error_noun = 'error' if self._file_fix_count == 1 else 'errors'
490 print 'Fixed %d %s in %s' % (
491 self._file_fix_count, error_noun, self._file_name)
492 f = open(self._file_name, 'w')
494 token = self._file_token
495 # Finding the first not deleted token.
496 while token.is_deleted:
498 # If something got inserted before first token (e.g. due to sorting)
499 # then move to start. Bug 8398202.
500 while token.previous:
501 token = token.previous
506 char_count += len(token.string)
508 if token.IsLastInLine():
509 # We distinguish if a blank line in html was from stripped original
510 # file or newly added error fix by looking at the "org_line_number"
511 # field on the token. It is only set in the tokenizer, so for all
512 # error fixes, the value should be None.
513 if (line or not self._file_is_html or
514 token.orig_line_number is None):
518 f.write(original_lines[token.orig_line_number - 1])
520 if char_count > 80 and token.line_number in self._file_changed_lines:
521 print 'WARNING: Line %d of %s is now longer than 80 characters.' % (
522 token.line_number, self._file_name)
528 if not self._external_file:
529 # Close the file if we created it