Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Conversation

deadalusai
Copy link
Contributor

Fix DecoratorClassVisitor to only emit ctorParameters when the class has a constructor.

This more closely matches the behavior of TSC which only emits the "design:paramtypes" metadata
when the class actually has a constructor. For example, compiling the following TypeScript snippet:

// compile with --target ES5 --experimentalDecorators --emitDecoratorMetadata
@Inject
class Bar {

}

@Inject
class Baz {
    constructor() {}
}

@Inject
class Foo {
    constructor(bar: Bar) {}
}

class Faz {
    constructor(@Inject bar: Bar) {}
}

Emits this JavaScript:

var Bar = /** @class */ (function () {
    function Bar() {
    }
    Bar = __decorate([
        Inject
    ], Bar);
    return Bar;
}());
var Baz = /** @class */ (function () {
    function Baz() {
    }
    Baz = __decorate([
        Inject,
        __metadata("design:paramtypes", [])
    ], Baz);
    return Baz;
}());
var Foo = /** @class */ (function () {
    function Foo(bar) {
    }
    Foo = __decorate([
        Inject,
        __metadata("design:paramtypes", [Bar])
    ], Foo);
    return Foo;
}());
var Faz = /** @class */ (function () {
    function Faz(bar) {
    }
    Faz = __decorate([
        __param(0, Inject),
        __metadata("design:paramtypes", [Bar])
    ], Faz);
    return Faz;
}());

@mprobst mprobst requested a review from evmar March 7, 2018 10:02
@mprobst
Copy link
Contributor

mprobst commented Mar 7, 2018

@deadalusai can you share what problem exactly this fixes?

I'm not sure if we have code (e.g. in Angular) that might depend on these empty constructors params being produced. Evan might know.

@deadalusai
Copy link
Contributor Author

deadalusai commented Mar 7, 2018

Hi Martin. Sorry for the curt PR - I meant to have a PR up for @angular/angular with links for context, but ran out of time yesterday.

I'm working to resolve an issue I found compiling though ng-packagr, where the constructor function generated for a subclass with an implicit constructor does not play nicely with the regex used to detect such a thing in Angular's ReflectionCapabilities._ownParameters.

Specifically, ng-packagr generates a "pass-through" constructor which does not match the regex on line 71, e.g.

function FooComponent () {
    __super.apply(this, __tslib.__spread(arguments));
}

Angular is expecting something like:

function FooComponent () {
    __super.apply(this, arguments);
}

tsickle has generated the static ctorParameters, but set it to an empty array. The reason this works is because Angular already had a work-around (the regex check) for a similar TypeScript issue (since resolved).

The problem is that the work-around is currently the "official" way to detect this edge case. My goal here is to bring tsickles behavior in line with tsc (do not generate ctorParameters metadata when the class does not have an explicit constructor). The next change will be to clean up the logic in Angular's ReflectionCapabilities to eliminate the reliance on that brittle regex check:

// Pseudocode
function getConstructorParams (typeConstructor) {
    // legacy regex check, for old versions of tsc
    if (DELEGATE_CTOR.test ... etc) {
        return null;
    }

    // check for tsickle metadata
    if (typeConstructor.ctorParameters) {
        return typeConstructor.ctorParameters;
    }

    let metadata = this._reflect.getOwnMetadata('design:paramtypes', typeConstructor);
    if (metadata) {
        return metadata;
    }

    // pass-through constructor, or no metadata available
    return null;
}

// the type either has a real constructor...
let parameters = getConstructorParams(constructorType);
if (!parameters && parentType !== Object) {
    // ...or we should use the parent constructor
    parameters = getConstructorParams(parentType);
}
return parameters || [];

The regex would be left behind as a legacy check to support code generated with old versions of tsc or tsickle, but would no longer be part of the happy path.

(The regex check has already grown from one to three different regexes in the last few minor versions of Angular)

@deadalusai
Copy link
Contributor Author

Just realized my examples above are missing the most important case of a subclass with an implicit constructor. Here's what tsc currently does:

@Injectable
class Base {
    constructor (d: Dependency) {}
}

@Injectable
class Sub extends Base {
}

Generating:

var Base = /** @class */ (function () {
    function Base(d) {
    }
    Base = __decorate([
        Injectable,
        __metadata("design:paramtypes", [Dependency])
    ], Base);
    return Base;
}());
var Sub = /** @class */ (function (_super) {
    __extends(Sub, _super);
    function Sub() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    Sub = __decorate([
        Injectable
        // NOTE: no "design:paramtypes" metadata for the subclass
    ], Sub);
    return Sub;
}(Base));

I'd like tsickle to do the same.

@mprobst
Copy link
Contributor

mprobst commented Mar 7, 2018

@mhevery can you figure out who's a good person to coordinate this change? I don't quite understand the interaction and timelines of the components involved. Can you assign a reviewer?

@deadalusai
Copy link
Contributor Author

deadalusai commented Mar 8, 2018

The Angular PR can be found here: angular/angular#22642

In the end I do not think this approach will work satisfactorily, however I will leave the PRs up for the time being as another dev may be able to see a way to detect and handle the edge case.

Might have found a solution for the problem I had. Feedback welcome.

@mhevery
Copy link

mhevery commented Mar 14, 2018

This is either @evmar or @mprobst

@mhevery
Copy link

mhevery commented Mar 15, 2018

So I looked into it, and the PRs seems reasonable, but it seems like we would have to coordinate angular/angular#22642 with this PR. This seems like an awful lot of work, along with supporting the legacy. Could we just fix ng-packagr/ng-packagr#679 to generate code which would pass current angular checks?

@mhevery
Copy link

mhevery commented Mar 15, 2018

I gave it some more thought and my recommendation would be to do nothing and wait for angular/angular#21706 to land which will make the whole ReflectionCapabilities obsolete, since that will completely remove the need for tsickle.

@trotyl
Copy link

trotyl commented Mar 16, 2018

@mhevery It’s only dropped in core, but requiring JitCompiler to provide the same functionality . And in Ivy this issue would become more severe due to the official support for consuming AOT'd libs in JIT.

@mhevery
Copy link

mhevery commented Mar 21, 2018

@trotyl I am not sure I understand your comment. Can you expand on it?

@trotyl
Copy link

trotyl commented Mar 21, 2018

@mhevery In Ivy the metadata constructor dependencies would be reflected in .n()/.factory() definition properties, like:

@Injectable()
class BaseComp {
  constructor(myService: MyService) { }
}

@Component({ ... })
class MyComp extends BaseComp { }

Would become:

class MyComp extends BaseComp {
  static ngComponentDef = defineComponent({
    factory: () => new MyComp(inject(MyService))
  })
}

So the ReflectionCapabilities in core (runtime) can indeed be removed. But then compiler must be able to determine whether some class has deps defined in ancestor classes.

If we don't have RegExp match any more and without this change in tsickle, then:

In AOT mode, nothing changed in metadata resolution since everything controlled by compiler-cli;
In JIT mode, libraries are preprocessed by ngc and deps are recorded in ctorParameters static property.

Imagine that the classes above comes from lib, with the format:

class BaseComp { }
BaseComp.ctorParameters = () => [ { type: MyService } ]

class MyComp extends BaseComp { }
MyComp.ctorParameters = () => []

Then in a JIT'ed Ivy app:

import { MyComp } from 'my-lib' 

class CustomComp extends MyComp { }

According to the inheritance semantics the deps for CustomComp should be [MyService], but once found empty [] on its base class MyComp, it would then think about MyComp has a deps overwrite, then make the deps to be [].

This case may only happen to back patch mode since I'm not sure how does pure Ivy lib looks like, metadata inheritance in Ivy seems not implemented yet, but for supporting JIT usage, there still needs to be record stands for ctorParameters in emitted JavaScript files.

@deadalusai
Copy link
Contributor Author

tsickle could inherit ctorParams on a derived class explicitly. I.e.

class MyComp extends BaseComp { }
MyComp.ctorParameters = BaseComp.ctorParameters;

Which (for an implicit constructor) should always be correct? This would eliminate the need to walk up the class hierarchy entirely. Unfortunately tsc does not do this for design:paramtypes (though I think it should).

@mhevery
Copy link

mhevery commented Mar 21, 2018

I see, thanks for the explanation, I see it better now. In which case I think I am for landing this. after angular/angular#22642 lands.

@mhevery
Copy link

mhevery commented Mar 21, 2018

Could we get this rebased on latest master?

@deadalusai
Copy link
Contributor Author

@mhevery I can rebase this today. Were you interested in this PR making tsickle emit the base classes' ctorParameters` for types with implicit constructors? E.g.

class BaseComp { }
BaseComp.ctorParameters = () => [ { type: MyService } ]

class MyComp extends BaseComp { }
MyComp.ctorParameters = BaseComp.ctorParameters;

The behavior would be different to tsc again, but I think more useful.

…s has a constructor

This more closely matches the behavior of TSC which only emits the "design:paramtypes" metadata
when the class actually has a constructor function.
@deadalusai
Copy link
Contributor Author

Rebased 👍

@mprobst mprobst merged commit 2fc02d0 into angular:master Mar 22, 2018
@mprobst
Copy link
Contributor

mprobst commented Mar 22, 2018

Merged, thanks for the contribution.

@LinboLen
Copy link

LinboLen commented Apr 9, 2018

devversion added a commit to devversion/material2 that referenced this pull request Aug 20, 2018
Fixes that the table does not render properly when used inside of a ES2015 application. This is due to an ongoing Angular issue which has been caused due to a brittle Regex check [introduced here](angular/angular#22356 (comment)) that should be replaced with a more clean approach for identifying classes that inherit metadata from another class.

* angular/angular#22642
* angular/tsickle#760

Fixes angular#9329
devversion added a commit to devversion/material2 that referenced this pull request Aug 20, 2018
Fixes that some components does not render properly when used inside of a ES2015 JIT application. This is due to an ongoing Angular issue which has been caused due to a brittle Regex check [introduced here](angular/angular#22356 (comment)) that should be replaced with a more clean approach for identifying classes that inherit metadata from another class.

* angular/angular#22642
* angular/tsickle#760

See the more detailed issue here: angular#12760

Fixes angular#9329.
jelbourn pushed a commit to angular/components that referenced this pull request Aug 21, 2018
Fixes that some components does not render properly when used inside of a ES2015 JIT application. This is due to an ongoing Angular issue which has been caused due to a brittle Regex check [introduced here](angular/angular#22356 (comment)) that should be replaced with a more clean approach for identifying classes that inherit metadata from another class.

* angular/angular#22642
* angular/tsickle#760

See the more detailed issue here: #12760

Fixes #9329.
jelbourn pushed a commit to angular/components that referenced this pull request Aug 29, 2018
Fixes that some components does not render properly when used inside of a ES2015 JIT application. This is due to an ongoing Angular issue which has been caused due to a brittle Regex check [introduced here](angular/angular#22356 (comment)) that should be replaced with a more clean approach for identifying classes that inherit metadata from another class.

* angular/angular#22642
* angular/tsickle#760

See the more detailed issue here: #12760

Fixes #9329.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants