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"""Tools to match goog.scope alias statements."""
17
18# Allow non-Google copyright
19# pylint: disable=g-bad-file-header
20
21__author__ = ('nnaze@google.com (Nathan Naze)')
22
23import itertools
24
25from closure_linter import ecmametadatapass
26from closure_linter import tokenutil
27from closure_linter.javascripttokens import JavaScriptTokenType
28
29
30
31def IsGoogScopeBlock(context):
32  """Whether the given context is a goog.scope block.
33
34  This function only checks that the block is a function block inside
35  a goog.scope() call.
36
37  TODO(nnaze): Implement goog.scope checks that verify the call is
38  in the root context and contains only a single function literal.
39
40  Args:
41    context: An EcmaContext of type block.
42
43  Returns:
44    Whether the context is a goog.scope block.
45  """
46
47  if context.type != ecmametadatapass.EcmaContext.BLOCK:
48    return False
49
50  if not _IsFunctionLiteralBlock(context):
51    return False
52
53  # Check that this function is contained by a group
54  # of form "goog.scope(...)".
55  parent = context.parent
56  if parent and parent.type is ecmametadatapass.EcmaContext.GROUP:
57
58    last_code_token = parent.start_token.metadata.last_code
59
60    if (last_code_token and
61        last_code_token.type is JavaScriptTokenType.IDENTIFIER and
62        last_code_token.string == 'goog.scope'):
63      return True
64
65  return False
66
67
68def _IsFunctionLiteralBlock(block_context):
69  """Check if a context is a function literal block (without parameters).
70
71  Example function literal block: 'function() {}'
72
73  Args:
74    block_context: An EcmaContext of type block.
75
76  Returns:
77    Whether this context is a function literal block.
78  """
79
80  previous_code_tokens_iter = itertools.ifilter(
81      lambda token: token not in JavaScriptTokenType.NON_CODE_TYPES,
82      reversed(block_context.start_token))
83
84  # Ignore the current token
85  next(previous_code_tokens_iter, None)
86
87  # Grab the previous three tokens and put them in correct order.
88  previous_code_tokens = list(itertools.islice(previous_code_tokens_iter, 3))
89  previous_code_tokens.reverse()
90
91  # There aren't three previous tokens.
92  if len(previous_code_tokens) is not 3:
93    return False
94
95  # Check that the previous three code tokens are "function ()"
96  previous_code_token_types = [token.type for token in previous_code_tokens]
97  if (previous_code_token_types == [
98      JavaScriptTokenType.FUNCTION_DECLARATION,
99      JavaScriptTokenType.START_PARAMETERS,
100      JavaScriptTokenType.END_PARAMETERS]):
101    return True
102
103  return False
104
105
106def IsInClosurizedNamespace(symbol, closurized_namespaces):
107  """Match a goog.scope alias.
108
109  Args:
110    symbol: An identifier like 'goog.events.Event'.
111    closurized_namespaces: Iterable of valid Closurized namespaces (strings).
112
113  Returns:
114    True if symbol is an identifier in a Closurized namespace, otherwise False.
115  """
116  for ns in closurized_namespaces:
117    if symbol.startswith(ns + '.'):
118      return True
119
120  return False
121
122
123def _GetVarAssignmentTokens(context):
124  """Returns the tokens from context if it is a var assignment.
125
126  Args:
127    context: An EcmaContext.
128
129  Returns:
130    If a var assignment, the tokens contained within it w/o the trailing
131    semicolon.
132  """
133  if context.type != ecmametadatapass.EcmaContext.VAR:
134    return
135
136  # Get the tokens in this statement.
137  if context.start_token and context.end_token:
138    statement_tokens = tokenutil.GetTokenRange(context.start_token,
139                                               context.end_token)
140  else:
141    return
142
143  # And now just those tokens that are actually code.
144  is_non_code_type = lambda t: t.type not in JavaScriptTokenType.NON_CODE_TYPES
145  code_tokens = filter(is_non_code_type, statement_tokens)
146
147  # Pop off the semicolon if present.
148  if code_tokens and code_tokens[-1].IsType(JavaScriptTokenType.SEMICOLON):
149    code_tokens.pop()
150
151  if len(code_tokens) < 4:
152    return
153
154  if (code_tokens[0].IsKeyword('var') and
155      code_tokens[1].IsType(JavaScriptTokenType.SIMPLE_LVALUE) and
156      code_tokens[2].IsOperator('=')):
157    return code_tokens
158
159
160def MatchAlias(context):
161  """Match an alias statement (some identifier assigned to a variable).
162
163  Example alias: var MyClass = proj.longNamespace.MyClass.
164
165  Args:
166    context: An EcmaContext of type EcmaContext.VAR.
167
168  Returns:
169    If a valid alias, returns a tuple of alias and symbol, otherwise None.
170  """
171  code_tokens = _GetVarAssignmentTokens(context)
172  if code_tokens is None:
173    return
174
175  if all(tokenutil.IsIdentifierOrDot(t) for t in code_tokens[3:]):
176    # var Foo = bar.Foo;
177    alias, symbol = code_tokens[1], code_tokens[3]
178    # Mark both tokens as an alias definition to not count them as usages.
179    alias.metadata.is_alias_definition = True
180    symbol.metadata.is_alias_definition = True
181    return alias.string, tokenutil.GetIdentifierForToken(symbol)
182
183
184def MatchModuleAlias(context):
185  """Match an alias statement in a goog.module style import.
186
187  Example alias: var MyClass = goog.require('proj.longNamespace.MyClass').
188
189  Args:
190    context: An EcmaContext.
191
192  Returns:
193    If a valid alias, returns a tuple of alias and symbol, otherwise None.
194  """
195  code_tokens = _GetVarAssignmentTokens(context)
196  if code_tokens is None:
197    return
198
199  if(code_tokens[3].IsType(JavaScriptTokenType.IDENTIFIER) and
200     code_tokens[3].string == 'goog.require'):
201    # var Foo = goog.require('bar.Foo');
202    alias = code_tokens[1]
203    symbol = tokenutil.GetStringAfterToken(code_tokens[3])
204    if symbol:
205      alias.metadata.is_alias_definition = True
206      return alias.string, symbol
207