fix(ngcc): recognize enum declarations emitted in JavaScript (#36550)
An enum declaration in TypeScript code will be emitted into JavaScript as a regular variable declaration, with the enum members being declared inside an IIFE. For ngcc to support interpreting such variable declarations as enum declarations with its members, ngcc needs to recognize the enum declaration emit structure and extract all member from the statements in the IIFE. This commit extends the `ConcreteDeclaration` structure in the `ReflectionHost` abstraction to be able to capture the enum members on a variable declaration, as a substitute for the original `ts.EnumDeclaration` as it existed in TypeScript code. The static interpreter has been extended to handle the extracted enum members as it would have done for `ts.EnumDeclaration`. Fixes #35584 Resolves FW-2069 PR Close #36550
This commit is contained in:
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
||||
import {Reference} from '../../imports';
|
||||
import {OwningModule} from '../../imports/src/references';
|
||||
import {DependencyTracker} from '../../incremental/api';
|
||||
import {Declaration, InlineDeclaration, ReflectionHost} from '../../reflection';
|
||||
import {ConcreteDeclaration, Declaration, EnumMember, InlineDeclaration, ReflectionHost, SpecialDeclarationKind} from '../../reflection';
|
||||
import {isDeclaration} from '../../util/src/typescript';
|
||||
|
||||
import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn} from './builtin';
|
||||
@ -225,6 +225,10 @@ export class StaticInterpreter {
|
||||
}
|
||||
if (decl.known !== null) {
|
||||
return resolveKnownDeclaration(decl.known);
|
||||
} else if (
|
||||
isConcreteDeclaration(decl) && decl.identity !== null &&
|
||||
decl.identity.kind === SpecialDeclarationKind.DownleveledEnum) {
|
||||
return this.getResolvedEnum(decl.node, decl.identity.enumMembers, context);
|
||||
}
|
||||
const declContext = {...context, ...joinModuleContext(context, node, decl)};
|
||||
// The identifier's declaration is either concrete (a ts.Declaration exists for it) or inline
|
||||
@ -279,7 +283,7 @@ export class StaticInterpreter {
|
||||
}
|
||||
|
||||
private visitEnumDeclaration(node: ts.EnumDeclaration, context: Context): ResolvedValue {
|
||||
const enumRef = this.getReference(node, context) as Reference<ts.EnumDeclaration>;
|
||||
const enumRef = this.getReference(node, context);
|
||||
const map = new Map<string, EnumValue>();
|
||||
node.members.forEach(member => {
|
||||
const name = this.stringNameFromPropertyName(member.name, context);
|
||||
@ -572,7 +576,21 @@ export class StaticInterpreter {
|
||||
}
|
||||
}
|
||||
|
||||
private getReference(node: ts.Declaration, context: Context): Reference {
|
||||
private getResolvedEnum(node: ts.Declaration, enumMembers: EnumMember[], context: Context):
|
||||
ResolvedValue {
|
||||
const enumRef = this.getReference(node, context);
|
||||
const map = new Map<string, EnumValue>();
|
||||
enumMembers.forEach(member => {
|
||||
const name = this.stringNameFromPropertyName(member.name, context);
|
||||
if (name !== undefined) {
|
||||
const resolved = this.visit(member.initializer, context);
|
||||
map.set(name, new EnumValue(enumRef, name, resolved));
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
private getReference<T extends ts.Declaration>(node: T, context: Context): Reference<T> {
|
||||
return new Reference(node, owningModule(context));
|
||||
}
|
||||
}
|
||||
@ -638,3 +656,11 @@ function owningModule(context: Context, override: OwningModule|null = null): Own
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type guard to workaround a narrowing limitation in g3, where testing for
|
||||
* `decl.node !== null` would not narrow `decl` to be of type `ConcreteDeclaration`.
|
||||
*/
|
||||
function isConcreteDeclaration(decl: Declaration): decl is ConcreteDeclaration {
|
||||
return decl.node !== null;
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export class ResolvedModule {
|
||||
*/
|
||||
export class EnumValue {
|
||||
constructor(
|
||||
readonly enumRef: Reference<ts.EnumDeclaration>, readonly name: string,
|
||||
readonly enumRef: Reference<ts.Declaration>, readonly name: string,
|
||||
readonly resolved: ResolvedValue) {}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {Reference} from '../../imports';
|
||||
import {DependencyTracker} from '../../incremental/api';
|
||||
import {Declaration, KnownDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
||||
import {Declaration, KnownDeclaration, SpecialDeclarationKind, TypeScriptReflectionHost} from '../../reflection';
|
||||
import {getDeclaration, makeProgram} from '../../testing';
|
||||
import {DynamicValue} from '../src/dynamic';
|
||||
import {PartialEvaluator} from '../src/interface';
|
||||
@ -488,10 +488,23 @@ runInEachFileSystem(() => {
|
||||
if (!(result instanceof EnumValue)) {
|
||||
return fail(`result is not an EnumValue`);
|
||||
}
|
||||
expect(result.enumRef.node.name.text).toBe('Foo');
|
||||
expect((result.enumRef.node as ts.EnumDeclaration).name.text).toBe('Foo');
|
||||
expect(result.name).toBe('B');
|
||||
});
|
||||
|
||||
it('enum resolution works when recognized in reflection host', () => {
|
||||
const {checker, expression} = makeExpression('var Foo;', 'Foo.ValueB');
|
||||
const reflectionHost = new DownleveledEnumReflectionHost(checker);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, null);
|
||||
const result = evaluator.evaluate(expression);
|
||||
if (!(result instanceof EnumValue)) {
|
||||
return fail(`result is not an EnumValue`);
|
||||
}
|
||||
expect(result.enumRef.node.parent.parent.getText()).toBe('var Foo;');
|
||||
expect(result.name).toBe('ValueB');
|
||||
expect(result.resolved).toBe('b');
|
||||
});
|
||||
|
||||
it('variable declaration resolution works', () => {
|
||||
const value = evaluate(`import {value} from './decl';`, 'value', [
|
||||
{name: _('/decl.d.ts'), contents: 'export declare let value: number;'},
|
||||
@ -843,6 +856,20 @@ runInEachFileSystem(() => {
|
||||
});
|
||||
});
|
||||
|
||||
class DownleveledEnumReflectionHost extends TypeScriptReflectionHost {
|
||||
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
|
||||
const declaration = super.getDeclarationOfIdentifier(id);
|
||||
if (declaration !== null && declaration.node !== null) {
|
||||
const enumMembers = [
|
||||
{name: ts.createStringLiteral('ValueA'), initializer: ts.createStringLiteral('a')},
|
||||
{name: ts.createStringLiteral('ValueB'), initializer: ts.createStringLiteral('b')},
|
||||
];
|
||||
declaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers};
|
||||
}
|
||||
return declaration;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizes the resolution of module exports and identifier declarations to recognize known
|
||||
* helper functions from `tslib`. Such functions are not handled specially in the default
|
||||
@ -872,6 +899,7 @@ runInEachFileSystem(() => {
|
||||
known: tsHelperFn,
|
||||
node: id,
|
||||
viaModule: null,
|
||||
identity: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user