cincheo / jsweet

A Java to JavaScript transpiler.
http://www.jsweet.org
Other
1.45k stars 160 forks source link

Import error when namespace and function have the same name #318

Open negora opened 7 years ago

negora commented 7 years ago

I've found a run-time error when a namespace and a class share the same name. I'm using the 2.0.0-SNAPSHOT version of JSweet.

I've a candy with the following structure:

def
  -> foo
    -> Foo.java
    -> package-info.java 
    -> foo
      -> Bar.java
      -> package-info.java

The file def/foo/package-info.java contains this:

@jsweet.lang.Root
package def.foo;

The file def/foo/foo/package-info.java contains this:

@jsweet.lang.Name (value = "Foo")
@jsweet.lang.Module (value = "foo")
package def.foo.foo;

I've done this so that the package "def.foo.foo" is transpiled as the namespace "Foo", which has the same name that the class "Foo". This way, both, the namespace and class, are exported as a single module. This is the resulting bundle.d.ts:

declare namespace Foo {}
declare namespace Foo {
    export class Bar {}
}
declare class Foo {}
declare module "foo" {
    export = Foo;
}

The result is right. However, the problem appears when I use this candy in a class of another JSweet project, and I enable the creation of AMD modules. This is the resulting TypeScript of the class (abbreviated):

import foo = require("foo"); 
import Bar = foo.Bar;
...
  new Foo();
  new Bar();

The Bar class is correctly imported. However, the Foo class isn't imported. The tsc compiler doesn't complain, because the bundle.d.ts file generated by JSweet first declares the "Foo" class in the global scope, before exporting it with the module. So tsc recognises it as an existing class, although it's not imported. But anyway, the code fails at run-time, when invoking new Foo();.

I guess that, instead, the JSweet transpiler should create something like this:

import foo = require("foo"); 
import Foo = foo; // <-- NEW IMPORT
import Bar = foo.Bar;
...
  new Foo();
  new Bar();

A real life example is the Pikaday library. This JavaScript library declares a class called Pikaday that, at the same time, is used as a namespace for the PikadayOptions and PikadayI18nConfig classes.

Thank you!

negora commented 7 years ago

I've been thinking about this issue and the issue #319 thoroughly, and I've came to the conclusion there are 2 different situations that cause import errors:

  1. Importing a module that represents a namespace and contains a class/function with the same name that the namespace. Example: namespace foo and function foo.foo. This belongs to the issue #318 (this issue).

  2. Importing a module that represents, at the same time, a namespace and a class/function. Example: namespace foo and function foo. This belongs to the issue #319.

In the first case, to avoid a conflict of names, the right import generated by JSweet could be something like this:

import foo_ns = require ("foo");
import foo = foo_ns.foo; // This avoids causing a circular "import foo = foo.foo;"
import bar = foo_ns.bar;
...
foo ();

And, in the second case:

import foo_ns = require ("foo");
import foo = foo_ns; // An extra import, to clarify that "foo" also exports a function/class.
import bar = foo_ns.bar;
...
foo ();

The namespace foo_ns would be generated combining the original name of the namespace (not the package name, as it happens nowadays), plus the "_ns" suffix.

In the last case, I know that the code could just import the module as foo and use it directly as a function, without that extra import. But I believe that using this nomenclature could make things more homogeneous and avoid some confusion.

In other words, JSweet could treat all the imports made with require() as namespaces. And then, depending on whether the namespace also represents a class/function or not, it would add that extra import or not.

negora commented 7 years ago

Just today I've found another bug under the same conditions: when the module exports a namespace and a class that have the same name, and both are in the same level of the package tree. But this bug happens only if you import the class alone, without any class from the namespace. In that case, no require() statement is generated by the transpiler.

Why does it happen? I believe that it's because the @jsweet.lang.Module annotation is declared in the package that represents the namespace, but the class is out of it. So, when the transpiler checks the package where the class is located, it doesn't find any @Module annotation, and doesn't generate the require() statement.

There is an ugly workaround, which consists in copying the @Module annotation from the package-info.java that represents the namespace to the package-info.java that contains the class, which is the one marked with @jsweet.lang.Root. That way, when the transpiler scans the package of the class, it will find the annotation and will add the require() statement. I said "ugly workaround" because the root package is not the module and shouldn't be annotated like that. But... It's the only way of making it work.

Thank you!

renaudpawlak commented 7 years ago

It looks like your issues would probably require to re-think how JSweet handles modules... maybe there is a general solution, or maybe there are limitations and we can live this it :) Definitely not the right time for version 2.0.0.

negora commented 7 years ago

I can manage with it for now ;) . Anyway, if I faced a new problem, I think it wouldn't be traumatic to switch to bundles instead of modules. Thank you.