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
21import re
22
23import gflags as flags
24from closure_linter import errors
25from closure_linter import javascriptstatetracker
26from closure_linter import javascripttokens
27from closure_linter import requireprovidesorter
28from closure_linter import tokenutil
29from closure_linter.common import errorhandler
30
31# Shorthand
32Token = javascripttokens.JavaScriptToken
33Type = javascripttokens.JavaScriptTokenType
34
35END_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)
39INVERTED_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
47FLAGS = flags.FLAGS
48flags.DEFINE_boolean('disable_indentation_fixing', False,
49                     'Whether to disable automatic fixing of indentation.')
50
51
52class 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()
448