1#!/usr/bin/env python
2#
3# Copyright 2011 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"""Contains logic for sorting goog.provide and goog.require statements.
18
19Closurized JavaScript files use goog.provide and goog.require statements at the
20top of the file to manage dependencies. These statements should be sorted
21alphabetically, however, it is common for them to be accompanied by inline
22comments or suppression annotations. In order to sort these statements without
23disrupting their comments and annotations, the association between statements
24and comments/annotations must be maintained while sorting.
25
26  RequireProvideSorter: Handles checking/fixing of provide/require statements.
27"""
28
29
30
31from closure_linter import javascripttokens
32from closure_linter import tokenutil
33
34# Shorthand
35Type = javascripttokens.JavaScriptTokenType
36
37
38class RequireProvideSorter(object):
39  """Checks for and fixes alphabetization of provide and require statements.
40
41  When alphabetizing, comments on the same line or comments directly above a
42  goog.provide or goog.require statement are associated with that statement and
43  stay with the statement as it gets sorted.
44  """
45
46  def CheckProvides(self, token):
47    """Checks alphabetization of goog.provide statements.
48
49    Iterates over tokens in given token stream, identifies goog.provide tokens,
50    and checks that they occur in alphabetical order by the object being
51    provided.
52
53    Args:
54      token: A token in the token stream before any goog.provide tokens.
55
56    Returns:
57      A tuple containing the first provide token in the token stream and a list
58      of provided objects sorted alphabetically. For example:
59
60      (JavaScriptToken, ['object.a', 'object.b', ...])
61
62      None is returned if all goog.provide statements are already sorted.
63    """
64    provide_tokens = self._GetRequireOrProvideTokens(token, 'goog.provide')
65    provide_strings = self._GetRequireOrProvideTokenStrings(provide_tokens)
66    sorted_provide_strings = sorted(provide_strings)
67    if provide_strings != sorted_provide_strings:
68      return [provide_tokens[0], sorted_provide_strings]
69    return None
70
71  def CheckRequires(self, token):
72    """Checks alphabetization of goog.require statements.
73
74    Iterates over tokens in given token stream, identifies goog.require tokens,
75    and checks that they occur in alphabetical order by the dependency being
76    required.
77
78    Args:
79      token: A token in the token stream before any goog.require tokens.
80
81    Returns:
82      A tuple containing the first require token in the token stream and a list
83      of required dependencies sorted alphabetically. For example:
84
85      (JavaScriptToken, ['object.a', 'object.b', ...])
86
87      None is returned if all goog.require statements are already sorted.
88    """
89    require_tokens = self._GetRequireOrProvideTokens(token, 'goog.require')
90    require_strings = self._GetRequireOrProvideTokenStrings(require_tokens)
91    sorted_require_strings = sorted(require_strings)
92    if require_strings != sorted_require_strings:
93      return (require_tokens[0], sorted_require_strings)
94    return None
95
96  def FixProvides(self, token):
97    """Sorts goog.provide statements in the given token stream alphabetically.
98
99    Args:
100      token: The first token in the token stream.
101    """
102    self._FixProvidesOrRequires(
103        self._GetRequireOrProvideTokens(token, 'goog.provide'))
104
105  def FixRequires(self, token):
106    """Sorts goog.require statements in the given token stream alphabetically.
107
108    Args:
109      token: The first token in the token stream.
110    """
111    self._FixProvidesOrRequires(
112        self._GetRequireOrProvideTokens(token, 'goog.require'))
113
114  def _FixProvidesOrRequires(self, tokens):
115    """Sorts goog.provide or goog.require statements.
116
117    Args:
118      tokens: A list of goog.provide or goog.require tokens in the order they
119              appear in the token stream. i.e. the first token in this list must
120              be the first goog.provide or goog.require token.
121    """
122    strings = self._GetRequireOrProvideTokenStrings(tokens)
123    sorted_strings = sorted(strings)
124
125    # Make a separate pass to remove any blank lines between goog.require/
126    # goog.provide tokens.
127    first_token = tokens[0]
128    last_token = tokens[-1]
129    i = last_token
130    while i != first_token:
131      if i.type is Type.BLANK_LINE:
132        tokenutil.DeleteToken(i)
133      i = i.previous
134
135    # A map from required/provided object name to tokens that make up the line
136    # it was on, including any comments immediately before it or after it on the
137    # same line.
138    tokens_map = self._GetTokensMap(tokens)
139
140    # Iterate over the map removing all tokens.
141    for name in tokens_map:
142      tokens_to_delete = tokens_map[name]
143      for i in tokens_to_delete:
144        tokenutil.DeleteToken(i)
145
146    # Re-add all tokens in the map in alphabetical order.
147    insert_after = tokens[0].previous
148    for string in sorted_strings:
149      for i in tokens_map[string]:
150        tokenutil.InsertTokenAfter(i, insert_after)
151        insert_after = i
152
153  def _GetRequireOrProvideTokens(self, token, token_string):
154    """Gets all goog.provide or goog.require tokens in the given token stream.
155
156    Args:
157      token: The first token in the token stream.
158      token_string: One of 'goog.provide' or 'goog.require' to indicate which
159                    tokens to find.
160
161    Returns:
162      A list of goog.provide or goog.require tokens in the order they appear in
163      the token stream.
164    """
165    tokens = []
166    while token:
167      if token.type == Type.IDENTIFIER:
168        if token.string == token_string:
169          tokens.append(token)
170        elif token.string not in ['goog.require', 'goog.provide']:
171          # The goog.provide and goog.require identifiers are at the top of the
172          # file. So if any other identifier is encountered, return.
173          break
174      token = token.next
175
176    return tokens
177
178  def _GetRequireOrProvideTokenStrings(self, tokens):
179    """Gets a list of strings corresponding to the given list of tokens.
180
181    The string will be the next string in the token stream after each token in
182    tokens. This is used to find the object being provided/required by a given
183    goog.provide or goog.require token.
184
185    Args:
186      tokens: A list of goog.provide or goog.require tokens.
187
188    Returns:
189      A list of object names that are being provided or required by the given
190      list of tokens. For example:
191
192      ['object.a', 'object.c', 'object.b']
193    """
194    token_strings = []
195    for token in tokens:
196      name = tokenutil.Search(token, Type.STRING_TEXT).string
197      token_strings.append(name)
198    return token_strings
199
200  def _GetTokensMap(self, tokens):
201    """Gets a map from object name to tokens associated with that object.
202
203    Starting from the goog.provide/goog.require token, searches backwards in the
204    token stream for any lines that start with a comment. These lines are
205    associated with the goog.provide/goog.require token. Also associates any
206    tokens on the same line as the goog.provide/goog.require token with that
207    token.
208
209    Args:
210      tokens: A list of goog.provide or goog.require tokens.
211
212    Returns:
213      A dictionary that maps object names to the tokens associated with the
214      goog.provide or goog.require of that object name. For example:
215
216      {
217        'object.a': [JavaScriptToken, JavaScriptToken, ...],
218        'object.b': [...]
219      }
220
221      The list of tokens includes any comment lines above the goog.provide or
222      goog.require statement and everything after the statement on the same
223      line. For example, all of the following would be associated with
224      'object.a':
225
226      /** @suppress {extraRequire} */
227      goog.require('object.a'); // Some comment.
228    """
229    tokens_map = {}
230    for token in tokens:
231      object_name = tokenutil.Search(token, Type.STRING_TEXT).string
232      # If the previous line starts with a comment, presume that the comment
233      # relates to the goog.require or goog.provide and keep them together when
234      # sorting.
235      first_token = token
236      previous_first_token = tokenutil.GetFirstTokenInPreviousLine(first_token)
237      while previous_first_token.IsAnyType(Type.COMMENT_TYPES):
238        first_token = previous_first_token
239        previous_first_token = tokenutil.GetFirstTokenInPreviousLine(
240            first_token)
241
242      # Find the last token on the line.
243      last_token = tokenutil.GetLastTokenInSameLine(token)
244
245      all_tokens = self._GetTokenList(first_token, last_token)
246      tokens_map[object_name] = all_tokens
247    return tokens_map
248
249  def _GetTokenList(self, first_token, last_token):
250    """Gets a list of all tokens from first_token to last_token, inclusive.
251
252    Args:
253      first_token: The first token to get.
254      last_token: The last token to get.
255
256    Returns:
257      A list of all tokens between first_token and last_token, including both
258      first_token and last_token.
259
260    Raises:
261      Exception: If the token stream ends before last_token is reached.
262    """
263    token_list = []
264    token = first_token
265    while token != last_token:
266      if not token:
267        raise Exception('ran out of tokens')
268      token_list.append(token)
269      token = token.next
270    token_list.append(last_token)
271
272    return token_list
273