Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / third_party / closure_linter / closure_linter / error_fixer.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2007 The Closure Linter Authors. All Rights Reserved.
4 #
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
8 #
9 #      http://www.apache.org/licenses/LICENSE-2.0
10 #
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.
16
17 """Main class responsible for automatically fixing simple style violations."""
18
19 # Allow non-Google copyright
20 # pylint: disable=g-bad-file-header
21
22 __author__ = 'robbyw@google.com (Robert Walker)'
23
24 import re
25
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
33
34 # Shorthand
35 Token = javascripttokens.JavaScriptToken
36 Type = javascripttokens.JavaScriptTokenType
37
38 END_OF_FLAG_TYPE = re.compile(r'(}?\s*)$')
39
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*)'
43                                   r'(?P<name>[^(]+)'
44                                   r'(?P<whitespace_after_name>\s+)'
45                                   r'\('
46                                   r'(?P<email>[^\s]+@[^)\s]+)'
47                                   r'\)'
48                                   r'(?P<trailing_characters>.*)')
49
50 FLAGS = flags.FLAGS
51 flags.DEFINE_boolean('disable_indentation_fixing', False,
52                      'Whether to disable automatic fixing of indentation.')
53
54
55 class ErrorFixer(errorhandler.ErrorHandler):
56   """Object that fixes simple style errors."""
57
58   def __init__(self, external_file=None):
59     """Initialize the error fixer.
60
61     Args:
62       external_file: If included, all output will be directed to this file
63           instead of overwriting the files the errors are found in.
64     """
65     errorhandler.ErrorHandler.__init__(self)
66
67     self._file_name = None
68     self._file_token = None
69     self._external_file = external_file
70
71   def HandleFile(self, filename, first_token):
72     """Notifies this ErrorPrinter that subsequent errors are in filename.
73
74     Args:
75       filename: The name of the file about to be checked.
76       first_token: The first token in the file.
77     """
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()
83
84   def _AddFix(self, tokens):
85     """Adds the fix to the internal count.
86
87     Args:
88       tokens: The token or sequence of tokens changed to fix an error.
89     """
90     self._file_fix_count += 1
91     if hasattr(tokens, 'line_number'):
92       self._file_changed_lines.add(tokens.line_number)
93     else:
94       for token in tokens:
95         self._file_changed_lines.add(token.line_number)
96
97   def HandleError(self, error):
98     """Attempts to fix the error.
99
100     Args:
101       error: The error object
102     """
103     code = error.code
104     token = error.token
105
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
110
111       leading_space = len(iterator.string) - len(iterator.string.lstrip())
112       iterator.string = '%s?%s' % (' ' * leading_space,
113                                    iterator.string.lstrip())
114
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
120
121       # Create a new flag object with updated type info.
122       token.attached_object = javascriptstatetracker.JsDocFlag(token)
123       self._AddFix(token)
124
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
129
130       ending_space = len(iterator.string) - len(iterator.string.rstrip())
131       iterator.string = '%s=%s' % (iterator.string.rstrip(),
132                                    ' ' * ending_space)
133
134       # Create a new flag object with updated type info.
135       token.attached_object = javascriptstatetracker.JsDocFlag(token)
136       self._AddFix(token)
137
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
142
143       starting_space = len(iterator.string) - len(iterator.string.lstrip())
144       iterator.string = '%s...%s' % (' ' * starting_space,
145                                      iterator.string.lstrip())
146
147       # Create a new flag object with updated type info.
148       token.attached_object = javascriptstatetracker.JsDocFlag(token)
149       self._AddFix(token)
150
151     elif code in (errors.MISSING_SEMICOLON_AFTER_FUNCTION,
152                   errors.MISSING_SEMICOLON):
153       semicolon_token = Token(';', Type.SEMICOLON, token.line,
154                               token.line_number)
155       tokenutil.InsertTokenAfter(semicolon_token, token)
156       token.metadata.is_implied_semicolon = False
157       semicolon_token.metadata.is_implied_semicolon = False
158       self._AddFix(token)
159
160     elif code in (errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
161                   errors.REDUNDANT_SEMICOLON,
162                   errors.COMMA_AT_END_OF_LITERAL):
163       self._DeleteToken(token)
164       self._AddFix(token)
165
166     elif code == errors.INVALID_JSDOC_TAG:
167       if token.string == '@returns':
168         token.string = '@return'
169         self._AddFix(token)
170
171     elif code == errors.FILE_MISSING_NEWLINE:
172       # This error is fixed implicitly by the way we restore the file
173       self._AddFix(token)
174
175     elif code == errors.MISSING_SPACE:
176       if error.fix_data:
177         token.string = error.fix_data
178         self._AddFix(token)
179       elif error.position:
180         if error.position.IsAtBeginning():
181           tokenutil.InsertSpaceTokenAfter(token.previous)
182         elif error.position.IsAtEnd(token.string):
183           tokenutil.InsertSpaceTokenAfter(token)
184         else:
185           token.string = error.position.Set(token.string, ' ')
186         self._AddFix(token)
187
188     elif code == errors.EXTRA_SPACE:
189       if error.position:
190         token.string = error.position.Set(token.string, '')
191         self._AddFix(token)
192
193     elif code == errors.MISSING_LINE:
194       if error.position.IsAtBeginning():
195         tokenutil.InsertBlankLineAfter(token.previous)
196       else:
197         tokenutil.InsertBlankLineAfter(token)
198       self._AddFix(token)
199
200     elif code == errors.EXTRA_LINE:
201       self._DeleteToken(token)
202       self._AddFix(token)
203
204     elif code == errors.WRONG_BLANK_LINE_COUNT:
205       if not token.previous:
206         # TODO(user): Add an insertBefore method to tokenutil.
207         return
208
209       num_lines = error.fix_data
210       should_delete = False
211
212       if num_lines < 0:
213         num_lines *= -1
214         should_delete = True
215
216       for unused_i in xrange(1, num_lines + 1):
217         if should_delete:
218           # TODO(user): DeleteToken should update line numbers.
219           self._DeleteToken(token.previous)
220         else:
221           tokenutil.InsertBlankLineAfter(token.previous)
222         self._AddFix(token)
223
224     elif code == errors.UNNECESSARY_DOUBLE_QUOTED_STRING:
225       end_quote = tokenutil.Search(token, Type.DOUBLE_QUOTE_STRING_END)
226       if end_quote:
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,
231             token.line_number)
232
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])
238
239     elif code == errors.MISSING_BRACES_AROUND_TYPE:
240       fixed_tokens = []
241       start_token = token.attached_object.type_start_token
242
243       if start_token.type != Type.DOC_START_BRACE:
244         leading_space = (
245             len(start_token.string) - len(start_token.string.lstrip()))
246         if leading_space:
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
251
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)
257
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
264         if not fixed_tokens:
265           last_type = end_token.previous
266
267         while last_type.string.isspace():
268           last_type = last_type.previous
269
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()))
276           if trailing_space:
277             tokenutil.SplitToken(last_type,
278                                  len(last_type.string) - trailing_space)
279
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)
285
286       self._AddFix(fixed_tokens)
287
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)
292
293       self._AddFix(require_start_token)
294
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)
299
300       self._AddFix(provide_start_token)
301
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])
307
308     elif code == errors.INVALID_AUTHOR_TAG_DESCRIPTION:
309       match = INVERTED_AUTHOR_SPEC.match(token.string)
310       if match:
311         token.string = '%s%s%s(%s)%s' % (match.group('leading_whitespace'),
312                                          match.group('email'),
313                                          match.group('whitespace_after_name'),
314                                          match.group('name'),
315                                          match.group('trailing_characters'))
316         self._AddFix(token)
317
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
323
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()
328         actual = 0
329
330       if token.type in (Type.WHITESPACE, Type.PARAMETERS) and actual != 0:
331         token.string = token.string.lstrip() + (' ' * expected)
332         self._AddFix([token])
333       else:
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])
342
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
351         removed_tokens = []
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,
355                                      Type.COMMENT):
356             removed_tokens.append(current_token)
357             current_token = current_token.next
358           else:
359             return
360
361         if removed_tokens:
362           self._DeleteTokens(removed_tokens[0], len(removed_tokens))
363
364         whitespace_token = Token('  ', Type.WHITESPACE, token.line,
365                                  token.line_number)
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,
369                               token.line_number)
370         insertion_tokens = [whitespace_token, start_comment_token,
371                             comment_token]
372
373         tokenutil.InsertTokensAfter(insertion_tokens, token.next.next)
374         self._AddFix(removed_tokens + insertion_tokens)
375
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)
380
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
384
385       missing_namespaces = error.fix_data[0]
386       need_blank_line = error.fix_data[1]
387
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.
392         return
393
394       insert_location = token.previous
395
396       # If inserting a missing require with no existing requires, insert a
397       # blank line first.
398       if need_blank_line and is_require:
399         tokenutil.InsertBlankLineAfter(insert_location)
400         insert_location = insert_location.next
401
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)
408
409       # If inserting a missing provide with no existing provides, insert a
410       # blank line after.
411       if need_blank_line and is_provide:
412         tokenutil.InsertBlankLineAfter(insert_location)
413
414   def _GetNewRequireOrProvideTokens(self, is_provide, namespace, line_number):
415     """Returns a list of tokens to create a goog.require/provide statement.
416
417     Args:
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
421           on.
422
423     Returns:
424       Tokens to create a new goog.require or goog.provide statement.
425     """
426     string = 'goog.require'
427     if is_provide:
428       string = 'goog.provide'
429     line_text = string + '(\'' + namespace + '\');\n'
430     return [
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)
438         ]
439
440   def _DeleteToken(self, token):
441     """Deletes the specified token from the linked list of tokens.
442
443     Updates instance variables pointing to tokens such as _file_token if
444     they reference the deleted token.
445
446     Args:
447       token: The token to delete.
448     """
449     if token == self._file_token:
450       self._file_token = token.next
451
452     tokenutil.DeleteToken(token)
453
454   def _DeleteTokens(self, token, token_count):
455     """Deletes the given number of tokens starting with the given token.
456
457     Updates instance variables pointing to tokens such as _file_token if
458     they reference the deleted token.
459
460     Args:
461       token: The first token to delete.
462       token_count: The total number of tokens to delete.
463     """
464     if token == self._file_token:
465       for unused_i in xrange(token_count):
466         self._file_token = self._file_token.next
467
468     tokenutil.DeleteTokens(token, token_count)
469
470   def FinishFile(self):
471     """Called when the current file has finished style checking.
472
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.
479     """
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()
485         f.close()
486
487       f = self._external_file
488       if not f:
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')
493
494       token = self._file_token
495       # Finding the first not deleted token.
496       while token.is_deleted:
497         token = token.next
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
502       char_count = 0
503       line = ''
504       while token:
505         line += token.string
506         char_count += len(token.string)
507
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):
515             f.write(line)
516             f.write('\n')
517           else:
518             f.write(original_lines[token.orig_line_number - 1])
519           line = ''
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)
523
524           char_count = 0
525
526         token = token.next
527
528       if not self._external_file:
529         # Close the file if we created it
530         f.close()