fix(ngcc): do not attempt compilation when analysis fails (#34889)

In #34288, ngtsc was refactored to separate the result of the analysis
and resolve phase for more granular incremental rebuilds. In this model,
any errors in one phase transition the trait into an error state, which
prevents it from being ran through subsequent phases. The ngcc compiler
on the other hand did not adopt this strict error model, which would
cause incomplete metadata—due to errors in earlier phases—to be offered
for compilation that could result in a hard crash.

This commit updates ngcc to take advantage of ngtsc's `TraitCompiler`,
that internally manages all Ivy classes that are part of the
compilation. This effectively replaces ngcc's own `AnalyzedFile` and
`AnalyzedClass` types, together with all of the logic to drive the
`DecoratorHandler`s. All of this is now handled in the `TraitCompiler`,
benefiting from its explicit state transitions of `Trait`s so that the
ngcc crash is a thing of the past.

Fixes #34500
Resolves FW-1788

PR Close #34889
This commit is contained in:
JoostK 2020-01-06 23:12:19 +01:00 committed by Andrew Kushnir
parent ba82532812
commit 7659f2e24b
20 changed files with 781 additions and 601 deletions

View File

@ -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 {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 {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 {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {CompileResult, DecoratorHandler} from '../../../src/ngtsc/transform'; import {DecoratorHandler} from '../../../src/ngtsc/transform';
import {NgccClassSymbol, NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {Migration} from '../migrations/migration'; import {Migration} from '../migrations/migration';
import {MissingInjectableMigration} from '../migrations/missing_injectable_migration'; import {MissingInjectableMigration} from '../migrations/missing_injectable_migration';
import {UndecoratedChildMigration} from '../migrations/undecorated_child_migration'; import {UndecoratedChildMigration} from '../migrations/undecorated_child_migration';
import {UndecoratedParentMigration} from '../migrations/undecorated_parent_migration'; import {UndecoratedParentMigration} from '../migrations/undecorated_parent_migration';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
import {isDefined} from '../utils';
import {DefaultMigrationHost} from './migration_host'; import {DefaultMigrationHost} from './migration_host';
import {AnalyzedClass, AnalyzedFile, CompiledClass, CompiledFile, DecorationAnalyses} from './types'; import {NgccTraitCompiler} from './ngcc_trait_compiler';
import {NOOP_DEPENDENCY_TRACKER, analyzeDecorators, isWithinPackage} from './util'; 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 packagePath = this.bundle.entryPoint.package;
private isCore = this.bundle.isCore; private isCore = this.bundle.isCore;
/**
* Map of NgModule declarations to the re-exports for that NgModule.
*/
private reexportMap = new Map<ts.Declaration, Map<string, [string, string]>>();
moduleResolver = moduleResolver =
new ModuleResolver(this.program, this.options, this.host, /* moduleResolutionCache */ null); new ModuleResolver(this.program, this.options, this.host, /* moduleResolutionCache */ null);
resourceManager = new NgccResourceLoader(this.fs); resourceManager = new NgccResourceLoader(this.fs);
@ -118,6 +113,7 @@ export class DecorationAnalyzer {
/* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER, /* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER,
/* annotateForClosureCompiler */ false, this.injectableRegistry), /* annotateForClosureCompiler */ false, this.injectableRegistry),
]; ];
compiler = new NgccTraitCompiler(this.handlers, this.reflectionHost);
migrations: Migration[] = [ migrations: Migration[] = [
new UndecoratedParentMigration(), new UndecoratedParentMigration(),
new UndecoratedChildMigration(), new UndecoratedChildMigration(),
@ -135,56 +131,54 @@ export class DecorationAnalyzer {
* @returns a map of the source files to the analysis for those files. * @returns a map of the source files to the analysis for those files.
*/ */
analyzeProgram(): DecorationAnalyses { 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 decorationAnalyses = new DecorationAnalyses();
const analyzedFiles = this.program.getSourceFiles() for (const analyzedFile of this.compiler.analyzedFiles) {
.filter(sourceFile => !sourceFile.isDeclarationFile) const compiledFile = this.compileFile(analyzedFile);
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile)) decorationAnalyses.set(compiledFile.sourceFile, compiledFile);
.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));
return decorationAnalyses; return decorationAnalyses;
} }
protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined { protected applyMigrations(): void {
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 {
const migrationHost = new DefaultMigrationHost( const migrationHost = new DefaultMigrationHost(
this.reflectionHost, this.fullMetaReader, this.evaluator, this.handlers, this.reflectionHost, this.fullMetaReader, this.evaluator, this.compiler,
this.bundle.entryPoint.path, analyzedFiles, this.diagnosticHandler); this.bundle.entryPoint.path);
this.migrations.forEach(migration => { this.migrations.forEach(migration => {
analyzedFiles.forEach(analyzedFile => { this.compiler.analyzedFiles.forEach(analyzedFile => {
analyzedFile.analyzedClasses.forEach(({declaration}) => { 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 { try {
const result = migration.apply(declaration, migrationHost); const result = migration.apply(record.node, migrationHost);
if (result !== null) { if (result !== null) {
this.diagnosticHandler(result); addDiagnostic(result);
} }
} catch (e) { } catch (e) {
if (isFatalDiagnosticError(e)) { if (isFatalDiagnosticError(e)) {
this.diagnosticHandler(e.toDiagnostic()); addDiagnostic(e.toDiagnostic());
} else { } else {
throw e; 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 constantPool = new ConstantPool();
const compiledClasses: CompiledClass[] = analyzedFile.analyzedClasses.map(analyzedClass => { const records = this.compiler.recordsFor(sourceFile);
const compilation = this.compileClass(analyzedClass, constantPool); if (records === null) {
const declaration = analyzedClass.declaration; throw new Error('Assertion error: file to compile must have records.');
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);
}
} }
return compilations;
}
protected resolveFile(analyzedFile: AnalyzedFile): void { const compiledClasses: CompiledClass[] = [];
for (const {declaration, matches} of analyzedFile.analyzedClasses) {
for (const match of matches) { for (const record of records) {
const {handler, analysis} = match; const compilation = this.compiler.compile(record.node, constantPool);
if ((handler.resolve !== undefined) && analysis) { if (compilation === null) {
const {reexports, diagnostics, data} = handler.resolve(declaration, analysis); continue;
if (reexports !== undefined) {
this.addReexports(reexports, declaration);
}
if (diagnostics !== undefined) {
diagnostics.forEach(error => this.diagnosticHandler(error));
}
match.resolution = data as Readonly<unknown>;
}
} }
}
}
private getReexportsForClass(declaration: ClassDeclaration<ts.Declaration>) { compiledClasses.push({
const reexports: Reexport[] = []; name: record.node.name.text,
if (this.reexportMap.has(declaration)) { decorators: this.compiler.getAllDecorators(record.node),
this.reexportMap.get(declaration) !.forEach(([fromModule, symbolName], asAlias) => { declaration: record.node, compilation
reexports.push({asAlias, fromModule, symbolName});
}); });
} }
return reexports;
const reexports = this.getReexportsForSourceFile(sourceFile);
return {constantPool, sourceFile: sourceFile, compiledClasses, reexports};
} }
private addReexports(reexports: Reexport[], declaration: ClassDeclaration<ts.Declaration>) { private getReexportsForSourceFile(sf: ts.SourceFile): Reexport[] {
const map = new Map<string, [string, string]>(); const exportStatements = this.compiler.exportStatements;
for (const reexport of reexports) { if (!exportStatements.has(sf.fileName)) {
map.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]); 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;
} }
} }

View File

@ -7,66 +7,40 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {MetadataReader} from '../../../src/ngtsc/metadata'; import {MetadataReader} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; 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 {NgccReflectionHost} from '../host/ngcc_host';
import {MigrationHost} from '../migrations/migration'; import {MigrationHost} from '../migrations/migration';
import {AnalyzedClass, AnalyzedFile} from './types'; import {NgccTraitCompiler} from './ngcc_trait_compiler';
import {analyzeDecorators, isWithinPackage} from './util'; import {isWithinPackage} from './util';
/** /**
* The standard implementation of `MigrationHost`, which is created by the * The standard implementation of `MigrationHost`, which is created by the `DecorationAnalyzer`.
* `DecorationAnalyzer`.
*/ */
export class DefaultMigrationHost implements MigrationHost { export class DefaultMigrationHost implements MigrationHost {
constructor( constructor(
readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader, readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader,
readonly evaluator: PartialEvaluator, readonly evaluator: PartialEvaluator, private compiler: NgccTraitCompiler,
private handlers: DecoratorHandler<unknown, unknown, unknown>[], private entryPointPath: AbsoluteFsPath) {}
private entryPointPath: AbsoluteFsPath, private analyzedFiles: AnalyzedFile[],
private diagnosticHandler: (error: ts.Diagnostic) => void) {}
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags): injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags):
void { void {
const classSymbol = this.reflectionHost.getClassSymbol(clazz) !; const migratedTraits = this.compiler.injectSyntheticDecorator(clazz, decorator, flags);
const newAnalyzedClass = analyzeDecorators(classSymbol, [decorator], this.handlers, flags);
if (newAnalyzedClass === null) {
return;
}
if (newAnalyzedClass.diagnostics !== undefined) { for (const trait of migratedTraits) {
for (const diagnostic of newAnalyzedClass.diagnostics) { if (trait.state === TraitState.ERRORED) {
this.diagnosticHandler(createMigrationDiagnostic(diagnostic, clazz, decorator)); 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 { getAllDecorators(clazz: ClassDeclaration): Decorator[]|null {
const sourceFile = clazz.getSourceFile(); return this.compiler.getAllDecorators(clazz);
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;
} }
isInScope(clazz: ClassDeclaration): boolean { 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 * Creates a diagnostic from another one, containing additional information about the synthetic
* decorator. * decorator.

View File

@ -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<unknown, unknown, unknown>[],
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<unknown, unknown, unknown>[] {
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<any> {
priorWorkFor(sf: ts.SourceFile): any[]|null { return null; }
}

View File

@ -9,23 +9,19 @@ import {ConstantPool} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reexport} from '../../../src/ngtsc/imports'; import {Reexport} from '../../../src/ngtsc/imports';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
import {CompileResult, DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform'; import {CompileResult} from '../../../src/ngtsc/transform';
export interface AnalyzedFile { export interface CompiledClass {
sourceFile: ts.SourceFile;
analyzedClasses: AnalyzedClass[];
}
export interface AnalyzedClass {
name: string; name: string;
decorators: Decorator[]|null; decorators: Decorator[]|null;
declaration: ClassDeclaration; declaration: ClassDeclaration;
diagnostics?: ts.Diagnostic[]; compilation: CompileResult[];
matches: MatchingHandler<unknown, unknown, unknown>[];
} }
export interface CompiledClass extends AnalyzedClass { export interface CompiledFile {
compilation: CompileResult[]; 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. * 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[]; reexports: Reexport[];
} }
export interface CompiledFile {
compiledClasses: CompiledClass[];
sourceFile: ts.SourceFile;
constantPool: ConstantPool;
}
export type DecorationAnalyses = Map<ts.SourceFile, CompiledFile>; export type DecorationAnalyses = Map<ts.SourceFile, CompiledFile>;
export const DecorationAnalyses = Map; export const DecorationAnalyses = Map;
export interface MatchingHandler<D, A, R> {
handler: DecoratorHandler<D, A, R>;
detected: DetectResult<D>;
analysis: Readonly<A>;
resolution: Readonly<R>;
}

View File

@ -7,104 +7,13 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {AbsoluteFsPath, absoluteFromSourceFile, relative} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath, absoluteFromSourceFile, relative} from '../../../src/ngtsc/file_system';
import {DependencyTracker} from '../../../src/ngtsc/incremental/api'; 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 { export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean {
return !relative(packagePath, absoluteFromSourceFile(sourceFile)).startsWith('..'); return !relative(packagePath, absoluteFromSourceFile(sourceFile)).startsWith('..');
} }
const NOT_YET_KNOWN: Readonly<unknown> = null as unknown as Readonly<unknown>;
export function analyzeDecorators(
classSymbol: NgccClassSymbol, decorators: Decorator[] | null,
handlers: DecoratorHandler<unknown, unknown, unknown>[], flags?: HandlerFlags): AnalyzedClass|
null {
const declaration = classSymbol.declaration.valueDeclaration;
const matchingHandlers: MatchingHandler<unknown, unknown, unknown>[] = [];
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<unknown, unknown, unknown>[] = [];
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<unknown, unknown, unknown>[] = [];
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 { class NoopDependencyTracker implements DependencyTracker {
addDependency(): void {} addDependency(): void {}
addResourceDependency(): void {} addResourceDependency(): void {}

View File

@ -43,6 +43,7 @@ export interface MigrationHost {
* given class. * given class.
* @param clazz the class to receive the new decorator. * @param clazz the class to receive the new decorator.
* @param decorator the decorator to inject. * @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): injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags):
void; void;

View File

@ -124,6 +124,7 @@ export class DtsRenderer {
// Capture the rendering info from the decoration analyses // Capture the rendering info from the decoration analyses
decorationAnalyses.forEach(compiledFile => { decorationAnalyses.forEach(compiledFile => {
let appliedReexports = false;
compiledFile.compiledClasses.forEach(compiledClass => { compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
if (dtsDeclaration) { 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 // 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 // the simplest version of this case, which is sufficient to handle most commonjs
// libraries. // libraries.
if (compiledClass.declaration.getSourceFile().fileName === if (!appliedReexports &&
dtsFile.fileName.replace(/\.d\.ts$/, '.js')) { compiledClass.declaration.getSourceFile().fileName ===
renderInfo.reexports.push(...compiledClass.reexports); dtsFile.fileName.replace(/\.d\.ts$/, '.js')) {
renderInfo.reexports.push(...compiledFile.reexports);
appliedReexports = true;
} }
dtsMap.set(dtsFile, renderInfo); dtsMap.set(dtsFile, renderInfo);
} }

View File

@ -88,13 +88,13 @@ export class Renderer {
const renderedStatements = const renderedStatements =
this.renderAdjacentStatements(compiledFile.sourceFile, clazz, importManager); this.renderAdjacentStatements(compiledFile.sourceFile, clazz, importManager);
this.srcFormatter.addAdjacentStatements(outputText, clazz, renderedStatements); 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( this.srcFormatter.addConstants(
outputText, outputText,
renderConstantPool( renderConstantPool(

View File

@ -44,6 +44,7 @@ runInEachFileSystem(() => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [ const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect', 'detect',
'analyze', 'analyze',
'register',
'resolve', 'resolve',
'compile', 'compile',
]); ]);
@ -67,6 +68,7 @@ runInEachFileSystem(() => {
} else { } else {
return { return {
metadata, metadata,
decorator: metadata,
trigger: metadata.node, trigger: metadata.node,
}; };
} }
@ -117,7 +119,9 @@ runInEachFileSystem(() => {
getFileSystem(), bundle, reflectionHost, referencesRegistry, getFileSystem(), bundle, reflectionHost, referencesRegistry,
(error) => diagnosticLogs.push(error)); (error) => diagnosticLogs.push(error));
testHandler = createTestHandler(options); 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 = []; migrationLogs = [];
const migration1 = new MockMigration('migration1', migrationLogs); const migration1 = new MockMigration('migration1', migrationLogs);
const migration2 = new MockMigration('migration2', migrationLogs); const migration2 = new MockMigration('migration2', migrationLogs);
@ -372,25 +376,49 @@ runInEachFileSystem(() => {
expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -996666})); expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -996666}));
}); });
it('should report analyze and resolve diagnostics to the `diagnosticHandler` callback', it('should report analyze diagnostics to the `diagnosticHandler` callback', () => {
() => { const analyzer = setUpAnalyzer(
const analyzer = setUpAnalyzer( [
[ {
{ name: _('/node_modules/test-package/index.js'),
name: _('/node_modules/test-package/index.js'), contents: `
contents: `
import {Component, Directive, Injectable} from '@angular/core'; import {Component, Directive, Injectable} from '@angular/core';
export class MyComponent {} export class MyComponent {}
MyComponent.decorators = [{type: Component}]; MyComponent.decorators = [{type: Component}];
`, `,
}, },
], ],
{analyzeError: true, resolveError: true}); {analyzeError: true, resolveError: true});
analyzer.analyzeProgram(); analyzer.analyzeProgram();
expect(diagnosticLogs.length).toEqual(2); expect(diagnosticLogs.length).toEqual(1);
expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999999})); expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999999}));
expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -999998})); 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', () => { describe('declaration files', () => {
@ -410,7 +438,9 @@ runInEachFileSystem(() => {
name: _('/node_modules/test-package/index.d.ts'), name: _('/node_modules/test-package/index.d.ts'),
contents: 'export declare class SomeDirective {}', 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(); result = analyzer.analyzeProgram();
expect(result.size).toBe(0); expect(result.size).toBe(0);
}); });

View File

@ -5,200 +5,86 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, makeDiagnostic} from '../../../src/ngtsc/diagnostics'; import {makeDiagnostic} from '../../../src/ngtsc/diagnostics';
import {AbsoluteFsPath, absoluteFrom} from '../../../src/ngtsc/file_system'; import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform'; 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 {DefaultMigrationHost} from '../../src/analysis/migration_host';
import {AnalyzedClass, AnalyzedFile} from '../../src/analysis/types'; import {NgccTraitCompiler} from '../../src/analysis/ngcc_trait_compiler';
import {NgccClassSymbol} from '../../src/host/ngcc_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {createComponentDecorator} from '../../src/migrations/utils'; 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(() => { runInEachFileSystem(() => {
describe('DefaultMigrationHost', () => { describe('DefaultMigrationHost', () => {
let _: typeof absoluteFrom; let _: typeof absoluteFrom;
let entryPointPath: AbsoluteFsPath;
let mockHost: any;
let mockMetadata: any = {}; let mockMetadata: any = {};
let mockEvaluator: any = {}; let mockEvaluator: any = {};
let mockClazz: any; let mockClazz: any;
let mockDecorator: any = {name: 'MockDecorator'}; let injectedDecorator: any = {name: 'InjectedDecorator'};
let diagnosticHandler = () => {};
beforeEach(() => { beforeEach(() => {
_ = absoluteFrom; _ = 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 = { const mockSourceFile: any = {
fileName: _('/node_modules/some-package/entry-point/test-file.js'), fileName: _('/node_modules/some-package/entry-point/test-file.js'),
}; };
mockClazz = { mockClazz = {
name: {text: 'MockClazz'}, name: {text: 'MockClazz'},
getSourceFile: () => mockSourceFile, getSourceFile: () => mockSourceFile,
getStart: () => 0,
getWidth: () => 0,
}; };
}); });
function createMigrationHost({entryPoint, handlers}: {
entryPoint: EntryPointBundle; handlers: DecoratorHandler<unknown, unknown, unknown>[]
}) {
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()', () => { describe('injectSyntheticDecorator()', () => {
it('should call `detect()` on each of the provided handlers', () => { it('should add the injected decorator into the compilation', () => {
const log: string[] = []; const handler = new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
const handler1 = new TestHandler('handler1', log); loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const handler2 = new TestHandler('handler2', log); const entryPoint =
const host = new DefaultMigrationHost( makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
mockHost, mockMetadata, mockEvaluator, [handler1, handler2], entryPointPath, [], const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
diagnosticHandler); host.injectSyntheticDecorator(mockClazz, injectedDecorator);
host.injectSyntheticDecorator(mockClazz, mockDecorator);
expect(log).toEqual([ const record = compiler.recordFor(mockClazz) !;
`handler1:detect:MockClazz:MockDecorator`, expect(record).toBeDefined();
`handler2:detect:MockClazz:MockDecorator`, 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', it('should mention the migration that failed in the diagnostics message', () => {
() => { const handler = new DiagnosticProducingHandler();
const log: string[] = []; loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const handler1 = new TestHandler('handler1', log); const entryPoint =
const handler2 = new AlwaysDetectHandler('handler2', log); makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const handler3 = new TestHandler('handler3', log); const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
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;
const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null}); const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null});
host.injectSyntheticDecorator(mockClazz, decorator); host.injectSyntheticDecorator(mockClazz, decorator);
expect(diagnostics.length).toBe(1); const record = compiler.recordFor(mockClazz) !;
expect(ts.flattenDiagnosticMessageText(diagnostics[0].messageText, '\n')) 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( .toEqual(
`test diagnostic\n` + `test diagnostic\n` +
` Occurs for @Component decorator inserted by an automatic migration\n` + ` Occurs for @Component decorator inserted by an automatic migration\n` +
@ -206,125 +92,114 @@ runInEachFileSystem(() => {
}); });
}); });
describe('getAllDecorators', () => { 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', () => { it('should include injected decorators', () => {
const log: string[] = []; const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
const handler = new AlwaysDetectHandler('handler', log); const injectedHandler =
const existingDecorator = { name: 'ExistingDecorator' } as Decorator; new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
const analyzedClass: AnalyzedClass = { loadTestFiles([{
name: 'MockClazz', name: _('/node_modules/test/index.js'),
declaration: mockClazz, contents: `
matches: [], import {Directive} from '@angular/core';
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 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.length).toBe(2);
expect(decorators[0]).toBe(existingDecorator); expect(decorators[0].name).toBe('Directive');
expect(decorators[1]).toBe(mockDecorator); expect(decorators[1].name).toBe('InjectedDecorator');
}); });
}); });
describe('isInScope', () => { describe('isInScope', () => {
it('should be true for nodes within the entry-point', () => { it('should be true for nodes within the entry-point', () => {
const analyzedFiles: AnalyzedFile[] = []; loadTestFiles([
const host = new DefaultMigrationHost( {name: _('/node_modules/test/index.js'), contents: `export * from './internal';`},
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles, {name: _('/node_modules/test/internal.js'), contents: `export class InternalClass {}`},
diagnosticHandler); ]);
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 = { expect(host.isInScope(internalClass)).toBe(true);
fileName: _('/node_modules/some-package/entry-point/relative.js'),
};
const clazz: any = {
getSourceFile: () => sourceFile,
};
expect(host.isInScope(clazz)).toBe(true);
}); });
it('should be false for nodes outside the entry-point', () => { it('should be false for nodes outside the entry-point', () => {
const analyzedFiles: AnalyzedFile[] = []; loadTestFiles([
const host = new DefaultMigrationHost( {name: _('/node_modules/external/index.js'), contents: `export class ExternalClass {}`},
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles, {
diagnosticHandler); 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 = { expect(host.isInScope(externalClass)).toBe(false);
fileName: _('/node_modules/some-package/other-entry/index.js'),
};
const clazz: any = {
getSourceFile: () => sourceFile,
};
expect(host.isInScope(clazz)).toBe(false);
}); });
}); });
}); });
}); });
class TestHandler implements DecoratorHandler<unknown, unknown, unknown> { class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, unknown> {
constructor(readonly name: string, protected log: string[]) {} readonly name = DetectDecoratorHandler.name;
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {}
precedence = HandlerPrecedence.PRIMARY;
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined { detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`); if (decorators === null) {
return undefined; return undefined;
} }
analyze(node: ClassDeclaration): AnalysisOutput<unknown> { const decorator = decorators.find(decorator => decorator.name === this.decorator);
this.log.push(this.name + ':analyze:' + node.name.text); if (decorator === undefined) {
return {}; return undefined;
} }
compile(node: ClassDeclaration): CompileResult|CompileResult[] { return {trigger: node, decorator, metadata: {}};
this.log.push(this.name + ':compile:' + node.name.text);
return [];
} }
analyze(node: ClassDeclaration): AnalysisOutput<unknown> { return {}; }
compile(node: ClassDeclaration): CompileResult|CompileResult[] { return []; }
} }
class AlwaysDetectHandler extends TestHandler { class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, unknown> {
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined { readonly name = DiagnosticProducingHandler.name;
super.detect(node, decorators); readonly precedence = HandlerPrecedence.PRIMARY;
return {trigger: node, metadata: {}};
} detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
} const decorator = decorators !== null ? decorators[0] : null;
return {trigger: node, decorator, metadata: {}};
}
class DiagnosticProducingHandler extends AlwaysDetectHandler {
analyze(node: ClassDeclaration): AnalysisOutput<any> { analyze(node: ClassDeclaration): AnalysisOutput<any> {
super.analyze(node);
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]}; return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
} }
compile(node: ClassDeclaration): CompileResult|CompileResult[] { return []; }
} }

View File

@ -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<unknown, unknown, unknown>[]
}) {
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<unknown, unknown, unknown> {
constructor(readonly name: string, protected log: string[]) {}
precedence = HandlerPrecedence.PRIMARY;
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`);
return undefined;
}
analyze(node: ClassDeclaration): AnalysisOutput<unknown> {
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<unknown>|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<unknown>|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<any> {
super.analyze(node);
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
}
}

View File

@ -109,6 +109,7 @@ export class ComponentDecoratorHandler implements
if (decorator !== undefined) { if (decorator !== undefined) {
return { return {
trigger: decorator.node, trigger: decorator.node,
decorator,
metadata: decorator, metadata: decorator,
}; };
} else { } else {

View File

@ -70,10 +70,11 @@ export class DirectiveDecoratorHandler implements
} }
return false; return false;
}); });
return angularField ? {trigger: angularField.node, metadata: null} : undefined; return angularField ? {trigger: angularField.node, decorator: null, metadata: null} :
undefined;
} else { } else {
const decorator = findAngularDecorator(decorators, 'Directive', this.isCore); const decorator = findAngularDecorator(decorators, 'Directive', this.isCore);
return decorator ? {trigger: decorator.node, metadata: decorator} : undefined; return decorator ? {trigger: decorator.node, decorator, metadata: decorator} : undefined;
} }
} }

View File

@ -54,6 +54,7 @@ export class InjectableDecoratorHandler implements
if (decorator !== undefined) { if (decorator !== undefined) {
return { return {
trigger: decorator.node, trigger: decorator.node,
decorator: decorator,
metadata: decorator, metadata: decorator,
}; };
} else { } else {

View File

@ -71,6 +71,7 @@ export class NgModuleDecoratorHandler implements
if (decorator !== undefined) { if (decorator !== undefined) {
return { return {
trigger: decorator.node, trigger: decorator.node,
decorator: decorator,
metadata: decorator, metadata: decorator,
}; };
} else { } else {

View File

@ -44,6 +44,7 @@ export class PipeDecoratorHandler implements DecoratorHandler<Decorator, PipeHan
if (decorator !== undefined) { if (decorator !== undefined) {
return { return {
trigger: decorator.node, trigger: decorator.node,
decorator: decorator,
metadata: decorator, metadata: decorator,
}; };
} else { } else {

View File

@ -85,11 +85,6 @@ export enum ErrorCode {
*/ */
NGMODULE_DECLARATION_NOT_UNIQUE = 6007, NGMODULE_DECLARATION_NOT_UNIQUE = 6007,
/**
* Raised when ngcc tries to inject a synthetic decorator over one that already exists.
*/
NGCC_MIGRATION_DECORATOR_INJECTION_ERROR = 7001,
/** /**
* An element name failed validation against the DOM schema. * An element name failed validation against the DOM schema.
*/ */

View File

@ -9,4 +9,5 @@
export * from './src/api'; export * from './src/api';
export {ClassRecord, TraitCompiler} from './src/compilation'; export {ClassRecord, TraitCompiler} from './src/compilation';
export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform, ReturnTypeTransform} from './src/declaration'; export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform, ReturnTypeTransform} from './src/declaration';
export {AnalyzedTrait, ErroredTrait, PendingTrait, ResolvedTrait, SkippedTrait, Trait, TraitState} from './src/trait';
export {ivyTransformFactory} from './src/transform'; export {ivyTransformFactory} from './src/transform';

View File

@ -153,8 +153,26 @@ export interface DecoratorHandler<D, A, R> {
constantPool: ConstantPool): CompileResult|CompileResult[]; 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<M> { export interface DetectResult<M> {
/**
* The node that triggered the match, which is typically a decorator.
*/
trigger: ts.Node|null; 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<M>; metadata: Readonly<M>;
} }

View File

@ -6,20 +6,18 @@
* found in the LICENSE file at https://angular.io/license * 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 * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ImportRewriter} from '../../imports';
import {IncrementalBuild} from '../../incremental/api'; import {IncrementalBuild} from '../../incremental/api';
import {IndexingContext} from '../../indexer'; import {IndexingContext} from '../../indexer';
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
import {PerfRecorder} from '../../perf'; import {PerfRecorder} from '../../perf';
import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {ClassDeclaration, Decorator, ReflectionHost} from '../../reflection';
import {TypeCheckContext} from '../../typecheck'; import {TypeCheckContext} from '../../typecheck';
import {getSourceFile, isExported} from '../../util/src/typescript'; 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 {DtsTransformRegistry} from './declaration';
import {Trait, TraitState} from './trait'; 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 * Maps source files to any class declaration(s) within them which have been discovered to contain
* Ivy traits. * Ivy traits.
*/ */
private fileToClasses = new Map<ts.SourceFile, Set<ClassDeclaration>>(); protected fileToClasses = new Map<ts.SourceFile, Set<ClassDeclaration>>();
private reexportMap = new Map<string, Map<string, [string, string]>>(); private reexportMap = new Map<string, Map<string, [string, string]>>();
@ -123,7 +121,7 @@ export class TraitCompiler {
} }
const visit = (node: ts.Node): void => { const visit = (node: ts.Node): void => {
if (isNamedClassDeclaration(node)) { if (this.reflector.isClass(node)) {
this.analyzeClass(node, preanalyze ? promises : null); this.analyzeClass(node, preanalyze ? promises : null);
} }
ts.forEachChild(node, visit); 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 { recordsFor(sf: ts.SourceFile): ClassRecord[]|null {
if (!this.fileToClasses.has(sf)) { if (!this.fileToClasses.has(sf)) {
return null; return null;
@ -192,14 +198,20 @@ export class TraitCompiler {
this.fileToClasses.get(sf) !.add(record.node); this.fileToClasses.get(sf) !.add(record.node);
} }
private scanClassForTraits(clazz: ClassDeclaration): ClassRecord|null { private scanClassForTraits(clazz: ClassDeclaration): Trait<unknown, unknown, unknown>[]|null {
if (!this.compileNonExportedClasses && !isExported(clazz)) { if (!this.compileNonExportedClasses && !isExported(clazz)) {
return null; return null;
} }
const decorators = this.reflector.getDecoratorsOfDeclaration(clazz); const decorators = this.reflector.getDecoratorsOfDeclaration(clazz);
let record: ClassRecord|null = null; return this.detectTraits(clazz, decorators);
}
protected detectTraits(clazz: ClassDeclaration, decorators: Decorator[]|null):
Trait<unknown, unknown, unknown>[]|null {
let record: ClassRecord|null = this.recordFor(clazz);
let foundTraits: Trait<unknown, unknown, unknown>[] = [];
for (const handler of this.handlers) { for (const handler of this.handlers) {
const result = handler.detect(clazz, decorators); const result = handler.detect(clazz, decorators);
@ -207,11 +219,12 @@ export class TraitCompiler {
continue; continue;
} }
const isPrimaryHandler = handler.precedence === HandlerPrecedence.PRIMARY; const isPrimaryHandler = handler.precedence === HandlerPrecedence.PRIMARY;
const isWeakHandler = handler.precedence === HandlerPrecedence.WEAK; const isWeakHandler = handler.precedence === HandlerPrecedence.WEAK;
const trait = Trait.pending(handler, result); const trait = Trait.pending(handler, result);
foundTraits.push(trait);
if (record === null) { if (record === null) {
// This is the first handler to match this class. This path is a fast path through which // This is the first handler to match this class. This path is a fast path through which
// most classes will flow. // most classes will flow.
@ -262,8 +275,8 @@ export class TraitCompiler {
length: clazz.getWidth(), length: clazz.getWidth(),
messageText: 'Two incompatible decorators on class', messageText: 'Two incompatible decorators on class',
}]; }];
record.traits = []; record.traits = foundTraits = [];
return record; break;
} }
// Otherwise, it's safe to accept the multiple decorators here. Update some of the metadata // 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<void>[]|null): void { protected analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise<void>[]|null): void {
const record = this.scanClassForTraits(clazz); 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. // There are no Ivy traits on the class, so it can safely be skipped.
return; return;
} }
for (const trait of record.traits) { for (const trait of traits) {
const analyze = () => this.analyzeTrait(clazz, trait); const analyze = () => this.analyzeTrait(clazz, trait);
let preanalysis: Promise<void>|null = null; let preanalysis: Promise<void>|null = null;
@ -299,7 +312,9 @@ export class TraitCompiler {
} }
} }
private analyzeTrait(clazz: ClassDeclaration, trait: Trait<unknown, unknown, unknown>): void { protected analyzeTrait(
clazz: ClassDeclaration, trait: Trait<unknown, unknown, unknown>,
flags?: HandlerFlags): void {
if (trait.state !== TraitState.PENDING) { if (trait.state !== TraitState.PENDING) {
throw new Error( throw new Error(
`Attempt to analyze trait of ${clazz.name.text} in state ${TraitState[trait.state]} (expected DETECTED)`); `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. // Attempt analysis. This could fail with a `FatalDiagnosticError`; catch it if it does.
let result: AnalysisOutput<unknown>; let result: AnalysisOutput<unknown>;
try { try {
result = trait.handler.analyze(clazz, trait.detected.metadata); result = trait.handler.analyze(clazz, trait.detected.metadata, flags);
} catch (err) { } catch (err) {
if (err instanceof FatalDiagnosticError) { if (err instanceof FatalDiagnosticError) {
trait = trait.toErrored([err.toDiagnostic()]); trait = trait.toErrored([err.toDiagnostic()]);
@ -425,7 +440,7 @@ export class TraitCompiler {
compile(clazz: ts.Declaration, constantPool: ConstantPool): CompileResult[]|null { compile(clazz: ts.Declaration, constantPool: ConstantPool): CompileResult[]|null {
const original = ts.getOriginalNode(clazz) as typeof clazz; 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)) { !this.classes.has(original)) {
return null; return null;
} }
@ -465,7 +480,7 @@ export class TraitCompiler {
decoratorsFor(node: ts.Declaration): ts.Decorator[] { decoratorsFor(node: ts.Declaration): ts.Decorator[] {
const original = ts.getOriginalNode(node) as typeof node; 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 []; return [];
} }