google / google/j2cl

@JsConstructor makes default field initialization run after java construction logic

Open
#268 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
1.4k
Forks
161
PR merge metrics
No merged PRs in 30d

Description

# 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](https://github.com/user-attachments/files/19255835/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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.