/* * Copyright 2010 The Closure Compiler Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.javascript.jscomp; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.javascript.jscomp.CompilerOptions.AliasTransformation; import com.google.javascript.jscomp.CompilerOptions.AliasTransformationHandler; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.SourcePosition; import java.util.Collection; import java.util.List; import java.util.Map; /** * Tests for {@link ScopedAliases} * * @author robbyw@google.com (Robby Walker) */ public class ScopedAliasesTest extends CompilerTestCase { private static final String GOOG_SCOPE_START_BLOCK = "goog.scope(function() {"; private static final String GOOG_SCOPE_END_BLOCK = "});"; private static String EXTERNS = "var window;"; AliasTransformationHandler transformationHandler = CompilerOptions.NULL_ALIAS_TRANSFORMATION_HANDLER; public ScopedAliasesTest() { super(EXTERNS); } private void testScoped(String code, String expected) { test(GOOG_SCOPE_START_BLOCK + code + GOOG_SCOPE_END_BLOCK, expected); } private void testScopedNoChanges(String aliases, String code) { testScoped(aliases + code, code); } public void testOneLevel() { testScoped("var g = goog;g.dom.createElement(g.dom.TagName.DIV);", "goog.dom.createElement(goog.dom.TagName.DIV);"); } public void testTwoLevel() { testScoped("var d = goog.dom;d.createElement(d.TagName.DIV);", "goog.dom.createElement(goog.dom.TagName.DIV);"); } public void testTransitive() { testScoped("var d = goog.dom;var DIV = d.TagName.DIV;d.createElement(DIV);", "goog.dom.createElement(goog.dom.TagName.DIV);"); } public void testTransitiveInSameVar() { testScoped("var d = goog.dom, DIV = d.TagName.DIV;d.createElement(DIV);", "goog.dom.createElement(goog.dom.TagName.DIV);"); } public void testMultipleTransitive() { testScoped( "var g=goog;var d=g.dom;var t=d.TagName;var DIV=t.DIV;" + "d.createElement(DIV);", "goog.dom.createElement(goog.dom.TagName.DIV);"); } public void testFourLevel() { testScoped("var DIV = goog.dom.TagName.DIV;goog.dom.createElement(DIV);", "goog.dom.createElement(goog.dom.TagName.DIV);"); } public void testWorksInClosures() { testScoped( "var DIV = goog.dom.TagName.DIV;" + "goog.x = function() {goog.dom.createElement(DIV);};", "goog.x = function() {goog.dom.createElement(goog.dom.TagName.DIV);};"); } public void testOverridden() { // Test that the alias doesn't get unaliased when it's overridden by a // parameter. testScopedNoChanges( "var g = goog;", "goog.x = function(g) {g.z()};"); // Same for a local. testScopedNoChanges( "var g = goog;", "goog.x = function() {var g = {}; g.z()};"); } public void testTwoScopes() { test( "goog.scope(function() {var g = goog;g.method()});" + "goog.scope(function() {g.method();});", "goog.method();g.method();"); } public void testTwoSymbolsInTwoScopes() { test( "var goog = {};" + "goog.scope(function() { var g = goog; g.Foo = function() {}; });" + "goog.scope(function() { " + " var Foo = goog.Foo; goog.bar = function() { return new Foo(); };" + "});", "var goog = {};" + "goog.Foo = function() {};" + "goog.bar = function() { return new goog.Foo(); };"); } public void testAliasOfSymbolInGoogScope() { test( "var goog = {};" + "goog.scope(function() {" + " var g = goog;" + " g.Foo = function() {};" + " var Foo = g.Foo;" + " Foo.prototype.bar = function() {};" + "});", "var goog = {}; goog.Foo = function() {};" + "goog.Foo.prototype.bar = function() {};"); } public void testScopedFunctionReturnThis() { test("goog.scope(function() { " + " var g = goog; g.f = function() { return this; };" + "});", "goog.f = function() { return this; };"); } public void testScopedFunctionAssignsToVar() { test("goog.scope(function() { " + " var g = goog; g.f = function(x) { x = 3; return x; };" + "});", "goog.f = function(x) { x = 3; return x; };"); } public void testScopedFunctionThrows() { test("goog.scope(function() { " + " var g = goog; g.f = function() { throw 'error'; };" + "});", "goog.f = function() { throw 'error'; };"); } public void testPropertiesNotChanged() { testScopedNoChanges("var x = goog.dom;", "y.x();"); } public void testShadowedVar() { test("var Popup = {};" + "var OtherPopup = {};" + "goog.scope(function() {" + " var Popup = OtherPopup;" + " Popup.newMethod = function() { return new Popup(); };" + "});", "var Popup = {};" + "var OtherPopup = {};" + "OtherPopup.newMethod = function() { return new OtherPopup(); };"); } public void testShadowedScopedVar() { test("var goog = {};" + "goog.bar = {};" + "goog.scope(function() {" + " var bar = goog.bar;" + // This is bogus, because when the aliases are expanded, goog will // shadow goog.bar. " bar.newMethod = function(goog) { return goog + bar; };" + "});", "var goog={};" + "goog.bar={};" + "goog.bar.newMethod=function(goog$$1){return goog$$1 + goog.bar}"); } public void testShadowedScopedVarTwoScopes() { test("var goog = {};" + "goog.bar = {};" + "goog.scope(function() {" + " var bar = goog.bar;" + " bar.newMethod = function(goog, a) { return bar + a; };" + "});" + "goog.scope(function() {" + " var bar = goog.bar;" + " bar.newMethod2 = function(goog, b) { return bar + b; };" + "});", "var goog={};" + "goog.bar={};" + "goog.bar.newMethod=function(goog$$1, a){return goog.bar + a};" + "goog.bar.newMethod2=function(goog$$1, b){return goog.bar + b};"); } public void testUsingObjectLiteralToEscapeScoping() { // There are many ways to shoot yourself in the foot with goog.scope // and make the compiler generate bad code. We generally don't care. // // We only try to protect against accidental mis-use, not deliberate // mis-use. test( "var goog = {};" + "goog.bar = {};" + "goog.scope(function() {" + " var bar = goog.bar;" + " var baz = goog.bar.baz;" + " goog.foo = function() {" + " goog.bar = {baz: 3};" + " return baz;" + " };" + "});", "var goog = {};" + "goog.bar = {};" + "goog.foo = function(){" + " goog.bar = {baz:3};" + " return goog.bar.baz;" + "};"); } private void testTypes(String aliases, String code) { testScopedNoChanges(aliases, code); verifyTypes(); } private void verifyTypes() { Compiler lastCompiler = getLastCompiler(); new TypeVerifyingPass(lastCompiler).process(lastCompiler.externsRoot, lastCompiler.jsRoot); } public void testJsDocType() { testTypes( "var x = goog.Timer;", "" + "/** @type {x} */ types.actual;" + "/** @type {goog.Timer} */ types.expected;"); } public void testJsDocParameter() { testTypes( "var x = goog.Timer;", "" + "/** @param {x} a */ types.actual;" + "/** @param {goog.Timer} a */ types.expected;"); } public void testJsDocExtends() { testTypes( "var x = goog.Timer;", "" + "/** @extends {x} */ types.actual;" + "/** @extends {goog.Timer} */ types.expected;"); } public void testJsDocImplements() { testTypes( "var x = goog.Timer;", "" + "/** @implements {x} */ types.actual;" + "/** @implements {goog.Timer} */ types.expected;"); } public void testJsDocEnum() { testTypes( "var x = goog.Timer;", "" + "/** @enum {x} */ types.actual;" + "/** @enum {goog.Timer} */ types.expected;"); } public void testJsDocReturn() { testTypes( "var x = goog.Timer;", "" + "/** @return {x} */ types.actual;" + "/** @return {goog.Timer} */ types.expected;"); } public void testJsDocThis() { testTypes( "var x = goog.Timer;", "" + "/** @this {x} */ types.actual;" + "/** @this {goog.Timer} */ types.expected;"); } public void testJsDocThrows() { testTypes( "var x = goog.Timer;", "" + "/** @throws {x} */ types.actual;" + "/** @throws {goog.Timer} */ types.expected;"); } public void testJsDocSubType() { testTypes( "var x = goog.Timer;", "" + "/** @type {x.Enum} */ types.actual;" + "/** @type {goog.Timer.Enum} */ types.expected;"); } public void testJsDocTypedef() { testTypes( "var x = goog.Timer;", "" + "/** @typedef {x} */ types.actual;" + "/** @typedef {goog.Timer} */ types.expected;"); } public void testArrayJsDoc() { testTypes( "var x = goog.Timer;", "" + "/** @type {Array.} */ types.actual;" + "/** @type {Array.} */ types.expected;"); } public void testObjectJsDoc() { testTypes( "var x = goog.Timer;", "" + "/** @type {{someKey: x}} */ types.actual;" + "/** @type {{someKey: goog.Timer}} */ types.expected;"); testTypes( "var x = goog.Timer;", "" + "/** @type {{x: number}} */ types.actual;" + "/** @type {{x: number}} */ types.expected;"); } public void testUnionJsDoc() { testTypes( "var x = goog.Timer;", "" + "/** @type {x|Object} */ types.actual;" + "/** @type {goog.Timer|Object} */ types.expected;"); } public void testFunctionJsDoc() { testTypes( "var x = goog.Timer;", "" + "/** @type {function(x) : void} */ types.actual;" + "/** @type {function(goog.Timer) : void} */ types.expected;"); testTypes( "var x = goog.Timer;", "" + "/** @type {function() : x} */ types.actual;" + "/** @type {function() : goog.Timer} */ types.expected;"); } public void testForwardJsDoc() { testScoped( "/**\n" + " * @constructor\n" + " */\n" + "foo.Foo = function() {};" + "/** @param {Foo.Bar} x */ foo.Foo.actual = function(x) {3};" + "var Foo = foo.Foo;" + "/** @constructor */ Foo.Bar = function() {};" + "/** @param {foo.Foo.Bar} x */ foo.Foo.expected = function(x) {};", "/**\n" + " * @constructor\n" + " */\n" + "foo.Foo = function() {};" + "/** @param {foo.Foo.Bar} x */ foo.Foo.actual = function(x) {3};" + "/** @constructor */ foo.Foo.Bar = function() {};" + "/** @param {foo.Foo.Bar} x */ foo.Foo.expected = function(x) {};"); verifyTypes(); } public void testTestTypes() { try { testTypes( "var x = goog.Timer;", "" + "/** @type {function() : x} */ types.actual;" + "/** @type {function() : wrong.wrong} */ types.expected;"); fail("Test types should fail here."); } catch (AssertionError e) { } } public void testNullType() { testTypes( "var x = goog.Timer;", "/** @param draggable */ types.actual;" + "/** @param draggable */ types.expected;"); } public void testIssue772() { testTypes( "var b = a.b;" + "var c = b.c;", "/** @param {c.MyType} x */ types.actual;" + "/** @param {a.b.c.MyType} x */ types.expected;"); } // TODO(robbyw): What if it's recursive? var goog = goog.dom; // FAILURE CASES private void testFailure(String code, DiagnosticType expectedError) { test(code, null, expectedError); } private void testScopedFailure(String code, DiagnosticType expectedError) { test("goog.scope(function() {" + code + "});", null, expectedError); } public void testScopedThis() { testScopedFailure("this.y = 10;", ScopedAliases.GOOG_SCOPE_REFERENCES_THIS); testScopedFailure("var x = this;", ScopedAliases.GOOG_SCOPE_REFERENCES_THIS); testScopedFailure("fn(this);", ScopedAliases.GOOG_SCOPE_REFERENCES_THIS); } public void testAliasRedefinition() { testScopedFailure("var x = goog.dom; x = goog.events;", ScopedAliases.GOOG_SCOPE_ALIAS_REDEFINED); } public void testAliasNonRedefinition() { test("var y = {}; goog.scope(function() { goog.dom = y; });", "var y = {}; goog.dom = y;"); } public void testScopedReturn() { testScopedFailure("return;", ScopedAliases.GOOG_SCOPE_USES_RETURN); testScopedFailure("var x = goog.dom; return;", ScopedAliases.GOOG_SCOPE_USES_RETURN); } public void testScopedThrow() { testScopedFailure("throw 'error';", ScopedAliases.GOOG_SCOPE_USES_THROW); } public void testUsedImproperly() { testFailure("var x = goog.scope(function() {});", ScopedAliases.GOOG_SCOPE_USED_IMPROPERLY); } public void testBadParameters() { testFailure("goog.scope()", ScopedAliases.GOOG_SCOPE_HAS_BAD_PARAMETERS); testFailure("goog.scope(10)", ScopedAliases.GOOG_SCOPE_HAS_BAD_PARAMETERS); testFailure("goog.scope(function() {}, 10)", ScopedAliases.GOOG_SCOPE_HAS_BAD_PARAMETERS); testFailure("goog.scope(function z() {})", ScopedAliases.GOOG_SCOPE_HAS_BAD_PARAMETERS); testFailure("goog.scope(function(a, b, c) {})", ScopedAliases.GOOG_SCOPE_HAS_BAD_PARAMETERS); } public void testNonAliasLocal() { testScopedFailure("var x = 10", ScopedAliases.GOOG_SCOPE_NON_ALIAS_LOCAL); testScopedFailure("var x = goog.dom + 10", ScopedAliases.GOOG_SCOPE_NON_ALIAS_LOCAL); testScopedFailure("var x = goog['dom']", ScopedAliases.GOOG_SCOPE_NON_ALIAS_LOCAL); testScopedFailure("var x = goog.dom, y = 10", ScopedAliases.GOOG_SCOPE_NON_ALIAS_LOCAL); testScopedFailure("function f() {}", ScopedAliases.GOOG_SCOPE_NON_ALIAS_LOCAL); } // Alias Recording Tests // TODO(tylerg) : update these to EasyMock style tests once available public void testNoGoogScope() { String fullJsCode = "var g = goog;\n g.dom.createElement(g.dom.TagName.DIV);"; TransformationHandlerSpy spy = new TransformationHandlerSpy(); transformationHandler = spy; test(fullJsCode, fullJsCode); assertTrue(spy.observedPositions.isEmpty()); } public void testRecordOneAlias() { String fullJsCode = GOOG_SCOPE_START_BLOCK + "var g = goog;\n g.dom.createElement(g.dom.TagName.DIV);\n" + GOOG_SCOPE_END_BLOCK; String expectedJsCode = "goog.dom.createElement(goog.dom.TagName.DIV);\n"; TransformationHandlerSpy spy = new TransformationHandlerSpy(); transformationHandler = spy; test(fullJsCode, expectedJsCode); assertTrue(spy.observedPositions.containsKey("testcode")); List> positions = spy.observedPositions.get("testcode"); assertEquals(1, positions.size()); verifyAliasTransformationPosition(1, 0, 2, 1, positions.get(0)); assertEquals(1, spy.constructedAliases.size()); AliasSpy aliasSpy = (AliasSpy) spy.constructedAliases.get(0); assertEquals("goog", aliasSpy.observedDefinitions.get("g")); } public void testRecordMultipleAliases() { String fullJsCode = GOOG_SCOPE_START_BLOCK + "var g = goog;\n var b= g.bar;\n var f = goog.something.foo;" + "g.dom.createElement(g.dom.TagName.DIV);\n b.foo();" + GOOG_SCOPE_END_BLOCK; String expectedJsCode = "goog.dom.createElement(goog.dom.TagName.DIV);\n goog.bar.foo();"; TransformationHandlerSpy spy = new TransformationHandlerSpy(); transformationHandler = spy; test(fullJsCode, expectedJsCode); assertTrue(spy.observedPositions.containsKey("testcode")); List> positions = spy.observedPositions.get("testcode"); assertEquals(1, positions.size()); verifyAliasTransformationPosition(1, 0, 3, 1, positions.get(0)); assertEquals(1, spy.constructedAliases.size()); AliasSpy aliasSpy = (AliasSpy) spy.constructedAliases.get(0); assertEquals("goog", aliasSpy.observedDefinitions.get("g")); assertEquals("g.bar", aliasSpy.observedDefinitions.get("b")); assertEquals("goog.something.foo", aliasSpy.observedDefinitions.get("f")); } public void testRecordAliasFromMultipleGoogScope() { String firstGoogScopeBlock = GOOG_SCOPE_START_BLOCK + "\n var g = goog;\n g.dom.createElement(g.dom.TagName.DIV);\n" + GOOG_SCOPE_END_BLOCK; String fullJsCode = firstGoogScopeBlock + "\n\nvar l = abc.def;\n\n" + GOOG_SCOPE_START_BLOCK + "\n var z = namespace.Zoo;\n z.getAnimals(l);\n" + GOOG_SCOPE_END_BLOCK; String expectedJsCode = "goog.dom.createElement(goog.dom.TagName.DIV);\n" + "\n\nvar l = abc.def;\n\n" + "\n namespace.Zoo.getAnimals(l);\n"; TransformationHandlerSpy spy = new TransformationHandlerSpy(); transformationHandler = spy; test(fullJsCode, expectedJsCode); assertTrue(spy.observedPositions.containsKey("testcode")); List> positions = spy.observedPositions.get("testcode"); assertEquals(2, positions.size()); verifyAliasTransformationPosition(1, 0, 6, 0, positions.get(0)); verifyAliasTransformationPosition(8, 0, 11, 4, positions.get(1)); assertEquals(2, spy.constructedAliases.size()); AliasSpy aliasSpy = (AliasSpy) spy.constructedAliases.get(0); assertEquals("goog", aliasSpy.observedDefinitions.get("g")); aliasSpy = (AliasSpy) spy.constructedAliases.get(1); assertEquals("namespace.Zoo", aliasSpy.observedDefinitions.get("z")); } private void verifyAliasTransformationPosition(int startLine, int startChar, int endLine, int endChar, SourcePosition pos) { assertEquals(startLine, pos.getStartLine()); assertEquals(startChar, pos.getPositionOnStartLine()); assertTrue( "expected endline >= " + endLine + ". Found " + pos.getEndLine(), pos.getEndLine() >= endLine); assertTrue("expected endChar >= " + endChar + ". Found " + pos.getPositionOnEndLine(), pos.getPositionOnEndLine() >= endChar); } @Override protected ScopedAliases getProcessor(Compiler compiler) { return new ScopedAliases(compiler, null, transformationHandler); } private static class TransformationHandlerSpy implements AliasTransformationHandler { private final Map>> observedPositions = Maps.newHashMap(); public final List constructedAliases = Lists.newArrayList(); @Override public AliasTransformation logAliasTransformation( String sourceFile, SourcePosition position) { if(!observedPositions.containsKey(sourceFile)) { observedPositions.put(sourceFile, Lists.> newArrayList()); } observedPositions.get(sourceFile).add(position); AliasTransformation spy = new AliasSpy(); constructedAliases.add(spy); return spy; } } private static class AliasSpy implements AliasTransformation { public final Map observedDefinitions = Maps.newHashMap(); @Override public void addAlias(String alias, String definition) { observedDefinitions.put(alias, definition); } } private static class TypeVerifyingPass implements CompilerPass, NodeTraversal.Callback { private final Compiler compiler; private List actualTypes = null; public TypeVerifyingPass(Compiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { JSDocInfo info = n.getJSDocInfo(); if (info != null) { Collection typeNodes = info.getTypeNodes(); if (typeNodes.size() > 0) { if (actualTypes != null) { List expectedTypes = Lists.newArrayList(); for (Node typeNode : info.getTypeNodes()) { expectedTypes.add(typeNode); } assertEquals("Wrong number of JsDoc types", expectedTypes.size(), actualTypes.size()); for (int i = 0; i < expectedTypes.size(); i++) { assertNull( expectedTypes.get(i).checkTreeEquals(actualTypes.get(i))); } } else { actualTypes = Lists.newArrayList(); for (Node typeNode : info.getTypeNodes()) { actualTypes.add(typeNode); } } } } } } }