
Cache reference resolution for external references as finding the declaration of a symbol is expensive and does not change for a program once created. This resolves a signficant performance regression in the langauge service. PR Close #21359
1029 lines
40 KiB
TypeScript
1029 lines
40 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {CompileSummaryKind} from '../compile_metadata';
|
|
import {CompileReflector} from '../compile_reflector';
|
|
import {MetadataFactory, createAttribute, createComponent, createContentChild, createContentChildren, createDirective, createHost, createHostBinding, createHostListener, createInject, createInjectable, createInput, createNgModule, createOptional, createOutput, createPipe, createSelf, createSkipSelf, createViewChild, createViewChildren} from '../core';
|
|
import * as o from '../output/output_ast';
|
|
import {SummaryResolver} from '../summary_resolver';
|
|
import {syntaxError} from '../util';
|
|
|
|
import {FormattedMessageChain, formattedError} from './formatted_error';
|
|
import {StaticSymbol} from './static_symbol';
|
|
import {StaticSymbolResolver} from './static_symbol_resolver';
|
|
|
|
const ANGULAR_CORE = '@angular/core';
|
|
const ANGULAR_ROUTER = '@angular/router';
|
|
|
|
const HIDDEN_KEY = /^\$.*\$$/;
|
|
|
|
const IGNORE = {
|
|
__symbolic: 'ignore'
|
|
};
|
|
|
|
const USE_VALUE = 'useValue';
|
|
const PROVIDE = 'provide';
|
|
const REFERENCE_SET = new Set([USE_VALUE, 'useFactory', 'data']);
|
|
const TYPEGUARD_POSTFIX = 'TypeGuard';
|
|
const USE_IF = 'UseIf';
|
|
|
|
function shouldIgnore(value: any): boolean {
|
|
return value && value.__symbolic == 'ignore';
|
|
}
|
|
|
|
/**
|
|
* A static reflector implements enough of the Reflector API that is necessary to compile
|
|
* templates statically.
|
|
*/
|
|
export class StaticReflector implements CompileReflector {
|
|
private annotationCache = new Map<StaticSymbol, any[]>();
|
|
private propertyCache = new Map<StaticSymbol, {[key: string]: any[]}>();
|
|
private parameterCache = new Map<StaticSymbol, any[]>();
|
|
private methodCache = new Map<StaticSymbol, {[key: string]: boolean}>();
|
|
private staticCache = new Map<StaticSymbol, string[]>();
|
|
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
|
|
private resolvedExternalReferences = new Map<string, StaticSymbol>();
|
|
private injectionToken: StaticSymbol;
|
|
private opaqueToken: StaticSymbol;
|
|
ROUTES: StaticSymbol;
|
|
private ANALYZE_FOR_ENTRY_COMPONENTS: StaticSymbol;
|
|
private annotationForParentClassWithSummaryKind =
|
|
new Map<CompileSummaryKind, MetadataFactory<any>[]>();
|
|
|
|
constructor(
|
|
private summaryResolver: SummaryResolver<StaticSymbol>,
|
|
private symbolResolver: StaticSymbolResolver,
|
|
knownMetadataClasses: {name: string, filePath: string, ctor: any}[] = [],
|
|
knownMetadataFunctions: {name: string, filePath: string, fn: any}[] = [],
|
|
private errorRecorder?: (error: any, fileName?: string) => void) {
|
|
this.initializeConversionMap();
|
|
knownMetadataClasses.forEach(
|
|
(kc) => this._registerDecoratorOrConstructor(
|
|
this.getStaticSymbol(kc.filePath, kc.name), kc.ctor));
|
|
knownMetadataFunctions.forEach(
|
|
(kf) => this._registerFunction(this.getStaticSymbol(kf.filePath, kf.name), kf.fn));
|
|
this.annotationForParentClassWithSummaryKind.set(
|
|
CompileSummaryKind.Directive, [createDirective, createComponent]);
|
|
this.annotationForParentClassWithSummaryKind.set(CompileSummaryKind.Pipe, [createPipe]);
|
|
this.annotationForParentClassWithSummaryKind.set(CompileSummaryKind.NgModule, [createNgModule]);
|
|
this.annotationForParentClassWithSummaryKind.set(
|
|
CompileSummaryKind.Injectable,
|
|
[createInjectable, createPipe, createDirective, createComponent, createNgModule]);
|
|
}
|
|
|
|
componentModuleUrl(typeOrFunc: StaticSymbol): string {
|
|
const staticSymbol = this.findSymbolDeclaration(typeOrFunc);
|
|
return this.symbolResolver.getResourcePath(staticSymbol);
|
|
}
|
|
|
|
resolveExternalReference(ref: o.ExternalReference, containingFile?: string): StaticSymbol {
|
|
let key: string|undefined = undefined;
|
|
if (!containingFile) {
|
|
key = `${ref.moduleName}:${ref.name}`;
|
|
const declarationSymbol = this.resolvedExternalReferences.get(key);
|
|
if (declarationSymbol) return declarationSymbol;
|
|
}
|
|
const refSymbol =
|
|
this.symbolResolver.getSymbolByModule(ref.moduleName !, ref.name !, containingFile);
|
|
const declarationSymbol = this.findSymbolDeclaration(refSymbol);
|
|
if (!containingFile) {
|
|
this.symbolResolver.recordModuleNameForFileName(refSymbol.filePath, ref.moduleName !);
|
|
this.symbolResolver.recordImportAs(declarationSymbol, refSymbol);
|
|
}
|
|
if (key) {
|
|
this.resolvedExternalReferences.set(key, declarationSymbol);
|
|
}
|
|
return declarationSymbol;
|
|
}
|
|
|
|
findDeclaration(moduleUrl: string, name: string, containingFile?: string): StaticSymbol {
|
|
return this.findSymbolDeclaration(
|
|
this.symbolResolver.getSymbolByModule(moduleUrl, name, containingFile));
|
|
}
|
|
|
|
tryFindDeclaration(moduleUrl: string, name: string): StaticSymbol {
|
|
return this.symbolResolver.ignoreErrorsFor(() => this.findDeclaration(moduleUrl, name));
|
|
}
|
|
|
|
findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol {
|
|
const resolvedSymbol = this.symbolResolver.resolveSymbol(symbol);
|
|
if (resolvedSymbol) {
|
|
let resolvedMetadata = resolvedSymbol.metadata;
|
|
if (resolvedMetadata && resolvedMetadata.__symbolic === 'resolved') {
|
|
resolvedMetadata = resolvedMetadata.symbol;
|
|
}
|
|
if (resolvedMetadata instanceof StaticSymbol) {
|
|
return this.findSymbolDeclaration(resolvedSymbol.metadata);
|
|
}
|
|
}
|
|
return symbol;
|
|
}
|
|
|
|
public annotations(type: StaticSymbol): any[] {
|
|
let annotations = this.annotationCache.get(type);
|
|
if (!annotations) {
|
|
annotations = [];
|
|
const classMetadata = this.getTypeMetadata(type);
|
|
const parentType = this.findParentType(type, classMetadata);
|
|
if (parentType) {
|
|
const parentAnnotations = this.annotations(parentType);
|
|
annotations.push(...parentAnnotations);
|
|
}
|
|
let ownAnnotations: any[] = [];
|
|
if (classMetadata['decorators']) {
|
|
ownAnnotations = this.simplify(type, classMetadata['decorators']);
|
|
annotations.push(...ownAnnotations);
|
|
}
|
|
if (parentType && !this.summaryResolver.isLibraryFile(type.filePath) &&
|
|
this.summaryResolver.isLibraryFile(parentType.filePath)) {
|
|
const summary = this.summaryResolver.resolveSummary(parentType);
|
|
if (summary && summary.type) {
|
|
const requiredAnnotationTypes =
|
|
this.annotationForParentClassWithSummaryKind.get(summary.type.summaryKind !) !;
|
|
const typeHasRequiredAnnotation = requiredAnnotationTypes.some(
|
|
(requiredType) => ownAnnotations.some(ann => requiredType.isTypeOf(ann)));
|
|
if (!typeHasRequiredAnnotation) {
|
|
this.reportError(
|
|
formatMetadataError(
|
|
metadataError(
|
|
`Class ${type.name} in ${type.filePath} extends from a ${CompileSummaryKind[summary.type.summaryKind!]} in another compilation unit without duplicating the decorator`,
|
|
/* summary */ undefined,
|
|
`Please add a ${requiredAnnotationTypes.map((type) => type.ngMetadataName).join(' or ')} decorator to the class`),
|
|
type),
|
|
type);
|
|
}
|
|
}
|
|
}
|
|
this.annotationCache.set(type, annotations.filter(ann => !!ann));
|
|
}
|
|
return annotations;
|
|
}
|
|
|
|
public propMetadata(type: StaticSymbol): {[key: string]: any[]} {
|
|
let propMetadata = this.propertyCache.get(type);
|
|
if (!propMetadata) {
|
|
const classMetadata = this.getTypeMetadata(type);
|
|
propMetadata = {};
|
|
const parentType = this.findParentType(type, classMetadata);
|
|
if (parentType) {
|
|
const parentPropMetadata = this.propMetadata(parentType);
|
|
Object.keys(parentPropMetadata).forEach((parentProp) => {
|
|
propMetadata ![parentProp] = parentPropMetadata[parentProp];
|
|
});
|
|
}
|
|
|
|
const members = classMetadata['members'] || {};
|
|
Object.keys(members).forEach((propName) => {
|
|
const propData = members[propName];
|
|
const prop = (<any[]>propData)
|
|
.find(a => a['__symbolic'] == 'property' || a['__symbolic'] == 'method');
|
|
const decorators: any[] = [];
|
|
if (propMetadata ![propName]) {
|
|
decorators.push(...propMetadata ![propName]);
|
|
}
|
|
propMetadata ![propName] = decorators;
|
|
if (prop && prop['decorators']) {
|
|
decorators.push(...this.simplify(type, prop['decorators']));
|
|
}
|
|
});
|
|
this.propertyCache.set(type, propMetadata);
|
|
}
|
|
return propMetadata;
|
|
}
|
|
|
|
public parameters(type: StaticSymbol): any[] {
|
|
if (!(type instanceof StaticSymbol)) {
|
|
this.reportError(
|
|
new Error(`parameters received ${JSON.stringify(type)} which is not a StaticSymbol`),
|
|
type);
|
|
return [];
|
|
}
|
|
try {
|
|
let parameters = this.parameterCache.get(type);
|
|
if (!parameters) {
|
|
const classMetadata = this.getTypeMetadata(type);
|
|
const parentType = this.findParentType(type, classMetadata);
|
|
const members = classMetadata ? classMetadata['members'] : null;
|
|
const ctorData = members ? members['__ctor__'] : null;
|
|
if (ctorData) {
|
|
const ctor = (<any[]>ctorData).find(a => a['__symbolic'] == 'constructor');
|
|
const rawParameterTypes = <any[]>ctor['parameters'] || [];
|
|
const parameterDecorators = <any[]>this.simplify(type, ctor['parameterDecorators'] || []);
|
|
parameters = [];
|
|
rawParameterTypes.forEach((rawParamType, index) => {
|
|
const nestedResult: any[] = [];
|
|
const paramType = this.trySimplify(type, rawParamType);
|
|
if (paramType) nestedResult.push(paramType);
|
|
const decorators = parameterDecorators ? parameterDecorators[index] : null;
|
|
if (decorators) {
|
|
nestedResult.push(...decorators);
|
|
}
|
|
parameters !.push(nestedResult);
|
|
});
|
|
} else if (parentType) {
|
|
parameters = this.parameters(parentType);
|
|
}
|
|
if (!parameters) {
|
|
parameters = [];
|
|
}
|
|
this.parameterCache.set(type, parameters);
|
|
}
|
|
return parameters;
|
|
} catch (e) {
|
|
console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private _methodNames(type: any): {[key: string]: boolean} {
|
|
let methodNames = this.methodCache.get(type);
|
|
if (!methodNames) {
|
|
const classMetadata = this.getTypeMetadata(type);
|
|
methodNames = {};
|
|
const parentType = this.findParentType(type, classMetadata);
|
|
if (parentType) {
|
|
const parentMethodNames = this._methodNames(parentType);
|
|
Object.keys(parentMethodNames).forEach((parentProp) => {
|
|
methodNames ![parentProp] = parentMethodNames[parentProp];
|
|
});
|
|
}
|
|
|
|
const members = classMetadata['members'] || {};
|
|
Object.keys(members).forEach((propName) => {
|
|
const propData = members[propName];
|
|
const isMethod = (<any[]>propData).some(a => a['__symbolic'] == 'method');
|
|
methodNames ![propName] = methodNames ![propName] || isMethod;
|
|
});
|
|
this.methodCache.set(type, methodNames);
|
|
}
|
|
return methodNames;
|
|
}
|
|
|
|
private _staticMembers(type: StaticSymbol): string[] {
|
|
let staticMembers = this.staticCache.get(type);
|
|
if (!staticMembers) {
|
|
const classMetadata = this.getTypeMetadata(type);
|
|
const staticMemberData = classMetadata['statics'] || {};
|
|
staticMembers = Object.keys(staticMemberData);
|
|
this.staticCache.set(type, staticMembers);
|
|
}
|
|
return staticMembers;
|
|
}
|
|
|
|
|
|
private findParentType(type: StaticSymbol, classMetadata: any): StaticSymbol|undefined {
|
|
const parentType = this.trySimplify(type, classMetadata['extends']);
|
|
if (parentType instanceof StaticSymbol) {
|
|
return parentType;
|
|
}
|
|
}
|
|
|
|
hasLifecycleHook(type: any, lcProperty: string): boolean {
|
|
if (!(type instanceof StaticSymbol)) {
|
|
this.reportError(
|
|
new Error(
|
|
`hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`),
|
|
type);
|
|
}
|
|
try {
|
|
return !!this._methodNames(type)[lcProperty];
|
|
} catch (e) {
|
|
console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
guards(type: any): {[key: string]: StaticSymbol} {
|
|
if (!(type instanceof StaticSymbol)) {
|
|
this.reportError(
|
|
new Error(`guards received ${JSON.stringify(type)} which is not a StaticSymbol`), type);
|
|
return {};
|
|
}
|
|
const staticMembers = this._staticMembers(type);
|
|
const result: {[key: string]: StaticSymbol} = {};
|
|
for (let name of staticMembers) {
|
|
if (name.endsWith(TYPEGUARD_POSTFIX)) {
|
|
let property = name.substr(0, name.length - TYPEGUARD_POSTFIX.length);
|
|
let value: any;
|
|
if (property.endsWith(USE_IF)) {
|
|
property = name.substr(0, property.length - USE_IF.length);
|
|
value = USE_IF;
|
|
} else {
|
|
value = this.getStaticSymbol(type.filePath, type.name, [name]);
|
|
}
|
|
result[property] = value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private _registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void {
|
|
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args));
|
|
}
|
|
|
|
private _registerFunction(type: StaticSymbol, fn: any): void {
|
|
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => fn.apply(undefined, args));
|
|
}
|
|
|
|
private initializeConversionMap(): void {
|
|
this.injectionToken = this.findDeclaration(ANGULAR_CORE, 'InjectionToken');
|
|
this.opaqueToken = this.findDeclaration(ANGULAR_CORE, 'OpaqueToken');
|
|
this.ROUTES = this.tryFindDeclaration(ANGULAR_ROUTER, 'ROUTES');
|
|
this.ANALYZE_FOR_ENTRY_COMPONENTS =
|
|
this.findDeclaration(ANGULAR_CORE, 'ANALYZE_FOR_ENTRY_COMPONENTS');
|
|
|
|
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), createHost);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Injectable'), createInjectable);
|
|
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), createSelf);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Inject'), createInject);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Optional'), createOptional);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Attribute'), createAttribute);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'ContentChild'), createContentChild);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'ContentChildren'), createContentChildren);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'ViewChild'), createViewChild);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'ViewChildren'), createViewChildren);
|
|
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Input'), createInput);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Output'), createOutput);
|
|
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Pipe'), createPipe);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'HostBinding'), createHostBinding);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'HostListener'), createHostListener);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Directive'), createDirective);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Component'), createComponent);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'NgModule'), createNgModule);
|
|
|
|
// Note: Some metadata classes can be used directly with Provider.deps.
|
|
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), createHost);
|
|
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), createSelf);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), createSkipSelf);
|
|
this._registerDecoratorOrConstructor(
|
|
this.findDeclaration(ANGULAR_CORE, 'Optional'), createOptional);
|
|
}
|
|
|
|
/**
|
|
* getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded.
|
|
* All types passed to the StaticResolver should be pseudo-types returned by this method.
|
|
*
|
|
* @param declarationFile the absolute path of the file where the symbol is declared
|
|
* @param name the name of the type.
|
|
*/
|
|
getStaticSymbol(declarationFile: string, name: string, members?: string[]): StaticSymbol {
|
|
return this.symbolResolver.getStaticSymbol(declarationFile, name, members);
|
|
}
|
|
|
|
/**
|
|
* Simplify but discard any errors
|
|
*/
|
|
private trySimplify(context: StaticSymbol, value: any): any {
|
|
const originalRecorder = this.errorRecorder;
|
|
this.errorRecorder = (error: any, fileName: string) => {};
|
|
const result = this.simplify(context, value);
|
|
this.errorRecorder = originalRecorder;
|
|
return result;
|
|
}
|
|
|
|
/** @internal */
|
|
public simplify(context: StaticSymbol, value: any): any {
|
|
const self = this;
|
|
let scope = BindingScope.empty;
|
|
const calling = new Map<StaticSymbol, boolean>();
|
|
const rootContext = context;
|
|
|
|
function simplifyInContext(
|
|
context: StaticSymbol, value: any, depth: number, references: number): any {
|
|
function resolveReferenceValue(staticSymbol: StaticSymbol): any {
|
|
const resolvedSymbol = self.symbolResolver.resolveSymbol(staticSymbol);
|
|
return resolvedSymbol ? resolvedSymbol.metadata : null;
|
|
}
|
|
|
|
function simplifyEagerly(value: any): any {
|
|
return simplifyInContext(context, value, depth, 0);
|
|
}
|
|
|
|
function simplifyLazily(value: any): any {
|
|
return simplifyInContext(context, value, depth, references + 1);
|
|
}
|
|
|
|
function simplifyNested(nestedContext: StaticSymbol, value: any): any {
|
|
if (nestedContext === context) {
|
|
// If the context hasn't changed let the exception propagate unmodified.
|
|
return simplifyInContext(nestedContext, value, depth + 1, references);
|
|
}
|
|
try {
|
|
return simplifyInContext(nestedContext, value, depth + 1, references);
|
|
} catch (e) {
|
|
if (isMetadataError(e)) {
|
|
// Propagate the message text up but add a message to the chain that explains how we got
|
|
// here.
|
|
// e.chain implies e.symbol
|
|
const summaryMsg = e.chain ? 'references \'' + e.symbol !.name + '\'' : errorSummary(e);
|
|
const summary = `'${nestedContext.name}' ${summaryMsg}`;
|
|
const chain = {message: summary, position: e.position, next: e.chain};
|
|
// TODO(chuckj): retrieve the position information indirectly from the collectors node
|
|
// map if the metadata is from a .ts file.
|
|
self.error(
|
|
{
|
|
message: e.message,
|
|
advise: e.advise,
|
|
context: e.context, chain,
|
|
symbol: nestedContext
|
|
},
|
|
context);
|
|
} else {
|
|
// It is probably an internal error.
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
function simplifyCall(
|
|
functionSymbol: StaticSymbol, targetFunction: any, args: any[], targetExpression: any) {
|
|
if (targetFunction && targetFunction['__symbolic'] == 'function') {
|
|
if (calling.get(functionSymbol)) {
|
|
self.error(
|
|
{
|
|
message: 'Recursion is not supported',
|
|
summary: `called '${functionSymbol.name}' recursively`,
|
|
value: targetFunction
|
|
},
|
|
functionSymbol);
|
|
}
|
|
try {
|
|
const value = targetFunction['value'];
|
|
if (value && (depth != 0 || value.__symbolic != 'error')) {
|
|
const parameters: string[] = targetFunction['parameters'];
|
|
const defaults: any[] = targetFunction.defaults;
|
|
args = args.map(arg => simplifyNested(context, arg))
|
|
.map(arg => shouldIgnore(arg) ? undefined : arg);
|
|
if (defaults && defaults.length > args.length) {
|
|
args.push(...defaults.slice(args.length).map((value: any) => simplify(value)));
|
|
}
|
|
calling.set(functionSymbol, true);
|
|
const functionScope = BindingScope.build();
|
|
for (let i = 0; i < parameters.length; i++) {
|
|
functionScope.define(parameters[i], args[i]);
|
|
}
|
|
const oldScope = scope;
|
|
let result: any;
|
|
try {
|
|
scope = functionScope.done();
|
|
result = simplifyNested(functionSymbol, value);
|
|
} finally {
|
|
scope = oldScope;
|
|
}
|
|
return result;
|
|
}
|
|
} finally {
|
|
calling.delete(functionSymbol);
|
|
}
|
|
}
|
|
|
|
if (depth === 0) {
|
|
// If depth is 0 we are evaluating the top level expression that is describing element
|
|
// decorator. In this case, it is a decorator we don't understand, such as a custom
|
|
// non-angular decorator, and we should just ignore it.
|
|
return IGNORE;
|
|
}
|
|
let position: Position|undefined = undefined;
|
|
if (targetExpression && targetExpression.__symbolic == 'resolved') {
|
|
const line = targetExpression.line;
|
|
const character = targetExpression.character;
|
|
const fileName = targetExpression.fileName;
|
|
if (fileName != null && line != null && character != null) {
|
|
position = {fileName, line, column: character};
|
|
}
|
|
}
|
|
self.error(
|
|
{
|
|
message: FUNCTION_CALL_NOT_SUPPORTED,
|
|
context: functionSymbol,
|
|
value: targetFunction, position
|
|
},
|
|
context);
|
|
}
|
|
|
|
function simplify(expression: any): any {
|
|
if (isPrimitive(expression)) {
|
|
return expression;
|
|
}
|
|
if (expression instanceof Array) {
|
|
const result: any[] = [];
|
|
for (const item of (<any>expression)) {
|
|
// Check for a spread expression
|
|
if (item && item.__symbolic === 'spread') {
|
|
// We call with references as 0 because we require the actual value and cannot
|
|
// tolerate a reference here.
|
|
const spreadArray = simplifyEagerly(item.expression);
|
|
if (Array.isArray(spreadArray)) {
|
|
for (const spreadItem of spreadArray) {
|
|
result.push(spreadItem);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
const value = simplify(item);
|
|
if (shouldIgnore(value)) {
|
|
continue;
|
|
}
|
|
result.push(value);
|
|
}
|
|
return result;
|
|
}
|
|
if (expression instanceof StaticSymbol) {
|
|
// Stop simplification at builtin symbols or if we are in a reference context and
|
|
// the symbol doesn't have members.
|
|
if (expression === self.injectionToken || self.conversionMap.has(expression) ||
|
|
(references > 0 && !expression.members.length)) {
|
|
return expression;
|
|
} else {
|
|
const staticSymbol = expression;
|
|
const declarationValue = resolveReferenceValue(staticSymbol);
|
|
if (declarationValue != null) {
|
|
return simplifyNested(staticSymbol, declarationValue);
|
|
} else {
|
|
return staticSymbol;
|
|
}
|
|
}
|
|
}
|
|
if (expression) {
|
|
if (expression['__symbolic']) {
|
|
let staticSymbol: StaticSymbol;
|
|
switch (expression['__symbolic']) {
|
|
case 'binop':
|
|
let left = simplify(expression['left']);
|
|
if (shouldIgnore(left)) return left;
|
|
let right = simplify(expression['right']);
|
|
if (shouldIgnore(right)) return right;
|
|
switch (expression['operator']) {
|
|
case '&&':
|
|
return left && right;
|
|
case '||':
|
|
return left || right;
|
|
case '|':
|
|
return left | right;
|
|
case '^':
|
|
return left ^ right;
|
|
case '&':
|
|
return left & right;
|
|
case '==':
|
|
return left == right;
|
|
case '!=':
|
|
return left != right;
|
|
case '===':
|
|
return left === right;
|
|
case '!==':
|
|
return left !== right;
|
|
case '<':
|
|
return left < right;
|
|
case '>':
|
|
return left > right;
|
|
case '<=':
|
|
return left <= right;
|
|
case '>=':
|
|
return left >= right;
|
|
case '<<':
|
|
return left << right;
|
|
case '>>':
|
|
return left >> right;
|
|
case '+':
|
|
return left + right;
|
|
case '-':
|
|
return left - right;
|
|
case '*':
|
|
return left * right;
|
|
case '/':
|
|
return left / right;
|
|
case '%':
|
|
return left % right;
|
|
}
|
|
return null;
|
|
case 'if':
|
|
let condition = simplify(expression['condition']);
|
|
return condition ? simplify(expression['thenExpression']) :
|
|
simplify(expression['elseExpression']);
|
|
case 'pre':
|
|
let operand = simplify(expression['operand']);
|
|
if (shouldIgnore(operand)) return operand;
|
|
switch (expression['operator']) {
|
|
case '+':
|
|
return operand;
|
|
case '-':
|
|
return -operand;
|
|
case '!':
|
|
return !operand;
|
|
case '~':
|
|
return ~operand;
|
|
}
|
|
return null;
|
|
case 'index':
|
|
let indexTarget = simplifyEagerly(expression['expression']);
|
|
let index = simplifyEagerly(expression['index']);
|
|
if (indexTarget && isPrimitive(index)) return indexTarget[index];
|
|
return null;
|
|
case 'select':
|
|
const member = expression['member'];
|
|
let selectContext = context;
|
|
let selectTarget = simplify(expression['expression']);
|
|
if (selectTarget instanceof StaticSymbol) {
|
|
const members = selectTarget.members.concat(member);
|
|
selectContext =
|
|
self.getStaticSymbol(selectTarget.filePath, selectTarget.name, members);
|
|
const declarationValue = resolveReferenceValue(selectContext);
|
|
if (declarationValue != null) {
|
|
return simplifyNested(selectContext, declarationValue);
|
|
} else {
|
|
return selectContext;
|
|
}
|
|
}
|
|
if (selectTarget && isPrimitive(member))
|
|
return simplifyNested(selectContext, selectTarget[member]);
|
|
return null;
|
|
case 'reference':
|
|
// Note: This only has to deal with variable references, as symbol references have
|
|
// been converted into 'resolved'
|
|
// in the StaticSymbolResolver.
|
|
const name: string = expression['name'];
|
|
const localValue = scope.resolve(name);
|
|
if (localValue != BindingScope.missing) {
|
|
return localValue;
|
|
}
|
|
break;
|
|
case 'resolved':
|
|
try {
|
|
return simplify(expression.symbol);
|
|
} catch (e) {
|
|
// If an error is reported evaluating the symbol record the position of the
|
|
// reference in the error so it can
|
|
// be reported in the error message generated from the exception.
|
|
if (isMetadataError(e) && expression.fileName != null &&
|
|
expression.line != null && expression.character != null) {
|
|
e.position = {
|
|
fileName: expression.fileName,
|
|
line: expression.line,
|
|
column: expression.character
|
|
};
|
|
}
|
|
throw e;
|
|
}
|
|
case 'class':
|
|
return context;
|
|
case 'function':
|
|
return context;
|
|
case 'new':
|
|
case 'call':
|
|
// Determine if the function is a built-in conversion
|
|
staticSymbol = simplifyInContext(
|
|
context, expression['expression'], depth + 1, /* references */ 0);
|
|
if (staticSymbol instanceof StaticSymbol) {
|
|
if (staticSymbol === self.injectionToken || staticSymbol === self.opaqueToken) {
|
|
// if somebody calls new InjectionToken, don't create an InjectionToken,
|
|
// but rather return the symbol to which the InjectionToken is assigned to.
|
|
|
|
// OpaqueToken is supported too as it is required by the language service to
|
|
// support v4 and prior versions of Angular.
|
|
return context;
|
|
}
|
|
const argExpressions: any[] = expression['arguments'] || [];
|
|
let converter = self.conversionMap.get(staticSymbol);
|
|
if (converter) {
|
|
const args = argExpressions.map(arg => simplifyNested(context, arg))
|
|
.map(arg => shouldIgnore(arg) ? undefined : arg);
|
|
return converter(context, args);
|
|
} else {
|
|
// Determine if the function is one we can simplify.
|
|
const targetFunction = resolveReferenceValue(staticSymbol);
|
|
return simplifyCall(
|
|
staticSymbol, targetFunction, argExpressions, expression['expression']);
|
|
}
|
|
}
|
|
return IGNORE;
|
|
case 'error':
|
|
let message = expression.message;
|
|
if (expression['line'] != null) {
|
|
self.error(
|
|
{
|
|
message,
|
|
context: expression.context,
|
|
value: expression,
|
|
position: {
|
|
fileName: expression['fileName'],
|
|
line: expression['line'],
|
|
column: expression['character']
|
|
}
|
|
},
|
|
context);
|
|
} else {
|
|
self.error({message, context: expression.context}, context);
|
|
}
|
|
return IGNORE;
|
|
case 'ignore':
|
|
return expression;
|
|
}
|
|
return null;
|
|
}
|
|
return mapStringMap(expression, (value, name) => {
|
|
if (REFERENCE_SET.has(name)) {
|
|
if (name === USE_VALUE && PROVIDE in expression) {
|
|
// If this is a provider expression, check for special tokens that need the value
|
|
// during analysis.
|
|
const provide = simplify(expression.provide);
|
|
if (provide === self.ROUTES || provide == self.ANALYZE_FOR_ENTRY_COMPONENTS) {
|
|
return simplify(value);
|
|
}
|
|
}
|
|
return simplifyLazily(value);
|
|
}
|
|
return simplify(value);
|
|
});
|
|
}
|
|
return IGNORE;
|
|
}
|
|
|
|
return simplify(value);
|
|
}
|
|
|
|
let result: any;
|
|
try {
|
|
result = simplifyInContext(context, value, 0, 0);
|
|
} catch (e) {
|
|
if (this.errorRecorder) {
|
|
this.reportError(e, context);
|
|
} else {
|
|
throw formatMetadataError(e, context);
|
|
}
|
|
}
|
|
if (shouldIgnore(result)) {
|
|
return undefined;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private getTypeMetadata(type: StaticSymbol): {[key: string]: any} {
|
|
const resolvedSymbol = this.symbolResolver.resolveSymbol(type);
|
|
return resolvedSymbol && resolvedSymbol.metadata ? resolvedSymbol.metadata :
|
|
{__symbolic: 'class'};
|
|
}
|
|
|
|
private reportError(error: Error, context: StaticSymbol, path?: string) {
|
|
if (this.errorRecorder) {
|
|
this.errorRecorder(
|
|
formatMetadataError(error, context), (context && context.filePath) || path);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private error(
|
|
{message, summary, advise, position, context, value, symbol, chain}: {
|
|
message: string,
|
|
summary?: string,
|
|
advise?: string,
|
|
position?: Position,
|
|
context?: any,
|
|
value?: any,
|
|
symbol?: StaticSymbol,
|
|
chain?: MetadataMessageChain
|
|
},
|
|
reportingContext: StaticSymbol) {
|
|
this.reportError(
|
|
metadataError(message, summary, advise, position, symbol, context, chain),
|
|
reportingContext);
|
|
}
|
|
}
|
|
|
|
interface Position {
|
|
fileName: string;
|
|
line: number;
|
|
column: number;
|
|
}
|
|
|
|
interface MetadataMessageChain {
|
|
message: string;
|
|
summary?: string;
|
|
position?: Position;
|
|
context?: any;
|
|
symbol?: StaticSymbol;
|
|
next?: MetadataMessageChain;
|
|
}
|
|
|
|
type MetadataError = Error & {
|
|
position?: Position;
|
|
advise?: string;
|
|
summary?: string;
|
|
context?: any;
|
|
symbol?: StaticSymbol;
|
|
chain?: MetadataMessageChain;
|
|
};
|
|
|
|
const METADATA_ERROR = 'ngMetadataError';
|
|
|
|
function metadataError(
|
|
message: string, summary?: string, advise?: string, position?: Position, symbol?: StaticSymbol,
|
|
context?: any, chain?: MetadataMessageChain): MetadataError {
|
|
const error = syntaxError(message) as MetadataError;
|
|
(error as any)[METADATA_ERROR] = true;
|
|
if (advise) error.advise = advise;
|
|
if (position) error.position = position;
|
|
if (summary) error.summary = summary;
|
|
if (context) error.context = context;
|
|
if (chain) error.chain = chain;
|
|
if (symbol) error.symbol = symbol;
|
|
return error;
|
|
}
|
|
|
|
function isMetadataError(error: Error): error is MetadataError {
|
|
return !!(error as any)[METADATA_ERROR];
|
|
}
|
|
|
|
const REFERENCE_TO_NONEXPORTED_CLASS = 'Reference to non-exported class';
|
|
const VARIABLE_NOT_INITIALIZED = 'Variable not initialized';
|
|
const DESTRUCTURE_NOT_SUPPORTED = 'Destructuring not supported';
|
|
const COULD_NOT_RESOLVE_TYPE = 'Could not resolve type';
|
|
const FUNCTION_CALL_NOT_SUPPORTED = 'Function call not supported';
|
|
const REFERENCE_TO_LOCAL_SYMBOL = 'Reference to a local symbol';
|
|
const LAMBDA_NOT_SUPPORTED = 'Lambda not supported';
|
|
|
|
function expandedMessage(message: string, context: any): string {
|
|
switch (message) {
|
|
case REFERENCE_TO_NONEXPORTED_CLASS:
|
|
if (context && context.className) {
|
|
return `References to a non-exported class are not supported in decorators but ${context.className} was referenced.`;
|
|
}
|
|
break;
|
|
case VARIABLE_NOT_INITIALIZED:
|
|
return 'Only initialized variables and constants can be referenced in decorators because the value of this variable is needed by the template compiler';
|
|
case DESTRUCTURE_NOT_SUPPORTED:
|
|
return 'Referencing an exported destructured variable or constant is not supported in decorators and this value is needed by the template compiler';
|
|
case COULD_NOT_RESOLVE_TYPE:
|
|
if (context && context.typeName) {
|
|
return `Could not resolve type ${context.typeName}`;
|
|
}
|
|
break;
|
|
case FUNCTION_CALL_NOT_SUPPORTED:
|
|
if (context && context.name) {
|
|
return `Function calls are not supported in decorators but '${context.name}' was called`;
|
|
}
|
|
return 'Function calls are not supported in decorators';
|
|
case REFERENCE_TO_LOCAL_SYMBOL:
|
|
if (context && context.name) {
|
|
return `Reference to a local (non-exported) symbols are not supported in decorators but '${context.name}' was referenced`;
|
|
}
|
|
break;
|
|
case LAMBDA_NOT_SUPPORTED:
|
|
return `Function expressions are not supported in decorators`;
|
|
}
|
|
return message;
|
|
}
|
|
|
|
function messageAdvise(message: string, context: any): string|undefined {
|
|
switch (message) {
|
|
case REFERENCE_TO_NONEXPORTED_CLASS:
|
|
if (context && context.className) {
|
|
return `Consider exporting '${context.className}'`;
|
|
}
|
|
break;
|
|
case DESTRUCTURE_NOT_SUPPORTED:
|
|
return 'Consider simplifying to avoid destructuring';
|
|
case REFERENCE_TO_LOCAL_SYMBOL:
|
|
if (context && context.name) {
|
|
return `Consider exporting '${context.name}'`;
|
|
}
|
|
break;
|
|
case LAMBDA_NOT_SUPPORTED:
|
|
return `Consider changing the function expression into an exported function`;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function errorSummary(error: MetadataError): string {
|
|
if (error.summary) {
|
|
return error.summary;
|
|
}
|
|
switch (error.message) {
|
|
case REFERENCE_TO_NONEXPORTED_CLASS:
|
|
if (error.context && error.context.className) {
|
|
return `references non-exported class ${error.context.className}`;
|
|
}
|
|
break;
|
|
case VARIABLE_NOT_INITIALIZED:
|
|
return 'is not initialized';
|
|
case DESTRUCTURE_NOT_SUPPORTED:
|
|
return 'is a destructured variable';
|
|
case COULD_NOT_RESOLVE_TYPE:
|
|
return 'could not be resolved';
|
|
case FUNCTION_CALL_NOT_SUPPORTED:
|
|
if (error.context && error.context.name) {
|
|
return `calls '${error.context.name}'`;
|
|
}
|
|
return `calls a function`;
|
|
case REFERENCE_TO_LOCAL_SYMBOL:
|
|
if (error.context && error.context.name) {
|
|
return `references local variable ${error.context.name}`;
|
|
}
|
|
return `references a local variable`;
|
|
}
|
|
return 'contains the error';
|
|
}
|
|
|
|
function mapStringMap(input: {[key: string]: any}, transform: (value: any, key: string) => any):
|
|
{[key: string]: any} {
|
|
if (!input) return {};
|
|
const result: {[key: string]: any} = {};
|
|
Object.keys(input).forEach((key) => {
|
|
const value = transform(input[key], key);
|
|
if (!shouldIgnore(value)) {
|
|
if (HIDDEN_KEY.test(key)) {
|
|
Object.defineProperty(result, key, {enumerable: false, configurable: true, value: value});
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function isPrimitive(o: any): boolean {
|
|
return o === null || (typeof o !== 'function' && typeof o !== 'object');
|
|
}
|
|
|
|
interface BindingScopeBuilder {
|
|
define(name: string, value: any): BindingScopeBuilder;
|
|
done(): BindingScope;
|
|
}
|
|
|
|
abstract class BindingScope {
|
|
abstract resolve(name: string): any;
|
|
public static missing = {};
|
|
public static empty: BindingScope = {resolve: name => BindingScope.missing};
|
|
|
|
public static build(): BindingScopeBuilder {
|
|
const current = new Map<string, any>();
|
|
return {
|
|
define: function(name, value) {
|
|
current.set(name, value);
|
|
return this;
|
|
},
|
|
done: function() {
|
|
return current.size > 0 ? new PopulatedScope(current) : BindingScope.empty;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
class PopulatedScope extends BindingScope {
|
|
constructor(private bindings: Map<string, any>) { super(); }
|
|
|
|
resolve(name: string): any {
|
|
return this.bindings.has(name) ? this.bindings.get(name) : BindingScope.missing;
|
|
}
|
|
}
|
|
|
|
function formatMetadataMessageChain(
|
|
chain: MetadataMessageChain, advise: string | undefined): FormattedMessageChain {
|
|
const expanded = expandedMessage(chain.message, chain.context);
|
|
const nesting = chain.symbol ? ` in '${chain.symbol.name}'` : '';
|
|
const message = `${expanded}${nesting}`;
|
|
const position = chain.position;
|
|
const next: FormattedMessageChain|undefined = chain.next ?
|
|
formatMetadataMessageChain(chain.next, advise) :
|
|
advise ? {message: advise} : undefined;
|
|
return {message, position, next};
|
|
}
|
|
|
|
function formatMetadataError(e: Error, context: StaticSymbol): Error {
|
|
if (isMetadataError(e)) {
|
|
// Produce a formatted version of the and leaving enough information in the original error
|
|
// to recover the formatting information to eventually produce a diagnostic error message.
|
|
const position = e.position;
|
|
const chain: MetadataMessageChain = {
|
|
message: `Error during template compile of '${context.name}'`,
|
|
position: position,
|
|
next: {message: e.message, next: e.chain, context: e.context, symbol: e.symbol}
|
|
};
|
|
const advise = e.advise || messageAdvise(e.message, e.context);
|
|
return formattedError(formatMetadataMessageChain(chain, advise));
|
|
}
|
|
return e;
|
|
}
|