Skip to content

@JsConstructor makes default field initialization run after java construction logic #268

Description

@DragonAxe

Description:

Adding @JsConstructor annotation with class inheritance changes constructor field initialization behavior. Default field initialization overwrites initialization by the programmer.

In the example code below, the superclass Shape calls the non-final, non-private method initGraphic. initGraphic is overridden in Box and sets the value of Box#drawLabel to true. (Calling non-final, non-private methods in a constructor is considered bad practice, but I'm working with a large amount of existing code written with this pattern.)

When the bug is triggered by having @JsConstructor on Shape's constructor, Shape's J2CL generated constructor() calls this.$ctor__com_example_Shape__void() which calls Box's initGraphic. When Shape's constructor() returns to Box's constructor(), Box's pre-initialization overwrites the initialization done in Box#initGraphic.

To Reproduce:

J2CL-maven-plugin minimum-reproducible test project:
constructor-bug-test.zip

(I'm assuming this bug is in J2CL rather than j2clmavenplugin because it has to do with generated code, but I could be wrong.)

Example Code:

package com.example;

import elemental2.dom.DomGlobal;
import jsinterop.annotations.JsConstructor;
import jsinterop.annotations.JsType;

@JsType
public class Main {
  public static void main(String[] args) {
    Box box = new Box();
    Main.print("5. Box final drawLabel=" + box.drawLabel);
  }

  public static void print(String s) {
    if (DomGlobal.console == null) {
      System.out.println(s);
    } else {
      DomGlobal.console.log(s);
    }
  }
}

abstract class Shape {
  // This annotation triggers bug by changing the generated J2CL constructor initialization order.
  // Commenting out this annotation will not trigger the bug. 
  @JsConstructor
  public Shape() {
    Main.print("1. ActiveBean ctor");
    initGraphic();
  }

  protected void initGraphic() {
    Main.print("2. ActiveBean initGraphic");
  }
}

class Box extends Shape {
  public boolean drawLabel;

  @JsConstructor
  public Box() {
    super();
    Main.print("4. Box ctor");
  }

  @Override
  protected void initGraphic() {
    super.initGraphic();
    Main.print("3. Box initGraphic");
    drawLabel = true;
  }
}

Output behavior with @JsConstructor annotation on `Shape (triggers bug):

1. ActiveBean ctor
2. ActiveBean initGraphic
3. Box initGraphic
4. Box ctor
5. Box final drawLabel=false        <---- BUG!? Expected 'true'

Output behavior without @JsConstructor annotation on Shape (works as expected):

1. ActiveBean ctor
2. ActiveBean initGraphic
3. Box initGraphic
4. Box ctor
5. Box final drawLabel=true

Generated Code

With @JsConstructor on Shape (bug):

class Shape extends j_l_Object {
 
 constructor() {
  Shape.$clinit();
  super();
  this.$ctor__com_example_Shape__void(); // This call is not present when @JsConstructor is not present.
 }
 /** @nodts */
 $ctor__com_example_Shape__void() {
  this.$ctor__java_lang_Object__void();
  Main.print('1. ActiveBean ctor');
  this.m_initGraphic__void(); // Calls overridden child class method.
 }
class Box extends Shape {
 
 constructor() {
  Box.$clinit();
  super();
  /**@type {boolean} @nodts*/
  this.f_drawLabel__com_example_Box = false; // Pre-initialization overwrites the work done by the super-class constructor.
  this.$ctor__com_example_Box__void();
 }
 /** @nodts */
 $ctor__com_example_Box__void() {
  Main.print('4. Box ctor');
 }

Without @JsConstructor on Shape (no bug):

class Shape extends j_l_Object {
 /** @protected @nodts */
 constructor() {
  super();
  // Does not call $ctor__com_example_Shape__void()
 }
 /** @nodts */
 $ctor__com_example_Shape__void() { // Not called until after Box's pre-initialization.
  this.$ctor__java_lang_Object__void();
  Main.print('1. ActiveBean ctor');
  this.m_initGraphic__void();
 }
class Box extends Shape {
 
 constructor() {
  Box.$clinit();
  super();
  /**@type {boolean} @nodts*/
  this.f_drawLabel__com_example_Box = false; // Pre-initialization
  this.$ctor__com_example_Box__void();
 }
 /** @nodts */
 $ctor__com_example_Box__void() {
  this.$ctor__com_example_Shape__void(); // Now Shape's java constructor is called after Box's pre-initialization.
  Main.print('4. Box ctor');
 }

Version:

J2clMavenPlugin: v20230718-1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions