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