- add sources.
[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 __author__ = 'robbyw@google.com (Robert Walker)'
20
21 import re
22
23 import gflags as flags
24 from closure_linter import errors
25 from closure_linter import javascriptstatetracker
26 from closure_linter import javascripttokens
27 from closure_linter import requireprovidesorter
28 from closure_linter import tokenutil
29 from closure_linter.common import errorhandler
30
31 # Shorthand
32 Token = javascripttokens.JavaScriptToken
33 Type = javascripttokens.JavaScriptTokenType
34
35 END_OF_FLAG_TYPE = re.compile(r'(}?\s*)$')
36
37 # Regex to represent common mistake inverting author name and email as
38 # @author User Name (user@company)
39 INVERTED_AUTHOR_SPEC = re.compile(r'(?P<leading_whitespace>\s*)'
40                                   '(?P<name>[^(]+)'
41                                   '(?P<whitespace_after_name>\s+)'
42                                   '\('
43                                   '(?P<email>[^\s]+@[^)\s]+)'
44                                   '\)'
45                                   '(?P<trailing_characters>.*)')
46
47 FLAGS = flags.FLAGS
48 flags.DEFINE_boolean('disable_indentation_fixing', False,
49                      'Whether to disable automatic fixing of indentation.')
50
51
52 class ErrorFixer(errorhandler.ErrorHandler):
53   """Object that fixes simple style errors."""
54
55   def __init__(self, external_file=None):
56     """Initialize the error fixer.
57
58     Args:
59       external_file: If included, all output will be directed to this file
60           instead of overwriting the files the errors are found in.
61     """
62     errorhandler.ErrorHandler.__init__(self)
63
64     self._file_name = None
65     self._file_token = None
66     self._external_file = external_file
67
68   def HandleFile(self, filename, first_token):
69     """Notifies this ErrorPrinter that subsequent errors are in filename.
70
71     Args:
72       filename: The name of the file about to be checked.
73       first_token: The first token in the file.
74     """
75     self._file_name = filename
76     self._file_token = first_token
77     self._file_fix_count = 0
78     self._file_changed_lines = set()
79
80   def _AddFix(self, tokens):
81     """Adds the fix to the internal count.
82
83     Args:
84       tokens: The token or sequence of tokens changed to fix an error.
85     """
86     self._file_fix_count += 1
87     if hasattr(tokens, 'line_number'):
88       self._file_changed_lines.add(tokens.line_number)
89     else:
90       for token in tokens:
91         self._file_changed_lines.add(token.line_number)
92
93   def HandleError(self, error):
94     """Attempts to fix the error.
95
96     Args:
97       error: The error object
98     """
99     code = error.code
100     token = error.token
101
102     if code == errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL:
103       iterator = token.attached_object.type_start_token
104       if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace():
105         iterator = iterator.next
106
107       leading_space = len(iterator.string) - len(iterator.string.lstrip())
108       iterator.string = '%s?%s' % (' ' * leading_space,
109                                    iterator.string.lstrip())
110
111       # Cover the no outer brace case where the end token is part of the type.
112       while iterator and iterator != token.attached_object.type_end_token.next:
113         iterator.string = iterator.string.replace(
114             'null|', '').replace('|null', '')
115         iterator = iterator.next
116
117       # Create a new flag object with updated type info.
118       token.attached_object = javascriptstatetracker.JsDocFlag(token)
119       self._AddFix(token)
120
121     elif code == errors.JSDOC_MISSING_OPTIONAL_TYPE:
122       iterator = token.attached_object.type_end_token
123       if iterator.type == Type.DOC_END_BRACE or iterator.string.isspace():
124         iterator = iterator.previous
125
126       ending_space = len(iterator.string) - len(iterator.string.rstrip())
127       iterator.string = '%s=%s' % (iterator.string.rstrip(),
128                                    ' ' * ending_space)
129
130       # Create a new flag object with updated type info.
131       token.attached_object = javascriptstatetracker.JsDocFlag(token)
132       self._AddFix(token)
133
134     elif code in (errors.MISSING_SEMICOLON_AFTER_FUNCTION,
135                   errors.MISSING_SEMICOLON):
136       semicolon_token = Token(';', Type.SEMICOLON, token.line,
137                               token.line_number)
138       tokenutil.InsertTokenAfter(semicolon_token, token)
139       token.metadata.is_implied_semicolon = False
140       semicolon_token.metadata.is_implied_semicolon = False
141       self._AddFix(token)
142
143     elif code in (errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
144                   errors.REDUNDANT_SEMICOLON,
145                   errors.COMMA_AT_END_OF_LITERAL):
146       tokenutil.DeleteToken(token)
147       self._AddFix(token)
148
149     elif code == errors.INVALID_JSDOC_TAG:
150       if token.string == '@returns':
151         token.string = '@return'
152         self._AddFix(token)
153
154     elif code == errors.FILE_MISSING_NEWLINE:
155       # This error is fixed implicitly by the way we restore the file
156       self._AddFix(token)
157
158     elif code == errors.MISSING_SPACE:
159       if error.position:
160         if error.position.IsAtBeginning():
161           tokenutil.InsertSpaceTokenAfter(token.previous)
162         elif error.position.IsAtEnd(token.string):
163           tokenutil.InsertSpaceTokenAfter(token)
164         else:
165           token.string = error.position.Set(token.string, ' ')
166         self._AddFix(token)
167
168     elif code == errors.EXTRA_SPACE:
169       if error.position:
170         token.string = error.position.Set(token.string, '')
171         self._AddFix(token)
172
173     elif code == errors.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER:
174       token.string = error.position.Set(token.string, '.')
175       self._AddFix(token)
176
177     elif code == errors.MISSING_LINE:
178       if error.position.IsAtBeginning():
179         tokenutil.InsertBlankLineAfter(token.previous)
180       else:
181         tokenutil.InsertBlankLineAfter(token)
182       self._AddFix(token)
183
184     elif code == errors.EXTRA_LINE:
185       tokenutil.DeleteToken(token)
186       self._AddFix(token)
187
188     elif code == errors.WRONG_BLANK_LINE_COUNT:
189       if not token.previous:
190         # TODO(user): Add an insertBefore method to tokenutil.
191         return
192
193       num_lines = error.fix_data
194       should_delete = False
195
196       if num_lines < 0:
197         num_lines *= -1
198         should_delete = True
199
200       for i in xrange(1, num_lines + 1):
201         if should_delete:
202           # TODO(user): DeleteToken should update line numbers.
203           tokenutil.DeleteToken(token.previous)
204         else:
205           tokenutil.InsertBlankLineAfter(token.previous)
206         self._AddFix(token)
207
208     elif code == errors.UNNECESSARY_DOUBLE_QUOTED_STRING:
209       end_quote = tokenutil.Search(token, Type.DOUBLE_QUOTE_STRING_END)
210       if end_quote:
211         single_quote_start = Token(
212             "'", Type.SINGLE_QUOTE_STRING_START, token.line, token.line_number)
213         single_quote_end = Token(
214             "'", Type.SINGLE_QUOTE_STRING_START, end_quote.line,
215             token.line_number)
216
217         tokenutil.InsertTokenAfter(single_quote_start, token)
218         tokenutil.InsertTokenAfter(single_quote_end, end_quote)
219         tokenutil.DeleteToken(token)
220         tokenutil.DeleteToken(end_quote)
221         self._AddFix([token, end_quote])
222
223     elif code == errors.MISSING_BRACES_AROUND_TYPE:
224       fixed_tokens = []
225       start_token = token.attached_object.type_start_token
226
227       if start_token.type != Type.DOC_START_BRACE:
228         leading_space = (
229             len(start_token.string) - len(start_token.string.lstrip()))
230         if leading_space:
231           start_token = tokenutil.SplitToken(start_token, leading_space)
232           # Fix case where start and end token were the same.
233           if token.attached_object.type_end_token == start_token.previous:
234             token.attached_object.type_end_token = start_token
235
236         new_token = Token('{', Type.DOC_START_BRACE, start_token.line,
237                           start_token.line_number)
238         tokenutil.InsertTokenAfter(new_token, start_token.previous)
239         token.attached_object.type_start_token = new_token
240         fixed_tokens.append(new_token)
241
242       end_token = token.attached_object.type_end_token
243       if end_token.type != Type.DOC_END_BRACE:
244         # If the start token was a brace, the end token will be a
245         # FLAG_ENDING_TYPE token, if there wasn't a starting brace then
246         # the end token is the last token of the actual type.
247         last_type = end_token
248         if not fixed_tokens:
249           last_type = end_token.previous
250
251         while last_type.string.isspace():
252           last_type = last_type.previous
253
254         # If there was no starting brace then a lone end brace wouldn't have
255         # been type end token. Now that we've added any missing start brace,
256         # see if the last effective type token was an end brace.
257         if last_type.type != Type.DOC_END_BRACE:
258           trailing_space = (len(last_type.string) -
259                             len(last_type.string.rstrip()))
260           if trailing_space:
261             tokenutil.SplitToken(last_type,
262                                  len(last_type.string) - trailing_space)
263
264           new_token = Token('}', Type.DOC_END_BRACE, last_type.line,
265                             last_type.line_number)
266           tokenutil.InsertTokenAfter(new_token, last_type)
267           token.attached_object.type_end_token = new_token
268           fixed_tokens.append(new_token)
269
270       self._AddFix(fixed_tokens)
271
272     elif code == errors.GOOG_REQUIRES_NOT_ALPHABETIZED:
273       require_start_token = error.fix_data
274       sorter = requireprovidesorter.RequireProvideSorter()
275       sorter.FixRequires(require_start_token)
276
277       self._AddFix(require_start_token)
278
279     elif code == errors.GOOG_PROVIDES_NOT_ALPHABETIZED:
280       provide_start_token = error.fix_data
281       sorter = requireprovidesorter.RequireProvideSorter()
282       sorter.FixProvides(provide_start_token)
283
284       self._AddFix(provide_start_token)
285
286     elif code == errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC:
287       if token.previous.string == '{' and token.next.string == '}':
288         tokenutil.DeleteToken(token.previous)
289         tokenutil.DeleteToken(token.next)
290         self._AddFix([token])
291
292     elif code == errors.INVALID_AUTHOR_TAG_DESCRIPTION:
293       match = INVERTED_AUTHOR_SPEC.match(token.string)
294       if match:
295         token.string = '%s%s%s(%s)%s' % (match.group('leading_whitespace'),
296                                          match.group('email'),
297                                          match.group('whitespace_after_name'),
298                                          match.group('name'),
299                                          match.group('trailing_characters'))
300         self._AddFix(token)
301
302     elif (code == errors.WRONG_INDENTATION and
303           not FLAGS.disable_indentation_fixing):
304       token = tokenutil.GetFirstTokenInSameLine(token)
305       actual = error.position.start
306       expected = error.position.length
307
308       if token.type in (Type.WHITESPACE, Type.PARAMETERS) and actual != 0:
309         token.string = token.string.lstrip() + (' ' * expected)
310         self._AddFix([token])
311       else:
312         # We need to add indentation.
313         new_token = Token(' ' * expected, Type.WHITESPACE,
314                           token.line, token.line_number)
315         # Note that we'll never need to add indentation at the first line,
316         # since it will always not be indented.  Therefore it's safe to assume
317         # token.previous exists.
318         tokenutil.InsertTokenAfter(new_token, token.previous)
319         self._AddFix([token])
320
321     elif code in [errors.MALFORMED_END_OF_SCOPE_COMMENT,
322                   errors.MISSING_END_OF_SCOPE_COMMENT]:
323       # Only fix cases where }); is found with no trailing content on the line
324       # other than a comment. Value of 'token' is set to } for this error.
325       if (token.type == Type.END_BLOCK and
326           token.next.type == Type.END_PAREN and
327           token.next.next.type == Type.SEMICOLON):
328         current_token = token.next.next.next
329         removed_tokens = []
330         while current_token and current_token.line_number == token.line_number:
331           if current_token.IsAnyType(Type.WHITESPACE,
332                                      Type.START_SINGLE_LINE_COMMENT,
333                                      Type.COMMENT):
334             removed_tokens.append(current_token)
335             current_token = current_token.next
336           else:
337             return
338
339         if removed_tokens:
340           tokenutil.DeleteTokens(removed_tokens[0], len(removed_tokens))
341
342         whitespace_token = Token('  ', Type.WHITESPACE, token.line,
343                                  token.line_number)
344         start_comment_token = Token('//', Type.START_SINGLE_LINE_COMMENT,
345                                     token.line, token.line_number)
346         comment_token = Token(' goog.scope', Type.COMMENT, token.line,
347                               token.line_number)
348         insertion_tokens = [whitespace_token, start_comment_token,
349                             comment_token]
350
351         tokenutil.InsertTokensAfter(insertion_tokens, token.next.next)
352         self._AddFix(removed_tokens + insertion_tokens)
353
354     elif code in [errors.EXTRA_GOOG_PROVIDE, errors.EXTRA_GOOG_REQUIRE]:
355       tokens_in_line = tokenutil.GetAllTokensInSameLine(token)
356       tokenutil.DeleteTokens(tokens_in_line[0], len(tokens_in_line))
357       self._AddFix(tokens_in_line)
358
359     elif code in [errors.MISSING_GOOG_PROVIDE, errors.MISSING_GOOG_REQUIRE]:
360       is_provide = code == errors.MISSING_GOOG_PROVIDE
361       is_require = code == errors.MISSING_GOOG_REQUIRE
362
363       missing_namespaces = error.fix_data[0]
364       need_blank_line = error.fix_data[1]
365
366       if need_blank_line is None:
367         # TODO(user): This happens when there are no existing
368         # goog.provide or goog.require statements to position new statements
369         # relative to. Consider handling this case with a heuristic.
370         return
371
372       insert_location = token.previous
373
374       # If inserting a missing require with no existing requires, insert a
375       # blank line first.
376       if need_blank_line and is_require:
377         tokenutil.InsertBlankLineAfter(insert_location)
378         insert_location = insert_location.next
379
380       for missing_namespace in missing_namespaces:
381         new_tokens = self._GetNewRequireOrProvideTokens(
382             is_provide, missing_namespace, insert_location.line_number + 1)
383         tokenutil.InsertLineAfter(insert_location, new_tokens)
384         insert_location = new_tokens[-1]
385         self._AddFix(new_tokens)
386
387       # If inserting a missing provide with no existing provides, insert a
388       # blank line after.
389       if need_blank_line and is_provide:
390         tokenutil.InsertBlankLineAfter(insert_location)
391
392   def _GetNewRequireOrProvideTokens(self, is_provide, namespace, line_number):
393     """Returns a list of tokens to create a goog.require/provide statement.
394
395     Args:
396       is_provide: True if getting tokens for a provide, False for require.
397       namespace: The required or provided namespaces to get tokens for.
398       line_number: The line number the new require or provide statement will be
399           on.
400
401     Returns:
402       Tokens to create a new goog.require or goog.provide statement.
403     """
404     string = 'goog.require'
405     if is_provide:
406       string = 'goog.provide'
407     line_text = string + '(\'' + namespace + '\');\n'
408     return [
409         Token(string, Type.IDENTIFIER, line_text, line_number),
410         Token('(', Type.START_PAREN, line_text, line_number),
411         Token('\'', Type.SINGLE_QUOTE_STRING_START, line_text, line_number),
412         Token(namespace, Type.STRING_TEXT, line_text, line_number),
413         Token('\'', Type.SINGLE_QUOTE_STRING_END, line_text, line_number),
414         Token(')', Type.END_PAREN, line_text, line_number),
415         Token(';', Type.SEMICOLON, line_text, line_number)
416         ]
417
418   def FinishFile(self):
419     """Called when the current file has finished style checking.
420
421     Used to go back and fix any errors in the file.
422     """
423     if self._file_fix_count:
424       f = self._external_file
425       if not f:
426         print 'Fixed %d errors in %s' % (self._file_fix_count, self._file_name)
427         f = open(self._file_name, 'w')
428
429       token = self._file_token
430       char_count = 0
431       while token:
432         f.write(token.string)
433         char_count += len(token.string)
434
435         if token.IsLastInLine():
436           f.write('\n')
437           if char_count > 80 and token.line_number in self._file_changed_lines:
438             print 'WARNING: Line %d of %s is now longer than 80 characters.' % (
439                 token.line_number, self._file_name)
440
441           char_count = 0
442
443         token = token.next
444
445       if not self._external_file:
446         # Close the file if we created it
447         f.close()