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