diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index b8bd0b9634..a4c5f105f6 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -15,20 +15,19 @@ import {FileSystem, LogicalFileSystem, absoluteFrom, dirname, resolve} from '../ import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports'; import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; -import {ClassDeclaration} from '../../../src/ngtsc/reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; -import {CompileResult, DecoratorHandler} from '../../../src/ngtsc/transform'; -import {NgccClassSymbol, NgccReflectionHost} from '../host/ngcc_host'; +import {DecoratorHandler} from '../../../src/ngtsc/transform'; +import {NgccReflectionHost} from '../host/ngcc_host'; import {Migration} from '../migrations/migration'; import {MissingInjectableMigration} from '../migrations/missing_injectable_migration'; import {UndecoratedChildMigration} from '../migrations/undecorated_child_migration'; import {UndecoratedParentMigration} from '../migrations/undecorated_parent_migration'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {isDefined} from '../utils'; import {DefaultMigrationHost} from './migration_host'; -import {AnalyzedClass, AnalyzedFile, CompiledClass, CompiledFile, DecorationAnalyses} from './types'; -import {NOOP_DEPENDENCY_TRACKER, analyzeDecorators, isWithinPackage} from './util'; +import {NgccTraitCompiler} from './ngcc_trait_compiler'; +import {CompiledClass, CompiledFile, DecorationAnalyses} from './types'; +import {NOOP_DEPENDENCY_TRACKER, isWithinPackage} from './util'; @@ -57,10 +56,6 @@ export class DecorationAnalyzer { private packagePath = this.bundle.entryPoint.package; private isCore = this.bundle.isCore; - /** - * Map of NgModule declarations to the re-exports for that NgModule. - */ - private reexportMap = new Map>(); moduleResolver = new ModuleResolver(this.program, this.options, this.host, /* moduleResolutionCache */ null); resourceManager = new NgccResourceLoader(this.fs); @@ -118,6 +113,7 @@ export class DecorationAnalyzer { /* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false, this.injectableRegistry), ]; + compiler = new NgccTraitCompiler(this.handlers, this.reflectionHost); migrations: Migration[] = [ new UndecoratedParentMigration(), new UndecoratedChildMigration(), @@ -135,56 +131,54 @@ export class DecorationAnalyzer { * @returns a map of the source files to the analysis for those files. */ analyzeProgram(): DecorationAnalyses { + for (const sourceFile of this.program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile && isWithinPackage(this.packagePath, sourceFile)) { + this.compiler.analyzeFile(sourceFile); + } + } + + this.applyMigrations(); + + this.compiler.resolve(); + + this.reportDiagnostics(); + const decorationAnalyses = new DecorationAnalyses(); - const analyzedFiles = this.program.getSourceFiles() - .filter(sourceFile => !sourceFile.isDeclarationFile) - .filter(sourceFile => isWithinPackage(this.packagePath, sourceFile)) - .map(sourceFile => this.analyzeFile(sourceFile)) - .filter(isDefined); - - this.applyMigrations(analyzedFiles); - - analyzedFiles.forEach(analyzedFile => this.resolveFile(analyzedFile)); - const compiledFiles = analyzedFiles.map(analyzedFile => this.compileFile(analyzedFile)); - compiledFiles.forEach( - compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile)); + for (const analyzedFile of this.compiler.analyzedFiles) { + const compiledFile = this.compileFile(analyzedFile); + decorationAnalyses.set(compiledFile.sourceFile, compiledFile); + } return decorationAnalyses; } - protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined { - const analyzedClasses = this.reflectionHost.findClassSymbols(sourceFile) - .map(symbol => this.analyzeClass(symbol)) - .filter(isDefined); - return analyzedClasses.length ? {sourceFile, analyzedClasses} : undefined; - } - - protected analyzeClass(symbol: NgccClassSymbol): AnalyzedClass|null { - const decorators = this.reflectionHost.getDecoratorsOfSymbol(symbol); - const analyzedClass = analyzeDecorators(symbol, decorators, this.handlers); - if (analyzedClass !== null && analyzedClass.diagnostics !== undefined) { - for (const diagnostic of analyzedClass.diagnostics) { - this.diagnosticHandler(diagnostic); - } - } - return analyzedClass; - } - - protected applyMigrations(analyzedFiles: AnalyzedFile[]): void { + protected applyMigrations(): void { const migrationHost = new DefaultMigrationHost( - this.reflectionHost, this.fullMetaReader, this.evaluator, this.handlers, - this.bundle.entryPoint.path, analyzedFiles, this.diagnosticHandler); + this.reflectionHost, this.fullMetaReader, this.evaluator, this.compiler, + this.bundle.entryPoint.path); this.migrations.forEach(migration => { - analyzedFiles.forEach(analyzedFile => { - analyzedFile.analyzedClasses.forEach(({declaration}) => { + this.compiler.analyzedFiles.forEach(analyzedFile => { + const records = this.compiler.recordsFor(analyzedFile); + if (records === null) { + throw new Error('Assertion error: file to migrate must have records.'); + } + + records.forEach(record => { + const addDiagnostic = (diagnostic: ts.Diagnostic) => { + if (record.metaDiagnostics === null) { + record.metaDiagnostics = []; + } + record.metaDiagnostics.push(diagnostic); + }; + try { - const result = migration.apply(declaration, migrationHost); + const result = migration.apply(record.node, migrationHost); if (result !== null) { - this.diagnosticHandler(result); + addDiagnostic(result); } } catch (e) { if (isFatalDiagnosticError(e)) { - this.diagnosticHandler(e.toDiagnostic()); + addDiagnostic(e.toDiagnostic()); } else { throw e; } @@ -194,67 +188,45 @@ export class DecorationAnalyzer { }); } - protected compileFile(analyzedFile: AnalyzedFile): CompiledFile { + protected reportDiagnostics() { this.compiler.diagnostics.forEach(this.diagnosticHandler); } + + protected compileFile(sourceFile: ts.SourceFile): CompiledFile { const constantPool = new ConstantPool(); - const compiledClasses: CompiledClass[] = analyzedFile.analyzedClasses.map(analyzedClass => { - const compilation = this.compileClass(analyzedClass, constantPool); - const declaration = analyzedClass.declaration; - const reexports: Reexport[] = this.getReexportsForClass(declaration); - return {...analyzedClass, compilation, reexports}; - }); - return {constantPool, sourceFile: analyzedFile.sourceFile, compiledClasses}; - } - - protected compileClass(clazz: AnalyzedClass, constantPool: ConstantPool): CompileResult[] { - const compilations: CompileResult[] = []; - for (const {handler, analysis, resolution} of clazz.matches) { - const result = handler.compile(clazz.declaration, analysis, resolution, constantPool); - if (Array.isArray(result)) { - result.forEach(current => { - if (!compilations.some(compilation => compilation.name === current.name)) { - compilations.push(current); - } - }); - } else if (!compilations.some(compilation => compilation.name === result.name)) { - compilations.push(result); - } + const records = this.compiler.recordsFor(sourceFile); + if (records === null) { + throw new Error('Assertion error: file to compile must have records.'); } - return compilations; - } - protected resolveFile(analyzedFile: AnalyzedFile): void { - for (const {declaration, matches} of analyzedFile.analyzedClasses) { - for (const match of matches) { - const {handler, analysis} = match; - if ((handler.resolve !== undefined) && analysis) { - const {reexports, diagnostics, data} = handler.resolve(declaration, analysis); - if (reexports !== undefined) { - this.addReexports(reexports, declaration); - } - if (diagnostics !== undefined) { - diagnostics.forEach(error => this.diagnosticHandler(error)); - } - match.resolution = data as Readonly; - } + const compiledClasses: CompiledClass[] = []; + + for (const record of records) { + const compilation = this.compiler.compile(record.node, constantPool); + if (compilation === null) { + continue; } - } - } - private getReexportsForClass(declaration: ClassDeclaration) { - const reexports: Reexport[] = []; - if (this.reexportMap.has(declaration)) { - this.reexportMap.get(declaration) !.forEach(([fromModule, symbolName], asAlias) => { - reexports.push({asAlias, fromModule, symbolName}); + compiledClasses.push({ + name: record.node.name.text, + decorators: this.compiler.getAllDecorators(record.node), + declaration: record.node, compilation }); } - return reexports; + + const reexports = this.getReexportsForSourceFile(sourceFile); + return {constantPool, sourceFile: sourceFile, compiledClasses, reexports}; } - private addReexports(reexports: Reexport[], declaration: ClassDeclaration) { - const map = new Map(); - for (const reexport of reexports) { - map.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]); + private getReexportsForSourceFile(sf: ts.SourceFile): Reexport[] { + const exportStatements = this.compiler.exportStatements; + if (!exportStatements.has(sf.fileName)) { + return []; } - this.reexportMap.set(declaration, map); + const exports = exportStatements.get(sf.fileName) !; + + const reexports: Reexport[] = []; + exports.forEach(([fromModule, symbolName], asAlias) => { + reexports.push({asAlias, fromModule, symbolName}); + }); + return reexports; } } diff --git a/packages/compiler-cli/ngcc/src/analysis/migration_host.ts b/packages/compiler-cli/ngcc/src/analysis/migration_host.ts index 9ed169ece2..d52476f359 100644 --- a/packages/compiler-cli/ngcc/src/analysis/migration_host.ts +++ b/packages/compiler-cli/ngcc/src/analysis/migration_host.ts @@ -7,66 +7,40 @@ */ import * as ts from 'typescript'; -import {ErrorCode, FatalDiagnosticError} from '../../../src/ngtsc/diagnostics'; import {AbsoluteFsPath} from '../../../src/ngtsc/file_system'; import {MetadataReader} from '../../../src/ngtsc/metadata'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; -import {DecoratorHandler, HandlerFlags} from '../../../src/ngtsc/transform'; +import {HandlerFlags, TraitState} from '../../../src/ngtsc/transform'; import {NgccReflectionHost} from '../host/ngcc_host'; import {MigrationHost} from '../migrations/migration'; -import {AnalyzedClass, AnalyzedFile} from './types'; -import {analyzeDecorators, isWithinPackage} from './util'; +import {NgccTraitCompiler} from './ngcc_trait_compiler'; +import {isWithinPackage} from './util'; /** - * The standard implementation of `MigrationHost`, which is created by the - * `DecorationAnalyzer`. + * The standard implementation of `MigrationHost`, which is created by the `DecorationAnalyzer`. */ export class DefaultMigrationHost implements MigrationHost { constructor( readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader, - readonly evaluator: PartialEvaluator, - private handlers: DecoratorHandler[], - private entryPointPath: AbsoluteFsPath, private analyzedFiles: AnalyzedFile[], - private diagnosticHandler: (error: ts.Diagnostic) => void) {} + readonly evaluator: PartialEvaluator, private compiler: NgccTraitCompiler, + private entryPointPath: AbsoluteFsPath) {} injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags): void { - const classSymbol = this.reflectionHost.getClassSymbol(clazz) !; - const newAnalyzedClass = analyzeDecorators(classSymbol, [decorator], this.handlers, flags); - if (newAnalyzedClass === null) { - return; - } + const migratedTraits = this.compiler.injectSyntheticDecorator(clazz, decorator, flags); - if (newAnalyzedClass.diagnostics !== undefined) { - for (const diagnostic of newAnalyzedClass.diagnostics) { - this.diagnosticHandler(createMigrationDiagnostic(diagnostic, clazz, decorator)); + for (const trait of migratedTraits) { + if (trait.state === TraitState.ERRORED) { + trait.diagnostics = + trait.diagnostics.map(diag => createMigrationDiagnostic(diag, clazz, decorator)); } } - - const analyzedFile = getOrCreateAnalyzedFile(this.analyzedFiles, clazz.getSourceFile()); - const oldAnalyzedClass = analyzedFile.analyzedClasses.find(c => c.declaration === clazz); - if (oldAnalyzedClass === undefined) { - analyzedFile.analyzedClasses.push(newAnalyzedClass); - } else { - mergeAnalyzedClasses(oldAnalyzedClass, newAnalyzedClass); - } } getAllDecorators(clazz: ClassDeclaration): Decorator[]|null { - const sourceFile = clazz.getSourceFile(); - const analyzedFile = this.analyzedFiles.find(file => file.sourceFile === sourceFile); - if (analyzedFile === undefined) { - return null; - } - - const analyzedClass = analyzedFile.analyzedClasses.find(c => c.declaration === clazz); - if (analyzedClass === undefined) { - return null; - } - - return analyzedClass.decorators; + return this.compiler.getAllDecorators(clazz); } isInScope(clazz: ClassDeclaration): boolean { @@ -74,43 +48,6 @@ export class DefaultMigrationHost implements MigrationHost { } } -function getOrCreateAnalyzedFile( - analyzedFiles: AnalyzedFile[], sourceFile: ts.SourceFile): AnalyzedFile { - const analyzedFile = analyzedFiles.find(file => file.sourceFile === sourceFile); - if (analyzedFile !== undefined) { - return analyzedFile; - } else { - const newAnalyzedFile: AnalyzedFile = {sourceFile, analyzedClasses: []}; - analyzedFiles.push(newAnalyzedFile); - return newAnalyzedFile; - } -} - -function mergeAnalyzedClasses(oldClass: AnalyzedClass, newClass: AnalyzedClass) { - if (newClass.decorators !== null) { - if (oldClass.decorators === null) { - oldClass.decorators = newClass.decorators; - } else { - for (const newDecorator of newClass.decorators) { - if (oldClass.decorators.some(d => d.name === newDecorator.name)) { - throw new FatalDiagnosticError( - ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR, newClass.declaration, - `Attempted to inject "${newDecorator.name}" decorator over a pre-existing decorator with the same name on the "${newClass.name}" class.`); - } - } - oldClass.decorators.push(...newClass.decorators); - } - } - - if (newClass.diagnostics !== undefined) { - if (oldClass.diagnostics === undefined) { - oldClass.diagnostics = newClass.diagnostics; - } else { - oldClass.diagnostics.push(...newClass.diagnostics); - } - } -} - /** * Creates a diagnostic from another one, containing additional information about the synthetic * decorator. diff --git a/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts b/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts new file mode 100644 index 0000000000..f4d7f80fba --- /dev/null +++ b/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts @@ -0,0 +1,85 @@ +/** + * @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 {IncrementalBuild} from '../../../src/ngtsc/incremental/api'; +import {NOOP_PERF_RECORDER} from '../../../src/ngtsc/perf'; +import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; +import {DecoratorHandler, DtsTransformRegistry, HandlerFlags, Trait, TraitCompiler} from '../../../src/ngtsc/transform'; +import {NgccReflectionHost} from '../host/ngcc_host'; +import {isDefined} from '../utils'; + +/** + * Specializes the `TraitCompiler` for ngcc purposes. Mainly, this includes an alternative way of + * scanning for classes to compile using the reflection host's `findClassSymbols`, together with + * support to inject synthetic decorators into the compilation for ad-hoc migrations that ngcc + * performs. + */ +export class NgccTraitCompiler extends TraitCompiler { + constructor( + handlers: DecoratorHandler[], + private ngccReflector: NgccReflectionHost) { + super( + handlers, ngccReflector, NOOP_PERF_RECORDER, new NoIncrementalBuild(), + /* compileNonExportedClasses */ true, new DtsTransformRegistry()); + } + + get analyzedFiles(): ts.SourceFile[] { return Array.from(this.fileToClasses.keys()); } + + /** + * Analyzes the source file in search for classes to process. For any class that is found in the + * file, a `ClassRecord` is created and the source file is included in the `analyzedFiles` array. + */ + analyzeFile(sf: ts.SourceFile): void { + const ngccClassSymbols = this.ngccReflector.findClassSymbols(sf); + for (const classSymbol of ngccClassSymbols) { + this.analyzeClass(classSymbol.declaration.valueDeclaration, null); + } + + return undefined; + } + + /** + * Associate a new synthesized decorator, which did not appear in the original source, with a + * given class. + * @param clazz the class to receive the new decorator. + * @param decorator the decorator to inject. + * @param flags optional bitwise flag to influence the compilation of the decorator. + */ + injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags): + Trait[] { + const migratedTraits = this.detectTraits(clazz, [decorator]); + if (migratedTraits === null) { + return []; + } + + for (const trait of migratedTraits) { + this.analyzeTrait(clazz, trait, flags); + } + + return migratedTraits; + } + + /** + * Returns all decorators that have been recognized for the provided class, including any + * synthetically injected decorators. + * @param clazz the declaration for which the decorators are returned. + */ + getAllDecorators(clazz: ClassDeclaration): Decorator[]|null { + const record = this.recordFor(clazz); + if (record === null) { + return null; + } + + return record.traits.map(trait => trait.detected.decorator).filter(isDefined); + } +} + +class NoIncrementalBuild implements IncrementalBuild { + priorWorkFor(sf: ts.SourceFile): any[]|null { return null; } +} diff --git a/packages/compiler-cli/ngcc/src/analysis/types.ts b/packages/compiler-cli/ngcc/src/analysis/types.ts index fd4859ea3b..19bb5f6ffa 100644 --- a/packages/compiler-cli/ngcc/src/analysis/types.ts +++ b/packages/compiler-cli/ngcc/src/analysis/types.ts @@ -9,23 +9,19 @@ import {ConstantPool} from '@angular/compiler'; import * as ts from 'typescript'; import {Reexport} from '../../../src/ngtsc/imports'; import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; -import {CompileResult, DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform'; +import {CompileResult} from '../../../src/ngtsc/transform'; -export interface AnalyzedFile { - sourceFile: ts.SourceFile; - analyzedClasses: AnalyzedClass[]; -} - -export interface AnalyzedClass { +export interface CompiledClass { name: string; decorators: Decorator[]|null; declaration: ClassDeclaration; - diagnostics?: ts.Diagnostic[]; - matches: MatchingHandler[]; + compilation: CompileResult[]; } -export interface CompiledClass extends AnalyzedClass { - compilation: CompileResult[]; +export interface CompiledFile { + compiledClasses: CompiledClass[]; + sourceFile: ts.SourceFile; + constantPool: ConstantPool; /** * Any re-exports which should be added next to this class, both in .js and (if possible) .d.ts. @@ -33,18 +29,5 @@ export interface CompiledClass extends AnalyzedClass { reexports: Reexport[]; } -export interface CompiledFile { - compiledClasses: CompiledClass[]; - sourceFile: ts.SourceFile; - constantPool: ConstantPool; -} - export type DecorationAnalyses = Map; export const DecorationAnalyses = Map; - -export interface MatchingHandler { - handler: DecoratorHandler; - detected: DetectResult; - analysis: Readonly; - resolution: Readonly; -} diff --git a/packages/compiler-cli/ngcc/src/analysis/util.ts b/packages/compiler-cli/ngcc/src/analysis/util.ts index 59dc3e11d7..a953d4608c 100644 --- a/packages/compiler-cli/ngcc/src/analysis/util.ts +++ b/packages/compiler-cli/ngcc/src/analysis/util.ts @@ -7,104 +7,13 @@ */ import * as ts from 'typescript'; -import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics'; import {AbsoluteFsPath, absoluteFromSourceFile, relative} from '../../../src/ngtsc/file_system'; import {DependencyTracker} from '../../../src/ngtsc/incremental/api'; -import {Decorator} from '../../../src/ngtsc/reflection'; -import {DecoratorHandler, HandlerFlags, HandlerPrecedence} from '../../../src/ngtsc/transform'; -import {NgccClassSymbol} from '../host/ngcc_host'; - -import {AnalyzedClass, MatchingHandler} from './types'; export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean { return !relative(packagePath, absoluteFromSourceFile(sourceFile)).startsWith('..'); } -const NOT_YET_KNOWN: Readonly = null as unknown as Readonly; - -export function analyzeDecorators( - classSymbol: NgccClassSymbol, decorators: Decorator[] | null, - handlers: DecoratorHandler[], flags?: HandlerFlags): AnalyzedClass| - null { - const declaration = classSymbol.declaration.valueDeclaration; - const matchingHandlers: MatchingHandler[] = []; - for (const handler of handlers) { - const detected = handler.detect(declaration, decorators); - if (detected !== undefined) { - matchingHandlers.push({ - handler, - detected, - analysis: NOT_YET_KNOWN, - resolution: NOT_YET_KNOWN, - }); - } - } - - if (matchingHandlers.length === 0) { - return null; - } - - const detections: MatchingHandler[] = []; - let hasWeakHandler: boolean = false; - let hasNonWeakHandler: boolean = false; - let hasPrimaryHandler: boolean = false; - - for (const match of matchingHandlers) { - const {handler} = match; - if (hasNonWeakHandler && handler.precedence === HandlerPrecedence.WEAK) { - continue; - } else if (hasWeakHandler && handler.precedence !== HandlerPrecedence.WEAK) { - // Clear all the WEAK handlers from the list of matches. - detections.length = 0; - } - if (hasPrimaryHandler && handler.precedence === HandlerPrecedence.PRIMARY) { - throw new Error(`TODO.Diagnostic: Class has multiple incompatible Angular decorators.`); - } - - detections.push(match); - if (handler.precedence === HandlerPrecedence.WEAK) { - hasWeakHandler = true; - } else if (handler.precedence === HandlerPrecedence.SHARED) { - hasNonWeakHandler = true; - } else if (handler.precedence === HandlerPrecedence.PRIMARY) { - hasNonWeakHandler = true; - hasPrimaryHandler = true; - } - } - - const matches: MatchingHandler[] = []; - const allDiagnostics: ts.Diagnostic[] = []; - for (const match of detections) { - try { - const {analysis, diagnostics} = - match.handler.analyze(declaration, match.detected.metadata, flags); - if (diagnostics !== undefined) { - allDiagnostics.push(...diagnostics); - } - if (analysis !== undefined) { - match.analysis = analysis; - if (match.handler.register !== undefined) { - match.handler.register(declaration, analysis); - } - } - matches.push(match); - } catch (e) { - if (isFatalDiagnosticError(e)) { - allDiagnostics.push(e.toDiagnostic()); - } else { - throw e; - } - } - } - return { - name: classSymbol.name, - declaration, - decorators, - matches, - diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined, - }; -} - class NoopDependencyTracker implements DependencyTracker { addDependency(): void {} addResourceDependency(): void {} diff --git a/packages/compiler-cli/ngcc/src/migrations/migration.ts b/packages/compiler-cli/ngcc/src/migrations/migration.ts index 2738dc3e53..b0da462538 100644 --- a/packages/compiler-cli/ngcc/src/migrations/migration.ts +++ b/packages/compiler-cli/ngcc/src/migrations/migration.ts @@ -43,6 +43,7 @@ export interface MigrationHost { * given class. * @param clazz the class to receive the new decorator. * @param decorator the decorator to inject. + * @param flags optional bitwise flag to influence the compilation of the decorator. */ injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags): void; diff --git a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts index 9063070c55..789ba9d8d5 100644 --- a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts @@ -124,6 +124,7 @@ export class DtsRenderer { // Capture the rendering info from the decoration analyses decorationAnalyses.forEach(compiledFile => { + let appliedReexports = false; compiledFile.compiledClasses.forEach(compiledClass => { const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); if (dtsDeclaration) { @@ -135,9 +136,11 @@ export class DtsRenderer { // to work, the typing file and JS file must be in parallel trees. This logic will detect // the simplest version of this case, which is sufficient to handle most commonjs // libraries. - if (compiledClass.declaration.getSourceFile().fileName === - dtsFile.fileName.replace(/\.d\.ts$/, '.js')) { - renderInfo.reexports.push(...compiledClass.reexports); + if (!appliedReexports && + compiledClass.declaration.getSourceFile().fileName === + dtsFile.fileName.replace(/\.d\.ts$/, '.js')) { + renderInfo.reexports.push(...compiledFile.reexports); + appliedReexports = true; } dtsMap.set(dtsFile, renderInfo); } diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index 0f30f7bc3a..fd2f873b46 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -88,13 +88,13 @@ export class Renderer { const renderedStatements = this.renderAdjacentStatements(compiledFile.sourceFile, clazz, importManager); this.srcFormatter.addAdjacentStatements(outputText, clazz, renderedStatements); - - if (!isEntryPoint && clazz.reexports.length > 0) { - this.srcFormatter.addDirectExports( - outputText, clazz.reexports, importManager, compiledFile.sourceFile); - } }); + if (!isEntryPoint && compiledFile.reexports.length > 0) { + this.srcFormatter.addDirectExports( + outputText, compiledFile.reexports, importManager, compiledFile.sourceFile); + } + this.srcFormatter.addConstants( outputText, renderConstantPool( diff --git a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts index 4f057ec35b..12b4b62b21 100644 --- a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -44,6 +44,7 @@ runInEachFileSystem(() => { const handler = jasmine.createSpyObj('TestDecoratorHandler', [ 'detect', 'analyze', + 'register', 'resolve', 'compile', ]); @@ -67,6 +68,7 @@ runInEachFileSystem(() => { } else { return { metadata, + decorator: metadata, trigger: metadata.node, }; } @@ -117,7 +119,9 @@ runInEachFileSystem(() => { getFileSystem(), bundle, reflectionHost, referencesRegistry, (error) => diagnosticLogs.push(error)); testHandler = createTestHandler(options); - analyzer.handlers = [testHandler]; + + // Replace the default handlers with the test handler in the original array of handlers + analyzer.handlers.splice(0, analyzer.handlers.length, testHandler); migrationLogs = []; const migration1 = new MockMigration('migration1', migrationLogs); const migration2 = new MockMigration('migration2', migrationLogs); @@ -372,25 +376,49 @@ runInEachFileSystem(() => { expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -996666})); }); - it('should report analyze and resolve diagnostics to the `diagnosticHandler` callback', - () => { - const analyzer = setUpAnalyzer( - [ - { - name: _('/node_modules/test-package/index.js'), - contents: ` + it('should report analyze diagnostics to the `diagnosticHandler` callback', () => { + const analyzer = setUpAnalyzer( + [ + { + name: _('/node_modules/test-package/index.js'), + contents: ` import {Component, Directive, Injectable} from '@angular/core'; export class MyComponent {} MyComponent.decorators = [{type: Component}]; `, - }, - ], - {analyzeError: true, resolveError: true}); - analyzer.analyzeProgram(); - expect(diagnosticLogs.length).toEqual(2); - expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999999})); - expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -999998})); - }); + }, + ], + {analyzeError: true, resolveError: true}); + analyzer.analyzeProgram(); + expect(diagnosticLogs.length).toEqual(1); + expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999999})); + expect(testHandler.analyze).toHaveBeenCalled(); + expect(testHandler.register).not.toHaveBeenCalled(); + expect(testHandler.resolve).not.toHaveBeenCalled(); + expect(testHandler.compile).not.toHaveBeenCalled(); + }); + + it('should report resolve diagnostics to the `diagnosticHandler` callback', () => { + const analyzer = setUpAnalyzer( + [ + { + name: _('/node_modules/test-package/index.js'), + contents: ` + import {Component, Directive, Injectable} from '@angular/core'; + export class MyComponent {} + MyComponent.decorators = [{type: Component}]; + `, + }, + ], + {analyzeError: false, resolveError: true}); + analyzer.analyzeProgram(); + expect(diagnosticLogs.length).toEqual(1); + expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999998})); + expect(testHandler.analyze).toHaveBeenCalled(); + expect(testHandler.register).toHaveBeenCalled(); + expect(testHandler.resolve).toHaveBeenCalled(); + expect(testHandler.compile).not.toHaveBeenCalled(); + }); }); describe('declaration files', () => { @@ -410,7 +438,9 @@ runInEachFileSystem(() => { name: _('/node_modules/test-package/index.d.ts'), contents: 'export declare class SomeDirective {}', }]); - analyzer.handlers = [new FakeDecoratorHandler()]; + + // Replace the default handlers with the test handler in the original array of handlers + analyzer.handlers.splice(0, analyzer.handlers.length, new FakeDecoratorHandler()); result = analyzer.analyzeProgram(); expect(result.size).toBe(0); }); diff --git a/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts b/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts index 3152dc9703..bceeadc9f3 100644 --- a/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts @@ -5,200 +5,86 @@ * 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 '../../../src/ngtsc/diagnostics'; -import {AbsoluteFsPath, absoluteFrom} from '../../../src/ngtsc/file_system'; +import {makeDiagnostic} from '../../../src/ngtsc/diagnostics'; +import {absoluteFrom} from '../../../src/ngtsc/file_system'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; -import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; -import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform'; +import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, TraitState} from '../../../src/ngtsc/transform'; +import {loadTestFiles} from '../../../test/helpers'; import {DefaultMigrationHost} from '../../src/analysis/migration_host'; -import {AnalyzedClass, AnalyzedFile} from '../../src/analysis/types'; -import {NgccClassSymbol} from '../../src/host/ngcc_host'; +import {NgccTraitCompiler} from '../../src/analysis/ngcc_trait_compiler'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {createComponentDecorator} from '../../src/migrations/utils'; +import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {MockLogger} from '../helpers/mock_logger'; +import {makeTestEntryPointBundle} from '../helpers/utils'; runInEachFileSystem(() => { describe('DefaultMigrationHost', () => { let _: typeof absoluteFrom; - let entryPointPath: AbsoluteFsPath; - let mockHost: any; let mockMetadata: any = {}; let mockEvaluator: any = {}; let mockClazz: any; - let mockDecorator: any = {name: 'MockDecorator'}; - let diagnosticHandler = () => {}; + let injectedDecorator: any = {name: 'InjectedDecorator'}; beforeEach(() => { _ = absoluteFrom; - entryPointPath = _('/node_modules/some-package/entry-point'); - mockHost = { - getClassSymbol: (node: any): NgccClassSymbol | undefined => { - const symbol = { valueDeclaration: node, name: node.name.text } as any; - return { - name: node.name.text, - declaration: symbol, - implementation: symbol, - }; - }, - }; const mockSourceFile: any = { fileName: _('/node_modules/some-package/entry-point/test-file.js'), }; mockClazz = { name: {text: 'MockClazz'}, getSourceFile: () => mockSourceFile, + getStart: () => 0, + getWidth: () => 0, }; }); + function createMigrationHost({entryPoint, handlers}: { + entryPoint: EntryPointBundle; handlers: DecoratorHandler[] + }) { + const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src); + const compiler = new NgccTraitCompiler(handlers, reflectionHost); + const host = new DefaultMigrationHost( + reflectionHost, mockMetadata, mockEvaluator, compiler, entryPoint.entryPoint.path); + return {compiler, host}; + } + describe('injectSyntheticDecorator()', () => { - it('should call `detect()` on each of the provided handlers', () => { - const log: string[] = []; - const handler1 = new TestHandler('handler1', log); - const handler2 = new TestHandler('handler2', log); - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler1, handler2], entryPointPath, [], - diagnosticHandler); - host.injectSyntheticDecorator(mockClazz, mockDecorator); - expect(log).toEqual([ - `handler1:detect:MockClazz:MockDecorator`, - `handler2:detect:MockClazz:MockDecorator`, - ]); + it('should add the injected decorator into the compilation', () => { + const handler = new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK); + loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]}); + host.injectSyntheticDecorator(mockClazz, injectedDecorator); + + const record = compiler.recordFor(mockClazz) !; + expect(record).toBeDefined(); + expect(record.traits.length).toBe(1); + expect(record.traits[0].detected.decorator).toBe(injectedDecorator); }); - it('should call `analyze()` on each of the provided handlers whose `detect()` call returns a result', - () => { - const log: string[] = []; - const handler1 = new TestHandler('handler1', log); - const handler2 = new AlwaysDetectHandler('handler2', log); - const handler3 = new TestHandler('handler3', log); - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler1, handler2, handler3], - entryPointPath, [], diagnosticHandler); - host.injectSyntheticDecorator(mockClazz, mockDecorator); - expect(log).toEqual([ - `handler1:detect:MockClazz:MockDecorator`, - `handler2:detect:MockClazz:MockDecorator`, - `handler3:detect:MockClazz:MockDecorator`, - 'handler2:analyze:MockClazz', - ]); - }); - - it('should add a newly `AnalyzedFile` to the `analyzedFiles` object', () => { - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = []; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - host.injectSyntheticDecorator(mockClazz, mockDecorator); - expect(analyzedFiles.length).toEqual(1); - expect(analyzedFiles[0].analyzedClasses.length).toEqual(1); - expect(analyzedFiles[0].analyzedClasses[0].name).toEqual('MockClazz'); - }); - - it('should add a newly `AnalyzedClass` to an existing `AnalyzedFile` object', () => { - const DUMMY_CLASS_1: any = {}; - const DUMMY_CLASS_2: any = {}; - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = [{ - sourceFile: mockClazz.getSourceFile(), - analyzedClasses: [DUMMY_CLASS_1, DUMMY_CLASS_2], - }]; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - host.injectSyntheticDecorator(mockClazz, mockDecorator); - expect(analyzedFiles.length).toEqual(1); - expect(analyzedFiles[0].analyzedClasses.length).toEqual(3); - expect(analyzedFiles[0].analyzedClasses[2].name).toEqual('MockClazz'); - }); - - it('should add a new decorator into an already existing `AnalyzedClass`', () => { - const analyzedClass: AnalyzedClass = { - name: 'MockClazz', - declaration: mockClazz, - matches: [], - decorators: null, - }; - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = [{ - sourceFile: mockClazz.getSourceFile(), - analyzedClasses: [analyzedClass], - }]; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - host.injectSyntheticDecorator(mockClazz, mockDecorator); - expect(analyzedFiles.length).toEqual(1); - expect(analyzedFiles[0].analyzedClasses.length).toEqual(1); - expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass); - expect(analyzedClass.decorators !.length).toEqual(1); - expect(analyzedClass.decorators ![0].name).toEqual('MockDecorator'); - }); - - it('should merge a new decorator into pre-existing decorators an already existing `AnalyzedClass`', - () => { - const analyzedClass: AnalyzedClass = { - name: 'MockClazz', - declaration: mockClazz, - matches: [], - decorators: [{name: 'OtherDecorator'} as Decorator], - }; - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = [{ - sourceFile: mockClazz.getSourceFile(), - analyzedClasses: [analyzedClass], - }]; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - host.injectSyntheticDecorator(mockClazz, mockDecorator); - expect(analyzedFiles.length).toEqual(1); - expect(analyzedFiles[0].analyzedClasses.length).toEqual(1); - expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass); - expect(analyzedClass.decorators !.length).toEqual(2); - expect(analyzedClass.decorators ![1].name).toEqual('MockDecorator'); - }); - - it('should throw an error if the injected decorator already exists', () => { - const analyzedClass: AnalyzedClass = { - name: 'MockClazz', - declaration: mockClazz, - matches: [], - decorators: [{name: 'MockDecorator'} as Decorator], - }; - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = [{ - sourceFile: mockClazz.getSourceFile(), - analyzedClasses: [analyzedClass], - }]; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - expect(() => host.injectSyntheticDecorator(mockClazz, mockDecorator)) - .toThrow(jasmine.objectContaining( - {code: ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR})); - }); - - it('should report diagnostics from handlers', () => { - const log: string[] = []; - const handler = new DiagnosticProducingHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = []; - const diagnostics: ts.Diagnostic[] = []; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnostic => diagnostics.push(diagnostic)); - mockClazz.getStart = () => 0; - mockClazz.getWidth = () => 0; - + it('should mention the migration that failed in the diagnostics message', () => { + const handler = new DiagnosticProducingHandler(); + loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]}); const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null}); host.injectSyntheticDecorator(mockClazz, decorator); - expect(diagnostics.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diagnostics[0].messageText, '\n')) + const record = compiler.recordFor(mockClazz) !; + const migratedTrait = record.traits[0]; + if (migratedTrait.state !== TraitState.ERRORED) { + return fail('Expected migrated class trait to be in an error state'); + } + + expect(migratedTrait.diagnostics.length).toBe(1); + expect(ts.flattenDiagnosticMessageText(migratedTrait.diagnostics[0].messageText, '\n')) .toEqual( `test diagnostic\n` + ` Occurs for @Component decorator inserted by an automatic migration\n` + @@ -206,125 +92,114 @@ runInEachFileSystem(() => { }); }); + + describe('getAllDecorators', () => { - it('should be null for unknown source files', () => { - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = []; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - - const decorators = host.getAllDecorators(mockClazz); - expect(decorators).toBeNull(); - }); - - it('should be null for unknown classes', () => { - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const analyzedFiles: AnalyzedFile[] = []; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - - const sourceFile: any = {}; - const unrelatedClass: any = { - getSourceFile: () => sourceFile, - }; - analyzedFiles.push({sourceFile, analyzedClasses: [unrelatedClass]}); - - const decorators = host.getAllDecorators(mockClazz); - expect(decorators).toBeNull(); - }); - it('should include injected decorators', () => { - const log: string[] = []; - const handler = new AlwaysDetectHandler('handler', log); - const existingDecorator = { name: 'ExistingDecorator' } as Decorator; - const analyzedClass: AnalyzedClass = { - name: 'MockClazz', - declaration: mockClazz, - matches: [], - decorators: [existingDecorator], - }; - const analyzedFiles: AnalyzedFile[] = [{ - sourceFile: mockClazz.getSourceFile(), - analyzedClasses: [analyzedClass], - }]; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles, - diagnosticHandler); - host.injectSyntheticDecorator(mockClazz, mockDecorator); + const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK); + const injectedHandler = + new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK); + loadTestFiles([{ + name: _('/node_modules/test/index.js'), + contents: ` + import {Directive} from '@angular/core'; - const decorators = host.getAllDecorators(mockClazz) !; + export class MyClass {}; + MyClass.decorators = [{ type: Directive }]; + ` + }]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const {host, compiler} = + createMigrationHost({entryPoint, handlers: [directiveHandler, injectedHandler]}); + const myClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass', + isNamedClassDeclaration); + + compiler.analyzeFile(entryPoint.src.file); + + host.injectSyntheticDecorator(myClass, injectedDecorator); + + const decorators = host.getAllDecorators(myClass) !; expect(decorators.length).toBe(2); - expect(decorators[0]).toBe(existingDecorator); - expect(decorators[1]).toBe(mockDecorator); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[1].name).toBe('InjectedDecorator'); }); }); describe('isInScope', () => { it('should be true for nodes within the entry-point', () => { - const analyzedFiles: AnalyzedFile[] = []; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles, - diagnosticHandler); + loadTestFiles([ + {name: _('/node_modules/test/index.js'), contents: `export * from './internal';`}, + {name: _('/node_modules/test/internal.js'), contents: `export class InternalClass {}`}, + ]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const {host} = createMigrationHost({entryPoint, handlers: []}); + const internalClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/internal.js'), 'InternalClass', + isNamedClassDeclaration); - const sourceFile: any = { - fileName: _('/node_modules/some-package/entry-point/relative.js'), - }; - const clazz: any = { - getSourceFile: () => sourceFile, - }; - expect(host.isInScope(clazz)).toBe(true); + expect(host.isInScope(internalClass)).toBe(true); }); it('should be false for nodes outside the entry-point', () => { - const analyzedFiles: AnalyzedFile[] = []; - const host = new DefaultMigrationHost( - mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles, - diagnosticHandler); + loadTestFiles([ + {name: _('/node_modules/external/index.js'), contents: `export class ExternalClass {}`}, + { + name: _('/node_modules/test/index.js'), + contents: ` + export {ExternalClass} from 'external'; + export class InternalClass {} + ` + }, + ]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const {host} = createMigrationHost({entryPoint, handlers: []}); + const externalClass = getDeclaration( + entryPoint.src.program, _('/node_modules/external/index.js'), 'ExternalClass', + isNamedClassDeclaration); - const sourceFile: any = { - fileName: _('/node_modules/some-package/other-entry/index.js'), - }; - const clazz: any = { - getSourceFile: () => sourceFile, - }; - expect(host.isInScope(clazz)).toBe(false); + expect(host.isInScope(externalClass)).toBe(false); }); }); }); }); -class TestHandler implements DecoratorHandler { - constructor(readonly name: string, protected log: string[]) {} +class DetectDecoratorHandler implements DecoratorHandler { + readonly name = DetectDecoratorHandler.name; + + constructor(private decorator: string, readonly precedence: HandlerPrecedence) {} - precedence = HandlerPrecedence.PRIMARY; detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { - this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`); - return undefined; - } - analyze(node: ClassDeclaration): AnalysisOutput { - this.log.push(this.name + ':analyze:' + node.name.text); - return {}; - } - compile(node: ClassDeclaration): CompileResult|CompileResult[] { - this.log.push(this.name + ':compile:' + node.name.text); - return []; + if (decorators === null) { + return undefined; + } + const decorator = decorators.find(decorator => decorator.name === this.decorator); + if (decorator === undefined) { + return undefined; + } + return {trigger: node, decorator, metadata: {}}; } + + analyze(node: ClassDeclaration): AnalysisOutput { return {}; } + + compile(node: ClassDeclaration): CompileResult|CompileResult[] { return []; } } -class AlwaysDetectHandler extends TestHandler { - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { - super.detect(node, decorators); - return {trigger: node, metadata: {}}; - } -} +class DiagnosticProducingHandler implements DecoratorHandler { + readonly name = DiagnosticProducingHandler.name; + readonly precedence = HandlerPrecedence.PRIMARY; + + detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + const decorator = decorators !== null ? decorators[0] : null; + return {trigger: node, decorator, metadata: {}}; + } -class DiagnosticProducingHandler extends AlwaysDetectHandler { analyze(node: ClassDeclaration): AnalysisOutput { - super.analyze(node); return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]}; } + + compile(node: ClassDeclaration): CompileResult|CompileResult[] { return []; } } diff --git a/packages/compiler-cli/ngcc/test/analysis/ngcc_trait_compiler_spec.ts b/packages/compiler-cli/ngcc/test/analysis/ngcc_trait_compiler_spec.ts new file mode 100644 index 0000000000..c164a76a5e --- /dev/null +++ b/packages/compiler-cli/ngcc/test/analysis/ngcc_trait_compiler_spec.ts @@ -0,0 +1,351 @@ +/** + * @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 {ErrorCode, makeDiagnostic, ngErrorCode} from '../../../src/ngtsc/diagnostics'; +import {absoluteFrom} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection'; +import {getDeclaration} from '../../../src/ngtsc/testing'; +import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, TraitState} from '../../../src/ngtsc/transform'; +import {loadTestFiles} from '../../../test/helpers'; +import {NgccTraitCompiler} from '../../src/analysis/ngcc_trait_compiler'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {createComponentDecorator} from '../../src/migrations/utils'; +import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {MockLogger} from '../helpers/mock_logger'; +import {makeTestEntryPointBundle} from '../helpers/utils'; + +runInEachFileSystem(() => { + describe('NgccTraitCompiler', () => { + let _: typeof absoluteFrom; + let mockClazz: any; + let injectedDecorator: any = {name: 'InjectedDecorator'}; + beforeEach(() => { + _ = absoluteFrom; + const mockSourceFile: any = { + fileName: _('/node_modules/some-package/entry-point/test-file.js'), + }; + mockClazz = { + name: {text: 'MockClazz'}, + getSourceFile: () => mockSourceFile, + getStart: () => 0, + getWidth: () => 0, + }; + }); + + function createCompiler({entryPoint, handlers}: { + entryPoint: EntryPointBundle; handlers: DecoratorHandler[] + }) { + const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src); + return new NgccTraitCompiler(handlers, reflectionHost); + } + + describe('injectSyntheticDecorator()', () => { + it('should call `detect()` on each of the provided handlers', () => { + const log: string[] = []; + const handler1 = new TestHandler('handler1', log); + const handler2 = new TestHandler('handler2', log); + loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = createCompiler({entryPoint, handlers: [handler1, handler2]}); + compiler.injectSyntheticDecorator(mockClazz, injectedDecorator); + expect(log).toEqual([ + `handler1:detect:MockClazz:InjectedDecorator`, + `handler2:detect:MockClazz:InjectedDecorator`, + ]); + }); + + it('should call `analyze()` on each of the provided handlers whose `detect()` call returns a result', + () => { + const log: string[] = []; + const handler1 = new TestHandler('handler1', log); + const handler2 = new AlwaysDetectHandler('handler2', log); + const handler3 = new TestHandler('handler3', log); + loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]); + const entryPoint = makeTestEntryPointBundle( + 'test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = createCompiler({entryPoint, handlers: [handler1, handler2, handler3]}); + compiler.injectSyntheticDecorator(mockClazz, injectedDecorator); + expect(log).toEqual([ + `handler1:detect:MockClazz:InjectedDecorator`, + `handler2:detect:MockClazz:InjectedDecorator`, + `handler3:detect:MockClazz:InjectedDecorator`, + 'handler2:analyze:MockClazz', + ]); + }); + + it('should inject a new class record into the compilation', () => { + const injectedHandler = + new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK); + loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = createCompiler({entryPoint, handlers: [injectedHandler]}); + compiler.injectSyntheticDecorator(mockClazz, injectedDecorator); + + const record = compiler.recordFor(mockClazz); + expect(record).toBeDefined(); + expect(record !.traits.length).toBe(1); + }); + + it('should add a new trait to an existing class record', () => { + const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK); + const injectedHandler = + new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK); + loadTestFiles([{ + name: _('/node_modules/test/index.js'), + contents: ` + import {Directive} from '@angular/core'; + + export class MyClass {}; + MyClass.decorators = [{ type: Directive }]; + ` + }]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = + createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]}); + const myClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass', + isNamedClassDeclaration); + + compiler.analyzeFile(entryPoint.src.file); + compiler.injectSyntheticDecorator(myClass, injectedDecorator); + + const record = compiler.recordFor(myClass) !; + expect(record).toBeDefined(); + expect(record.traits.length).toBe(2); + expect(record.traits[0].detected.decorator !.name).toBe('Directive'); + expect(record.traits[1].detected.decorator !.name).toBe('InjectedDecorator'); + }); + + it('should not add a weak handler when a primary handler already exists', () => { + const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.PRIMARY); + const injectedHandler = + new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK); + loadTestFiles([{ + name: _('/node_modules/test/index.js'), + contents: ` + import {Directive} from '@angular/core'; + + export class MyClass {}; + MyClass.decorators = [{ type: Directive }]; + ` + }]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = + createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]}); + const myClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass', + isNamedClassDeclaration); + + compiler.analyzeFile(entryPoint.src.file); + + compiler.injectSyntheticDecorator(myClass, injectedDecorator); + + const record = compiler.recordFor(myClass) !; + expect(record).toBeDefined(); + expect(record.traits.length).toBe(1); + expect(record.traits[0].detected.decorator !.name).toBe('Directive'); + }); + + it('should replace an existing weak handler when injecting a primary handler', () => { + const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK); + const injectedHandler = + new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.PRIMARY); + loadTestFiles([{ + name: _('/node_modules/test/index.js'), + contents: ` + import {Directive} from '@angular/core'; + + export class MyClass {}; + MyClass.decorators = [{ type: Directive }]; + ` + }]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = + createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]}); + const myClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass', + isNamedClassDeclaration); + + compiler.analyzeFile(entryPoint.src.file); + + compiler.injectSyntheticDecorator(myClass, injectedDecorator); + + const record = compiler.recordFor(myClass) !; + expect(record).toBeDefined(); + expect(record.traits.length).toBe(1); + expect(record.traits[0].detected.decorator !.name).toBe('InjectedDecorator'); + }); + + it('should produce an error when a primary handler is added when a primary handler is already present', + () => { + const directiveHandler = + new DetectDecoratorHandler('Directive', HandlerPrecedence.PRIMARY); + const injectedHandler = + new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.PRIMARY); + loadTestFiles([{ + name: _('/node_modules/test/index.js'), + contents: ` + import {Directive} from '@angular/core'; + + export class MyClass {}; + MyClass.decorators = [{ type: Directive }]; + ` + }]); + const entryPoint = makeTestEntryPointBundle( + 'test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = + createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]}); + const myClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass', + isNamedClassDeclaration); + + compiler.analyzeFile(entryPoint.src.file); + + compiler.injectSyntheticDecorator(myClass, injectedDecorator); + + const record = compiler.recordFor(myClass) !; + expect(record).toBeDefined(); + expect(record.metaDiagnostics).toBeDefined(); + expect(record.metaDiagnostics !.length).toBe(1); + expect(record.metaDiagnostics ![0].code) + .toBe(ngErrorCode(ErrorCode.DECORATOR_COLLISION)); + expect(record.traits.length).toBe(0); + }); + + it('should report diagnostics from handlers', () => { + const log: string[] = []; + const handler = new DiagnosticProducingHandler('handler', log); + loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = createCompiler({entryPoint, handlers: [handler]}); + const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null}); + compiler.injectSyntheticDecorator(mockClazz, decorator); + + const record = compiler.recordFor(mockClazz) !; + const migratedTrait = record.traits[0]; + if (migratedTrait.state !== TraitState.ERRORED) { + return fail('Expected migrated class trait to be in an error state'); + } + + expect(migratedTrait.diagnostics.length).toBe(1); + expect(migratedTrait.diagnostics[0].messageText).toEqual(`test diagnostic`); + }); + }); + + + + describe('getAllDecorators', () => { + it('should be null for classes without decorators', () => { + loadTestFiles( + [{name: _('/node_modules/test/index.js'), contents: `export class MyClass {};`}]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = createCompiler({entryPoint, handlers: []}); + const myClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass', + isNamedClassDeclaration); + + const decorators = compiler.getAllDecorators(myClass); + expect(decorators).toBeNull(); + }); + + it('should include injected decorators', () => { + const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK); + const injectedHandler = + new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK); + loadTestFiles([{ + name: _('/node_modules/test/index.js'), + contents: ` + import {Directive} from '@angular/core'; + + export class MyClass {}; + MyClass.decorators = [{ type: Directive }]; + ` + }]); + const entryPoint = + makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]); + const compiler = + createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]}); + const myClass = getDeclaration( + entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass', + isNamedClassDeclaration); + + compiler.analyzeFile(entryPoint.src.file); + + compiler.injectSyntheticDecorator(myClass, injectedDecorator); + + const decorators = compiler.getAllDecorators(myClass) !; + expect(decorators.length).toBe(2); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[1].name).toBe('InjectedDecorator'); + }); + }); + + }); +}); + +class TestHandler implements DecoratorHandler { + constructor(readonly name: string, protected log: string[]) {} + + precedence = HandlerPrecedence.PRIMARY; + + detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`); + return undefined; + } + + analyze(node: ClassDeclaration): AnalysisOutput { + this.log.push(this.name + ':analyze:' + node.name.text); + return {}; + } + + compile(node: ClassDeclaration): CompileResult|CompileResult[] { + this.log.push(this.name + ':compile:' + node.name.text); + return []; + } +} + +class AlwaysDetectHandler extends TestHandler { + detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + super.detect(node, decorators); + const decorator = decorators !== null ? decorators[0] : null; + return {trigger: node, decorator, metadata: {}}; + } +} + +class DetectDecoratorHandler extends TestHandler { + constructor(private decorator: string, readonly precedence: HandlerPrecedence) { + super(decorator, []); + } + + detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + super.detect(node, decorators); + if (decorators === null) { + return undefined; + } + const decorator = decorators.find(decorator => decorator.name === this.decorator); + if (decorator === undefined) { + return undefined; + } + return {trigger: node, decorator, metadata: {}}; + } +} + +class DiagnosticProducingHandler extends AlwaysDetectHandler { + analyze(node: ClassDeclaration): AnalysisOutput { + super.analyze(node); + return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]}; + } +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 258e850489..b912dd2c16 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -109,6 +109,7 @@ export class ComponentDecoratorHandler implements if (decorator !== undefined) { return { trigger: decorator.node, + decorator, metadata: decorator, }; } else { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 4439c12ea8..9e2ed4fbdb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -70,10 +70,11 @@ export class DirectiveDecoratorHandler implements } return false; }); - return angularField ? {trigger: angularField.node, metadata: null} : undefined; + return angularField ? {trigger: angularField.node, decorator: null, metadata: null} : + undefined; } else { const decorator = findAngularDecorator(decorators, 'Directive', this.isCore); - return decorator ? {trigger: decorator.node, metadata: decorator} : undefined; + return decorator ? {trigger: decorator.node, decorator, metadata: decorator} : undefined; } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 978bf693e2..9785798910 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -54,6 +54,7 @@ export class InjectableDecoratorHandler implements if (decorator !== undefined) { return { trigger: decorator.node, + decorator: decorator, metadata: decorator, }; } else { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 1abeb1d9e8..940ef2458c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -71,6 +71,7 @@ export class NgModuleDecoratorHandler implements if (decorator !== undefined) { return { trigger: decorator.node, + decorator: decorator, metadata: decorator, }; } else { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index c328cde05c..cc99211d16 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -44,6 +44,7 @@ export class PipeDecoratorHandler implements DecoratorHandler { constantPool: ConstantPool): CompileResult|CompileResult[]; } +/** + * The output of detecting a trait for a declaration as the result of the first phase of the + * compilation pipeline. + */ export interface DetectResult { + /** + * The node that triggered the match, which is typically a decorator. + */ trigger: ts.Node|null; + + /** + * Refers to the decorator that was recognized for this detection, if any. This can be a concrete + * decorator that is actually present in a file, or a synthetic decorator as inserted + * programmatically. + */ + decorator: Decorator|null; + + /** + * An arbitrary object to carry over from the detection phase into the analysis phase. + */ metadata: Readonly; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index a47b59a0df..4f567a20f4 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -6,20 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {ConstantPool, Type} from '@angular/compiler'; +import {ConstantPool} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {ImportRewriter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; import {IndexingContext} from '../../indexer'; -import {ModuleWithProvidersScanner} from '../../modulewithproviders'; import {PerfRecorder} from '../../perf'; -import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection'; +import {ClassDeclaration, Decorator, ReflectionHost} from '../../reflection'; import {TypeCheckContext} from '../../typecheck'; import {getSourceFile, isExported} from '../../util/src/typescript'; -import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from './api'; +import {AnalysisOutput, CompileResult, DecoratorHandler, HandlerFlags, HandlerPrecedence, ResolveResult} from './api'; import {DtsTransformRegistry} from './declaration'; import {Trait, TraitState} from './trait'; @@ -80,7 +78,7 @@ export class TraitCompiler { * Maps source files to any class declaration(s) within them which have been discovered to contain * Ivy traits. */ - private fileToClasses = new Map>(); + protected fileToClasses = new Map>(); private reexportMap = new Map>(); @@ -123,7 +121,7 @@ export class TraitCompiler { } const visit = (node: ts.Node): void => { - if (isNamedClassDeclaration(node)) { + if (this.reflector.isClass(node)) { this.analyzeClass(node, preanalyze ? promises : null); } ts.forEachChild(node, visit); @@ -138,6 +136,14 @@ export class TraitCompiler { } } + recordFor(clazz: ClassDeclaration): ClassRecord|null { + if (this.classes.has(clazz)) { + return this.classes.get(clazz) !; + } else { + return null; + } + } + recordsFor(sf: ts.SourceFile): ClassRecord[]|null { if (!this.fileToClasses.has(sf)) { return null; @@ -192,14 +198,20 @@ export class TraitCompiler { this.fileToClasses.get(sf) !.add(record.node); } - private scanClassForTraits(clazz: ClassDeclaration): ClassRecord|null { + private scanClassForTraits(clazz: ClassDeclaration): Trait[]|null { if (!this.compileNonExportedClasses && !isExported(clazz)) { return null; } const decorators = this.reflector.getDecoratorsOfDeclaration(clazz); - let record: ClassRecord|null = null; + return this.detectTraits(clazz, decorators); + } + + protected detectTraits(clazz: ClassDeclaration, decorators: Decorator[]|null): + Trait[]|null { + let record: ClassRecord|null = this.recordFor(clazz); + let foundTraits: Trait[] = []; for (const handler of this.handlers) { const result = handler.detect(clazz, decorators); @@ -207,11 +219,12 @@ export class TraitCompiler { continue; } - const isPrimaryHandler = handler.precedence === HandlerPrecedence.PRIMARY; const isWeakHandler = handler.precedence === HandlerPrecedence.WEAK; const trait = Trait.pending(handler, result); + foundTraits.push(trait); + if (record === null) { // This is the first handler to match this class. This path is a fast path through which // most classes will flow. @@ -262,8 +275,8 @@ export class TraitCompiler { length: clazz.getWidth(), messageText: 'Two incompatible decorators on class', }]; - record.traits = []; - return record; + record.traits = foundTraits = []; + break; } // Otherwise, it's safe to accept the multiple decorators here. Update some of the metadata @@ -273,18 +286,18 @@ export class TraitCompiler { } } - return record; + return foundTraits.length > 0 ? foundTraits : null; } - private analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise[]|null): void { - const record = this.scanClassForTraits(clazz); + protected analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise[]|null): void { + const traits = this.scanClassForTraits(clazz); - if (record === null) { + if (traits === null) { // There are no Ivy traits on the class, so it can safely be skipped. return; } - for (const trait of record.traits) { + for (const trait of traits) { const analyze = () => this.analyzeTrait(clazz, trait); let preanalysis: Promise|null = null; @@ -299,7 +312,9 @@ export class TraitCompiler { } } - private analyzeTrait(clazz: ClassDeclaration, trait: Trait): void { + protected analyzeTrait( + clazz: ClassDeclaration, trait: Trait, + flags?: HandlerFlags): void { if (trait.state !== TraitState.PENDING) { throw new Error( `Attempt to analyze trait of ${clazz.name.text} in state ${TraitState[trait.state]} (expected DETECTED)`); @@ -308,7 +323,7 @@ export class TraitCompiler { // Attempt analysis. This could fail with a `FatalDiagnosticError`; catch it if it does. let result: AnalysisOutput; try { - result = trait.handler.analyze(clazz, trait.detected.metadata); + result = trait.handler.analyze(clazz, trait.detected.metadata, flags); } catch (err) { if (err instanceof FatalDiagnosticError) { trait = trait.toErrored([err.toDiagnostic()]); @@ -425,7 +440,7 @@ export class TraitCompiler { compile(clazz: ts.Declaration, constantPool: ConstantPool): CompileResult[]|null { const original = ts.getOriginalNode(clazz) as typeof clazz; - if (!isNamedClassDeclaration(clazz) || !isNamedClassDeclaration(original) || + if (!this.reflector.isClass(clazz) || !this.reflector.isClass(original) || !this.classes.has(original)) { return null; } @@ -465,7 +480,7 @@ export class TraitCompiler { decoratorsFor(node: ts.Declaration): ts.Decorator[] { const original = ts.getOriginalNode(node) as typeof node; - if (!isNamedClassDeclaration(original) || !this.classes.has(original)) { + if (!this.reflector.isClass(original) || !this.classes.has(original)) { return []; }