-
Notifications
You must be signed in to change notification settings - Fork 309
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
test: add failing test cases with single file transpilation for JIT transformers #2615
base: main
Are you sure you want to change the base?
Conversation
4f9eb2a
to
b5b8e99
Compare
Hi @dgp1130, as discussed via DM before, I have copied most of tests from https://github.com/angular/angular/blob/main/packages/compiler-cli/src/ngtsc/transform/jit/test/downlevel_decorators_transform_spec.ts Some tests I found not needed for Jest so I didn't include them here. The amount of copied tests in this PR I think should be enough to cover all cases. There are indeed a few cases where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some general thoughts on the failing tests. I don't know that I have great answers for why they're failing, but hopefully the extra context is helpful.
I do wonder how much of this is downlevel_decorators_transform
being incompatible with isolatedModules
and how much is just not taking the effort to support it. Would some of these failures be fixable upstream or at least gracefully degrade and/or give a better error message about what works?
]; | ||
exports.MyDir = MyDir = tslib_1.__decorate([ | ||
(0, core_1.Directive)(), | ||
tslib_1.__metadata("design:paramtypes", Object]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding of emitDecoratorMetadata
is that it changes the output slightly based on whether or not the constructor parameter type is local or imported. It wants to emit tslib_1.__metadata("design:paramtypes", MyOtherClass)
, but TS can't introspect the type of an imported symbol with isolatedModules
and doesn't know MyOtherClass
is a class
. Instead, TS needs to generate a small JS snippet to determine this at runtime. See the output from this example. Is that the output you get which causes the test failure?
Interestingly, tsc
seems to add this snippet whether or not isolatedModules
is enabled. In theory I can see that TS wouldn't know MyOtherClass
is a class as opposed to an interface and maybe needs to pick Object
because it can't be certain MyOtherClass
is valid in a value reference position.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes the output from TS playground was exactly the output which caused the test failture. If we leave the output to be similar like TS playground, will that cause any issues when testing real application codes?
Is emitDecoratorMetadata: true
still important to Angular apps these days? I remember somewhere I saw a commit from Angular which allows to disable emitDecoratorMetadata
and application still works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that emitted JS is probably fine? Hard to be 100% sure unfortunately.
ng new
does not enable emitDecoratorMetadata
in the scaffolded tsconfig.json
files, so I suspect it's not required, but I wouldn't be surprised if there are other effects from enabling it. I can ask around and see if anyone has more context.
it.failing( | ||
'should allow for type-only references to be removed with `emitDecoratorMetadata` from custom decorators', | ||
() => { | ||
// This test only passes when using `import type { ExternalInterface } from './external-interface';` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one makes sense to me because with isolatedModules
TS shouldn't know that it's an interface and not eligible to be moved into a value reference position.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output of this case is
exports.Foo = Foo;
tslib_1.__decorate([
CustomDecorator(),
tslib_1.__metadata(\"design:type\", Function),
tslib_1.__metadata(\"design:paramtypes\", []),
tslib_1.__metadata(\"design:returntype\", typeof (_a = typeof external_interface_1.ExternalInterface !== \"undefined\" && external_interface_1.ExternalInterface) === \"function\" ? _a : Object)
], Foo, \"test\", null);
which made the assertion expect(code).not.toContain('ExternalInterface');
fail.
Will the output like that cause any issues when testing real application codes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typeof external_interface_1.ExternalInterface
sounds wrong to me. Interfaces are erased by TypeScript, so this expression can't work at runtime. I would expect this to be something like Object
in this scenario since it can't be any more specific about the type.
I don't think Angular uses design:returntype
anywhere. I think we only care about constructor parameters. So it probably doesn't matter what's actually in the __metadata
call, as long as it doesn't throw.
// This test doesn't pass because `transpileModule` can't resolve `import *` | ||
const { code, diagnostics } = transformCjs(` | ||
import {Directive} from '@angular/core'; | ||
import * as externalFile from './other-file'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised this would be different than import { MyOtherClass } from './other-file';
and makes me wonder if there's a bug in entityNameToExpression
somewhere? We have special handling of aliased imports, I wonder if we also need special handling for namespaced imports?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can confirm name import and default import work, only namespace import doesn't
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] }, | ||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] }, | ||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] }, | ||
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$keyCodes',] }] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If type
is expected to be undefined
, then why are we trying to resolve the import *
? It doesn't seem like we shouldn't need that type? I wonder if the correct behavior on resolution error would just be to fall back to any
as the type (and emit undefined
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This case produces very interesting output
Object.defineProperty(exports, \"__esModule\", { value: true });
exports.MyDir = void 0;
const tslib_1 = require(\"tslib\");
const core_1 = require(\"@angular/core\");
const external_1 = require(\"./external\");
const external_2 = tslib_1.__importDefault(require(\"./external\"));
let MyDir = class MyDir {
constructor(param, other, fromDefaultImport, keyCodes) { }
};
exports.MyDir = MyDir;
MyDir.ctorParameters = () => [
{ type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] },
{ type: external_1.IOverlay, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] },
{ type: external_2.default, decorators: [{ type: core_1.Inject, args: ['$default',] }] },
{ type: external_1.KeyCodes, decorators: [{ type: core_1.Inject, args: ['$keyCodes',] }] }
];
exports.MyDir = MyDir = tslib_1.__decorate([
(0, core_1.Directive)()
], MyDir);
I saw that the type
was resolved correctly when using import name or default import but fail with import namespace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, any of these could be an interface which wouldn't be valid in type
though. I wonder if the type system is falling back to any
because it can't be resolved and then we're assuming the import is a valid value reference. I wonder if the safer assumption would be to fall back to type: undefined
in that scenario?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You were right. When I do typeChecker.getTypeOfSymbol(symbol)
, it gave me any
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In retrospect, any
might not necessarily be a problem. Both type references and value references can be typed any
, so that might be fine. I wonder if it's possible to ask TS whether or not each import is a value reference or a type reference. I'm guessing it's assuming imported symbols are a value reference when it can't resolve the import and we'd probably want to reduce that assumption to a type reference in this scenario.
Unfortunately I'm not very familiar with the TS API and docs are pretty lacking, so I'm not sure about the best way to verify that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably work around this problem a different way. Since this is the Angular decorator transform, we know a few things:
- constructor parameters without
@Inject
must reference a type declaration that's also a value (and thus it's fine to "assume" when transpiling). - constructor parameters with
@Inject
don't need atype
output at all, and we can unconditionally generatetype: undefined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the 2nd case, can we unconditionally NOT generating undefined
so that it’s possible to run tests and leave it up to Jest runtime to validate if that is valid runtime value?
For example:
import foo from ‘./foo’
would resolve in type
as type: foo.<something>
And the same applies to import * as foo from ‘./foo’
If we set type: undefined
, it will break all existing tests and it makes it not possible to run tests in transpilation mode because there will be error like Can't resolve all parameters for AppComponent: (?)
which I observe when debugging this issue #963
a102b3a
to
e31377c
Compare
e31377c
to
c5a0f81
Compare
transpileModule
with JIT transformersc5a0f81
to
0db3190
Compare
hi, we are experiencing quite some performance issues when running unit tests using I wanted to first of all thank for your precious contributions, and I also wanted to ask if this PR is supposed to make tests run faster and how much is the expected impact. |
@Francesco-Borzi this PR opens up a possibility to improve both test speed and memory usage without breaking existing test suites. At the moment, you can already use
|
hi @ahnpnl , apart from the new compilation errors, I don't seem to get any performance improvement using |
Just to be sure, I mean
|
@ahnpnl thanks for the clarification! |
Test plan
Green CI
Does this PR introduce a breaking change?
Other information
N.A.