1#!/usr/bin/env python
2#
3# Copyright 2010 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"""Unit tests for ClosurizedNamespacesInfo."""
18
19
20
21import unittest as googletest
22from closure_linter import aliaspass
23from closure_linter import closurizednamespacesinfo
24from closure_linter import ecmametadatapass
25from closure_linter import javascriptstatetracker
26from closure_linter import javascripttokens
27from closure_linter import testutil
28from closure_linter import tokenutil
29
30# pylint: disable=g-bad-name
31TokenType = javascripttokens.JavaScriptTokenType
32
33
34class ClosurizedNamespacesInfoTest(googletest.TestCase):
35  """Tests for ClosurizedNamespacesInfo."""
36
37  _test_cases = {
38      'goog.global.anything': None,
39      'package.CONSTANT': 'package',
40      'package.methodName': 'package',
41      'package.subpackage.methodName': 'package.subpackage',
42      'package.subpackage.methodName.apply': 'package.subpackage',
43      'package.ClassName.something': 'package.ClassName',
44      'package.ClassName.Enum.VALUE.methodName': 'package.ClassName',
45      'package.ClassName.CONSTANT': 'package.ClassName',
46      'package.namespace.CONSTANT.methodName': 'package.namespace',
47      'package.ClassName.inherits': 'package.ClassName',
48      'package.ClassName.apply': 'package.ClassName',
49      'package.ClassName.methodName.apply': 'package.ClassName',
50      'package.ClassName.methodName.call': 'package.ClassName',
51      'package.ClassName.prototype.methodName': 'package.ClassName',
52      'package.ClassName.privateMethod_': 'package.ClassName',
53      'package.className.privateProperty_': 'package.className',
54      'package.className.privateProperty_.methodName': 'package.className',
55      'package.ClassName.PrivateEnum_': 'package.ClassName',
56      'package.ClassName.prototype.methodName.apply': 'package.ClassName',
57      'package.ClassName.property.subProperty': 'package.ClassName',
58      'package.className.prototype.something.somethingElse': 'package.className'
59  }
60
61  def testGetClosurizedNamespace(self):
62    """Tests that the correct namespace is returned for various identifiers."""
63    namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
64        closurized_namespaces=['package'], ignored_extra_namespaces=[])
65    for identifier, expected_namespace in self._test_cases.items():
66      actual_namespace = namespaces_info.GetClosurizedNamespace(identifier)
67      self.assertEqual(
68          expected_namespace,
69          actual_namespace,
70          'expected namespace "' + str(expected_namespace) +
71          '" for identifier "' + str(identifier) + '" but was "' +
72          str(actual_namespace) + '"')
73
74  def testIgnoredExtraNamespaces(self):
75    """Tests that ignored_extra_namespaces are ignored."""
76    token = self._GetRequireTokens('package.Something')
77    namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
78        closurized_namespaces=['package'],
79        ignored_extra_namespaces=['package.Something'])
80
81    self.assertFalse(namespaces_info.IsExtraRequire(token),
82                     'Should be valid since it is in ignored namespaces.')
83
84    namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
85        ['package'], [])
86
87    self.assertTrue(namespaces_info.IsExtraRequire(token),
88                    'Should be invalid since it is not in ignored namespaces.')
89
90  def testIsExtraProvide_created(self):
91    """Tests that provides for created namespaces are not extra."""
92    input_lines = [
93        'goog.provide(\'package.Foo\');',
94        'package.Foo = function() {};'
95    ]
96
97    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
98        input_lines, ['package'])
99
100    self.assertFalse(namespaces_info.IsExtraProvide(token),
101                     'Should not be extra since it is created.')
102
103  def testIsExtraProvide_createdIdentifier(self):
104    """Tests that provides for created identifiers are not extra."""
105    input_lines = [
106        'goog.provide(\'package.Foo.methodName\');',
107        'package.Foo.methodName = function() {};'
108    ]
109
110    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
111        input_lines, ['package'])
112
113    self.assertFalse(namespaces_info.IsExtraProvide(token),
114                     'Should not be extra since it is created.')
115
116  def testIsExtraProvide_notCreated(self):
117    """Tests that provides for non-created namespaces are extra."""
118    input_lines = ['goog.provide(\'package.Foo\');']
119
120    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
121        input_lines, ['package'])
122
123    self.assertTrue(namespaces_info.IsExtraProvide(token),
124                    'Should be extra since it is not created.')
125
126  def testIsExtraProvide_duplicate(self):
127    """Tests that providing a namespace twice makes the second one extra."""
128    input_lines = [
129        'goog.provide(\'package.Foo\');',
130        'goog.provide(\'package.Foo\');',
131        'package.Foo = function() {};'
132    ]
133
134    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
135        input_lines, ['package'])
136
137    # Advance to the second goog.provide token.
138    token = tokenutil.Search(token.next, TokenType.IDENTIFIER)
139
140    self.assertTrue(namespaces_info.IsExtraProvide(token),
141                    'Should be extra since it is already provided.')
142
143  def testIsExtraProvide_notClosurized(self):
144    """Tests that provides of non-closurized namespaces are not extra."""
145    input_lines = ['goog.provide(\'notclosurized.Foo\');']
146
147    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
148        input_lines, ['package'])
149
150    self.assertFalse(namespaces_info.IsExtraProvide(token),
151                     'Should not be extra since it is not closurized.')
152
153  def testIsExtraRequire_used(self):
154    """Tests that requires for used namespaces are not extra."""
155    input_lines = [
156        'goog.require(\'package.Foo\');',
157        'var x = package.Foo.methodName();'
158    ]
159
160    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
161        input_lines, ['package'])
162
163    self.assertFalse(namespaces_info.IsExtraRequire(token),
164                     'Should not be extra since it is used.')
165
166  def testIsExtraRequire_usedIdentifier(self):
167    """Tests that requires for used methods on classes are extra."""
168    input_lines = [
169        'goog.require(\'package.Foo.methodName\');',
170        'var x = package.Foo.methodName();'
171    ]
172
173    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
174        input_lines, ['package'])
175
176    self.assertTrue(namespaces_info.IsExtraRequire(token),
177                    'Should require the package, not the method specifically.')
178
179  def testIsExtraRequire_notUsed(self):
180    """Tests that requires for unused namespaces are extra."""
181    input_lines = ['goog.require(\'package.Foo\');']
182
183    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
184        input_lines, ['package'])
185
186    self.assertTrue(namespaces_info.IsExtraRequire(token),
187                    'Should be extra since it is not used.')
188
189  def testIsExtraRequire_notClosurized(self):
190    """Tests that requires of non-closurized namespaces are not extra."""
191    input_lines = ['goog.require(\'notclosurized.Foo\');']
192
193    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
194        input_lines, ['package'])
195
196    self.assertFalse(namespaces_info.IsExtraRequire(token),
197                     'Should not be extra since it is not closurized.')
198
199  def testIsExtraRequire_objectOnClass(self):
200    """Tests that requiring an object on a class is extra."""
201    input_lines = [
202        'goog.require(\'package.Foo.Enum\');',
203        'var x = package.Foo.Enum.VALUE1;',
204    ]
205
206    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
207        input_lines, ['package'])
208
209    self.assertTrue(namespaces_info.IsExtraRequire(token),
210                    'The whole class, not the object, should be required.');
211
212  def testIsExtraRequire_constantOnClass(self):
213    """Tests that requiring a constant on a class is extra."""
214    input_lines = [
215        'goog.require(\'package.Foo.CONSTANT\');',
216        'var x = package.Foo.CONSTANT',
217    ]
218
219    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
220        input_lines, ['package'])
221
222    self.assertTrue(namespaces_info.IsExtraRequire(token),
223                    'The class, not the constant, should be required.');
224
225  def testIsExtraRequire_constantNotOnClass(self):
226    """Tests that requiring a constant not on a class is OK."""
227    input_lines = [
228        'goog.require(\'package.subpackage.CONSTANT\');',
229        'var x = package.subpackage.CONSTANT',
230    ]
231
232    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
233        input_lines, ['package'])
234
235    self.assertFalse(namespaces_info.IsExtraRequire(token),
236                    'Constants can be required except on classes.');
237
238  def testIsExtraRequire_methodNotOnClass(self):
239    """Tests that requiring a method not on a class is OK."""
240    input_lines = [
241        'goog.require(\'package.subpackage.method\');',
242        'var x = package.subpackage.method()',
243    ]
244
245    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
246        input_lines, ['package'])
247
248    self.assertFalse(namespaces_info.IsExtraRequire(token),
249                    'Methods can be required except on classes.');
250
251  def testIsExtraRequire_defaults(self):
252    """Tests that there are no warnings about extra requires for test utils"""
253    input_lines = ['goog.require(\'goog.testing.jsunit\');']
254
255    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
256        input_lines, ['goog'])
257
258    self.assertFalse(namespaces_info.IsExtraRequire(token),
259                     'Should not be extra since it is for testing.')
260
261  def testGetMissingProvides_provided(self):
262    """Tests that provided functions don't cause a missing provide."""
263    input_lines = [
264        'goog.provide(\'package.Foo\');',
265        'package.Foo = function() {};'
266    ]
267
268    namespaces_info = self._GetNamespacesInfoForScript(
269        input_lines, ['package'])
270
271    self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
272
273  def testGetMissingProvides_providedIdentifier(self):
274    """Tests that provided identifiers don't cause a missing provide."""
275    input_lines = [
276        'goog.provide(\'package.Foo.methodName\');',
277        'package.Foo.methodName = function() {};'
278    ]
279
280    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
281    self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
282
283  def testGetMissingProvides_providedParentIdentifier(self):
284    """Tests that provided identifiers on a class don't cause a missing provide
285    on objects attached to that class."""
286    input_lines = [
287        'goog.provide(\'package.foo.ClassName\');',
288        'package.foo.ClassName.methodName = function() {};',
289        'package.foo.ClassName.ObjectName = 1;',
290    ]
291
292    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
293    self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
294
295  def testGetMissingProvides_unprovided(self):
296    """Tests that unprovided functions cause a missing provide."""
297    input_lines = ['package.Foo = function() {};']
298
299    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
300
301    missing_provides = namespaces_info.GetMissingProvides()
302    self.assertEquals(1, len(missing_provides))
303    missing_provide = missing_provides.popitem()
304    self.assertEquals('package.Foo', missing_provide[0])
305    self.assertEquals(1, missing_provide[1])
306
307  def testGetMissingProvides_privatefunction(self):
308    """Tests that unprovided private functions don't cause a missing provide."""
309    input_lines = ['package.Foo_ = function() {};']
310
311    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
312    self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
313
314  def testGetMissingProvides_required(self):
315    """Tests that required namespaces don't cause a missing provide."""
316    input_lines = [
317        'goog.require(\'package.Foo\');',
318        'package.Foo.methodName = function() {};'
319    ]
320
321    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
322    self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
323
324  def testGetMissingRequires_required(self):
325    """Tests that required namespaces don't cause a missing require."""
326    input_lines = [
327        'goog.require(\'package.Foo\');',
328        'package.Foo();'
329    ]
330
331    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
332    self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
333
334  def testGetMissingRequires_requiredIdentifier(self):
335    """Tests that required namespaces satisfy identifiers on that namespace."""
336    input_lines = [
337        'goog.require(\'package.Foo\');',
338        'package.Foo.methodName();'
339    ]
340
341    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
342    self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
343
344  def testGetMissingRequires_requiredParentClass(self):
345    """Tests that requiring a parent class of an object is sufficient to prevent
346    a missing require on that object."""
347    input_lines = [
348        'goog.require(\'package.Foo\');',
349        'package.Foo.methodName();',
350        'package.Foo.methodName(package.Foo.ObjectName);'
351    ]
352
353    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
354    self.assertEquals(0, len(namespaces_info.GetMissingRequires()))
355
356  def testGetMissingRequires_unrequired(self):
357    """Tests that unrequired namespaces cause a missing require."""
358    input_lines = ['package.Foo();']
359
360    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
361
362    missing_requires = namespaces_info.GetMissingRequires()
363    self.assertEquals(1, len(missing_requires))
364    missing_req = missing_requires.popitem()
365    self.assertEquals('package.Foo', missing_req[0])
366    self.assertEquals(1, missing_req[1])
367
368  def testGetMissingRequires_provided(self):
369    """Tests that provided namespaces satisfy identifiers on that namespace."""
370    input_lines = [
371        'goog.provide(\'package.Foo\');',
372        'package.Foo.methodName();'
373    ]
374
375    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
376    self.assertEquals(0, len(namespaces_info.GetMissingRequires()))
377
378  def testGetMissingRequires_created(self):
379    """Tests that created namespaces do not satisfy usage of an identifier."""
380    input_lines = [
381        'package.Foo = function();',
382        'package.Foo.methodName();',
383        'package.Foo.anotherMethodName1();',
384        'package.Foo.anotherMethodName2();'
385    ]
386
387    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
388
389    missing_requires = namespaces_info.GetMissingRequires()
390    self.assertEquals(1, len(missing_requires))
391    missing_require = missing_requires.popitem()
392    self.assertEquals('package.Foo', missing_require[0])
393    # Make sure line number of first occurrence is reported
394    self.assertEquals(2, missing_require[1])
395
396  def testGetMissingRequires_createdIdentifier(self):
397    """Tests that created identifiers satisfy usage of the identifier."""
398    input_lines = [
399        'package.Foo.methodName = function();',
400        'package.Foo.methodName();'
401    ]
402
403    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
404    self.assertEquals(0, len(namespaces_info.GetMissingRequires()))
405
406  def testGetMissingRequires_objectOnClass(self):
407    """Tests that we should require a class, not the object on the class."""
408    input_lines = [
409        'goog.require(\'package.Foo.Enum\');',
410        'var x = package.Foo.Enum.VALUE1;',
411    ]
412
413    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
414    self.assertEquals(1, len(namespaces_info.GetMissingRequires()),
415                      'The whole class, not the object, should be required.')
416
417  def testGetMissingRequires_variableWithSameName(self):
418    """Tests that we should not goog.require variables and parameters.
419
420    b/5362203 Variables in scope are not missing namespaces.
421    """
422    input_lines = [
423        'goog.provide(\'Foo\');',
424        'Foo.A = function();',
425        'Foo.A.prototype.method = function(ab) {',
426        '  if (ab) {',
427        '    var docs;',
428        '    var lvalue = new Obj();',
429        '    // Variable in scope hence not goog.require here.',
430        '    docs.foo.abc = 1;',
431        '    lvalue.next();',
432        '  }',
433        '  // Since js is function scope this should also not goog.require.',
434        '  docs.foo.func();',
435        '  // Its not a variable in scope hence goog.require.',
436        '  dummy.xyz.reset();',
437        ' return this.method2();',
438        '};',
439        'Foo.A.prototype.method1 = function(docs, abcd, xyz) {',
440        '  // Parameter hence not goog.require.',
441        '  docs.nodes.length = 2;',
442        '  lvalue.abc.reset();',
443        '};'
444    ]
445
446    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['Foo',
447                                                                     'docs',
448                                                                     'lvalue',
449                                                                     'dummy'])
450    missing_requires = namespaces_info.GetMissingRequires()
451    self.assertEquals(2, len(missing_requires))
452    self.assertItemsEqual(
453        {'dummy.xyz': 14,
454         'lvalue.abc': 20}, missing_requires)
455
456  def testIsFirstProvide(self):
457    """Tests operation of the isFirstProvide method."""
458    input_lines = [
459        'goog.provide(\'package.Foo\');',
460        'package.Foo.methodName();'
461    ]
462
463    token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
464        input_lines, ['package'])
465    self.assertTrue(namespaces_info.IsFirstProvide(token))
466
467  def testGetWholeIdentifierString(self):
468    """Tests that created identifiers satisfy usage of the identifier."""
469    input_lines = [
470        'package.Foo.',
471        '    veryLong.',
472        '    identifier;'
473    ]
474
475    token = testutil.TokenizeSource(input_lines)
476
477    self.assertEquals('package.Foo.veryLong.identifier',
478                      tokenutil.GetIdentifierForToken(token))
479
480    self.assertEquals(None,
481                      tokenutil.GetIdentifierForToken(token.next))
482
483  def testScopified(self):
484    """Tests that a goog.scope call is noticed."""
485    input_lines = [
486        'goog.scope(function() {',
487        '});'
488        ]
489
490    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
491    self.assertTrue(namespaces_info._scopified_file)
492
493  def testScope_unusedAlias(self):
494    """Tests that an used alias symbol doesn't result in a require."""
495    input_lines = [
496        'goog.scope(function() {',
497        'var Event = goog.events.Event;',
498        '});'
499        ]
500
501    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
502    missing_requires = namespaces_info.GetMissingRequires()
503    self.assertEquals({}, missing_requires)
504
505  def testScope_usedAlias(self):
506    """Tests that aliased symbols result in correct requires."""
507    input_lines = [
508        'goog.scope(function() {',
509        'var Event = goog.events.Event;',
510        'var dom = goog.dom;',
511        'Event(dom.classes.get);',
512        '});'
513        ]
514
515    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
516    missing_requires = namespaces_info.GetMissingRequires()
517    self.assertEquals({'goog.dom.classes': 4, 'goog.events.Event': 4},
518                      missing_requires)
519
520  def testScope_provides(self):
521    """Tests that aliased symbols result in correct provides."""
522    input_lines = [
523        'goog.scope(function() {',
524        'goog.bar = {};',
525        'var bar = goog.bar;',
526        'bar.Foo = {};',
527        '});'
528        ]
529
530    namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
531    missing_provides = namespaces_info.GetMissingProvides()
532    self.assertEquals({'goog.bar.Foo': 4}, missing_provides)
533
534  def testSetTestOnlyNamespaces(self):
535    """Tests that a namespace in setTestOnly makes it a valid provide."""
536    namespaces_info = self._GetNamespacesInfoForScript([
537        'goog.setTestOnly(\'goog.foo.barTest\');'
538        ], ['goog'])
539
540    token = self._GetProvideTokens('goog.foo.barTest')
541    self.assertFalse(namespaces_info.IsExtraProvide(token))
542
543    token = self._GetProvideTokens('goog.foo.bazTest')
544    self.assertTrue(namespaces_info.IsExtraProvide(token))
545
546  def testSetTestOnlyComment(self):
547    """Ensure a comment in setTestOnly does not cause a created namespace."""
548    namespaces_info = self._GetNamespacesInfoForScript([
549        'goog.setTestOnly(\'this is a comment\');'
550        ], ['goog'])
551
552    self.assertEquals(
553        [], namespaces_info._created_namespaces,
554        'A comment in setTestOnly should not modify created namespaces.')
555
556  def _GetNamespacesInfoForScript(self, script, closurized_namespaces=None):
557    _, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
558        script, closurized_namespaces)
559
560    return namespaces_info
561
562  def _GetStartTokenAndNamespacesInfoForScript(
563      self, script, closurized_namespaces):
564
565    token = testutil.TokenizeSource(script)
566    return token, self._GetInitializedNamespacesInfo(
567        token, closurized_namespaces, [])
568
569  def _GetInitializedNamespacesInfo(self, token, closurized_namespaces,
570                                    ignored_extra_namespaces):
571    """Returns a namespaces info initialized with the given token stream."""
572    namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
573        closurized_namespaces=closurized_namespaces,
574        ignored_extra_namespaces=ignored_extra_namespaces)
575    state_tracker = javascriptstatetracker.JavaScriptStateTracker()
576
577    ecma_pass = ecmametadatapass.EcmaMetaDataPass()
578    ecma_pass.Process(token)
579
580    alias_pass = aliaspass.AliasPass(closurized_namespaces)
581    alias_pass.Process(token)
582
583    while token:
584      state_tracker.HandleToken(token, state_tracker.GetLastNonSpaceToken())
585      namespaces_info.ProcessToken(token, state_tracker)
586      state_tracker.HandleAfterToken(token)
587      token = token.next
588
589    return namespaces_info
590
591  def _GetProvideTokens(self, namespace):
592    """Returns a list of tokens for a goog.require of the given namespace."""
593    line_text = 'goog.require(\'' + namespace + '\');\n'
594    return testutil.TokenizeSource([line_text])
595
596  def _GetRequireTokens(self, namespace):
597    """Returns a list of tokens for a goog.require of the given namespace."""
598    line_text = 'goog.require(\'' + namespace + '\');\n'
599    return testutil.TokenizeSource([line_text])
600
601if __name__ == '__main__':
602  googletest.main()
603