feat(ivy): throw compilation error when providing undecorated classes (#34460)

Adds a compilation error if the consumer tries to pass in an undecorated class into the `providers` of an `NgModule`, or the `providers`/`viewProviders` arrays of a `Directive`/`Component`.

PR Close #34460
This commit is contained in:
crisbeto 2019-12-11 17:59:05 +01:00 committed by Kara Erickson
parent 6057c7a373
commit dcc8ff4ce7
20 changed files with 613 additions and 60 deletions

View File

@ -7,12 +7,13 @@
*/ */
import {ConstantPool} from '@angular/compiler'; import {ConstantPool} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations'; import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles'; import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics'; import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {FileSystem, LogicalFileSystem, absoluteFrom, dirname, resolve} from '../../../src/ngtsc/file_system'; import {FileSystem, LogicalFileSystem, absoluteFrom, dirname, resolve} from '../../../src/ngtsc/file_system';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata'; import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration} from '../../../src/ngtsc/reflection'; import {ClassDeclaration} from '../../../src/ngtsc/reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
@ -30,6 +31,7 @@ import {AnalyzedClass, AnalyzedFile, CompiledClass, CompiledFile, DecorationAnal
import {NOOP_DEPENDENCY_TRACKER, analyzeDecorators, isWithinPackage} from './util'; import {NOOP_DEPENDENCY_TRACKER, analyzeDecorators, isWithinPackage} from './util';
/** /**
* Simple class that resolves and loads files directly from the filesystem. * Simple class that resolves and loads files directly from the filesystem.
*/ */
@ -85,6 +87,7 @@ export class DecorationAnalyzer {
new PartialEvaluator(this.reflectionHost, this.typeChecker, /* dependencyTracker */ null); new PartialEvaluator(this.reflectionHost, this.typeChecker, /* dependencyTracker */ null);
importGraph = new ImportGraph(this.moduleResolver); importGraph = new ImportGraph(this.moduleResolver);
cycleAnalyzer = new CycleAnalyzer(this.importGraph); cycleAnalyzer = new CycleAnalyzer(this.importGraph);
injectableRegistry = new InjectableClassRegistry(this.reflectionHost);
handlers: DecoratorHandler<unknown, unknown, unknown>[] = [ handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler( new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader, this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
@ -92,28 +95,28 @@ export class DecorationAnalyzer {
/* defaultPreserveWhitespaces */ false, /* defaultPreserveWhitespaces */ false,
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat, /* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
NOOP_DEPENDENCY_TRACKER, /* annotateForClosureCompiler */ false), NOOP_DEPENDENCY_TRACKER, this.injectableRegistry, /* annotateForClosureCompiler */ false),
// See the note in ngtsc about why this cast is needed. // See the note in ngtsc about why this cast is needed.
// clang-format off // clang-format off
new DirectiveDecoratorHandler( new DirectiveDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry, this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry,
NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore,
/* annotateForClosureCompiler */ false) as DecoratorHandler<unknown, unknown, unknown>, /* annotateForClosureCompiler */ false) as DecoratorHandler<unknown, unknown, unknown>,
// clang-format on // clang-format on
// Pipe handler must be before injectable handler in list so pipe factories are printed // Pipe handler must be before injectable handler in list so pipe factories are printed
// before injectable factories (so injectable factories can delegate to them) // before injectable factories (so injectable factories can delegate to them)
new PipeDecoratorHandler( new PipeDecoratorHandler(
this.reflectionHost, this.evaluator, this.metaRegistry, this.scopeRegistry, this.reflectionHost, this.evaluator, this.metaRegistry, this.scopeRegistry,
NOOP_DEFAULT_IMPORT_RECORDER, this.isCore), NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore),
new InjectableDecoratorHandler( new InjectableDecoratorHandler(
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore,
/* strictCtorDeps */ false, /* errorOnDuplicateProv */ false), /* strictCtorDeps */ false, this.injectableRegistry, /* errorOnDuplicateProv */ false),
new NgModuleDecoratorHandler( new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry, this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry,
this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null,
this.refEmitter, this.refEmitter,
/* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER, /* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER,
/* annotateForClosureCompiler */ false), /* annotateForClosureCompiler */ false, this.injectableRegistry),
]; ];
migrations: Migration[] = [ migrations: Migration[] = [
new UndecoratedParentMigration(), new UndecoratedParentMigration(),

View File

@ -15,7 +15,7 @@ import {absoluteFrom, relative} from '../../file_system';
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {DependencyTracker} from '../../incremental/api'; import {DependencyTracker} from '../../incremental/api';
import {IndexingContext} from '../../indexer'; import {IndexingContext} from '../../indexer';
import {DirectiveMeta, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata'; import {DirectiveMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, extractDirectiveGuards} from '../../metadata';
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance'; import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
@ -26,10 +26,11 @@ import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300';
import {SubsetOfKeys} from '../../util/src/typescript'; import {SubsetOfKeys} from '../../util/src/typescript';
import {ResourceLoader} from './api'; import {ResourceLoader} from './api';
import {getProviderDiagnostics} from './diagnostics';
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive'; import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {compileNgFactoryDefField} from './factory'; import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata'; import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, makeDuplicateDeclarationError, readBaseClass, unwrapExpression, wrapFunctionExpressionsInParens} from './util'; import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
const EMPTY_MAP = new Map<string, Expression>(); const EMPTY_MAP = new Map<string, Expression>();
const EMPTY_ARRAY: any[] = []; const EMPTY_ARRAY: any[] = [];
@ -53,6 +54,18 @@ export interface ComponentAnalysisData {
guards: ReturnType<typeof extractDirectiveGuards>; guards: ReturnType<typeof extractDirectiveGuards>;
template: ParsedTemplateWithSource; template: ParsedTemplateWithSource;
metadataStmt: Statement|null; metadataStmt: Statement|null;
/**
* Providers extracted from the `providers` field of the component annotation which will require
* an Angular factory definition at runtime.
*/
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
/**
* Providers extracted from the `viewProviders` field of the component annotation which will
* require an Angular factory definition at runtime.
*/
viewProvidersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
} }
export type ComponentResolutionData = Pick<R3ComponentMetadata, ComponentMetadataResolvedFields>; export type ComponentResolutionData = Pick<R3ComponentMetadata, ComponentMetadataResolvedFields>;
@ -71,7 +84,9 @@ export class ComponentDecoratorHandler implements
private enableI18nLegacyMessageIdFormat: boolean, private moduleResolver: ModuleResolver, private enableI18nLegacyMessageIdFormat: boolean, private moduleResolver: ModuleResolver,
private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter, private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter,
private defaultImportRecorder: DefaultImportRecorder, private defaultImportRecorder: DefaultImportRecorder,
private depTracker: DependencyTracker|null, private annotateForClosureCompiler: boolean) {} private depTracker: DependencyTracker|null,
private injectableRegistry: InjectableClassRegistry,
private annotateForClosureCompiler: boolean) {}
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>(); private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
private elementSchemaRegistry = new DomElementSchemaRegistry(); private elementSchemaRegistry = new DomElementSchemaRegistry();
@ -186,12 +201,27 @@ export class ComponentDecoratorHandler implements
} }
}, undefined) !; }, undefined) !;
const viewProviders: Expression|null = component.has('viewProviders') ?
new WrappedNodeExpr( // Note that we could technically combine the `viewProvidersRequiringFactory` and
this.annotateForClosureCompiler ? // `providersRequiringFactory` into a single set, but we keep the separate so that
wrapFunctionExpressionsInParens(component.get('viewProviders') !) : // we can distinguish where an error is coming from when logging the diagnostics in `resolve`.
component.get('viewProviders') !) : let viewProvidersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
null; let providersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
let wrappedViewProviders: Expression|null = null;
if (component.has('viewProviders')) {
const viewProviders = component.get('viewProviders') !;
viewProvidersRequiringFactory =
resolveProvidersRequiringFactory(viewProviders, this.reflector, this.evaluator);
wrappedViewProviders = new WrappedNodeExpr(
this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(viewProviders) :
viewProviders);
}
if (component.has('providers')) {
providersRequiringFactory = resolveProvidersRequiringFactory(
component.get('providers') !, this.reflector, this.evaluator);
}
// Parse the template. // Parse the template.
// If a preanalyze phase was executed, the template may already exist in parsed form, so check // If a preanalyze phase was executed, the template may already exist in parsed form, so check
@ -290,7 +320,7 @@ export class ComponentDecoratorHandler implements
// These will be replaced during the compilation step, after all `NgModule`s have been // These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized. // analyzed and the full compilation scope for the component can be realized.
animations, animations,
viewProviders, viewProviders: wrappedViewProviders,
i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath, i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath,
}, },
guards: extractDirectiveGuards(node, this.reflector), guards: extractDirectiveGuards(node, this.reflector),
@ -298,6 +328,8 @@ export class ComponentDecoratorHandler implements
node, this.reflector, this.defaultImportRecorder, this.isCore, node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler), this.annotateForClosureCompiler),
template, template,
providersRequiringFactory,
viewProvidersRequiringFactory,
}, },
}; };
if (changeDetection !== null) { if (changeDetection !== null) {
@ -321,6 +353,8 @@ export class ComponentDecoratorHandler implements
isComponent: true, isComponent: true,
baseClass: analysis.baseClass, ...analysis.guards, baseClass: analysis.baseClass, ...analysis.guards,
}); });
this.injectableRegistry.registerInjectable(node);
} }
index( index(
@ -395,14 +429,6 @@ export class ComponentDecoratorHandler implements
resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>): resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>):
ResolveResult<ComponentResolutionData> { ResolveResult<ComponentResolutionData> {
const duplicateDeclData = this.scopeRegistry.getDuplicateDeclarations(node);
if (duplicateDeclData !== null) {
// This component was declared twice (or more).
return {
diagnostics: [makeDuplicateDeclarationError(node, duplicateDeclData, 'Component')],
};
}
const context = node.getSourceFile(); const context = node.getSourceFile();
// Check whether this component was registered with an NgModule. If so, it should be compiled // Check whether this component was registered with an NgModule. If so, it should be compiled
// under that module's compilation scope. // under that module's compilation scope.
@ -505,6 +531,34 @@ export class ComponentDecoratorHandler implements
this.scopeRegistry.setComponentAsRequiringRemoteScoping(node); this.scopeRegistry.setComponentAsRequiringRemoteScoping(node);
} }
} }
const diagnostics: ts.Diagnostic[] = [];
if (analysis.providersRequiringFactory !== null &&
analysis.meta.providers instanceof WrappedNodeExpr) {
const providerDiagnostics = getProviderDiagnostics(
analysis.providersRequiringFactory, analysis.meta.providers !.node,
this.injectableRegistry);
diagnostics.push(...providerDiagnostics);
}
if (analysis.viewProvidersRequiringFactory !== null &&
analysis.meta.viewProviders instanceof WrappedNodeExpr) {
const viewProviderDiagnostics = getProviderDiagnostics(
analysis.viewProvidersRequiringFactory, analysis.meta.viewProviders !.node,
this.injectableRegistry);
diagnostics.push(...viewProviderDiagnostics);
}
const duplicateDeclData = this.scopeRegistry.getDuplicateDeclarations(node);
if (duplicateDeclData !== null) {
diagnostics.push(makeDuplicateDeclarationError(node, duplicateDeclData, 'Component'));
}
if (diagnostics.length > 0) {
return {diagnostics};
}
return {data}; return {data};
} }

View File

@ -0,0 +1,43 @@
/**
* @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 * as ts from 'typescript';
import {ErrorCode, makeDiagnostic} from '../../diagnostics';
import {Reference} from '../../imports';
import {InjectableClassRegistry} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
/**
* Gets the diagnostics for a set of provider classes.
* @param providerClasses Classes that should be checked.
* @param providersDeclaration Node that declares the providers array.
* @param registry Registry that keeps track of the registered injectable classes.
*/
export function getProviderDiagnostics(
providerClasses: Set<Reference<ClassDeclaration>>, providersDeclaration: ts.Expression,
registry: InjectableClassRegistry): ts.Diagnostic[] {
const diagnostics: ts.Diagnostic[] = [];
for (const provider of providerClasses) {
if (registry.isInjectable(provider.node)) {
continue;
}
const contextNode = provider.getOriginForDiagnostics(providersDeclaration);
diagnostics.push(makeDiagnostic(
ErrorCode.UNDECORATED_PROVIDER, contextNode,
`The class '${provider.node.name.text}' cannot be created via dependency injection, as it does not have an Angular decorator. This will result in an error at runtime.
Either add the @Injectable() decorator to '${provider.node.name.text}', or configure a different provider (such as a provider with 'useFactory').
`,
[{node: provider.node, messageText: `'${provider.node.name.text}' is declared here.`}]));
}
return diagnostics;
}

View File

@ -11,16 +11,17 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder, Reference} from '../../imports'; import {DefaultImportRecorder, Reference} from '../../imports';
import {MetadataRegistry} from '../../metadata'; import {InjectableClassRegistry, MetadataRegistry} from '../../metadata';
import {extractDirectiveGuards} from '../../metadata/src/util'; import {extractDirectiveGuards} from '../../metadata/src/util';
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
import {LocalModuleScopeRegistry} from '../../scope'; import {LocalModuleScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
import {getProviderDiagnostics} from './diagnostics';
import {compileNgFactoryDefField} from './factory'; import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata'; import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, getConstructorDependencies, isAngularDecorator, makeDuplicateDeclarationError, readBaseClass, unwrapConstructorDependencies, unwrapExpression, unwrapForwardRef, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference} from './util'; import {findAngularDecorator, getConstructorDependencies, isAngularDecorator, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapConstructorDependencies, unwrapExpression, unwrapForwardRef, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {}; const EMPTY_OBJECT: {[key: string]: string} = {};
const FIELD_DECORATORS = [ const FIELD_DECORATORS = [
@ -37,13 +38,16 @@ export interface DirectiveHandlerData {
guards: ReturnType<typeof extractDirectiveGuards>; guards: ReturnType<typeof extractDirectiveGuards>;
meta: R3DirectiveMetadata; meta: R3DirectiveMetadata;
metadataStmt: Statement|null; metadataStmt: Statement|null;
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
} }
export class DirectiveDecoratorHandler implements export class DirectiveDecoratorHandler implements
DecoratorHandler<Decorator|null, DirectiveHandlerData, unknown> { DecoratorHandler<Decorator|null, DirectiveHandlerData, unknown> {
constructor( constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
private defaultImportRecorder: DefaultImportRecorder, private isCore: boolean, private defaultImportRecorder: DefaultImportRecorder,
private injectableRegistry: InjectableClassRegistry, private isCore: boolean,
private annotateForClosureCompiler: boolean) {} private annotateForClosureCompiler: boolean) {}
readonly precedence = HandlerPrecedence.PRIMARY; readonly precedence = HandlerPrecedence.PRIMARY;
@ -88,6 +92,12 @@ export class DirectiveDecoratorHandler implements
return {}; return {};
} }
let providersRequiringFactory: Set<Reference<ClassDeclaration>>|null = null;
if (directiveResult !== undefined && directiveResult.decorator.has('providers')) {
providersRequiringFactory = resolveProvidersRequiringFactory(
directiveResult.decorator.get('providers') !, this.reflector, this.evaluator);
}
return { return {
analysis: { analysis: {
meta: analysis, meta: analysis,
@ -95,7 +105,7 @@ export class DirectiveDecoratorHandler implements
node, this.reflector, this.defaultImportRecorder, this.isCore, node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler), this.annotateForClosureCompiler),
baseClass: readBaseClass(node, this.reflector, this.evaluator), baseClass: readBaseClass(node, this.reflector, this.evaluator),
guards: extractDirectiveGuards(node, this.reflector), guards: extractDirectiveGuards(node, this.reflector), providersRequiringFactory
} }
}; };
} }
@ -115,18 +125,28 @@ export class DirectiveDecoratorHandler implements
isComponent: false, isComponent: false,
baseClass: analysis.baseClass, ...analysis.guards, baseClass: analysis.baseClass, ...analysis.guards,
}); });
this.injectableRegistry.registerInjectable(node);
} }
resolve(node: ClassDeclaration): ResolveResult<unknown> { resolve(node: ClassDeclaration, analysis: DirectiveHandlerData): ResolveResult<unknown> {
const diagnostics: ts.Diagnostic[] = [];
if (analysis.providersRequiringFactory !== null &&
analysis.meta.providers instanceof WrappedNodeExpr) {
const providerDiagnostics = getProviderDiagnostics(
analysis.providersRequiringFactory, analysis.meta.providers !.node,
this.injectableRegistry);
diagnostics.push(...providerDiagnostics);
}
const duplicateDeclData = this.scopeRegistry.getDuplicateDeclarations(node); const duplicateDeclData = this.scopeRegistry.getDuplicateDeclarations(node);
if (duplicateDeclData !== null) { if (duplicateDeclData !== null) {
// This directive was declared twice (or more). // This directive was declared twice (or more).
return { diagnostics.push(makeDuplicateDeclarationError(node, duplicateDeclData, 'Directive'));
diagnostics: [makeDuplicateDeclarationError(node, duplicateDeclData, 'Directive')],
};
} }
return {}; return {diagnostics: diagnostics.length > 0 ? diagnostics : undefined};
} }
compile( compile(
@ -160,10 +180,8 @@ export function extractDirectiveMetadata(
clazz: ClassDeclaration, decorator: Readonly<Decorator|null>, reflector: ReflectionHost, clazz: ClassDeclaration, decorator: Readonly<Decorator|null>, reflector: ReflectionHost,
evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean, evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean,
flags: HandlerFlags, annotateForClosureCompiler: boolean, flags: HandlerFlags, annotateForClosureCompiler: boolean,
defaultSelector: string | null = null): { defaultSelector: string | null =
decorator: Map<string, ts.Expression>, null): {decorator: Map<string, ts.Expression>, metadata: R3DirectiveMetadata}|undefined {
metadata: R3DirectiveMetadata,
}|undefined {
let directive: Map<string, ts.Expression>; let directive: Map<string, ts.Expression>;
if (decorator === null || decorator.args === null || decorator.args.length === 0) { if (decorator === null || decorator.args === null || decorator.args.length === 0) {
directive = new Map<string, ts.Expression>(); directive = new Map<string, ts.Expression>();
@ -390,7 +408,6 @@ export function extractQueriesFromDecorator(
view: R3QueryMetadata[], view: R3QueryMetadata[],
} { } {
const content: R3QueryMetadata[] = [], view: R3QueryMetadata[] = []; const content: R3QueryMetadata[] = [], view: R3QueryMetadata[] = [];
const expr = unwrapExpression(queryData);
if (!ts.isObjectLiteralExpression(queryData)) { if (!ts.isObjectLiteralExpression(queryData)) {
throw new Error(`queries metadata must be an object literal`); throw new Error(`queries metadata must be an object literal`);
} }

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder} from '../../imports'; import {DefaultImportRecorder} from '../../imports';
import {InjectableClassRegistry} from '../../metadata';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
@ -33,6 +34,7 @@ export class InjectableDecoratorHandler implements
constructor( constructor(
private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder, private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder,
private isCore: boolean, private strictCtorDeps: boolean, private isCore: boolean, private strictCtorDeps: boolean,
private injectableRegistry: InjectableClassRegistry,
/** /**
* What to do if the injectable already contains a ɵprov property. * What to do if the injectable already contains a ɵprov property.
* *
@ -80,6 +82,8 @@ export class InjectableDecoratorHandler implements
}; };
} }
register(node: ClassDeclaration): void { this.injectableRegistry.registerInjectable(node); }
compile(node: ClassDeclaration, analysis: Readonly<InjectableHandlerData>): CompileResult[] { compile(node: ClassDeclaration, analysis: Readonly<InjectableHandlerData>): CompileResult[] {
const res = compileIvyInjectable(analysis.meta); const res = compileIvyInjectable(analysis.meta);
const statements = res.statements; const statements = res.statements;

View File

@ -11,8 +11,8 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics';
import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports'; import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports';
import {MetadataReader, MetadataRegistry} from '../../metadata'; import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {PartialEvaluator, ResolvedValue, ResolvedValueArray} from '../../partial_evaluator';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
import {NgModuleRouteAnalyzer} from '../../routing'; import {NgModuleRouteAnalyzer} from '../../routing';
import {LocalModuleScopeRegistry, ScopeData} from '../../scope'; import {LocalModuleScopeRegistry, ScopeData} from '../../scope';
@ -20,9 +20,10 @@ import {FactoryTracker} from '../../shims';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
import {getSourceFile} from '../../util/src/typescript'; import {getSourceFile} from '../../util/src/typescript';
import {getProviderDiagnostics} from './diagnostics';
import {generateSetClassMetadataCall} from './metadata'; import {generateSetClassMetadataCall} from './metadata';
import {ReferencesRegistry} from './references_registry'; import {ReferencesRegistry} from './references_registry';
import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, isExpressionForwardReference, toR3Reference, unwrapExpression, wrapFunctionExpressionsInParens, wrapTypeReference} from './util'; import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, isExpressionForwardReference, resolveProvidersRequiringFactory, toR3Reference, unwrapExpression, wrapFunctionExpressionsInParens, wrapTypeReference} from './util';
export interface NgModuleAnalysis { export interface NgModuleAnalysis {
mod: R3NgModuleMetadata; mod: R3NgModuleMetadata;
@ -35,6 +36,8 @@ export interface NgModuleAnalysis {
exports: Reference<ClassDeclaration>[]; exports: Reference<ClassDeclaration>[];
id: Expression|null; id: Expression|null;
factorySymbolName: string; factorySymbolName: string;
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
providers: ts.Expression|null;
} }
export interface NgModuleResolution { injectorImports: Expression[]; } export interface NgModuleResolution { injectorImports: Expression[]; }
@ -54,7 +57,8 @@ export class NgModuleDecoratorHandler implements
private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter, private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter,
private factoryTracker: FactoryTracker|null, private factoryTracker: FactoryTracker|null,
private defaultImportRecorder: DefaultImportRecorder, private defaultImportRecorder: DefaultImportRecorder,
private annotateForClosureCompiler: boolean, private localeId?: string) {} private annotateForClosureCompiler: boolean,
private injectableRegistry: InjectableClassRegistry, private localeId?: string) {}
readonly precedence = HandlerPrecedence.PRIMARY; readonly precedence = HandlerPrecedence.PRIMARY;
readonly name = NgModuleDecoratorHandler.name; readonly name = NgModuleDecoratorHandler.name;
@ -240,7 +244,7 @@ export class NgModuleDecoratorHandler implements
}; };
const rawProviders = ngModule.has('providers') ? ngModule.get('providers') ! : null; const rawProviders = ngModule.has('providers') ? ngModule.get('providers') ! : null;
const providers = rawProviders !== null ? const wrapperProviders = rawProviders !== null ?
new WrappedNodeExpr( new WrappedNodeExpr(
this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(rawProviders) : this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(rawProviders) :
rawProviders) : rawProviders) :
@ -264,7 +268,7 @@ export class NgModuleDecoratorHandler implements
internalType, internalType,
deps: getValidConstructorDependencies( deps: getValidConstructorDependencies(
node, this.reflector, this.defaultImportRecorder, this.isCore), node, this.reflector, this.defaultImportRecorder, this.isCore),
providers, providers: wrapperProviders,
imports: injectorImports, imports: injectorImports,
}; };
@ -277,6 +281,10 @@ export class NgModuleDecoratorHandler implements
declarations: declarationRefs, rawDeclarations, declarations: declarationRefs, rawDeclarations,
imports: importRefs, imports: importRefs,
exports: exportRefs, exports: exportRefs,
providers: rawProviders,
providersRequiringFactory: rawProviders ?
resolveProvidersRequiringFactory(rawProviders, this.reflector, this.evaluator) :
null,
metadataStmt: generateSetClassMetadataCall( metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore, node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler), this.annotateForClosureCompiler),
@ -301,6 +309,8 @@ export class NgModuleDecoratorHandler implements
if (this.factoryTracker !== null) { if (this.factoryTracker !== null) {
this.factoryTracker.track(node.getSourceFile(), analysis.factorySymbolName); this.factoryTracker.track(node.getSourceFile(), analysis.factorySymbolName);
} }
this.injectableRegistry.registerInjectable(node);
} }
resolve(node: ClassDeclaration, analysis: Readonly<NgModuleAnalysis>): resolve(node: ClassDeclaration, analysis: Readonly<NgModuleAnalysis>):
@ -313,6 +323,12 @@ export class NgModuleDecoratorHandler implements
diagnostics.push(...scopeDiagnostics); diagnostics.push(...scopeDiagnostics);
} }
if (analysis.providersRequiringFactory !== null) {
const providerDiagnostics = getProviderDiagnostics(
analysis.providersRequiringFactory, analysis.providers !, this.injectableRegistry);
diagnostics.push(...providerDiagnostics);
}
const data: NgModuleResolution = { const data: NgModuleResolution = {
injectorImports: [], injectorImports: [],
}; };

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder, Reference} from '../../imports'; import {DefaultImportRecorder, Reference} from '../../imports';
import {MetadataRegistry} from '../../metadata'; import {InjectableClassRegistry, MetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {LocalModuleScopeRegistry} from '../../scope'; import {LocalModuleScopeRegistry} from '../../scope';
@ -30,7 +30,8 @@ export class PipeDecoratorHandler implements DecoratorHandler<Decorator, PipeHan
constructor( constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
private defaultImportRecorder: DefaultImportRecorder, private isCore: boolean) {} private defaultImportRecorder: DefaultImportRecorder,
private injectableRegistry: InjectableClassRegistry, private isCore: boolean) {}
readonly precedence = HandlerPrecedence.PRIMARY; readonly precedence = HandlerPrecedence.PRIMARY;
readonly name = PipeDecoratorHandler.name; readonly name = PipeDecoratorHandler.name;
@ -115,6 +116,8 @@ export class PipeDecoratorHandler implements DecoratorHandler<Decorator, PipeHan
register(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): void { register(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): void {
const ref = new Reference(node); const ref = new Reference(node);
this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName}); this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName});
this.injectableRegistry.registerInjectable(node);
} }
resolve(node: ClassDeclaration): ResolveResult<unknown> { resolve(node: ClassDeclaration): ResolveResult<unknown> {

View File

@ -408,6 +408,50 @@ export function makeDuplicateDeclarationError(
`The ${kind} '${node.name.text}' is declared by more than one NgModule.`, context); `The ${kind} '${node.name.text}' is declared by more than one NgModule.`, context);
} }
/**
* Resolves the given `rawProviders` into `ClassDeclarations` and returns
* a set containing those that are known to require a factory definition.
* @param rawProviders Expression that declared the providers array in the source.
*/
export function resolveProvidersRequiringFactory(
rawProviders: ts.Expression, reflector: ReflectionHost,
evaluator: PartialEvaluator): Set<Reference<ClassDeclaration>> {
const providers = new Set<Reference<ClassDeclaration>>();
const resolvedProviders = evaluator.evaluate(rawProviders);
if (!Array.isArray(resolvedProviders)) {
return providers;
}
resolvedProviders.forEach(function processProviders(provider) {
let tokenClass: Reference|null = null;
if (Array.isArray(provider)) {
// If we ran into an array, recurse into it until we've resolve all the classes.
provider.forEach(processProviders);
} else if (provider instanceof Reference) {
tokenClass = provider;
} else if (provider instanceof Map && provider.has('useClass') && !provider.has('deps')) {
const useExisting = provider.get('useClass') !;
if (useExisting instanceof Reference) {
tokenClass = useExisting;
}
}
if (tokenClass !== null && reflector.isClass(tokenClass.node)) {
const constructorParameters = reflector.getConstructorParameters(tokenClass.node);
// Note that we only want to capture providers with a non-trivial constructor,
// because they're the ones that might be using DI and need to be decorated.
if (constructorParameters !== null && constructorParameters.length > 0) {
providers.add(tokenClass as Reference<ClassDeclaration>);
}
}
});
return providers;
}
/** /**
* Create an R3Reference for a class. * Create an R3Reference for a class.
* *

View File

@ -10,7 +10,7 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFrom} from '../../file_system'; import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing'; import {runInEachFileSystem} from '../../file_system/testing';
import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
@ -59,13 +59,14 @@ runInEachFileSystem(() => {
new ReferenceEmitter([]), null); new ReferenceEmitter([]), null);
const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]);
const refEmitter = new ReferenceEmitter([]); const refEmitter = new ReferenceEmitter([]);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const handler = new ComponentDecoratorHandler( const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry,
/* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''], /* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''],
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
/* enableI18nLegacyMessageIdFormat */ false, moduleResolver, cycleAnalyzer, refEmitter, /* enableI18nLegacyMessageIdFormat */ false, moduleResolver, cycleAnalyzer, refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry,
/* annotateForClosureCompiler */ false); /* annotateForClosureCompiler */ false);
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration); const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));

View File

@ -8,7 +8,7 @@
import {absoluteFrom} from '../../file_system'; import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing'; import {runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {ClassDeclaration, TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
@ -48,8 +48,10 @@ runInEachFileSystem(() => {
const scopeRegistry = new LocalModuleScopeRegistry( const scopeRegistry = new LocalModuleScopeRegistry(
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null); null);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const handler = new DirectiveDecoratorHandler( const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, reflectionHost, evaluator, scopeRegistry, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
injectableRegistry,
/* isCore */ false, /* annotateForClosureCompiler */ false); /* isCore */ false, /* annotateForClosureCompiler */ false);
const analyzeDirective = (dirName: string) => { const analyzeDirective = (dirName: string) => {

View File

@ -9,6 +9,7 @@ import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics';
import {absoluteFrom} from '../../file_system'; import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing'; import {runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../imports'; import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../imports';
import {InjectableClassRegistry} from '../../metadata';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing'; import {getDeclaration, makeProgram} from '../../testing';
import {InjectableDecoratorHandler} from '../src/injectable'; import {InjectableDecoratorHandler} from '../src/injectable';
@ -67,9 +68,10 @@ function setupHandler(errorOnDuplicateProv: boolean) {
]); ]);
const checker = program.getTypeChecker(); const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const handler = new InjectableDecoratorHandler( const handler = new InjectableDecoratorHandler(
reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, /* isCore */ false, reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, /* isCore */ false,
/* strictCtorDeps */ false, errorOnDuplicateProv); /* strictCtorDeps */ false, injectableRegistry, errorOnDuplicateProv);
const TestClass = getDeclaration(program, ENTRY_FILE, 'TestClass', isNamedClassDeclaration); const TestClass = getDeclaration(program, ENTRY_FILE, 'TestClass', isNamedClassDeclaration);
const ɵprov = reflectionHost.getMembersOfClass(TestClass).find(member => member.name === 'ɵprov'); const ɵprov = reflectionHost.getMembersOfClass(TestClass).find(member => member.name === 'ɵprov');
if (ɵprov === undefined) { if (ɵprov === undefined) {

View File

@ -12,7 +12,7 @@ import * as ts from 'typescript';
import {absoluteFrom} from '../../file_system'; import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing'; import {runInEachFileSystem} from '../../file_system/testing';
import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
@ -66,11 +66,12 @@ runInEachFileSystem(() => {
metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null),
new ReferenceEmitter([]), null); new ReferenceEmitter([]), null);
const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const handler = new NgModuleDecoratorHandler( const handler = new NgModuleDecoratorHandler(
reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry,
/* isCore */ false, /* routeAnalyzer */ null, refEmitter, /* factoryTracker */ null, /* isCore */ false, /* routeAnalyzer */ null, refEmitter, /* factoryTracker */ null,
NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false); NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false, injectableRegistry);
const TestModule = const TestModule =
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
const detected = const detected =

View File

@ -26,6 +26,9 @@ export enum ErrorCode {
PARAM_MISSING_TOKEN = 2003, PARAM_MISSING_TOKEN = 2003,
DIRECTIVE_MISSING_SELECTOR = 2004, DIRECTIVE_MISSING_SELECTOR = 2004,
/** Raised when an undecorated class is passed in as a provider to a module or a directive. */
UNDECORATED_PROVIDER = 2005,
SYMBOL_NOT_EXPORTED = 3001, SYMBOL_NOT_EXPORTED = 3001,
SYMBOL_EXPORTED_UNDER_DIFFERENT_NAME = 3002, SYMBOL_EXPORTED_UNDER_DIFFERENT_NAME = 3002,

View File

@ -8,5 +8,5 @@
export * from './src/api'; export * from './src/api';
export {DtsMetadataReader} from './src/dts'; export {DtsMetadataReader} from './src/dts';
export {CompoundMetadataRegistry, LocalMetadataRegistry} from './src/registry'; export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry';
export {extractDirectiveGuards, CompoundMetadataReader} from './src/util'; export {extractDirectiveGuards, CompoundMetadataReader} from './src/util';

View File

@ -7,9 +7,10 @@
*/ */
import {Reference} from '../../imports'; import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection'; import {ClassDeclaration, ReflectionHost} from '../../reflection';
import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from './api'; import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from './api';
import {hasInjectableFields} from './util';
/** /**
* A registry of directive, pipe, and module metadata for types defined in the current compilation * A registry of directive, pipe, and module metadata for types defined in the current compilation
@ -59,3 +60,22 @@ export class CompoundMetadataRegistry implements MetadataRegistry {
} }
} }
} }
/**
* Registry that keeps track of classes that can be constructed via dependency injection (e.g.
* injectables, directives, pipes).
*/
export class InjectableClassRegistry {
private classes = new Set<ClassDeclaration>();
constructor(private host: ReflectionHost) {}
registerInjectable(declaration: ClassDeclaration): void { this.classes.add(declaration); }
isInjectable(declaration: ClassDeclaration): boolean {
// Figure out whether the class is injectable based on the registered classes, otherwise
// fall back to looking at its members since we might not have been able register the class
// if it was compiled already.
return this.classes.has(declaration) || hasInjectableFields(declaration, this.host);
}
}

View File

@ -174,3 +174,10 @@ function afterUnderscore(str: string): string {
} }
return str.substr(pos + 1); return str.substr(pos + 1);
} }
/** Returns whether a class declaration has the necessary class fields to make it injectable. */
export function hasInjectableFields(clazz: ClassDeclaration, host: ReflectionHost): boolean {
const members = host.getMembersOfClass(clazz);
return members.some(
({isStatic, name}) => isStatic && (name === 'ɵprov' || name === 'ɵfac' || name === 'ɵinj'));
}

View File

@ -23,6 +23,7 @@ import {IncrementalDriver} from './incremental';
import {IndexedComponent, IndexingContext} from './indexer'; import {IndexedComponent, IndexingContext} from './indexer';
import {generateAnalysis} from './indexer/src/transform'; import {generateAnalysis} from './indexer/src/transform';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata'; import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata';
import {InjectableClassRegistry} from './metadata/src/registry';
import {ModuleWithProvidersScanner} from './modulewithproviders'; import {ModuleWithProvidersScanner} from './modulewithproviders';
import {PartialEvaluator} from './partial_evaluator'; import {PartialEvaluator} from './partial_evaluator';
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf'; import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
@ -629,6 +630,7 @@ export class NgtscProgram implements api.Program {
localMetaReader, depScopeReader, this.refEmitter, this.aliasingHost); localMetaReader, depScopeReader, this.refEmitter, this.aliasingHost);
const scopeReader: ComponentScopeReader = this.scopeRegistry; const scopeReader: ComponentScopeReader = this.scopeRegistry;
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, this.scopeRegistry]); const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, this.scopeRegistry]);
const injectableRegistry = new InjectableClassRegistry(this.reflector);
this.metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]); this.metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]);
@ -658,26 +660,27 @@ export class NgtscProgram implements api.Program {
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false, this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.moduleResolver, this.options.enableI18nLegacyMessageIdFormat !== false, this.moduleResolver,
this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker, this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker,
this.incrementalDriver.depGraph, this.closureCompilerEnabled), this.incrementalDriver.depGraph, injectableRegistry, this.closureCompilerEnabled),
// TODO(alxhub): understand why the cast here is necessary (something to do with `null` not // TODO(alxhub): understand why the cast here is necessary (something to do with `null` not
// being assignable to `unknown` when wrapped in `Readonly`). // being assignable to `unknown` when wrapped in `Readonly`).
// clang-format off // clang-format off
new DirectiveDecoratorHandler( new DirectiveDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.scopeRegistry, this.defaultImportTracker, this.reflector, evaluator, metaRegistry, this.scopeRegistry, this.defaultImportTracker, injectableRegistry,
this.isCore, this.closureCompilerEnabled) as Readonly<DecoratorHandler<unknown, unknown, unknown>>, this.isCore, this.closureCompilerEnabled) as Readonly<DecoratorHandler<unknown, unknown, unknown>>,
// clang-format on // clang-format on
// Pipe handler must be before injectable handler in list so pipe factories are printed // Pipe handler must be before injectable handler in list so pipe factories are printed
// before injectable factories (so injectable factories can delegate to them) // before injectable factories (so injectable factories can delegate to them)
new PipeDecoratorHandler( new PipeDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.scopeRegistry, this.defaultImportTracker, this.reflector, evaluator, metaRegistry, this.scopeRegistry, this.defaultImportTracker,
this.isCore), injectableRegistry, this.isCore),
new InjectableDecoratorHandler( new InjectableDecoratorHandler(
this.reflector, this.defaultImportTracker, this.isCore, this.reflector, this.defaultImportTracker, this.isCore,
this.options.strictInjectionParameters || false), this.options.strictInjectionParameters || false, injectableRegistry),
new NgModuleDecoratorHandler( new NgModuleDecoratorHandler(
this.reflector, evaluator, this.metaReader, metaRegistry, this.scopeRegistry, this.reflector, evaluator, this.metaReader, metaRegistry, this.scopeRegistry,
referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, this.factoryTracker, referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, this.factoryTracker,
this.defaultImportTracker, this.closureCompilerEnabled, this.options.i18nInLocale), this.defaultImportTracker, this.closureCompilerEnabled, injectableRegistry,
this.options.i18nInLocale),
]; ];
return new TraitCompiler( return new TraitCompiler(

View File

@ -363,7 +363,7 @@ export class TraitCompiler {
} }
} }
if (result.diagnostics !== undefined) { if (result.diagnostics !== undefined && result.diagnostics.length > 0) {
trait = trait.toErrored(result.diagnostics); trait = trait.toErrored(result.diagnostics);
} else { } else {
if (result.data !== undefined) { if (result.data !== undefined) {

View File

@ -89,3 +89,5 @@ export class EventEmitter<T> {
export interface QueryList<T>/* implements Iterable<T> */ { [Symbol.iterator]: () => Iterator<T>; } export interface QueryList<T>/* implements Iterable<T> */ { [Symbol.iterator]: () => Iterator<T>; }
export type NgIterable<T> = Array<T>| Iterable<T>; export type NgIterable<T> = Array<T>| Iterable<T>;
export class NgZone {}

View File

@ -5222,6 +5222,334 @@ export const Foo = Foo__PRE_R3__;
}); });
}); });
describe('undecorated providers', () => {
it('should error when an undecorated class, with a non-trivial constructor, is provided directly in a module',
() => {
env.write('test.ts', `
import {NgModule, NgZone} from '@angular/core';
class NotAService {
constructor(ngZone: NgZone) {}
}
@NgModule({
providers: [NotAService]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('cannot be created via dependency injection');
});
it('should error when an undecorated class is provided via useClass', () => {
env.write('test.ts', `
import {NgModule, Injectable, NgZone} from '@angular/core';
@Injectable({providedIn: 'root'})
class Service {}
class NotAService {
constructor(ngZone: NgZone) {}
}
@NgModule({
providers: [{provide: Service, useClass: NotAService}]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('cannot be created via dependency injection');
});
it('should not error when an undecorated class is provided via useClass with deps', () => {
env.write('test.ts', `
import {NgModule, Injectable, NgZone} from '@angular/core';
@Injectable({providedIn: 'root'})
class Service {}
class NotAService {
constructor(ngZone: NgZone) {}
}
@NgModule({
providers: [{provide: Service, useClass: NotAService, deps: [NgZone]}]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should error when an undecorated class is provided via an array', () => {
env.write('test.ts', `
import {NgModule, Injectable, NgZone} from '@angular/core';
@Injectable({providedIn: 'root'})
class Service {}
class NotAService {
constructor(ngZone: NgZone) {}
}
@NgModule({
providers: [Service, [NotAService]]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('cannot be created via dependency injection');
});
it('should error when an undecorated class is provided to a directive', () => {
env.write('test.ts', `
import {NgModule, Directive, NgZone} from '@angular/core';
class NotAService {
constructor(ngZone: NgZone) {}
}
@Directive({
selector: '[some-dir]',
providers: [NotAService]
})
class SomeDirective {}
@NgModule({
declarations: [SomeDirective]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('cannot be created via dependency injection');
});
it('should error when an undecorated class is provided to a component', () => {
env.write('test.ts', `
import {NgModule, Component, NgZone} from '@angular/core';
class NotAService {
constructor(ngZone: NgZone) {}
}
@Component({
selector: 'some-comp',
template: '',
providers: [NotAService]
})
class SomeComponent {}
@NgModule({
declarations: [SomeComponent]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('cannot be created via dependency injection');
});
it('should error when an undecorated class is provided to a component via viewProviders',
() => {
env.write('test.ts', `
import {NgModule, Component, NgZone} from '@angular/core';
class NotAService {
constructor(ngZone: NgZone) {}
}
@Component({
selector: 'some-comp',
template: '',
viewProviders: [NotAService]
})
class SomeComponent {}
@NgModule({
declarations: [SomeComponent]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('cannot be created via dependency injection');
});
it('should not error when a class with a factory is provided', () => {
env.write('test.ts', `
import {NgModule, Pipe} from '@angular/core';
@Pipe({
name: 'some-pipe'
})
class SomePipe {}
@NgModule({
declarations: [SomePipe],
providers: [SomePipe]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should not error when an NgModule is provided', () => {
env.write('test.ts', `
import {Injectable, NgModule} from '@angular/core';
@Injectable()
export class Service {}
@NgModule({
})
class SomeModule {
constructor(dep: Service) {}
}
@NgModule({
providers: [SomeModule],
})
export class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should not error when an undecorated class from a declaration file is provided', () => {
env.write('node_modules/@angular/core/testing/index.d.ts', `
export declare class Testability {
}
`);
env.write('test.ts', `
import {NgModule} from '@angular/core';
import {Testability} from '@angular/core/testing';
@NgModule({
providers: [Testability]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should not error when an undecorated class without a constructor from a declaration file is provided via useClass',
() => {
env.write('node_modules/@angular/core/testing/index.d.ts', `
export declare class Testability {
}
`);
env.write('test.ts', `
import {NgModule, Injectable} from '@angular/core';
import {Testability} from '@angular/core/testing';
@Injectable()
class TestingService {}
@NgModule({
providers: [{provide: TestingService, useClass: Testability}]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should not error if the undecorated class does not have a constructor or the constructor is blank',
() => {
env.write('test.ts', `
import {NgModule, NgZone} from '@angular/core';
class NoConstructorService {
}
class BlankConstructorService {
}
@NgModule({
providers: [NoConstructorService, BlankConstructorService]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should error when an undecorated class with a non-trivial constructor in a declaration file is provided via useClass',
() => {
env.write('node_modules/@angular/core/testing/index.d.ts', `
export declare class NgZone {}
export declare class Testability {
constructor(ngZone: NgZone) {}
}
`);
env.write('test.ts', `
import {NgModule, Injectable} from '@angular/core';
import {Testability} from '@angular/core/testing';
@Injectable()
class TestingService {}
@NgModule({
providers: [{provide: TestingService, useClass: Testability}]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('cannot be created via dependency injection');
});
it('should not error when an class with a factory definition and a non-trivial constructor in a declaration file is provided via useClass',
() => {
env.write('node_modules/@angular/core/testing/index.d.ts', `
import * as i0 from '@angular/core';
export declare class NgZone {}
export declare class Testability {
static ɵfac: i0.ɵɵFactoryDef<Testability>;
constructor(ngZone: NgZone) {}
}
`);
env.write('test.ts', `
import {NgModule, Injectable} from '@angular/core';
import {Testability} from '@angular/core/testing';
@Injectable()
class TestingService {}
@NgModule({
providers: [{provide: TestingService, useClass: Testability}]
})
export class SomeModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
});
}); });
function expectTokenAtPosition<T extends ts.Node>( function expectTokenAtPosition<T extends ts.Node>(