feat(ivy): port the static resolver to use the ReflectionHost (#24862)
Previously, the static resolver did its own interpretation of statements in the TypeScript AST, which only functioned on TypeScript code. ES5 code in particular would not work with the resolver as it had hard-coded assumptions about AST structure. This commit changes the resolver to use a ReflectionHost instead, which abstracts away understanding of the structural side of the AST. It adds 3 new methods to the ReflectionHost in support of this functionality: * getDeclarationOfIdentifier * getExportsOfModule * isClass PR Close #24862
This commit is contained in:

committed by
Victor Berchet

parent
2e724ec68b
commit
5d7005eef5
@ -43,7 +43,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||
const component = reflectObjectLiteral(meta);
|
||||
|
||||
if (this.resourceLoader.preload !== undefined && component.has('templateUrl')) {
|
||||
const templateUrl = staticallyResolve(component.get('templateUrl') !, this.checker);
|
||||
const templateUrl =
|
||||
staticallyResolve(component.get('templateUrl') !, this.reflector, this.checker);
|
||||
if (typeof templateUrl !== 'string') {
|
||||
throw new Error(`templateUrl should be a string`);
|
||||
}
|
||||
@ -73,7 +74,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||
|
||||
let templateStr: string|null = null;
|
||||
if (component.has('templateUrl')) {
|
||||
const templateUrl = staticallyResolve(component.get('templateUrl') !, this.checker);
|
||||
const templateUrl =
|
||||
staticallyResolve(component.get('templateUrl') !, this.reflector, this.checker);
|
||||
if (typeof templateUrl !== 'string') {
|
||||
throw new Error(`templateUrl should be a string`);
|
||||
}
|
||||
@ -81,7 +83,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||
templateStr = this.resourceLoader.load(url);
|
||||
} else if (component.has('template')) {
|
||||
const templateExpr = component.get('template') !;
|
||||
const resolvedTemplate = staticallyResolve(templateExpr, this.checker);
|
||||
const resolvedTemplate = staticallyResolve(templateExpr, this.reflector, this.checker);
|
||||
if (typeof resolvedTemplate !== 'string') {
|
||||
throw new Error(`Template must statically resolve to a string: ${node.name!.text}`);
|
||||
}
|
||||
@ -92,7 +94,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||
|
||||
let preserveWhitespaces: boolean = false;
|
||||
if (component.has('preserveWhitespaces')) {
|
||||
const value = staticallyResolve(component.get('preserveWhitespaces') !, this.checker);
|
||||
const value =
|
||||
staticallyResolve(component.get('preserveWhitespaces') !, this.reflector, this.checker);
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new Error(`preserveWhitespaces must resolve to a boolean if present`);
|
||||
}
|
||||
@ -116,9 +119,11 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||
// Construct the list of view queries.
|
||||
const coreModule = this.isCore ? undefined : '@angular/core';
|
||||
const viewChildFromFields = queriesFromFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.checker);
|
||||
filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.reflector,
|
||||
this.checker);
|
||||
const viewChildrenFromFields = queriesFromFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.checker);
|
||||
filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.reflector,
|
||||
this.checker);
|
||||
const viewQueries = [...viewChildFromFields, ...viewChildrenFromFields];
|
||||
|
||||
if (component.has('queries')) {
|
||||
|
@ -90,19 +90,21 @@ export function extractDirectiveMetadata(
|
||||
// Construct the map of inputs both from the @Directive/@Component
|
||||
// decorator, and the decorated
|
||||
// fields.
|
||||
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', checker);
|
||||
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', reflector, checker);
|
||||
const inputsFromFields = parseDecoratedFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), checker);
|
||||
filterToMembersWithDecorator(decoratedElements, 'Input', coreModule), reflector, checker);
|
||||
|
||||
// And outputs.
|
||||
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker);
|
||||
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', reflector, checker);
|
||||
const outputsFromFields = parseDecoratedFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), checker);
|
||||
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), reflector, checker);
|
||||
// Construct the list of queries.
|
||||
const contentChildFromFields = queriesFromFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), checker);
|
||||
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), reflector,
|
||||
checker);
|
||||
const contentChildrenFromFields = queriesFromFields(
|
||||
filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), checker);
|
||||
filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), reflector,
|
||||
checker);
|
||||
|
||||
const queries = [...contentChildFromFields, ...contentChildrenFromFields];
|
||||
|
||||
@ -115,14 +117,14 @@ export function extractDirectiveMetadata(
|
||||
// Parse the selector.
|
||||
let selector = '';
|
||||
if (directive.has('selector')) {
|
||||
const resolved = staticallyResolve(directive.get('selector') !, checker);
|
||||
const resolved = staticallyResolve(directive.get('selector') !, reflector, checker);
|
||||
if (typeof resolved !== 'string') {
|
||||
throw new Error(`Selector must be a string`);
|
||||
}
|
||||
selector = resolved;
|
||||
}
|
||||
|
||||
const host = extractHostBindings(directive, decoratedElements, checker, coreModule);
|
||||
const host = extractHostBindings(directive, decoratedElements, reflector, checker, coreModule);
|
||||
|
||||
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
|
||||
const usesOnChanges = members.find(
|
||||
@ -148,12 +150,12 @@ export function extractDirectiveMetadata(
|
||||
|
||||
export function extractQueryMetadata(
|
||||
name: string, args: ReadonlyArray<ts.Expression>, propertyName: string,
|
||||
checker: ts.TypeChecker): R3QueryMetadata {
|
||||
reflector: ReflectionHost, checker: ts.TypeChecker): R3QueryMetadata {
|
||||
if (args.length === 0) {
|
||||
throw new Error(`@${name} must have arguments`);
|
||||
}
|
||||
const first = name === 'ViewChild' || name === 'ContentChild';
|
||||
const arg = staticallyResolve(args[0], checker);
|
||||
const arg = staticallyResolve(args[0], reflector, checker);
|
||||
|
||||
// Extract the predicate
|
||||
let predicate: Expression|string[]|null = null;
|
||||
@ -182,7 +184,7 @@ export function extractQueryMetadata(
|
||||
}
|
||||
|
||||
if (options.has('descendants')) {
|
||||
const descendantsValue = staticallyResolve(options.get('descendants') !, checker);
|
||||
const descendantsValue = staticallyResolve(options.get('descendants') !, reflector, checker);
|
||||
if (typeof descendantsValue !== 'boolean') {
|
||||
throw new Error(`@${name} options.descendants must be a boolean`);
|
||||
}
|
||||
@ -220,7 +222,8 @@ export function extractQueriesFromDecorator(
|
||||
throw new Error(`query metadata must be an instance of a query type`);
|
||||
}
|
||||
|
||||
const query = extractQueryMetadata(type.name, queryExpr.arguments || [], propertyName, checker);
|
||||
const query = extractQueryMetadata(
|
||||
type.name, queryExpr.arguments || [], propertyName, reflector, checker);
|
||||
if (type.name.startsWith('Content')) {
|
||||
content.push(query);
|
||||
} else {
|
||||
@ -248,14 +251,14 @@ function isStringArrayOrDie(value: any, name: string): value is string[] {
|
||||
* correctly shaped metadata object.
|
||||
*/
|
||||
function parseFieldToPropertyMapping(
|
||||
directive: Map<string, ts.Expression>, field: string,
|
||||
directive: Map<string, ts.Expression>, field: string, reflector: ReflectionHost,
|
||||
checker: ts.TypeChecker): {[field: string]: string} {
|
||||
if (!directive.has(field)) {
|
||||
return EMPTY_OBJECT;
|
||||
}
|
||||
|
||||
// Resolve the field of interest from the directive metadata to a string[].
|
||||
const metaValues = staticallyResolve(directive.get(field) !, checker);
|
||||
const metaValues = staticallyResolve(directive.get(field) !, reflector, checker);
|
||||
if (!isStringArrayOrDie(metaValues, field)) {
|
||||
throw new Error(`Failed to resolve @Directive.${field}`);
|
||||
}
|
||||
@ -276,7 +279,7 @@ function parseFieldToPropertyMapping(
|
||||
* object.
|
||||
*/
|
||||
function parseDecoratedFields(
|
||||
fields: {member: ClassMember, decorators: Decorator[]}[],
|
||||
fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost,
|
||||
checker: ts.TypeChecker): {[field: string]: string} {
|
||||
return fields.reduce(
|
||||
(results, field) => {
|
||||
@ -287,7 +290,7 @@ function parseDecoratedFields(
|
||||
if (decorator.args == null || decorator.args.length === 0) {
|
||||
results[fieldName] = fieldName;
|
||||
} else if (decorator.args.length === 1) {
|
||||
const property = staticallyResolve(decorator.args[0], checker);
|
||||
const property = staticallyResolve(decorator.args[0], reflector, checker);
|
||||
if (typeof property !== 'string') {
|
||||
throw new Error(`Decorator argument must resolve to a string`);
|
||||
}
|
||||
@ -304,7 +307,7 @@ function parseDecoratedFields(
|
||||
}
|
||||
|
||||
export function queriesFromFields(
|
||||
fields: {member: ClassMember, decorators: Decorator[]}[],
|
||||
fields: {member: ClassMember, decorators: Decorator[]}[], reflector: ReflectionHost,
|
||||
checker: ts.TypeChecker): R3QueryMetadata[] {
|
||||
return fields.map(({member, decorators}) => {
|
||||
if (decorators.length !== 1) {
|
||||
@ -313,7 +316,8 @@ export function queriesFromFields(
|
||||
throw new Error(`Query decorator must go on a property-type member`);
|
||||
}
|
||||
const decorator = decorators[0];
|
||||
return extractQueryMetadata(decorator.name, decorator.args || [], member.name, checker);
|
||||
return extractQueryMetadata(
|
||||
decorator.name, decorator.args || [], member.name, reflector, checker);
|
||||
});
|
||||
}
|
||||
|
||||
@ -327,15 +331,15 @@ type StringMap = {
|
||||
};
|
||||
|
||||
function extractHostBindings(
|
||||
metadata: Map<string, ts.Expression>, members: ClassMember[], checker: ts.TypeChecker,
|
||||
coreModule: string | undefined): {
|
||||
metadata: Map<string, ts.Expression>, members: ClassMember[], reflector: ReflectionHost,
|
||||
checker: ts.TypeChecker, coreModule: string | undefined): {
|
||||
attributes: StringMap,
|
||||
listeners: StringMap,
|
||||
properties: StringMap,
|
||||
} {
|
||||
let hostMetadata: StringMap = {};
|
||||
if (metadata.has('host')) {
|
||||
const hostMetaMap = staticallyResolve(metadata.get('host') !, checker);
|
||||
const hostMetaMap = staticallyResolve(metadata.get('host') !, reflector, checker);
|
||||
if (!(hostMetaMap instanceof Map)) {
|
||||
throw new Error(`Decorator host metadata must be an object`);
|
||||
}
|
||||
@ -358,7 +362,7 @@ function extractHostBindings(
|
||||
throw new Error(`@HostBinding() can have at most one argument`);
|
||||
}
|
||||
|
||||
const resolved = staticallyResolve(decorator.args[0], checker);
|
||||
const resolved = staticallyResolve(decorator.args[0], reflector, checker);
|
||||
if (typeof resolved !== 'string') {
|
||||
throw new Error(`@HostBinding()'s argument must be a string`);
|
||||
}
|
||||
@ -380,7 +384,7 @@ function extractHostBindings(
|
||||
throw new Error(`@HostListener() can have at most two arguments`);
|
||||
}
|
||||
|
||||
const resolved = staticallyResolve(decorator.args[0], checker);
|
||||
const resolved = staticallyResolve(decorator.args[0], reflector, checker);
|
||||
if (typeof resolved !== 'string') {
|
||||
throw new Error(`@HostListener()'s event name argument must be a string`);
|
||||
}
|
||||
@ -388,7 +392,7 @@ function extractHostBindings(
|
||||
eventName = resolved;
|
||||
|
||||
if (decorator.args.length === 2) {
|
||||
const resolvedArgs = staticallyResolve(decorator.args[1], checker);
|
||||
const resolvedArgs = staticallyResolve(decorator.args[1], reflector, checker);
|
||||
if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args')) {
|
||||
throw new Error(`@HostListener second argument must be a string array`);
|
||||
}
|
||||
|
@ -59,20 +59,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
||||
// Extract the module declarations, imports, and exports.
|
||||
let declarations: Reference[] = [];
|
||||
if (ngModule.has('declarations')) {
|
||||
const declarationMeta = staticallyResolve(ngModule.get('declarations') !, this.checker);
|
||||
const declarationMeta =
|
||||
staticallyResolve(ngModule.get('declarations') !, this.reflector, this.checker);
|
||||
declarations = resolveTypeList(declarationMeta, 'declarations');
|
||||
}
|
||||
let imports: Reference[] = [];
|
||||
if (ngModule.has('imports')) {
|
||||
const importsMeta = staticallyResolve(
|
||||
ngModule.get('imports') !, this.checker,
|
||||
ngModule.get('imports') !, this.reflector, this.checker,
|
||||
node => this._extractModuleFromModuleWithProvidersFn(node));
|
||||
imports = resolveTypeList(importsMeta, 'imports');
|
||||
}
|
||||
let exports: Reference[] = [];
|
||||
if (ngModule.has('exports')) {
|
||||
const exportsMeta = staticallyResolve(
|
||||
ngModule.get('exports') !, this.checker,
|
||||
ngModule.get('exports') !, this.reflector, this.checker,
|
||||
node => this._extractModuleFromModuleWithProvidersFn(node));
|
||||
exports = resolveTypeList(exportsMeta, 'exports');
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
|
||||
if (!pipe.has('name')) {
|
||||
throw new Error(`@Pipe decorator is missing name field`);
|
||||
}
|
||||
const pipeName = staticallyResolve(pipe.get('name') !, this.checker);
|
||||
const pipeName = staticallyResolve(pipe.get('name') !, this.reflector, this.checker);
|
||||
if (typeof pipeName !== 'string') {
|
||||
throw new Error(`@Pipe.name must be a string`);
|
||||
}
|
||||
@ -52,7 +52,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
|
||||
|
||||
let pure = true;
|
||||
if (pipe.has('pure')) {
|
||||
const pureValue = staticallyResolve(pipe.get('pure') !, this.checker);
|
||||
const pureValue = staticallyResolve(pipe.get('pure') !, this.reflector, this.checker);
|
||||
if (typeof pureValue !== 'boolean') {
|
||||
throw new Error(`@Pipe.pure must be a boolean`);
|
||||
}
|
||||
|
Reference in New Issue
Block a user