/** * @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 {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompileNgModuleMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenName, tokenReference} from './compile_metadata'; import {CompileReflector} from './compile_reflector'; import {Identifiers, createTokenForExternalReference} from './identifiers'; import {ParseError, ParseSourceSpan} from './parse_util'; import {AttrAst, DirectiveAst, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst} from './template_parser/template_ast'; export class ProviderError extends ParseError { constructor(message: string, span: ParseSourceSpan) { super(span, message); } } export interface QueryWithId { meta: CompileQueryMetadata; queryId: number; } export class ProviderViewContext { /** * @internal */ viewQueries: Map; /** * @internal */ viewProviders: Map; errors: ProviderError[] = []; constructor(public reflector: CompileReflector, public component: CompileDirectiveMetadata) { this.viewQueries = _getViewQueries(component); this.viewProviders = new Map(); component.viewProviders.forEach((provider) => { if (this.viewProviders.get(tokenReference(provider.token)) == null) { this.viewProviders.set(tokenReference(provider.token), true); } }); } } export class ProviderElementContext { private _contentQueries: Map; private _transformedProviders = new Map(); private _seenProviders = new Map(); private _allProviders: Map; private _attrs: {[key: string]: string}; private _queriedTokens = new Map(); public readonly transformedHasViewContainer: boolean = false; constructor( public viewContext: ProviderViewContext, private _parent: ProviderElementContext, private _isViewRoot: boolean, private _directiveAsts: DirectiveAst[], attrs: AttrAst[], refs: ReferenceAst[], isTemplate: boolean, contentQueryStartId: number, private _sourceSpan: ParseSourceSpan) { this._attrs = {}; attrs.forEach((attrAst) => this._attrs[attrAst.name] = attrAst.value); const directivesMeta = _directiveAsts.map(directiveAst => directiveAst.directive); this._allProviders = _resolveProvidersFromDirectives(directivesMeta, _sourceSpan, viewContext.errors); this._contentQueries = _getContentQueries(contentQueryStartId, directivesMeta); Array.from(this._allProviders.values()).forEach((provider) => { this._addQueryReadsTo(provider.token, provider.token, this._queriedTokens); }); if (isTemplate) { const templateRefId = createTokenForExternalReference(this.viewContext.reflector, Identifiers.TemplateRef); this._addQueryReadsTo(templateRefId, templateRefId, this._queriedTokens); } refs.forEach((refAst) => { let defaultQueryValue = refAst.value || createTokenForExternalReference(this.viewContext.reflector, Identifiers.ElementRef); this._addQueryReadsTo({value: refAst.name}, defaultQueryValue, this._queriedTokens); }); if (this._queriedTokens.get( this.viewContext.reflector.resolveExternalReference(Identifiers.ViewContainerRef))) { this.transformedHasViewContainer = true; } // create the providers that we know are eager first Array.from(this._allProviders.values()).forEach((provider) => { const eager = provider.eager || this._queriedTokens.get(tokenReference(provider.token)); if (eager) { this._getOrCreateLocalProvider(provider.providerType, provider.token, true); } }); } afterElement() { // collect lazy providers Array.from(this._allProviders.values()).forEach((provider) => { this._getOrCreateLocalProvider(provider.providerType, provider.token, false); }); } get transformProviders(): ProviderAst[] { // Note: Maps keep their insertion order. const lazyProviders: ProviderAst[] = []; const eagerProviders: ProviderAst[] = []; this._transformedProviders.forEach(provider => { if (provider.eager) { eagerProviders.push(provider); } else { lazyProviders.push(provider); } }); return lazyProviders.concat(eagerProviders); } get transformedDirectiveAsts(): DirectiveAst[] { const sortedProviderTypes = this.transformProviders.map(provider => provider.token.identifier); const sortedDirectives = this._directiveAsts.slice(); sortedDirectives.sort( (dir1, dir2) => sortedProviderTypes.indexOf(dir1.directive.type) - sortedProviderTypes.indexOf(dir2.directive.type)); return sortedDirectives; } get queryMatches(): QueryMatch[] { const allMatches: QueryMatch[] = []; this._queriedTokens.forEach((matches: QueryMatch[]) => { allMatches.push(...matches); }); return allMatches; } private _addQueryReadsTo( token: CompileTokenMetadata, defaultValue: CompileTokenMetadata, queryReadTokens: Map) { this._getQueriesFor(token).forEach((query) => { const queryValue = query.meta.read || defaultValue; const tokenRef = tokenReference(queryValue); let queryMatches = queryReadTokens.get(tokenRef); if (!queryMatches) { queryMatches = []; queryReadTokens.set(tokenRef, queryMatches); } queryMatches.push({queryId: query.queryId, value: queryValue}); }); } private _getQueriesFor(token: CompileTokenMetadata): QueryWithId[] { const result: QueryWithId[] = []; let currentEl: ProviderElementContext = this; let distance = 0; let queries: QueryWithId[]|undefined; while (currentEl !== null) { queries = currentEl._contentQueries.get(tokenReference(token)); if (queries) { result.push(...queries.filter((query) => query.meta.descendants || distance <= 1)); } if (currentEl._directiveAsts.length > 0) { distance++; } currentEl = currentEl._parent; } queries = this.viewContext.viewQueries.get(tokenReference(token)); if (queries) { result.push(...queries); } return result; } private _getOrCreateLocalProvider( requestingProviderType: ProviderAstType, token: CompileTokenMetadata, eager: boolean): ProviderAst|null { const resolvedProvider = this._allProviders.get(tokenReference(token)); if (!resolvedProvider || ((requestingProviderType === ProviderAstType.Directive || requestingProviderType === ProviderAstType.PublicService) && resolvedProvider.providerType === ProviderAstType.PrivateService) || ((requestingProviderType === ProviderAstType.PrivateService || requestingProviderType === ProviderAstType.PublicService) && resolvedProvider.providerType === ProviderAstType.Builtin)) { return null; } let transformedProviderAst = this._transformedProviders.get(tokenReference(token)); if (transformedProviderAst) { return transformedProviderAst; } if (this._seenProviders.get(tokenReference(token)) != null) { this.viewContext.errors.push(new ProviderError( `Cannot instantiate cyclic dependency! ${tokenName(token)}`, this._sourceSpan)); return null; } this._seenProviders.set(tokenReference(token), true); const transformedProviders = resolvedProvider.providers.map((provider) => { let transformedUseValue = provider.useValue; let transformedUseExisting = provider.useExisting !; let transformedDeps: CompileDiDependencyMetadata[] = undefined !; if (provider.useExisting != null) { const existingDiDep = this._getDependency( resolvedProvider.providerType, {token: provider.useExisting}, eager) !; if (existingDiDep.token != null) { transformedUseExisting = existingDiDep.token; } else { transformedUseExisting = null !; transformedUseValue = existingDiDep.value; } } else if (provider.useFactory) { const deps = provider.deps || provider.useFactory.diDeps; transformedDeps = deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep, eager) !); } else if (provider.useClass) { const deps = provider.deps || provider.useClass.diDeps; transformedDeps = deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep, eager) !); } return _transformProvider(provider, { useExisting: transformedUseExisting, useValue: transformedUseValue, deps: transformedDeps }); }); transformedProviderAst = _transformProviderAst(resolvedProvider, {eager: eager, providers: transformedProviders}); this._transformedProviders.set(tokenReference(token), transformedProviderAst); return transformedProviderAst; } private _getLocalDependency( requestingProviderType: ProviderAstType, dep: CompileDiDependencyMetadata, eager: boolean = false): CompileDiDependencyMetadata|null { if (dep.isAttribute) { const attrValue = this._attrs[dep.token !.value]; return {isValue: true, value: attrValue == null ? null : attrValue}; } if (dep.token != null) { // access builtints if ((requestingProviderType === ProviderAstType.Directive || requestingProviderType === ProviderAstType.Component)) { if (tokenReference(dep.token) === this.viewContext.reflector.resolveExternalReference(Identifiers.Renderer) || tokenReference(dep.token) === this.viewContext.reflector.resolveExternalReference(Identifiers.ElementRef) || tokenReference(dep.token) === this.viewContext.reflector.resolveExternalReference( Identifiers.ChangeDetectorRef) || tokenReference(dep.token) === this.viewContext.reflector.resolveExternalReference(Identifiers.TemplateRef)) { return dep; } if (tokenReference(dep.token) === this.viewContext.reflector.resolveExternalReference(Identifiers.ViewContainerRef)) { (this as{transformedHasViewContainer: boolean}).transformedHasViewContainer = true; } } // access the injector if (tokenReference(dep.token) === this.viewContext.reflector.resolveExternalReference(Identifiers.Injector)) { return dep; } // access providers if (this._getOrCreateLocalProvider(requestingProviderType, dep.token, eager) != null) { return dep; } } return null; } private _getDependency( requestingProviderType: ProviderAstType, dep: CompileDiDependencyMetadata, eager: boolean = false): CompileDiDependencyMetadata|null { let currElement: ProviderElementContext = this; let currEager: boolean = eager; let result: CompileDiDependencyMetadata|null = null; if (!dep.isSkipSelf) { result = this._getLocalDependency(requestingProviderType, dep, eager); } if (dep.isSelf) { if (!result && dep.isOptional) { result = {isValue: true, value: null}; } } else { // check parent elements while (!result && currElement._parent) { const prevElement = currElement; currElement = currElement._parent; if (prevElement._isViewRoot) { currEager = false; } result = currElement._getLocalDependency(ProviderAstType.PublicService, dep, currEager); } // check @Host restriction if (!result) { if (!dep.isHost || this.viewContext.component.isHost || this.viewContext.component.type.reference === tokenReference(dep.token !) || this.viewContext.viewProviders.get(tokenReference(dep.token !)) != null) { result = dep; } else { result = dep.isOptional ? {isValue: true, value: null} : null; } } } if (!result) { this.viewContext.errors.push( new ProviderError(`No provider for ${tokenName(dep.token!)}`, this._sourceSpan)); } return result; } } export class NgModuleProviderAnalyzer { private _transformedProviders = new Map(); private _seenProviders = new Map(); private _allProviders: Map; private _errors: ProviderError[] = []; constructor( private reflector: CompileReflector, ngModule: CompileNgModuleMetadata, extraProviders: CompileProviderMetadata[], sourceSpan: ParseSourceSpan) { this._allProviders = new Map(); ngModule.transitiveModule.modules.forEach((ngModuleType: CompileTypeMetadata) => { const ngModuleProvider = {token: {identifier: ngModuleType}, useClass: ngModuleType}; _resolveProviders( [ngModuleProvider], ProviderAstType.PublicService, true, sourceSpan, this._errors, this._allProviders, /* isModule */ true); }); _resolveProviders( ngModule.transitiveModule.providers.map(entry => entry.provider).concat(extraProviders), ProviderAstType.PublicService, false, sourceSpan, this._errors, this._allProviders, /* isModule */ false); } parse(): ProviderAst[] { Array.from(this._allProviders.values()).forEach((provider) => { this._getOrCreateLocalProvider(provider.token, provider.eager); }); if (this._errors.length > 0) { const errorString = this._errors.join('\n'); throw new Error(`Provider parse errors:\n${errorString}`); } // Note: Maps keep their insertion order. const lazyProviders: ProviderAst[] = []; const eagerProviders: ProviderAst[] = []; this._transformedProviders.forEach(provider => { if (provider.eager) { eagerProviders.push(provider); } else { lazyProviders.push(provider); } }); return lazyProviders.concat(eagerProviders); } private _getOrCreateLocalProvider(token: CompileTokenMetadata, eager: boolean): ProviderAst|null { const resolvedProvider = this._allProviders.get(tokenReference(token)); if (!resolvedProvider) { return null; } let transformedProviderAst = this._transformedProviders.get(tokenReference(token)); if (transformedProviderAst) { return transformedProviderAst; } if (this._seenProviders.get(tokenReference(token)) != null) { this._errors.push(new ProviderError( `Cannot instantiate cyclic dependency! ${tokenName(token)}`, resolvedProvider.sourceSpan)); return null; } this._seenProviders.set(tokenReference(token), true); const transformedProviders = resolvedProvider.providers.map((provider) => { let transformedUseValue = provider.useValue; let transformedUseExisting = provider.useExisting !; let transformedDeps: CompileDiDependencyMetadata[] = undefined !; if (provider.useExisting != null) { const existingDiDep = this._getDependency({token: provider.useExisting}, eager, resolvedProvider.sourceSpan); if (existingDiDep.token != null) { transformedUseExisting = existingDiDep.token; } else { transformedUseExisting = null !; transformedUseValue = existingDiDep.value; } } else if (provider.useFactory) { const deps = provider.deps || provider.useFactory.diDeps; transformedDeps = deps.map((dep) => this._getDependency(dep, eager, resolvedProvider.sourceSpan)); } else if (provider.useClass) { const deps = provider.deps || provider.useClass.diDeps; transformedDeps = deps.map((dep) => this._getDependency(dep, eager, resolvedProvider.sourceSpan)); } return _transformProvider(provider, { useExisting: transformedUseExisting, useValue: transformedUseValue, deps: transformedDeps }); }); transformedProviderAst = _transformProviderAst(resolvedProvider, {eager: eager, providers: transformedProviders}); this._transformedProviders.set(tokenReference(token), transformedProviderAst); return transformedProviderAst; } private _getDependency( dep: CompileDiDependencyMetadata, eager: boolean = false, requestorSourceSpan: ParseSourceSpan): CompileDiDependencyMetadata { let foundLocal = false; if (!dep.isSkipSelf && dep.token != null) { // access the injector if (tokenReference(dep.token) === this.reflector.resolveExternalReference(Identifiers.Injector) || tokenReference(dep.token) === this.reflector.resolveExternalReference(Identifiers.ComponentFactoryResolver)) { foundLocal = true; // access providers } else if (this._getOrCreateLocalProvider(dep.token, eager) != null) { foundLocal = true; } } return dep; } } function _transformProvider( provider: CompileProviderMetadata, {useExisting, useValue, deps}: {useExisting: CompileTokenMetadata, useValue: any, deps: CompileDiDependencyMetadata[]}) { return { token: provider.token, useClass: provider.useClass, useExisting: useExisting, useFactory: provider.useFactory, useValue: useValue, deps: deps, multi: provider.multi }; } function _transformProviderAst( provider: ProviderAst, {eager, providers}: {eager: boolean, providers: CompileProviderMetadata[]}): ProviderAst { return new ProviderAst( provider.token, provider.multiProvider, provider.eager || eager, providers, provider.providerType, provider.lifecycleHooks, provider.sourceSpan, provider.isModule); } function _resolveProvidersFromDirectives( directives: CompileDirectiveSummary[], sourceSpan: ParseSourceSpan, targetErrors: ParseError[]): Map { const providersByToken = new Map(); directives.forEach((directive) => { const dirProvider: CompileProviderMetadata = {token: {identifier: directive.type}, useClass: directive.type}; _resolveProviders( [dirProvider], directive.isComponent ? ProviderAstType.Component : ProviderAstType.Directive, true, sourceSpan, targetErrors, providersByToken, /* isModule */ false); }); // Note: directives need to be able to overwrite providers of a component! const directivesWithComponentFirst = directives.filter(dir => dir.isComponent).concat(directives.filter(dir => !dir.isComponent)); directivesWithComponentFirst.forEach((directive) => { _resolveProviders( directive.providers, ProviderAstType.PublicService, false, sourceSpan, targetErrors, providersByToken, /* isModule */ false); _resolveProviders( directive.viewProviders, ProviderAstType.PrivateService, false, sourceSpan, targetErrors, providersByToken, /* isModule */ false); }); return providersByToken; } function _resolveProviders( providers: CompileProviderMetadata[], providerType: ProviderAstType, eager: boolean, sourceSpan: ParseSourceSpan, targetErrors: ParseError[], targetProvidersByToken: Map, isModule: boolean) { providers.forEach((provider) => { let resolvedProvider = targetProvidersByToken.get(tokenReference(provider.token)); if (resolvedProvider != null && !!resolvedProvider.multiProvider !== !!provider.multi) { targetErrors.push(new ProviderError( `Mixing multi and non multi provider is not possible for token ${tokenName(resolvedProvider.token)}`, sourceSpan)); } if (!resolvedProvider) { const lifecycleHooks = provider.token.identifier && (provider.token.identifier).lifecycleHooks ? (provider.token.identifier).lifecycleHooks : []; const isUseValue = !(provider.useClass || provider.useExisting || provider.useFactory); resolvedProvider = new ProviderAst( provider.token, !!provider.multi, eager || isUseValue, [provider], providerType, lifecycleHooks, sourceSpan, isModule); targetProvidersByToken.set(tokenReference(provider.token), resolvedProvider); } else { if (!provider.multi) { resolvedProvider.providers.length = 0; } resolvedProvider.providers.push(provider); } }); } function _getViewQueries(component: CompileDirectiveMetadata): Map { // Note: queries start with id 1 so we can use the number in a Bloom filter! let viewQueryId = 1; const viewQueries = new Map(); if (component.viewQueries) { component.viewQueries.forEach( (query) => _addQueryToTokenMap(viewQueries, {meta: query, queryId: viewQueryId++})); } return viewQueries; } function _getContentQueries( contentQueryStartId: number, directives: CompileDirectiveSummary[]): Map { let contentQueryId = contentQueryStartId; const contentQueries = new Map(); directives.forEach((directive, directiveIndex) => { if (directive.queries) { directive.queries.forEach( (query) => _addQueryToTokenMap(contentQueries, {meta: query, queryId: contentQueryId++})); } }); return contentQueries; } function _addQueryToTokenMap(map: Map, query: QueryWithId) { query.meta.selectors.forEach((token: CompileTokenMetadata) => { let entry = map.get(tokenReference(token)); if (!entry) { entry = []; map.set(tokenReference(token), entry); } entry.push(query); }); }