Giter VIP home page Giter VIP logo

jsinterop-generator's Introduction

JsInterop Generator · Build Status

The jsinterop generator is a java program that takes closure extern files as input and generates Java classes annotated with JsInterop annotations.

This project is used for building Elemental2. Any other uses are experimental. You can use it to generate java APIs for other javascript libraries but we don't provide any official support. Feel free to open issues on the github issue tracker, though.

Run with Bazel

If your project uses Bazel. You can use jsinterop_generator rule to generate java code.

You need to add this repository as an external dependency in your WORKSPACE file

new_http_archive(
  name = "com_google_jsinterop_generator",
  url="https://github.com/google/jsinterop-generator/archive/master.zip",
  strip_prefix="jsinterop-generator-master",
)

and then define a jsinterop_generator target in your 'BUILD' file

load("@com_google_jsinterop_generator//:jsinterop_generator.bzl", "jsinterop_generator")

jsinterop_generator(
    name = "my_thirdparty_lib",
    srcs = ["my_externs.js"],
)

You can now directly depend on :my_thirdparty_lib target in your java_library rules or build the jar files with bazel build //path/to/your/BUILD/file/directory:my_thirdparty_lib. The jar files with the generated source will created in bazel-bin/path/to/your/BUILD/file/directory.

Run as a standalone java program

Build the generator from source

    $ npm install -g @bazel/bazelisk
    $ alias bazel=bazelisk
  • Clone this git repository:
    $ git clone https://github.com/google/jsinterop-generator.git
  • Build the binary:
        $ cd jsinterop-generator
        $ bazel build //java/jsinterop/generator/closure:ClosureJsinteropGenerator_deploy.jar

The generated jar file can be found at bazel-bin/java/jsinterop/generator/closure/ClosureJsinteropGenerator_deploy.jar

Or download the generator

TODO(dramaix): provides link to download the generator.

Run the generator

Now you have the jar file, just invoke

java -jar /path/to/ClosureJsinteropGenerator_deploy.jar [options] externs_file...

List of required options :

Option Meaning
--output file Path to the jar file that will contain the generated java classes
--output_dependency_file file Path to the dependency file generated by the generator.
--package_prefix string Prefix used when we build the java package
--extension_type_prefix string Value used for prefixing extension types. It's a good practice to pass the name of the library in upper camel case.

Contributing

Please refer to the contributing document.

Licensing

Please refer to the license file.

Disclaimer

This is not an official Google product.

jsinterop-generator's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jsinterop-generator's Issues

`@record` types with no members have no create() method

Steps to reproduce, create a record type with no members.

/**
 * @record
 */
function MyRecord() {}

Expected, a Java interface with a static create() method. Actual output is a Java interface with no contents.

--

While this might seem like a weird edge case, there are at least two ways that this comes up. First, when tsickle generates externs from typescript where a type is re-exported in another file, tsickle will generate a new type that extends the existing type. Second, a record type that extends from two other record types, but doesn't contribute new fields.

/**
 * @record
 */
function ParentRecordA() {}

/**
 * @type {string}
 */
ParentRecordA.prototype.name;

/**
 * @record
 */
function ParentRecordB() {}

/**
 * @type {number}
 */
ParentRecordB.prototype.value;

/**
 * @record
 * @extends {ParentRecordA}
 * @extends {ParentRecordB}
 */
function ChildRecord() {}

Expected Java output,

@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public interface ChildRecord extends ParentRecordA, ParentRecordB {
  @JsOverlay
  static ChildRecord create() {
    return Js.uncheckedCast(JsPropertyMap.of());
  }
}

Actual Java output

@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public interface ChildRecord extends ParentRecordA, ParentRecordB { }

An easy workaround is to just create an instance of a supertype, or just a plain JsObject.create(null) or JsPropertyMap.of(), then wrap in a cast as the usual implementation does. But this isn't expected to be the correct way, since all other records get a generated factory method.

Add nullability annotations on generated code

Closure externs already have the concept of nullable and non-nullable values. Some types are implicitly nullable and some are implicitly nonnull although these can be explicitly overridden. See Types-in-the-Closure-Type-System for further details.

When translating the externs into java code this nullability is not modelled in the java code. There seems to be @JsNonNull and @JsNullable annotations that may be able to be used. Another possibility is to use @javax.annotation.Nonnull and @javax.annotation.Nullable which would integrate much better with existing java tools. Alternatively they could both be applied where appropriate.

Adding the javax.annotation annotation almost instantly improves the experience when editing in an IDE like IDEA which will highlight where the developer has failed to check for null or conversely, checks for null without cause.

The one disadvantage is that if the upstream closure compiler externs are incorrectly annotated (and some of them are), it will confuse the developer and it may force us to fix the upstream externs.

In the future I plan to tackle this issue but no set timeline

Usage of deprecated union operator

Builds of elemental2 fail with Bazel 0.26.0 due to the usage of the deprecated "+" operator on depsets (see tracking issue bazelbuild/bazel#5817):

ERROR: /(...)/external/com_google_elemental2/java/elemental2/core/BUILD:38:1: in _jsinterop_generator rule @com_google_elemental2//java/elemental2/core:core__internal_src_generated: 
Traceback (most recent call last):
        File "/(...)/external/com_google_elemental2/java/elemental2/core/BUILD", line 38
                _jsinterop_generator(name = 'core__internal_src_generated')
        File "/(...)/external/com_google_jsinterop_generator/jsinterop_generator.bzl", line 124, in _jsinterop_generator_impl
                _get_generator_files(ctx.attr.deps)
        File "/(...)/external/com_google_jsinterop_generator/jsinterop_generator.bzl", line 49, in _get_generator_files
                transitive_srcs += target_provider.transitive_sources
`+` operator on a depset is forbidden. See https://docs.bazel.build/versions/master/skylark/depsets.html for recommendations. Use --incompatible_depset_union=false to temporarily disable this check.

Annotate elements with annotation or @since tag

In a lot of cases it would be desirable to document when an API became available. It would be great if this somehow made it through to generated java code. JsDoc has a @since tag that is roughly equivalent to javadocs @since tag. This is primitive but may work and we would need to figure out a way to represent versions either as a combination of @since spec-version or @since browser-version or some combo.

We could use this but as this is not present in closure compilers externs we may need to either add it to the externs (which seems unlikely to be accepted upstream) or use some sort of mechanism to walk the AST and add @since based on some rules (i.e. everything in w3c_dom1.js has @since dom.1. However even this may be too laborious and/or error prone.

A better approach may be to actually define a java annotation that can be used to annotated each method/field to indicate which browsers support method and what version ranges. i.e.
@SupportedBy({ @Browser(name = "Chrome", version="66", removed="74"), @Browser(name = "Firefox", version="47"), ... }). This could be built from the canisue database. This seems to be the "best" alternative but it is unclear how easy it would be to align the names of symbols.

The annotation based approach would also allow people to write annotation processors that verified that they were not using APIs they did not intend to.

Union types unnecessarily Autobox primitive values

It seems that the union types do not get optimally generated when passed as parameters to methods and one of the union types is a primitive value. Consider the type

/**
 * @param {string} type
 * @param {EventListener|function(!Event):*} listener
 * @param {(boolean|!AddEventListenerOptions)=} opt_options
 * @return {undefined}
 * @see https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener
 */
EventTarget.prototype.addEventListener = function(type, listener, opt_options) {

This will generate

  void addEventListener(
      String type, EventListener listener, EventTarget.AddEventListenerOptionsUnionType options);

  @JsOverlay
  default void addEventListener(String type, EventListener listener, boolean options) {
    addEventListener(
        type, listener, Js.<EventTarget.AddEventListenerOptionsUnionType>uncheckedCast(options));
  }
...

This means that code such as

DomGlobal.window.addEventListener( "hashchange", _listener, false )

will optimize in GWT2.x to something like a.addEventListener(b,c,($g(),d?true:false)) where g$ is the Boolean.<clinit>() call. which is of course less optimal than a.addEventListener(b,c,d).

A possible solution may be to add @DonNotAutobox to the union parameter although this solution has yet to be investigated.

Operating on non-externs?

I'm experimenting with taking Closure-annotated JS (both hand-written and generated from TypeScript via tsickle). I was surprised to discover that when the generator runs, it only emits jsinterop types that represent the provided externs - "actual" JS gets no java generated for it.

As an example, take a j2cl-based project with closure-compatible JS libraries that need to be used/consumed from Java. Feeding non-@extern js results in no output (and a chmod error in format_jarsrc), probably because jsinterop.generator.closure.ClosureJsInteropGenerator#generateJarFile removes non-extern files - removing that check results in the tool trying to also generate java for dependencies (like elemental2-core+promise, which effectively every library must depend on).

--

Going a bit further: It appears from poking around in the repo that I might just be thinking about this wrong or using the tool wrong, and that this isn't a real limitation. For example, DependencyFileWriter specifically calls out TypeScript rather than closure externs?

This file makes the link between typescript type and java type and is used when a generated library is used as dependency of another generated library.

Are typescript .d.ts files intended to be supported, specifically those generated via tsickle? The sample I tried nearly works, at least with this one addition to jsinterop.generator.closure.ClosureJsInteropGenerator#createCompilerOptions:

options.setLanguageIn(ECMASCRIPT6_TYPED);

but even then the library I'm working with hits errors, whether operating tsc-generated .d.ts files, or ones emitted by tsickle (and for the record, I've been unable to get tsickle yet to emit actual externs, it only wants to provide either .mjs files with an empty externs.js, or .js+.d.ts. Perhaps it works better if you avoid bazel, but my goal was to stay within the ecosystem).

--

Are these use cases intended to be supported, or am I just doing it wrong?

Giving a class both a static and instance members with a matching union type causes invalid java to be generated

Given two members on a class, one static and one instance, one will be generated with a _STATIC suffix. That is,

Foo.noArgMethod = function() {};
Foo.prototype.noArgMethod = function() {};

will cause this to be generated:

  @JsMethod(name = "noArgMethod")
  public static native Object noArgMethod_STATIC();

  public native Object noArgMethod();

(removed an incorrect statement, will follow up separately)

However, if both of these have a parameter which is unioned, then the generated *UnionType will be emitted twice, as the names are not distinct - unlike the actual member name, the union type's name is only based on the method name and argument name.

Additionally, once a native method is generated for this, two or more @JsOverlay methods will be generated as well, and when those are renamed to append _STATIC to them, a @JsMethod/@JsProperty annotation will also be decorated on them, which will cause a compiler error in J2CL.

Example js with a method:

/**
 * @param {string|number} arg
 */
Foo.unionArgMethod = function(arg) {};
/**
 * @param {string|number} arg
 */
Foo.prototype.unionArgMethod = function(arg) {};

Resulting java:

  @JsType(isNative = true, name = "?", namespace = JsPackage.GLOBAL)
  public interface UnionArgMethodArgUnionType {
    @JsOverlay
    static Foo.UnionArgMethodArgUnionType of(Object o) {
      return Js.cast(o);
    }

    @JsOverlay
    default double asDouble() {
      return Js.asDouble(this);
    }

    @JsOverlay
    default String asString() {
      return Js.asString(this);
    }

    @JsOverlay
    default boolean isDouble() {
      return (Object) this instanceof Double;
    }

    @JsOverlay
    default boolean isString() {
      return (Object) this instanceof String;
    }
  }

  @JsType(isNative = true, name = "?", namespace = JsPackage.GLOBAL)
  public interface UnionArgMethodArgUnionType {
    @JsOverlay
    static Foo.UnionArgMethodArgUnionType of(Object o) {
      return Js.cast(o);
    }

    @JsOverlay
    default double asDouble() {
      return Js.asDouble(this);
    }

    @JsOverlay
    default String asString() {
      return Js.asString(this);
    }

    @JsOverlay
    default boolean isDouble() {
      return (Object) this instanceof Double;
    }

    @JsOverlay
    default boolean isString() {
      return (Object) this instanceof String;
    }
  }

  @JsOverlay
  @JsMethod(name = "unionArgMethod")
  public static final Object unionArgMethod_STATIC(String arg) {
    return unionArgMethod(Js.<Foo.UnionArgMethodArgUnionType>uncheckedCast(arg));
  }

  @JsMethod(name = "unionArgMethod")
  public static native Object unionArgMethod_STATIC(Foo.UnionArgMethodArgUnionType arg);

  @JsOverlay
  @JsMethod(name = "unionArgMethod")
  public static final Object unionArgMethod_STATIC(double arg) {
    return unionArgMethod(Js.<Foo.UnionArgMethodArgUnionType>uncheckedCast(arg));
  }

  @JsOverlay
  public final Object unionArgMethod(String arg) {
    return unionArgMethod(Js.<Foo.UnionArgMethodArgUnionType>uncheckedCast(arg));
  }

  public native Object unionArgMethod(Foo.UnionArgMethodArgUnionType arg);

  @JsOverlay
  public final Object unionArgMethod(double arg) {
    return unionArgMethod(Js.<Foo.UnionArgMethodArgUnionType>uncheckedCast(arg));
  }

Example JS with a property:

/**
 * @type {string|number}
 */
Foo.unionProperty;
/**
 * @type {string|number}
 */
Foo.prototype.unionProperty;

Resulting Java:

  @JsType(isNative = true, name = "?", namespace = JsPackage.GLOBAL)
  public interface UnionPropertyUnionType {
    @JsOverlay
    static Foo.UnionPropertyUnionType of(Object o) {
      return Js.cast(o);
    }

    @JsOverlay
    default double asDouble() {
      return Js.asDouble(this);
    }

    @JsOverlay
    default String asString() {
      return Js.asString(this);
    }

    @JsOverlay
    default boolean isDouble() {
      return (Object) this instanceof Double;
    }

    @JsOverlay
    default boolean isString() {
      return (Object) this instanceof String;
    }
  }

  @JsType(isNative = true, name = "?", namespace = JsPackage.GLOBAL)
  public interface UnionPropertyUnionType {
    @JsOverlay
    static Foo.UnionPropertyUnionType of(Object o) {
      return Js.cast(o);
    }

    @JsOverlay
    default double asDouble() {
      return Js.asDouble(this);
    }

    @JsOverlay
    default String asString() {
      return Js.asString(this);
    }

    @JsOverlay
    default boolean isDouble() {
      return (Object) this instanceof Double;
    }

    @JsOverlay
    default boolean isString() {
      return (Object) this instanceof String;
    }
  }

  public static Foo.UnionPropertyUnionType unionProperty;

There is also a separate correctness issue which I'll file separately - in some cases, there is only one unionProperty, the static variant, as the non-static member is failed to be emitted.

Error when @override method takes a type union as a parameter

As part of an attempt to whittle down the remaining errors in google/elemental2 builds I tried to define the standardized Document.prototype.write in an extern accessible via Elemental2.

The extern looks something like:

/**
 * @param {!TrustedHTML|string} text
 * @return {undefined}
 * @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-write
 */
Document.prototype.write = function(text) {};

This is overridden in w3c_dom2.js with

/**
 * @param {!TrustedHTML|string} text
 * @return {undefined}
 * @see http://www.w3.org/TR/2000/CR-DOM-Level-2-20000510/html.html#ID-75233634
 * @override
 */
HTMLDocument.prototype.write = function(text) {};

Unfortunately jsinterop-generator generates the overlay methods for union types in Document class like

  @JsOverlay
  public final void write(TrustedHTML text) {
    write(Js.<Document.WriteTextUnionType>uncheckedCast(text));
  }

and attempts to define a similar but different overlay in HTMLDocument like

  @JsOverlay
  public final void write(TrustedHTML text) {
    write(Js.<HTMLDocument.WriteTextUnionType>uncheckedCast(text));
  }

There does not seem to get the overriding method to use the union type and thus the overlay methods declared by the overridden method. Thus a compile error.

Local JDK dependency warnings when building with elemental2

Hello esteemed Googlers,

I get these warnings when building with elemental2, and I was wondering if there is anything I can do to fix it? I would be happy to contribute a PR, but I'm having trouble tracking down where the dependency is. I've looked in elemental2 and jsinterop-generator but with no luck yet.

WARNING: /private/.../external/bazel_tools/tools/jdk/BUILD:116:1: in alias rule @bazel_tools//tools/jdk:jar: target '@bazel_tools//tools/jdk:jar' depends on deprecated target '@local_jdk//:jar': Don't depend on targets in the JDK workspace; use @bazel_tools//tools/jdk:current_java_runtime instead (see https://github.com/bazelbuild/bazel/issues/5594)

WARNING: /private/.../external/com_google_jsinterop_generator/third_party/BUILD:62:1: in alias rule @com_google_jsinterop_generator//third_party:jar: target '@com_google_jsinterop_generator//third_party:jar' depends on deprecated target '@local_jdk//:jar': Don't depend on targets in the JDK workspace; use @bazel_tools//tools/jdk:current_java_runtime instead (see https://github.com/bazelbuild/bazel/issues/5594)

WARNING: /private/.../external/com_google_elemental2/java/elemental2/core/BUILD:38:1: in _jsinterop_generator rule @com_google_elemental2//java/elemental2/core:core__internal_src_generated: target '@com_google_elemental2//java/elemental2/core:core__internal_src_generated' depends on deprecated target '@local_jdk//:jar': Don't depend on targets in the JDK workspace; use @bazel_tools//tools/jdk:current_java_runtime instead (see https://github.com/bazelbuild/bazel/issues/5594)

WARNING: /.../external/com_google_elemental2/java/elemental2/dom/BUILD:95:1: in _jsinterop_generator rule @com_google_elemental2//java/elemental2/dom:dom__internal_src_generated: target '@com_google_elemental2//java/elemental2/dom:dom__internal_src_generated' depends on deprecated target '@local_jdk//:jar': Don't depend on targets in the JDK workspace; use @bazel_tools//tools/jdk:current_java_runtime instead (see https://github.com/bazelbuild/bazel/issues/5594)

I'm not quite sure this is the right place to file this issue, because the warnings mention a number of repositories. Local Bazel version:

Build label: 0.24.1
Build target: bazel-out/darwin-opt/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar
Build time: Tue Apr 2 16:32:47 2019 (1554222767)
Build timestamp: 1554222767
Build timestamp as int: 1554222767

Infinite loop when transpiling to Java html5.js from Closure Compiler contribs.

Hello,

We are working on providing to the GWT community Java bindings (Java source code) to many standard Javascript libraries: see https://github.com/didier-durand/JSClosure2Interop
This work is based on this JSInterop Generator and Javscript contribs to the Closure Compiler project (replicated wih identical structure in js directory)

So, in the gen directory, you will see that successful transpiling back to Java for various small Javascript is achieved : GeoJson, Async-2.0, es3, etc.

But, our final targets is bigger libraries like Google Maps (prio 1), jQuery, Ace, Angular, etc. For those, the list of dependency is bigger.
For example, you will see that Google Maps requires 27 dependencies (see src/test/java/oss/jsinterop/cl2jsi/test/TestConverterGmaps.java)

As they use many browser features, those large libraries end up (see src/test/java/oss/jsinterop/cl2jsi/test/TestConverterFailure.java) all involving html5.js to solve their dependencies.

html5.js (see /js/google-closure-compiler/externs/browser/html5.js) comes from Google Closure Compiler contribs.

At this point in time, when its isolated transpiling is tried, JSInterop Genarator seems to go into an infinite loop (please, run src/test/java/oss/jsinterop/cl2jsi/test/TestConverterHtml5.java - The jar of Generator is embarked, so it should run).

Same thing with the transpiling of all large libraries requiring it: when we introduce html5.js to complete the needs, the transpiler loops forever. Its Jar was created from master of March, 21st (commit: e68a27e)

The debug mode of the Generator doesn't say anything user about this loop.

Is the error on our side?

Thanks!

Didier

@typedef on a property fails to build

Example JS, as put in the externs/modules/modules.js file:

/**
 * @typedef {(string|number|boolean)}
 */
baz.structural;

Results in this stack trace:

Exception in thread "main" java.lang.UnsupportedOperationException
        at com.google.javascript.rhino.jstype.Property.getScope(Property.java:185)
        at jsinterop.generator.closure.visitor.AbstractClosureVisitor.acceptTypedef(AbstractClosureVisitor.java:175)
        at jsinterop.generator.closure.visitor.AbstractClosureVisitor.accept(AbstractClosureVisitor.java:86)
        at jsinterop.generator.closure.visitor.AbstractClosureVisitor.lambda$acceptProperties$1(AbstractClosureVisitor.java:247)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
        at java.base/java.util.TreeMap$KeySpliterator.forEachRemaining(TreeMap.java:2739)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
        at jsinterop.generator.closure.visitor.AbstractClosureVisitor.acceptProperties(AbstractClosureVisitor.java:244)
        at jsinterop.generator.closure.visitor.AbstractClosureVisitor.acceptModule(AbstractClosureVisitor.java:195)
        at jsinterop.generator.closure.visitor.AbstractClosureVisitor.accept(AbstractClosureVisitor.java:84)
        at jsinterop.generator.closure.visitor.AbstractClosureVisitor.accept(AbstractClosureVisitor.java:64)
        at jsinterop.generator.closure.visitor.TypeCollector.accept(TypeCollector.java:41)
        at jsinterop.generator.closure.ClosureJsInteropGenerator.generateJavaProgram(ClosureJsInteropGenerator.java:170)
        at jsinterop.generator.closure.ClosureJsInteropGenerator.convert(ClosureJsInteropGenerator.java:68)

This is because the StaticTypedSlot instance passed here to AbstractClosureVisitor.acceptTypedef(...) is a closure Property, which throws when it is asked for its scope:
https://github.com/google/closure-compiler/blob/00ad3658b30221987fbbfe4a59db7e298d68cd72/src/com/google/javascript/rhino/jstype/Property.java#L183-L186

  @Override
  public StaticTypedScope getScope() {
    throw new UnsupportedOperationException();
  }

My workaround for now is to just remove these typedefs and hope they don't matter to the eventual compilation, but they are hard to avoid:

I explored options for a fix briefly, but I'm struggling to find examples in closure-compiler which show how to read a Property to see its type, without either just asking Property.getType() or having a valid "scope" for it.

Broken build: Closure's StaticTypedSlot has no method getScope()

ERROR: /Users/colin/workspace/jsinterop-generator/java/jsinterop/generator/closure/visitor/BUILD:11:1: Building java/jsinterop/generator/closure/visitor/libvisitor.jar (7 source files) failed (Exit 1).
java/jsinterop/generator/closure/visitor/AbstractClosureVisitor.java:177: error: cannot find symbol
        typedef.getScope(), typedef.getName()));
               ^
  symbol:   method getScope()
  location: variable typedef of type StaticTypedSlot<JSType>

Likely WORKSPACE needs to be updated to indicate the minimum required version of com_google_closure_compiler.

Emit constant values from externs

So some of closures externs have values for properties marked with @const. i.e. WebSocket. CONNECTING.

Currently jsinterop-generator just assumes the values will be external and must be extracted at runtime. However, given that most of these values are constant and specified by specs, they are unlikely to be changed and could be directly imported into java code. i.e. rather than:

@JsOverlay public static final int CONNECTING = WebSocket__Constants.CONNECTING;

we could generate

@JsOverlay public static final int CONNECTING = 0;

In some scenarios this can lead to a slight code size improvement. However a better reason is that it improves the developer experience to see the values inline.

It seems that on the closure team there is mixed opinions on whether including constant values is a good thing or a bad thing. Sometimes PRs get merged adding them and sometimes not ;) The main reason for not having them is articulated in google/closure-compiler#3293 as

Any compiler that is consuming externs files with values, and changing behavior with respect to whether the externs has values or not is in my opinion showcasing a bug of it's own

Before I ran into this I had already started to play with importing the values into the generator (at least integer and double values). It was an ugly hack that mostly looked something like

      final Node next = jsField.getDeclaration().getNode().getNext();
      final Double value = null != next && next.isNumber() ? next.getDouble() : null;

With a little more sanity checking and type conversion.

However I was considering cleaning this up and submitting it if it would be accepted. At this stage it is unclear whether the closure team will allow values to be added to the externs or not. If they allow them, then it will be simple to read them.

If they decided to strip them, it will be relatively easy to retrieve the constant values from the WebIDL. I have been playing with data from Microsofts Typescript schema generator and think that would make things relatively easy.

Add javadoc descriptions to generated types

Documentation can be combined from multiple sources. Probably the best option atm is to gather type documentation from apiDescriptions.json as most of the hard work is done and it seems to be kept uptodate.

The per function and property documentation could be grabbed from either:

Allowing a generated project to depend on another generated project (like elemental2)

Technically I did get this to work, but it seems awkward, and so I am probably using jsinterop-generator or bazel wrong, or both.

I have a new git repo of a bazel project using jsinterop-generator to turn some closure-compiler provided externs into jsinterop Java. As with any set of externs I can imagine, this depends on some elemental2 content - both core and dom - so I wrote the jsinterop_generator target like this:

jsinterop_generator(
    name = "googlemaps",
    srcs = [":google_maps_api_patched.js"],
    extension_type_prefix = "Maps",
    integer_entities_files = ["integer_entities.txt"],
    # override auto generated js_deps in order not to provide extern files
    # Common extern file are included by default.
    j2cl_js_deps = [],
    deps = [
        "//third_party:elemental2-core",
        "//third_party:elemental2-dom",
    ],
)

In keeping with bazel conventions, I created an alias for elemental2-core and dom in third_party/, which reference the targets that elemental2 provides:

alias(
    name = "elemental2-core",
    actual = "@com_google_elemental2//:elemental2-core",
)
alias(
    name = "elemental2-dom",
    actual = "@com_google_elemental2//:elemental2-dom",
)

This however was not sufficient. In addition to those two targets, these four are also required in that same BUILD file, and would generally be emitted automatically a jsinterop_generator invocation.

alias(
    name = "elemental2-core-j2cl",
    actual = "@com_google_elemental2//:elemental2-core-j2cl",
)
alias(
    name = "elemental2-dom-j2cl",
    actual = "@com_google_elemental2//:elemental2-dom-j2cl",
)
alias(
    name = "elemental2-core__internal_src_generated",
    actual = "@com_google_elemental2//:elemental2-core__internal_src_generated",
)
alias(
    name = "elemental2-dom__internal_src_generated",
    actual = "@com_google_elemental2//:elemental2-dom__internal_src_generated",
)

The only other thought I had to make these work cleanly was to avoid alias() and instead use jsinterop_generator() in third party:

jsinterop_generator(
    name = "elemental2-core",
    exports = ["@com_google_elemental2//:elemental2-core"],
)
jsinterop_generator(
    name = "elemental2-dom",
    exports = ["@com_google_elemental2//:elemental2-dom"],
)

But that seems a bit silly, and doesn't appear to work (jsinterop_generator perhaps is concating labels assuming that targets are local?):

ERROR: /home/colin/workspace/gwt-googlemaps-api/third_party/BUILD:19:1: //third_party:elemental2-core__internal_src_generated: invalid label '//third_party/@com_google_elemental2//:elemental2-core__internal_src_generated' in element 0 of attribute 'exports' in '_jsinterop_generator_export' rule: invalid package name 'third_party/@com_google_elemental2//': package names may not end with '/'
ERROR: /home/colin/workspace/gwt-googlemaps-api/third_party/BUILD:19:1: //third_party:elemental2-core-j2cl: invalid label '//third_party/@com_google_elemental2//:elemental2-core-j2cl' in element 0 of attribute 'exports' in 'j2cl_library' rule: invalid package name 'third_party/@com_google_elemental2//': package names may not end with '/'
ERROR: /home/colin/workspace/gwt-googlemaps-api/third_party/BUILD:19:1: //third_party:elemental2-core: invalid label '//third_party/@com_google_elemental2//:elemental2-core' in element 0 of attribute 'exports' in 'java_library' rule: invalid package name 'third_party/@com_google_elemental2//': package names may not end with '/'
ERROR: /home/colin/workspace/gwt-googlemaps-api/third_party/BUILD:23:1: //third_party:elemental2-dom__internal_src_generated: invalid label '//third_party/@com_google_elemental2//:elemental2-dom__internal_src_generated' in element 0 of attribute 'exports' in '_jsinterop_generator_export' rule: invalid package name 'third_party/@com_google_elemental2//': package names may not end with '/'
ERROR: /home/colin/workspace/gwt-googlemaps-api/third_party/BUILD:23:1: //third_party:elemental2-dom-j2cl: invalid label '//third_party/@com_google_elemental2//:elemental2-dom-j2cl' in element 0 of attribute 'exports' in 'j2cl_library' rule: invalid package name 'third_party/@com_google_elemental2//': package names may not end with '/'
ERROR: /home/colin/workspace/gwt-googlemaps-api/third_party/BUILD:23:1: //third_party:elemental2-dom: invalid label '//third_party/@com_google_elemental2//:elemental2-dom' in element 0 of attribute 'exports' in 'java_library' rule: invalid package name 'third_party/@com_google_elemental2//': package names may not end with '/'
ERROR: error loading package 'third_party': Package 'third_party' contains errors

What is the expected way to invoke jsinterop_generator() in a bazel project, without just forking elemental2 and including its own setup directly in our projects?

Full project is available at https://github.com/Vertispan/gwt-googlemaps-api, but please note that until google/elemental2#137 is resolved, this repo will not build as-is, you need a local_repository of elemental2 with the latest commit reverted.

@JsEnum types cannot be used in a union

Steps to repro - use an externs file with an enum that is referenced by a union type. Roughly:

/**
 * @enum {number}
 */
foo.bar.SomeEnum = {
  ONE: 0,
  TWO: 1,
  THREE: 2,
  FIVE: 3,
  SEVEN: 4
};

/**
 * @type {foo.bar.SomeEnum|string}
 */
foo.bar.ObjectWithProperties.prototype.thing;

The error is when j2cl tries to transpile the sources into js, it is reported that the union type is attempting an instanceof on the enum instead of just treating it like a number (which in this case is all it is).

Easy workaround: rewrite ObjectWithProperties.thing's type to be {number|string}, though that defeats the purpose a bit.

Proposed fix (not having given it great thought): The union type should instanceof based on the type label: in this case, number. This seems to be a bit difficult to implement though, since the original @enum might be already made into a java type? The jsinterop.generator.model.Type instances don't keep track of the type label at this time, so that would need to be added? It could potentially also be helpful in general, j2cl itself could implement $isInstance with this knowledge if it were part of the @JsEnum annotation?

Easier error reporting option: instead of waiting for j2cl to fail the generated java, something like this could throw when the generator is trying to emit the union's isFoo() method:

index b72a70a..c9d2e7b 100644
--- a/java/jsinterop/generator/visitor/UnionTypeHelperTypeCreator.java
+++ b/java/jsinterop/generator/visitor/UnionTypeHelperTypeCreator.java
@@ -262,6 +262,10 @@ public class UnionTypeHelperTypeCreator implements ModelVisitor {
       return new ArrayTypeReference(OBJECT);
     }
 
+    if (typeReference.getTypeDeclaration() != null && typeReference.getTypeDeclaration().isEnum()) {
+      throw new IllegalStateException("Can't use @enum in instanceof, so cannot construct union type");
+    }
+
     // remove Type parameters
     if (typeReference instanceof ParametrizedTypeReference) {
       return ((ParametrizedTypeReference) typeReference).getMainType();

Add a mechanism for testing whether feature is present

Sometimes a particular property/operator is only available on some browsers. The generator will generate the field/methods regardless and the downstream consumer is left to try and feature detect in some other way. The simplest approach may be to just generate a @JsOverlay boolean supportsMyFeature() method that will allow caller to detect whether a feature is supported.

It would be too costly to generate supports*() methods for every feature so maybe the generator could load the canisue database and only generate supports*() methods if they are not supported compared to a particular base line (IE11?).

See also #29 because these features could be combined and driven by the same data.

If a static property and an instance property are defined on the same type, the instance property is omitted

(Discovered while testing #46)

Given this JS, the expected behavior presently is that _STATIC will be appended to the static version since these would otherwise collide. Instead, we see a few different behaviors, I can't yet explain what is happening here:

Example JS:

/**
 * @type {string}
 */
Foo.plainProperty;

/**
 * @type {string}
 */
Foo.prototype.plainProperty;

Resulting Java, with only one field and no _STATIC present on the static member:

  public static String plainProperty;

Example JS:

/**
 * @type {string|number}
 */
Foo.unionProperty;
/**
 * @type {string|number}
 */
Foo.prototype.unionProperty;

Resulting Java (union type omittied), correctly with two fields:

  @JsProperty(name = "unionProperty")
  public static Foo.StaticUnionPropertyUnionType unionProperty_STATIC;

  public Foo.UnionPropertyUnionType unionProperty;

Global scope is always assumed to be "window"

ModelHelper.createGlobalJavaType always assumes that window is the correct name to use:

  public static Type createGlobalJavaType(String packagePrefix, String globalScopeClassName) {
    Type type = new Type(NAMESPACE);
    type.setName(globalScopeClassName);
    type.setPackageName(packagePrefix);
    type.setNativeNamespace(GLOBAL_NAMESPACE);
    type.addAnnotation(
        builder()
            .type(JS_TYPE)
            .isNativeAttribute(true)
            .namespaceAttribute("")
            .nameAttribute("window")
            .build());
    return type;
  }

However, this is only correct in a case like DomGlobal, which only makes sense in a normal window context. For classes like Global, this should instead be something like self, so that it works both in windows and in workers (as well as other non-browser JS contexts).

My proposal would be to change this to invoke nameAttribute("self"), since that seems as though it should be valid in window and worker contexts alike (see [1], [2]), but perhaps there is a better, more flexible solution?

[1] https://developer.mozilla.org/en-US/docs/Web/API/Window/self
[2] https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/self

In the mean time, objects in Global can't be used. Workarounds for others who find this:

  • For Symbol, make your own jstype to represent it, with a static function instead of a constructor to create it
  • For JSON, make your own JSONType with static methods instead of instance methods
  • For NaN/Infinity/undefined and other global methods, use your own static @JsProperty-annotated fields.

In all of these cases, JsPackage.GLOBAL should be used as the namespace to avoid needing to reference window.

Generic closure `@interface` types emit create() factory methods with raw return type

A specific example of this is elemental2.dom.CustomEventInit, which at least as of elemental2 1.1.0 is generic on T, but the factory method is raw:

@JsType(isNative = true, namespace = JsPackage.GLOBAL)
public interface CustomEventInit<T> extends EventInit {
  @JsOverlay
  static CustomEventInit create() {
    return Js.uncheckedCast(JsPropertyMap.of());
  }

The bug appears to stem from jsinterop.generator.visitor.DictionaryTypeVisitor#addFactoryMethod, which creates a wrapper JavaTypeReference around the provided Type. Instead, it should probably be a ParametrizedTypeReference in this case. Perhaps the addFactoryMethod can test if type has type params and if so create a ParameterizedTypeReference instance instead?

I didn't see a utility to generically create types like this, so perhaps this is the only use case for this, but maybe there are other synthetic methods that are created only to let Java do things that Js can natively do.

A few other examples in elemental2 - while making this list wI was looking for other places other than a create() method where this was a problem, but didn't spot any so far:

  • elemental2.dom.MessageEventInit
  • elemental2.dom.ITransformStream
  • elemental2.dom.ReadableStreamSource
  • elemental2.core.JsIIterableResult
  • elemental2.dom.TransformStreamTransformer

If the approach sounds reasonable, I can put together a sample patch to try fixing this?

Add @Override annotation where appropriate

It serves no real purpose in generated code other than communicating to the developer reading the source code. However this seems worth it, particularly if it is not hard to do. Presumably this will simply be adding annotation in java where the equivalent is present in closure.

Should "@return {void}" be the default.

In commit google/j2cl@2169efb the @return {void} closure jsdoc has been removed from the generated output with reasoning Do not emit "@return {void}" since this is the default.

Should this tool follow this assumption as currently if a function is not annotated with a return of void or undefined, this tool will default to generating an Object return value.

update .bazelversion to 2.2.0

I installed last version of Bazel on my Mac : it is 2.2.0.

When I launch project build command, it fails because .bazelversion is currently in master at 1.0.0

Can you upgrade to .bazelversion to 2.2.0?
I did it on my machine and it works fine: I get "INFO: Build completed successfully, 233 total actions"

Thanks!
Didier

Eliminating <clinit> on Global objects

Currently the global object created for each module ends up potentially having a <clinit> if there is variables accessed in scope. For example elemental2.dom.DomGlobal has a elemental2.dom.DomGlobal__Constants. The DomGlobal__Constants class has static fields mapped to javascript and then DomGlobal re-esposes those fields by using a static final field. This results in DomGlobal having a <clinit> method that simply assigns the DomGlobal__Constants fields to the DomGlobal fields. It also mandates that most code that accesses these fields must call the clinit method first.

The whole <clinit> dance seems like unnecessary overhead and I am trying to understand why the fields are not moved directly to the DomGlobal class. It would result in less code and have practically the same developer experience - the only difference that I am aware is that it does not stop user code re-assigning the variables as can not be final.

Is there any other reason for this approach?

Add @Generated annotation to all classes

It would be nice to annotate every generated class with @Generated annotation. This allows some IDEs to exclude these classes form certain code analysis. However, the jre standard @Generated was not introduced until java9 and Elemental2 is still supporting java8. In which case we could delay this change until java9 is the minimum supported version.

Alternatively, we could use the "enterprise java" @javax.annotations.Generated annotation but this would add a dependency solely for this annotation which seems sub-optimal. If we implement #25 and include @javax.annotations.Nonnull then the @javax.annotations.Generated annotation will be present in which case it would make sense to adopt this annotation for now.

Add a fail on error mode

Currently Elemental2 builds can produce warnings that do not halt the build. As a result it is possible for to introduce more problems in code generated from this tool without realizing it. For example a a recent commit introduced the error

Mar 26, 2019 8:03:44 PM com.google.javascript.jscomp.LoggerErrorManager println
WARNING: external/com_google_closure_compiler/externs/browser/url.js:162: WARNING - Bad type annotation. Unknown type MediaSource
 * @param {!File|!Blob|!MediaSource|!MediaStream} obj

A possible solution is to update the generator tool so that it will fail if warnings that are not part of a known problems list will cause the build to fail. A possibly simpler alternative is to just introduce a fail on warning flag and fix the problems in Elemental2.

@DoNotAutobox is absent from several Js methods - why?

I am just trying to understand why several of the methods in the Js class do not have the @DoNotAutobox annotation on their object parameters when presumably they should. Examples include the "as" methods and supporting infrastructure. i.e. Js.asBoolean(obj) and thus InternalJsUtil.asBoolean(obj), as well as Js.coerceToDouble(obj).

Is it just because it is assumed that humans invoking this are smart enough to not misuse them and the code is unlikely to be used in code generators? What would be the penalty of adding them?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.