Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Static methods unnecessarily copied to subclass constructor #3177

Open
rictic opened this issue Dec 20, 2018 · 15 comments
Open

Static methods unnecessarily copied to subclass constructor #3177

rictic opened this issue Dec 20, 2018 · 15 comments
Labels
internal-issue-created An internal Google issue has been created to track this GitHub issue triage-done Has been reviewed by someone on triage rotation.

Comments

@rictic
Copy link
Member

rictic commented Dec 20, 2018

This code:

class Super {
  /**  
     * @nocollapse 
     */
  static method() {
    return 'foo';
  }
}

class Sub extends Super {}

console.log(Sub.method()); // should log 'foo'
console.log(Sub.hasOwnProperty('method')); // should log 'false' because Sub.method is inherited

Is lowered to ES5 that is essentially:

function Super() {}
Super.method = function() { return 'foo' }

function Sub() {}
Sub.prototype = Object.create(Super.prototype);
Object.setPrototypeOf(Sub, Super);
Sub.method = Super.method;

console.log(Sub.method()); // logs 'foo', correctly
console.log(Sub.hasOwnProperty('method')); // logs 'true', incorrectly

The Sub.method = Super.method; line is unnecessary, because Object.setPrototypeOf(Sub, Super); has already ensured that Sub.method would work correctly, and it also causes Sub.method to be an ownproperty of Sub, which diverges from the original code's semantics.

@ChadKillingsworth
Copy link
Collaborator

@rictic This is only true for browsers that support setPrototypeOf and doesn't nicely line up with any current language out parameter (other than ES6 - which is kinda pointless). IE 11 supports setPrototypeOf, but no earlier versions do. So while IE 9 and 10 support all the ES5 feature set, they do not support setPrototypeOf and therefore require the static methods to be copied.

Copying the static methods is not a perfect transpilation method, but is better than none.

@rictic
Copy link
Member Author

rictic commented Dec 26, 2018

The actual code uses $jscomp.inherits instead of Object.setPrototypeOf. $jscomp.inherits looks like:

$jscomp.inherits = function(a, b) {
  a.prototype = $jscomp.objectCreate(b.prototype);
  a.prototype.constructor = a;
  if ($jscomp.setPrototypeOf) {
    var c = $jscomp.setPrototypeOf;
    c(a, b);
  } else {
    for (c in b) {
      if ("prototype" != c) {
        if (Object.defineProperties) {
          var d = Object.getOwnPropertyDescriptor(b, c);
          d && Object.defineProperty(a, c, d);
        } else {
          a[c] = b[c];
        }
      }
    }
  }
  a.superClass_ = b.prototype;
};

The else block looks like it already loops through the properties of Super and copies them onto Sub. As a result, the Sub.method = Super.method; still seems duplicated to me.

The specific case that this came up in was a project that does not support IE <11, so they can depend on setPrototypeOf existing. If the Sub.method = Super.method; line were removed from the output, the output would be smaller, and the behavior would be more correct on modern browsers.

@rictic rictic reopened this Dec 26, 2018
@ChadKillingsworth
Copy link
Collaborator

There's code that copies those methods outside of the setPrototypeOf polyfill?

I understand the extra code for projects that don't depend on <IE11, but this was the best compromise we could achieve.

@brad4d
Copy link
Contributor

brad4d commented Dec 27, 2018

Internal issue created b/122077271

@brad4d brad4d added the internal-issue-created An internal Google issue has been created to track this GitHub issue label Dec 27, 2018
@brad4d
Copy link
Contributor

brad4d commented Dec 27, 2018

This is working as intended.
On a browser that supports Object.setPrototypeOf() $jscomp.setPrototypeOf will be non-null and will be used to just set the prototype of the child class constructor a to be the super class constructor b.
On older browsers, the properties on b will be copied to a either directly or by using Object.defineProperty

As @ChadKillingsworth said, this was the best compromise we could reach to make sure code that depends on inheritance of static methods would work on both old and new browsers.

@brad4d brad4d closed this as completed Dec 27, 2018
@rictic
Copy link
Member Author

rictic commented Dec 28, 2018

@rictic rictic reopened this Dec 28, 2018
@ChadKillingsworth
Copy link
Collaborator

I think that's here: https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/Es6ToEs3ClassSideInheritance.java#L41-L42

And it sounds like this entire pass can be removed now that transpilation is after type checking.

@brad4d
Copy link
Contributor

brad4d commented Dec 28, 2018

@rictic thanks for the clarification.
@ChadKillingsworth I agree that we want to remove Es6ToEs3ClassSideInheritance pass now,
but I think there's something blocking that, possibly something related to getters and setters.

We may have to just try it and see what breaks.

@lauraharker
Copy link
Contributor

We tried removing this pass a few months ago, but it caused CollapseProperties to break class-side inheritance after transpilation. The pass didn't realize that Sub.method referred to Super.method, so would replace Super.method -> Super$method without changing Sub.method..

It should be possible to merge the pass's logic into property collapsing, though, instead of running it as part of transpilation.

// This pass used to be necessary to make the typechecker happy with class-side inheritance.

@brad4d brad4d added the triage-done Has been reviewed by someone on triage rotation. label Apr 8, 2019
@justinfagnani
Copy link

It looks like that pass is being kept for optimization now under the name ConcretizeStaticInheritanceForInlining.

Could the step that copies statics be removed though? https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/ConcretizeStaticInheritanceForInlining.java#L162

@brad4d
Copy link
Contributor

brad4d commented May 12, 2023

I think the problem that @lauraharker mentioned above still exists, so no.

@justinfagnani what brought this to your attention? Do you have a specific use-case where changing this would make a big difference?

@justinfagnani
Copy link

@brad4d we still hit problems where code that works externally breaks in google3 in sometimes vary hard to debug ways, resulting in fixes like lit/lit#4257 where we have to access a static property only via computed property access to work around the bug. In this latest case I'm worries that the workaround has a performance cost since computed property declarations are slower, at least in V8.

@rictic
Copy link
Member Author

rictic commented Oct 4, 2023

A minimal repro of the issue we're seeing now:

class Foo {
  /** 
   * Using export to avoid needing reflect.objectProperty below.
   * @export
   * @nocollapse 
   */
  static foo: string = 'Foo base value';
  
  /** @nocollapse */
  static finalize() {
    if (!this.hasOwnProperty('foo')) {
      this.foo = 'a subclass of Foo';
    }
  }
}

class Bar extends Foo {}

function assertEqual(actual: string, expected: string, name: string) {
  if (actual === expected) {
    return;
  }
  throw new Error(`Expected ${name} to be ${expected} but was ${actual}`);
}

Foo.finalize();
Bar.finalize();
assertEqual(Foo.foo, 'Foo base value', 'Foo.foo');
assertEqual(Bar.foo, 'a subclass of Foo', 'Bar.foo');

When targeting ES5 with advanced optimizations, this throws Error: Expected Bar.foo to be a subclass of Foo but was Foo base value

Ersatz link

The Closure Compiler Playground version times out when compiling with advanced optimizations:

@brad4d
Copy link
Contributor

brad4d commented Oct 4, 2023

Thanks for pinging this issue.

We're working on moving transpilation passes to occur after normalization.
After that, I hope to also move them after the InlineAndCollapseProperties pass.

I expect that doing that will allow us to fix this problem along with a lot of other ones.
I've marked the internal copy of this issue as blocked on our efforts to move the transpilation passes, so we won't forget to make sure this gets fixed as part of that effort.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
internal-issue-created An internal Google issue has been created to track this GitHub issue triage-done Has been reviewed by someone on triage rotation.
Projects
None yet
Development

No branches or pull requests

5 participants