Modify GLOBAL.window semantics and add relevant tests.

This patch removes special handling of the "window" namespace and
introduces a special, internal and undocumented "<window>" namespace.

When "<window>" namespace is used the actual namespace is the
unqualified top scope (which is normally the iframe and not the top
window).

Bug: #9423
Bug-Link: https://github.com/gwtproject/gwt/issues/9423
Change-Id: I57022ab3d832f686775b227d11866b5c6241b2a2
diff --git a/dev/core/src/com/google/gwt/dev/javac/JsInteropUtil.java b/dev/core/src/com/google/gwt/dev/javac/JsInteropUtil.java
index e15b624..454e5f8 100644
--- a/dev/core/src/com/google/gwt/dev/javac/JsInteropUtil.java
+++ b/dev/core/src/com/google/gwt/dev/javac/JsInteropUtil.java
@@ -23,10 +23,14 @@
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JParameter;
 import com.google.gwt.dev.jjs.ast.JPrimitiveType;
+import com.google.gwt.thirdparty.guava.common.base.Joiner;
+import com.google.gwt.thirdparty.guava.common.collect.Lists;
 
 import org.eclipse.jdt.internal.compiler.ast.Annotation;
 import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding;
 
+import java.util.List;
+
 /**
  * Utility functions to interact with JDT classes for JsInterop.
  */
@@ -38,6 +42,26 @@
     return "<global>".equals(jsNamespace);
   }
 
+  public static boolean isWindow(String jsNamespace) {
+    return "<window>".equals(jsNamespace);
+  }
+
+  public static String normalizeQualifier(String qualifier) {
+    assert !qualifier.isEmpty();
+    List<String> components = Lists.newArrayList(qualifier.split("\\."));
+    if (isWindow(components.get(0))) {
+      // Emit unqualified if '<window>' namespace was specified.
+      components.remove(0);
+    } else if (isGlobal(components.get(0))) {
+      // Replace global with $wnd.
+      components.set(0, "$wnd");
+    } else {
+      // still emit $wnd as rest implicitly points to global.
+      components.add(0, "$wnd");
+    }
+    return Joiner.on('.').join(components);
+  }
+
   public static void maybeSetJsInteropProperties(JDeclaredType type, Annotation[] annotations) {
     AnnotationBinding jsType = getInteropAnnotation(annotations, "JsType");
     String namespace = JdtUtil.getAnnotationParameterString(jsType, "namespace");
@@ -129,4 +153,5 @@
   private static AnnotationBinding getInteropAnnotation(Annotation[] annotations, String name) {
     return JdtUtil.getAnnotation(annotations, "jsinterop.annotations." + name);
   }
+
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java
index 511b10f..4071518 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java
@@ -172,9 +172,9 @@
     if (actualJsName.isEmpty()) {
       assert !needsDynamicDispatch();
       return namespace;
-    } else if (JsInteropUtil.isGlobal(namespace)) {
+    } else if (JsInteropUtil.isGlobal(namespace) || JsInteropUtil.isWindow(namespace)) {
       assert !needsDynamicDispatch();
-      return actualJsName;
+      return namespace + "." + actualJsName;
     } else {
       return namespace + (isStatic() ? "." : ".prototype.") + actualJsName;
     }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ClosureJsInteropExportsGenerator.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ClosureJsInteropExportsGenerator.java
index 0ae3aa3..f8174eb 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/ClosureJsInteropExportsGenerator.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ClosureJsInteropExportsGenerator.java
@@ -25,6 +25,7 @@
 import com.google.gwt.dev.js.ast.JsNameRef;
 import com.google.gwt.dev.js.ast.JsStatement;
 import com.google.gwt.dev.js.ast.JsStringLiteral;
+import com.google.gwt.thirdparty.guava.common.base.Joiner;
 
 import java.util.HashSet;
 import java.util.List;
@@ -91,6 +92,7 @@
 
   private void generateExport(String exportNamespace, String qualifiedExportName,
       JsExpression bridgeOrAlias, SourceInfo sourceInfo) {
+    assert !JsInteropUtil.isWindow(exportNamespace);
     // goog.provide("a.b.c")
     ensureGoogProvide(exportNamespace, sourceInfo);
     // a.b.c = a_b_c_obf
@@ -115,7 +117,11 @@
   }
 
   private static JsExpression createExportQualifier(String namespace, SourceInfo sourceInfo) {
-    return JsUtils.createQualifiedNameRef(
-        JsInteropUtil.isGlobal(namespace) ? "window" : namespace, sourceInfo);
+    String components[] = namespace.split("\\.");
+    if (JsInteropUtil.isGlobal(components[0])) {
+      assert components.length != 0;
+      components[0] = null;
+    }
+    return JsUtils.createQualifiedNameRef(Joiner.on(".").skipNulls().join(components), sourceInfo);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/DefaultJsInteropExportsGenerator.java b/dev/core/src/com/google/gwt/dev/jjs/impl/DefaultJsInteropExportsGenerator.java
index 97083fe..8cc5661 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/DefaultJsInteropExportsGenerator.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/DefaultJsInteropExportsGenerator.java
@@ -86,6 +86,7 @@
 
   private void ensureProvideNamespace(JMember member, JsExpression ctor) {
     String namespace = member.getJsNamespace();
+    assert !JsInteropUtil.isWindow(namespace);
     namespace = JsInteropUtil.isGlobal(namespace) ? "" : namespace;
     if (namespace.equals(lastExportedNamespace)) {
       return;
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index 9612aeb..6a47400 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -25,6 +25,7 @@
 import com.google.gwt.dev.PrecompileTaskOptions;
 import com.google.gwt.dev.cfg.PermutationProperties;
 import com.google.gwt.dev.common.InliningMode;
+import com.google.gwt.dev.javac.JsInteropUtil;
 import com.google.gwt.dev.jjs.HasSourceInfo;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.SourceInfo;
@@ -899,7 +900,7 @@
       JsNameRef methodNameRef;
       if (method.isJsNative()) {
         // Construct Constructor.prototype.jsname or Constructor.
-        methodNameRef = createGlobalQualifier(method, sourceInfo);
+        methodNameRef = createGlobalQualifier(method.getQualifiedJsName(), sourceInfo);
       } else if (method.isConstructor()) {
         /*
          * Constructor calls through {@code this} and {@code super} are always dispatched statically
@@ -981,7 +982,7 @@
       JConstructor ctor = newInstance.getTarget();
       JsName ctorName = names.get(ctor);
       JsNameRef  reference = ctor.isJsNative()
-          ? createGlobalQualifier(ctor, sourceInfo)
+          ? createGlobalQualifier(ctor.getQualifiedJsName(), sourceInfo)
           : ctorName.makeRef(sourceInfo);
       List<JsExpression> arguments = transform(newInstance.getArgs());
 
@@ -1106,7 +1107,7 @@
       JMethod method = jsniMethodRef.getTarget();
       if (method.isJsNative()) {
         // Construct Constructor.prototype.jsname or Constructor.
-        return createGlobalQualifier(method, jsniMethodRef.getSourceInfo());
+        return createGlobalQualifier(method.getQualifiedJsName(), jsniMethodRef.getSourceInfo());
       }
       return names.get(method).makeRef(jsniMethodRef.getSourceInfo());
     }
@@ -1721,7 +1722,7 @@
     private JsNameRef createStaticReference(JMember member, SourceInfo sourceInfo) {
       assert member.isStatic();
       return member.isJsNative()
-          ? createGlobalQualifier(member, sourceInfo)
+          ? createGlobalQualifier(member.getQualifiedJsName(), sourceInfo)
           : names.get(member).makeRef(sourceInfo);
     }
 
@@ -3075,24 +3076,7 @@
     return names.get(program.getIndexedField(indexedName));
   }
 
-  private static final String WINDOW = "window";
-
-  private static boolean isWindow(String jsNamespace) {
-    return jsNamespace != null
-        && (WINDOW.equals(jsNamespace) || jsNamespace.startsWith(WINDOW + "."));
-  }
-
-  private static JsNameRef createGlobalQualifier(JMember member, SourceInfo sourceInfo) {
-    if (isWindow(member.getJsNamespace())) {
-      return JsUtils.createQualifiedNameRef(
-          member.getQualifiedJsName().substring(WINDOW.length() + 1), sourceInfo);
-    }
-    return createGlobalQualifier(member.getQualifiedJsName(), sourceInfo);
-  }
-
   private static JsNameRef createGlobalQualifier(String qualifier, SourceInfo sourceInfo) {
-    assert !qualifier.isEmpty();
-
-    return JsUtils.createQualifiedNameRef("$wnd." + qualifier, sourceInfo);
+     return JsUtils.createQualifiedNameRef(JsInteropUtil.normalizeQualifier(qualifier), sourceInfo);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
index a23d114..08cce1c 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
@@ -575,11 +575,17 @@
     }
   }
 
-  private <T extends HasJsName & HasSourceInfo> void checkJsNamespace(T item) {
+  private <T extends HasJsName & HasSourceInfo & CanBeJsNative> void checkJsNamespace(T item) {
     if (JsInteropUtil.isGlobal(item.getJsNamespace())) {
       return;
     }
-    if (item.getJsNamespace().isEmpty()) {
+    if (JsInteropUtil.isWindow(item.getJsNamespace())) {
+      if (item.isJsNative()) {
+        return;
+      }
+      logError(item, "'%s' can only be used as a namespace of native types and members.",
+          item.getJsNamespace());
+    } else if (item.getJsNamespace().isEmpty()) {
       logError(item, "%s cannot have an empty namespace.", getDescription(item));
     } else if (!JsUtils.isValidJsQualifiedName(item.getJsNamespace())) {
       logError(item, "%s has invalid namespace '%s'.", getDescription(item), item.getJsNamespace());
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
index 7c53281..e4363b7 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
@@ -1209,6 +1209,10 @@
         "   @JsMethod(namespace = \"\") public static void o() {}",
         "   @JsProperty(namespace = \"\") public int p;",
         "   @JsMethod(namespace = \"a\") public void q() {}",
+        "}",
+        "@JsType(namespace = \"<window>\") public static class JsTypeOnWindow{",
+        "   @JsProperty(namespace = \"<window>\") public static int r;",
+        "   @JsMethod(namespace = \"<window>\") public static  void s() {}",
         "}");
 
     assertBuggyFails(
@@ -1217,7 +1221,10 @@
         "Line 8: 'int EntryPoint.Buggy.n' has invalid namespace 's^'.",
         "Line 9: 'void EntryPoint.Buggy.o()' cannot have an empty namespace.",
         "Line 10: Instance member 'int EntryPoint.Buggy.p' cannot declare a namespace.",
-        "Line 11: Instance member 'void EntryPoint.Buggy.q()' cannot declare a namespace.");
+        "Line 11: Instance member 'void EntryPoint.Buggy.q()' cannot declare a namespace.",
+        "Line 13: '<window>' can only be used as a namespace of native types and members.",
+        "Line 14: '<window>' can only be used as a namespace of native types and members.",
+        "Line 15: '<window>' can only be used as a namespace of native types and members.");
   }
 
   public void testJsNameGlobalNamespacesSucceeds() throws Exception {
@@ -1232,10 +1239,16 @@
         "   @JsMethod(namespace = JsPackage.GLOBAL, name = \"a.b\") public static native void o();",
         "}",
         "@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = \"a.c\")",
-        "public static class OtherBuggy {",
+        "public static class NativeOnGlobalNamespace {",
         "   @JsMethod(namespace = JsPackage.GLOBAL, name = \"a.d\") static native void o();",
         "   @JsMethod(namespace = JsPackage.GLOBAL, name = \"a.e\") static native void getP();",
         "   @JsProperty(namespace = JsPackage.GLOBAL, name = \"a.f\") public static int n;",
+        "}",
+        "@JsType(isNative = true, namespace = \"<window>\", name = \"a.g\")",
+        "public static class NativeOnWindowNamespace {",
+        "   @JsMethod(namespace = \"<window>\", name = \"a.h\") static native void q();",
+        "   @JsMethod(namespace = \"<window>\", name = \"a.i\") static native void getR();",
+        "   @JsProperty(namespace = \"<window>\", name = \"a.j\") public static int s;",
         "}");
 
     assertBuggySucceeds();
diff --git a/user/super/com/google/gwt/emul/java/lang/String.java b/user/super/com/google/gwt/emul/java/lang/String.java
index b7c9e99..d0e6bdb 100644
--- a/user/super/com/google/gwt/emul/java/lang/String.java
+++ b/user/super/com/google/gwt/emul/java/lang/String.java
@@ -161,7 +161,7 @@
     public native String apply(String thisContext, Object[] argsArray);
   }
 
-  @JsProperty(name = "String.fromCharCode", namespace = "window")
+  @JsProperty(name = "String.fromCharCode", namespace = "<window>")
   private static native NativeFunction getFromCharCodeFunction();
 
   public static String valueOf(char[] x) {
@@ -753,7 +753,7 @@
     return start > 0 || end < length ? substring(start, end) : this;
   }
 
-  @JsType(isNative = true, name = "String", namespace = "window")
+  @JsType(isNative = true, name = "String", namespace = "<window>")
   private static class NativeString {
     public static native String fromCharCode(char x);
     public int length;
diff --git a/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java b/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java
index a40452f..8dc10c7 100644
--- a/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java
+++ b/user/test/com/google/gwt/core/interop/NativeJsTypeTest.java
@@ -25,6 +25,7 @@
 import jsinterop.annotations.JsFunction;
 import jsinterop.annotations.JsOverlay;
 import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsProperty;
 import jsinterop.annotations.JsType;
 
 /**
@@ -436,4 +437,35 @@
     assertEquals(5, NativeClassWithStaticOverlayFields.initializedInt);
     assertNull(NativeClassWithStaticOverlayFields.uninitializedString);
   }
+
+  @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "window")
+  private static class MainWindow {
+    public static Object window;
+  }
+
+  // <window> is a special qualifier that allows referencing the iframe window instead of the main
+  // window.
+  @JsType(isNative = true, namespace = "<window>", name = "window")
+  private static class IFrameWindow {
+    public static Object window;
+  }
+
+  @JsType(isNative = true)
+  private static class AlsoMainWindow {
+    @JsProperty(namespace = JsPackage.GLOBAL)
+    public static Object window;
+  }
+
+  @JsType(isNative = true)
+  private static class AlsoIFrameWindow {
+    @JsProperty(namespace = "<window>")
+    public static Object window;
+  }
+
+  public void testMainWindowIsNotIFrameWindow() {
+    assertSame(IFrameWindow.window, AlsoIFrameWindow.window);
+    assertNotSame(AlsoIFrameWindow.window, AlsoMainWindow.window);
+    assertNotSame(IFrameWindow.window, MainWindow.window);
+    assertSame(MainWindow.window, AlsoMainWindow.window);
+  }
 }