OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # | |
3 # Copyright 2012 The Closure Linter Authors. All Rights Reserved. | |
4 # Licensed under the Apache License, Version 2.0 (the "License"); | |
5 # you may not use this file except in compliance with the License. | |
6 # You may obtain a copy of the License at | |
7 # | |
8 # http://www.apache.org/licenses/LICENSE-2.0 | |
9 # | |
10 # Unless required by applicable law or agreed to in writing, software | |
11 # distributed under the License is distributed on an "AS-IS" BASIS, | |
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 # See the License for the specific language governing permissions and | |
14 # limitations under the License. | |
15 | |
16 """Pass that scans for goog.scope aliases and lint/usage errors.""" | |
17 | |
18 # Allow non-Google copyright | |
19 # pylint: disable=g-bad-file-header | |
20 | |
21 __author__ = ('nnaze@google.com (Nathan Naze)') | |
22 | |
23 from closure_linter import ecmametadatapass | |
24 from closure_linter import errors | |
25 from closure_linter import javascripttokens | |
26 from closure_linter import scopeutil | |
27 from closure_linter import tokenutil | |
28 from closure_linter.common import error | |
29 | |
30 | |
31 # TODO(nnaze): Create a Pass interface and move this class, EcmaMetaDataPass, | |
32 # and related classes onto it. | |
33 | |
34 | |
35 def _GetAliasForIdentifier(identifier, alias_map): | |
36 """Returns the aliased_symbol name for an identifier. | |
37 | |
38 Example usage: | |
39 >>> alias_map = {'MyClass': 'goog.foo.MyClass'} | |
40 >>> _GetAliasForIdentifier('MyClass.prototype.action', alias_map) | |
41 'goog.foo.MyClass.prototype.action' | |
42 | |
43 >>> _GetAliasForIdentifier('MyClass.prototype.action', {}) | |
44 None | |
45 | |
46 Args: | |
47 identifier: The identifier. | |
48 alias_map: A dictionary mapping a symbol to an alias. | |
49 | |
50 Returns: | |
51 The aliased symbol name or None if not found. | |
52 """ | |
53 ns = identifier.split('.', 1)[0] | |
54 aliased_symbol = alias_map.get(ns) | |
55 if aliased_symbol: | |
56 return aliased_symbol + identifier[len(ns):] | |
57 | |
58 | |
59 def _SetTypeAlias(js_type, alias_map): | |
60 """Updates the alias for identifiers in a type. | |
61 | |
62 Args: | |
63 js_type: A typeannotation.TypeAnnotation instance. | |
64 alias_map: A dictionary mapping a symbol to an alias. | |
65 """ | |
66 aliased_symbol = _GetAliasForIdentifier(js_type.identifier, alias_map) | |
67 if aliased_symbol: | |
68 js_type.alias = aliased_symbol | |
69 for sub_type in js_type.IterTypes(): | |
70 _SetTypeAlias(sub_type, alias_map) | |
71 | |
72 | |
73 class AliasPass(object): | |
74 """Pass to identify goog.scope() usages. | |
75 | |
76 Identifies goog.scope() usages and finds lint/usage errors. Notes any | |
77 aliases of symbols in Closurized namespaces (that is, reassignments | |
78 such as "var MyClass = goog.foo.MyClass;") and annotates identifiers | |
79 when they're using an alias (so they may be expanded to the full symbol | |
80 later -- that "MyClass.prototype.action" refers to | |
81 "goog.foo.MyClass.prototype.action" when expanded.). | |
82 """ | |
83 | |
84 def __init__(self, closurized_namespaces=None, error_handler=None): | |
85 """Creates a new pass. | |
86 | |
87 Args: | |
88 closurized_namespaces: A set of Closurized namespaces (e.g. 'goog'). | |
89 error_handler: An error handler to report lint errors to. | |
90 """ | |
91 | |
92 self._error_handler = error_handler | |
93 | |
94 # If we have namespaces, freeze the set. | |
95 if closurized_namespaces: | |
96 closurized_namespaces = frozenset(closurized_namespaces) | |
97 | |
98 self._closurized_namespaces = closurized_namespaces | |
99 | |
100 def Process(self, start_token): | |
101 """Runs the pass on a token stream. | |
102 | |
103 Args: | |
104 start_token: The first token in the stream. | |
105 """ | |
106 | |
107 if start_token is None: | |
108 return | |
109 | |
110 # TODO(nnaze): Add more goog.scope usage checks. | |
111 self._CheckGoogScopeCalls(start_token) | |
112 | |
113 # If we have closurized namespaces, identify aliased identifiers. | |
114 if self._closurized_namespaces: | |
115 context = start_token.metadata.context | |
116 root_context = context.GetRoot() | |
117 self._ProcessRootContext(root_context) | |
118 | |
119 def _CheckGoogScopeCalls(self, start_token): | |
120 """Check goog.scope calls for lint/usage errors.""" | |
121 | |
122 def IsScopeToken(token): | |
123 return (token.type is javascripttokens.JavaScriptTokenType.IDENTIFIER and | |
124 token.string == 'goog.scope') | |
125 | |
126 # Find all the goog.scope tokens in the file | |
127 scope_tokens = [t for t in start_token if IsScopeToken(t)] | |
128 | |
129 for token in scope_tokens: | |
130 scope_context = token.metadata.context | |
131 | |
132 if not (scope_context.type == ecmametadatapass.EcmaContext.STATEMENT and | |
133 scope_context.parent.type == ecmametadatapass.EcmaContext.ROOT): | |
134 self._MaybeReportError( | |
135 error.Error(errors.INVALID_USE_OF_GOOG_SCOPE, | |
136 'goog.scope call not in global scope', token)) | |
137 | |
138 # There should be only one goog.scope reference. Register errors for | |
139 # every instance after the first. | |
140 for token in scope_tokens[1:]: | |
141 self._MaybeReportError( | |
142 error.Error(errors.EXTRA_GOOG_SCOPE_USAGE, | |
143 'More than one goog.scope call in file.', token)) | |
144 | |
145 def _MaybeReportError(self, err): | |
146 """Report an error to the handler (if registered).""" | |
147 if self._error_handler: | |
148 self._error_handler.HandleError(err) | |
149 | |
150 @classmethod | |
151 def _YieldAllContexts(cls, context): | |
152 """Yields all contexts that are contained by the given context.""" | |
153 yield context | |
154 for child_context in context.children: | |
155 for descendent_child in cls._YieldAllContexts(child_context): | |
156 yield descendent_child | |
157 | |
158 @staticmethod | |
159 def _IsTokenInParentBlock(token, parent_block): | |
160 """Determines whether the given token is contained by the given block. | |
161 | |
162 Args: | |
163 token: A token | |
164 parent_block: An EcmaContext. | |
165 | |
166 Returns: | |
167 Whether the token is in a context that is or is a child of the given | |
168 parent_block context. | |
169 """ | |
170 context = token.metadata.context | |
171 | |
172 while context: | |
173 if context is parent_block: | |
174 return True | |
175 context = context.parent | |
176 | |
177 return False | |
178 | |
179 def _ProcessRootContext(self, root_context): | |
180 """Processes all goog.scope blocks under the root context.""" | |
181 | |
182 assert root_context.type is ecmametadatapass.EcmaContext.ROOT | |
183 | |
184 # Process aliases in statements in the root scope for goog.module-style | |
185 # aliases. | |
186 global_alias_map = {} | |
187 for context in root_context.children: | |
188 if context.type == ecmametadatapass.EcmaContext.STATEMENT: | |
189 for statement_child in context.children: | |
190 if statement_child.type == ecmametadatapass.EcmaContext.VAR: | |
191 match = scopeutil.MatchModuleAlias(statement_child) | |
192 if match: | |
193 # goog.require aliases cannot use further aliases, the symbol is | |
194 # the second part of match, directly. | |
195 symbol = match[1] | |
196 if scopeutil.IsInClosurizedNamespace(symbol, | |
197 self._closurized_namespaces): | |
198 global_alias_map[match[0]] = symbol | |
199 | |
200 # Process each block to find aliases. | |
201 for context in root_context.children: | |
202 self._ProcessBlock(context, global_alias_map) | |
203 | |
204 def _ProcessBlock(self, context, global_alias_map): | |
205 """Scans a goog.scope block to find aliases and mark alias tokens.""" | |
206 alias_map = global_alias_map.copy() | |
207 | |
208 # Iterate over every token in the context. Each token points to one | |
209 # context, but multiple tokens may point to the same context. We only want | |
210 # to check each context once, so keep track of those we've seen. | |
211 seen_contexts = set() | |
212 token = context.start_token | |
213 while token and self._IsTokenInParentBlock(token, context): | |
214 token_context = token.metadata.context if token.metadata else None | |
215 | |
216 # Check to see if this token is an alias. | |
217 if token_context and token_context not in seen_contexts: | |
218 seen_contexts.add(token_context) | |
219 | |
220 # If this is a alias statement in the goog.scope block. | |
221 if (token_context.type == ecmametadatapass.EcmaContext.VAR and | |
222 scopeutil.IsGoogScopeBlock(token_context.parent.parent)): | |
223 match = scopeutil.MatchAlias(token_context) | |
224 | |
225 # If this is an alias, remember it in the map. | |
226 if match: | |
227 alias, symbol = match | |
228 symbol = _GetAliasForIdentifier(symbol, alias_map) or symbol | |
229 if scopeutil.IsInClosurizedNamespace(symbol, | |
230 self._closurized_namespaces): | |
231 alias_map[alias] = symbol | |
232 | |
233 # If this token is an identifier that matches an alias, | |
234 # mark the token as an alias to the original symbol. | |
235 if (token.type is javascripttokens.JavaScriptTokenType.SIMPLE_LVALUE or | |
236 token.type is javascripttokens.JavaScriptTokenType.IDENTIFIER): | |
237 identifier = tokenutil.GetIdentifierForToken(token) | |
238 if identifier: | |
239 aliased_symbol = _GetAliasForIdentifier(identifier, alias_map) | |
240 if aliased_symbol: | |
241 token.metadata.aliased_symbol = aliased_symbol | |
242 | |
243 elif token.type == javascripttokens.JavaScriptTokenType.DOC_FLAG: | |
244 flag = token.attached_object | |
245 if flag and flag.HasType() and flag.jstype: | |
246 _SetTypeAlias(flag.jstype, alias_map) | |
247 | |
248 token = token.next # Get next token | |
OLD | NEW |