fix(ngcc): ensure reflection hosts can handle TS 3.9 IIFE wrapped classes (#36989)

In TS 3.9, ES2015 output can contain ES classes that are wrapped in an
IIFE. So now ES2015 class declarations can look like one of:

```
class OuterClass1 {}
```

```
let OuterClass = class InnerClass {};
```

```
var AliasClass;
let OuterClass = AliasClass = class InnerClass {};
```

```
let OuterClass = (() => class InnerClass {}};
```

```
var AliasClass;
let OuterClass = AliasClass = (() => class InnerClass {})();
```

```
let OuterClass = (() => {
  let AdjacentClass = class InnerClass {};
  // ... static properties or decorators attached to `AdjacentClass`
  return AdjacentClass;
})();
```

```
var AliasClass;
let OuterClass = AliasClass = (() => {
  let AdjacentClass = class InnerClass {};
  // ... static properties or decorators attached to `AdjacentClass`
  return AdjacentClass;
})();
```

The `Esm5ReflectionHost` already handles slightly different IIFE wrappers
around function-based classes. This can be substantially reused when
fixing `Esm2015ReflectionHost`, since there is a lot of commonality
between the two.

This commit moves code from the `Esm5ReflectionHost` into the `Esm2015ReflectionHost`
and looks to share as much as possible between the two hosts.

PR Close #36989
This commit is contained in:
Pete Bacon Darwin
2020-05-12 08:20:00 +01:00
committed by Kara Erickson
parent a2b8dc1cfb
commit d7440c452a
8 changed files with 792 additions and 467 deletions

View File

@ -133,6 +133,14 @@ runInEachFileSystem(() => {
class NoDecoratorConstructorClass {
constructor(foo) {}
}
let SimpleWrappedClass = /** @class */ (() => {
class SimpleWrappedClassInner {}
return SimpleWrappedClassInner;
})();
let AliasedWrappedClass = /** @class */ (() => {
let AliasedWrappedClassAdjacent = class AliasedWrappedClassInner {};
return SimpleWrappedClassAdjacent;
})();
`,
};
@ -166,6 +174,11 @@ runInEachFileSystem(() => {
];
return AliasedWrappedClass;
})();
let DecoratedWrappedClass = /** @class */ (() => {
let DecoratedWrappedClass = class DecoratedWrappedClass {}
// ... add decorations ...
return DecoratedWrappedClass;
})();
let usageOfWrappedClass = AliasedWrappedClass_1;
`,
};
@ -534,7 +547,27 @@ runInEachFileSystem(() => {
{ type: Directive, args: [{ selector: '[b]' }] }
];
class C {}
export { A, x, C };
var AliasedClass_1;
let AliasedClass = AliasedClass_1 = class AliasedClass {}
AliasedClass.decorators = [
{ type: Directive, args: [{ selector: '[aliased]' },] }
];
let Wrapped1 = /** @class */ (() => {
let Wrapped1 = class Wrapped1 {
};
Wrapped1 = __decorate([
Directive({selector: '[wrapped-1]'})
], Wrapped1);
return Wrapped1;
})();
let Wrapped2 = /** @class */ (() => {
class Wrapped2 {}
Wrapped2.decorators = [
{ type: Directive, args: [{ selector: '[wrapped-2]' },] }
];
return Wrapped2;
})();
export { A, x, C, AliasedClass, Wrapped1, Wrapped2 };
`
},
{
@ -1605,6 +1638,30 @@ runInEachFileSystem(() => {
.toBe(classDeclaration);
});
it('should return the correct declaration for an inner class identifier inside an IIFE',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerDeclaration = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
const innerDeclaration =
((((outerDeclaration.initializer as ts.CallExpression).expression as
ts.ParenthesizedExpression)
.expression as ts.ArrowFunction)
.body as ts.Block)
.statements[0] as ts.ClassDeclaration;
const outerIdentifier = outerDeclaration.name as ts.Identifier;
const innerIdentifier = innerDeclaration.name as ts.Identifier;
expect(host.getDeclarationOfIdentifier(outerIdentifier)!.node).toBe(outerDeclaration);
expect(host.getDeclarationOfIdentifier(innerIdentifier)!.node).toBe(outerDeclaration);
});
it('should recognize enum declarations with string values', () => {
const testFile: TestFile = {
name: _('/node_modules/test-package/some/file.js'),
@ -1767,6 +1824,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeDefined();
expect(classSymbol!.declaration.valueDeclaration).toBe(node);
expect(classSymbol!.implementation.valueDeclaration).toBe(node);
expect(classSymbol!.adjacent).toBeUndefined();
});
it('should return the class symbol for a class expression (outer variable declaration)',
@ -1784,6 +1842,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeDefined();
expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode);
expect(classSymbol!.adjacent).toBeUndefined();
});
it('should return the class symbol for a class expression (inner class expression)', () => {
@ -1798,6 +1857,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeDefined();
expect(classSymbol!.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol!.implementation.valueDeclaration).toBe(innerNode);
expect(classSymbol!.adjacent).toBeUndefined();
});
it('should return the same class symbol (of the outer declaration) for outer and inner declarations',
@ -1837,6 +1897,7 @@ runInEachFileSystem(() => {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
expect(classSymbol.adjacent).toBeUndefined();
});
it('should return the class symbol for a wrapped class expression (inner class expression)',
@ -1861,6 +1922,7 @@ runInEachFileSystem(() => {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name.text).toBe('SimpleWrappedClass');
expect(classSymbol.adjacent).toBeUndefined();
});
it('should return the same class symbol (of the outer declaration) for wrapped outer and inner declarations',
@ -1881,6 +1943,87 @@ runInEachFileSystem(() => {
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
});
it('should return the class symbol for a decorated wrapped class expression (outer variable declaration)',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'DecoratedWrappedClass',
isNamedVariableDeclaration);
const classSymbol = host.getClassSymbol(outerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('DecoratedWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
if (!ts.isClassExpression(classSymbol.implementation.valueDeclaration)) {
return fail('Expected a named class declaration');
}
expect(classSymbol.implementation.valueDeclaration.name!.text)
.toBe('DecoratedWrappedClass');
if (classSymbol.adjacent === undefined ||
!isNamedVariableDeclaration(classSymbol.adjacent.valueDeclaration)) {
return fail('Expected a named variable declaration for the adjacent symbol');
}
expect(classSymbol.adjacent.valueDeclaration.name.text).toBe('DecoratedWrappedClass');
});
it('should return the class symbol for a decorated wrapped class expression (inner class expression)',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'DecoratedWrappedClass',
isNamedVariableDeclaration);
const innerNode: ts.ClassExpression =
(outerNode as any)
.initializer.expression.expression.body.statements[0]
.declarationList.declarations[0]
.initializer;
const classSymbol = host.getClassSymbol(innerNode);
if (classSymbol === undefined) {
return fail('Expected classSymbol to be defined');
}
expect(classSymbol.name).toEqual('DecoratedWrappedClass');
expect(classSymbol.declaration.valueDeclaration).toBe(outerNode);
expect(classSymbol.implementation.valueDeclaration).toBe(innerNode);
if (classSymbol.adjacent === undefined ||
!isNamedVariableDeclaration(classSymbol.adjacent.valueDeclaration)) {
return fail('Expected a named variable declaration for the adjacent symbol');
}
expect(classSymbol.adjacent.valueDeclaration.name.text).toBe('DecoratedWrappedClass');
});
it('should return the same class symbol (of the outer declaration) for decorated wrapped outer and inner declarations',
() => {
loadTestFiles([WRAPPED_CLASS_EXPRESSION_FILE]);
const bundle = makeTestBundleProgram(WRAPPED_CLASS_EXPRESSION_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const outerNode = getDeclaration(
bundle.program, WRAPPED_CLASS_EXPRESSION_FILE.name, 'DecoratedWrappedClass',
isNamedVariableDeclaration);
const innerNode: ts.ClassExpression =
(outerNode as any)
.initializer.expression.expression.body.statements[0]
.declarationList.declarations[0]
.initializer;
const innerSymbol = host.getClassSymbol(innerNode)!;
const outerSymbol = host.getClassSymbol(outerNode)!;
expect(innerSymbol.declaration).toBe(outerSymbol.declaration);
expect(innerSymbol.implementation).toBe(outerSymbol.implementation);
});
it('should return undefined if node is not a class', () => {
loadTestFiles([FOO_FUNCTION_FILE]);
const bundle = makeTestBundleProgram(FOO_FUNCTION_FILE.name);
@ -1892,7 +2035,7 @@ runInEachFileSystem(() => {
expect(classSymbol).toBeUndefined();
});
it('should return undefined if variable declaration is not initialized using a class expression',
it('should return undefined if variable declaration is not initialized to a valid class definition',
() => {
const testFile = {
name: _('/test.js'),
@ -2115,8 +2258,9 @@ runInEachFileSystem(() => {
const secondaryFile = getSourceFileOrError(bundle.program, DECORATED_FILES[1].name);
const classSymbolsPrimary = host.findClassSymbols(primaryFile);
expect(classSymbolsPrimary.length).toEqual(3);
expect(classSymbolsPrimary.map(c => c.name)).toEqual(['A', 'B', 'C']);
expect(classSymbolsPrimary.map(c => c.name)).toEqual([
'A', 'B', 'C', 'AliasedClass', 'Wrapped1', 'Wrapped2'
]);
const classSymbolsSecondary = host.findClassSymbols(secondaryFile);
expect(classSymbolsSecondary.length).toEqual(1);
@ -2134,10 +2278,14 @@ runInEachFileSystem(() => {
const classSymbolsPrimary = host.findClassSymbols(primaryFile);
const classDecoratorsPrimary = classSymbolsPrimary.map(s => host.getDecoratorsOfSymbol(s));
expect(classDecoratorsPrimary.length).toEqual(3);
expect(classDecoratorsPrimary.length).toEqual(6);
expect(classDecoratorsPrimary[0]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[1]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[2]).toBe(null);
expect(classDecoratorsPrimary[3]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[4]!.map(d => d.name)).toEqual(['Directive']);
expect(classDecoratorsPrimary[5]!.map(d => d.name)).toEqual(['Directive']);
const classSymbolsSecondary = host.findClassSymbols(secondaryFile);
const classDecoratorsSecondary =
@ -2339,7 +2487,7 @@ runInEachFileSystem(() => {
});
describe('getInternalNameOfClass()', () => {
it('should return the name of the class (there is no separate inner class in ES2015)', () => {
it('should return the name of the class (if there is no separate inner class)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
@ -2347,10 +2495,32 @@ runInEachFileSystem(() => {
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.getInternalNameOfClass(node).text).toEqual('EmptyClass');
});
it('should return the name of the inner class (if there is an IIFE)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(node).text).toEqual('SimpleWrappedClassInner');
});
it('should return the name of the inner variable declaration (if there is an aliased class in an IIFE)',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'AliasedWrappedClass',
isNamedVariableDeclaration);
expect(host.getInternalNameOfClass(node).text).toEqual('AliasedWrappedClassInner');
});
});
describe('getAdjacentNameOfClass()', () => {
it('should return the name of the class (there is no separate inner class in ES2015)', () => {
it('should return the name of the class (if there is no separate inner class)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
@ -2358,6 +2528,28 @@ runInEachFileSystem(() => {
bundle.program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration);
expect(host.getAdjacentNameOfClass(node).text).toEqual('EmptyClass');
});
it('should return the name of the inner class (if there is an IIFE)', () => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host = createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'SimpleWrappedClass',
isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(node).text).toEqual('SimpleWrappedClassInner');
});
it('should return the name of the inner variable declaration (if there is an aliased class in an IIFE)',
() => {
loadTestFiles([SIMPLE_CLASS_FILE]);
const bundle = makeTestBundleProgram(SIMPLE_CLASS_FILE.name);
const host =
createHost(bundle, new Esm2015ReflectionHost(new MockLogger(), false, bundle));
const node = getDeclaration(
bundle.program, SIMPLE_CLASS_FILE.name, 'AliasedWrappedClass',
isNamedVariableDeclaration);
expect(host.getAdjacentNameOfClass(node).text).toEqual('AliasedWrappedClassAdjacent');
});
});
describe('getEndOfClass()', () => {