aboutsummaryrefslogtreecommitdiff
path: root/tools/closure_linter-2.3.4/closure_linter/requireprovidesorter.py
blob: dc0c63f27e96b2e71b3eb7545547b653f7fa9397 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/env python
#
# Copyright 2011 The Closure Linter Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Contains logic for sorting goog.provide and goog.require statements.

Closurized JavaScript files use goog.provide and goog.require statements at the
top of the file to manage dependencies. These statements should be sorted
alphabetically, however, it is common for them to be accompanied by inline
comments or suppression annotations. In order to sort these statements without
disrupting their comments and annotations, the association between statements
and comments/annotations must be maintained while sorting.

  RequireProvideSorter: Handles checking/fixing of provide/require statements.
"""



from closure_linter import javascripttokens
from closure_linter import tokenutil

# Shorthand
Type = javascripttokens.JavaScriptTokenType


class RequireProvideSorter(object):
  """Checks for and fixes alphabetization of provide and require statements.

  When alphabetizing, comments on the same line or comments directly above a
  goog.provide or goog.require statement are associated with that statement and
  stay with the statement as it gets sorted.
  """

  def CheckProvides(self, token):
    """Checks alphabetization of goog.provide statements.

    Iterates over tokens in given token stream, identifies goog.provide tokens,
    and checks that they occur in alphabetical order by the object being
    provided.

    Args:
      token: A token in the token stream before any goog.provide tokens.

    Returns:
      A tuple containing the first provide token in the token stream and a list
      of provided objects sorted alphabetically. For example:

      (JavaScriptToken, ['object.a', 'object.b', ...])

      None is returned if all goog.provide statements are already sorted.
    """
    provide_tokens = self._GetRequireOrProvideTokens(token, 'goog.provide')
    provide_strings = self._GetRequireOrProvideTokenStrings(provide_tokens)
    sorted_provide_strings = sorted(provide_strings)
    if provide_strings != sorted_provide_strings:
      return [provide_tokens[0], sorted_provide_strings]
    return None

  def CheckRequires(self, token):
    """Checks alphabetization of goog.require statements.

    Iterates over tokens in given token stream, identifies goog.require tokens,
    and checks that they occur in alphabetical order by the dependency being
    required.

    Args:
      token: A token in the token stream before any goog.require tokens.

    Returns:
      A tuple containing the first require token in the token stream and a list
      of required dependencies sorted alphabetically. For example:

      (JavaScriptToken, ['object.a', 'object.b', ...])

      None is returned if all goog.require statements are already sorted.
    """
    require_tokens = self._GetRequireOrProvideTokens(token, 'goog.require')
    require_strings = self._GetRequireOrProvideTokenStrings(require_tokens)
    sorted_require_strings = sorted(require_strings)
    if require_strings != sorted_require_strings:
      return (require_tokens[0], sorted_require_strings)
    return None

  def FixProvides(self, token):
    """Sorts goog.provide statements in the given token stream alphabetically.

    Args:
      token: The first token in the token stream.
    """
    self._FixProvidesOrRequires(
        self._GetRequireOrProvideTokens(token, 'goog.provide'))

  def FixRequires(self, token):
    """Sorts goog.require statements in the given token stream alphabetically.

    Args:
      token: The first token in the token stream.
    """
    self._FixProvidesOrRequires(
        self._GetRequireOrProvideTokens(token, 'goog.require'))

  def _FixProvidesOrRequires(self, tokens):
    """Sorts goog.provide or goog.require statements.

    Args:
      tokens: A list of goog.provide or goog.require tokens in the order they
              appear in the token stream. i.e. the first token in this list must
              be the first goog.provide or goog.require token.
    """
    strings = self._GetRequireOrProvideTokenStrings(tokens)
    sorted_strings = sorted(strings)

    # A map from required/provided object name to tokens that make up the line
    # it was on, including any comments immediately before it or after it on the
    # same line.
    tokens_map = self._GetTokensMap(tokens)

    # Iterate over the map removing all tokens.
    for name in tokens_map:
      tokens_to_delete = tokens_map[name]
      for i in tokens_to_delete:
        tokenutil.DeleteToken(i)

    # Re-add all tokens in the map in alphabetical order.
    insert_after = tokens[0].previous
    for string in sorted_strings:
      for i in tokens_map[string]:
        tokenutil.InsertTokenAfter(i, insert_after)
        insert_after = i

  def _GetRequireOrProvideTokens(self, token, token_string):
    """Gets all goog.provide or goog.require tokens in the given token stream.

    Args:
      token: The first token in the token stream.
      token_string: One of 'goog.provide' or 'goog.require' to indicate which
                    tokens to find.

    Returns:
      A list of goog.provide or goog.require tokens in the order they appear in
      the token stream.
    """
    tokens = []
    while token:
      if token.type == Type.IDENTIFIER:
        if token.string == token_string:
          tokens.append(token)
        elif token.string not in ['goog.require', 'goog.provide']:
          # The goog.provide and goog.require identifiers are at the top of the
          # file. So if any other identifier is encountered, return.
          break
      token = token.next

    return tokens

  def _GetRequireOrProvideTokenStrings(self, tokens):
    """Gets a list of strings corresponding to the given list of tokens.

    The string will be the next string in the token stream after each token in
    tokens. This is used to find the object being provided/required by a given
    goog.provide or goog.require token.

    Args:
      tokens: A list of goog.provide or goog.require tokens.

    Returns:
      A list of object names that are being provided or required by the given
      list of tokens. For example:

      ['object.a', 'object.c', 'object.b']
    """
    token_strings = []
    for token in tokens:
      name = tokenutil.Search(token, Type.STRING_TEXT).string
      token_strings.append(name)
    return token_strings

  def _GetTokensMap(self, tokens):
    """Gets a map from object name to tokens associated with that object.

    Starting from the goog.provide/goog.require token, searches backwards in the
    token stream for any lines that start with a comment. These lines are
    associated with the goog.provide/goog.require token. Also associates any
    tokens on the same line as the goog.provide/goog.require token with that
    token.

    Args:
      tokens: A list of goog.provide or goog.require tokens.

    Returns:
      A dictionary that maps object names to the tokens associated with the
      goog.provide or goog.require of that object name. For example:

      {
        'object.a': [JavaScriptToken, JavaScriptToken, ...],
        'object.b': [...]
      }

      The list of tokens includes any comment lines above the goog.provide or
      goog.require statement and everything after the statement on the same
      line. For example, all of the following would be associated with
      'object.a':

      /** @suppress {extraRequire} */
      goog.require('object.a'); // Some comment.
    """
    tokens_map = {}
    for token in tokens:
      object_name = tokenutil.Search(token, Type.STRING_TEXT).string
      # If the previous line starts with a comment, presume that the comment
      # relates to the goog.require or goog.provide and keep them together when
      # sorting.
      first_token = token
      previous_first_token = tokenutil.GetFirstTokenInPreviousLine(first_token)
      while previous_first_token.IsAnyType(Type.COMMENT_TYPES):
        first_token = previous_first_token
        previous_first_token = tokenutil.GetFirstTokenInPreviousLine(
            first_token)

      # Find the last token on the line.
      last_token = tokenutil.GetLastTokenInSameLine(token)

      all_tokens = self._GetTokenList(first_token, last_token)
      tokens_map[object_name] = all_tokens
    return tokens_map

  def _GetTokenList(self, first_token, last_token):
    """Gets a list of all tokens from first_token to last_token, inclusive.

    Args:
      first_token: The first token to get.
      last_token: The last token to get.

    Returns:
      A list of all tokens between first_token and last_token, including both
      first_token and last_token.

    Raises:
      Exception: If the token stream ends before last_token is reached.
    """
    token_list = []
    token = first_token
    while token != last_token:
      if not token:
        raise Exception('ran out of tokens')
      token_list.append(token)
      token = token.next
    token_list.append(last_token)

    return token_list