refactor(migrations): support running existing migrations with plain TS programs (#58541)

Previously we always ran Tsurge migrations with an Angular program, even
if it's a plain `ts_library` target. This has changed now, so we also
need to properly handle the case where a `ts_library` is analyzed, but
no Angular program is available.

PR Close #58541
This commit is contained in:
Paul Gschwendtner 2024-11-07 12:43:36 +00:00 committed by Jessica Janiuk
parent 1a0cee543e
commit 55fde8dbac
14 changed files with 103 additions and 80 deletions

View file

@ -100,7 +100,7 @@ export enum ImportFlags {
*/
export type ImportedFile = ts.SourceFile | 'unknown' | null;
export const enum ReferenceEmitKind {
export enum ReferenceEmitKind {
Success,
Failed,
}

View file

@ -101,11 +101,14 @@ export class OutputMigration extends TsurgeFunnelMigration<
const reflector = new TypeScriptReflectionHost(checker);
const dtsReader = new DtsMetadataReader(checker, reflector);
const evaluator = new PartialEvaluator(reflector, checker, null);
const ngCompiler = info.ngCompiler;
assert(ngCompiler !== null, 'Requires ngCompiler to run the migration');
const resourceLoader = ngCompiler['resourceManager'];
// Pre-Analyze the program and get access to the template type checker.
const {templateTypeChecker} = ngCompiler['ensureAnalyzed']();
const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
// Pre-analyze the program and get access to the template type checker.
// If we are processing a non-Angular target, there is no template info.
const {templateTypeChecker} = info.ngCompiler?.['ensureAnalyzed']() ?? {
templateTypeChecker: null,
};
const knownFields: KnownFields<ClassFieldDescriptor> = {
// Note: We don't support cross-target migration of `Partial<T>` usages.
// This is an acceptable limitation for performance reasons.

View file

@ -16,7 +16,6 @@ import {ResourceLoader} from '@angular/compiler-cli/src/ngtsc/annotations';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {ReferenceEmitter} from '@angular/compiler-cli/src/ngtsc/imports';
import {TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import assert from 'assert';
import {ProgramInfo} from '../../../utils/tsurge/program_info';
/**
@ -26,12 +25,13 @@ import {ProgramInfo} from '../../../utils/tsurge/program_info';
export interface AnalysisProgramInfo extends ProgramInfo {
reflector: TypeScriptReflectionHost;
typeChecker: ts.TypeChecker;
templateTypeChecker: TemplateTypeChecker;
metaRegistry: MetadataReader;
dtsMetadataReader: DtsMetadataReader;
evaluator: PartialEvaluator;
refEmitter: ReferenceEmitter;
resourceLoader: ResourceLoader;
templateTypeChecker: TemplateTypeChecker | null;
metaRegistry: MetadataReader | null;
refEmitter: ReferenceEmitter | null;
resourceLoader: ResourceLoader | null;
}
/**
@ -42,37 +42,37 @@ export interface AnalysisProgramInfo extends ProgramInfo {
*/
export function prepareAnalysisInfo(
userProgram: ts.Program,
compiler: NgCompiler,
compiler: NgCompiler | null,
programAbsoluteRootPaths?: string[],
) {
// Analyze sync and retrieve necessary dependencies.
// Note: `getTemplateTypeChecker` requires the `enableTemplateTypeChecker` flag, but
// this has negative effects as it causes optional TCB operations to execute, which may
// error with unsuccessful reference emits that previously were ignored outside of the migration.
// The migration is resilient to TCB information missing, so this is fine, and all the information
// we need is part of required TCB operations anyway.
const {refEmitter, metaReader, templateTypeChecker} = compiler['ensureAnalyzed']();
let refEmitter: ReferenceEmitter | null = null;
let metaReader: MetadataReader | null = null;
let templateTypeChecker: TemplateTypeChecker | null = null;
let resourceLoader: ResourceLoader | null = null;
// Generate all type check blocks.
templateTypeChecker.generateAllTypeCheckBlocks();
if (compiler !== null) {
// Analyze sync and retrieve necessary dependencies.
// Note: `getTemplateTypeChecker` requires the `enableTemplateTypeChecker` flag, but
// this has negative effects as it causes optional TCB operations to execute, which may
// error with unsuccessful reference emits that previously were ignored outside of the migration.
// The migration is resilient to TCB information missing, so this is fine, and all the information
// we need is part of required TCB operations anyway.
const state = compiler['ensureAnalyzed']();
resourceLoader = compiler['resourceManager'];
refEmitter = state.refEmitter;
metaReader = state.metaReader;
templateTypeChecker = state.templateTypeChecker;
// Generate all type check blocks.
state.templateTypeChecker.generateAllTypeCheckBlocks();
}
const typeChecker = userProgram.getTypeChecker();
const reflector = new TypeScriptReflectionHost(typeChecker);
const evaluator = new PartialEvaluator(reflector, typeChecker, null);
const dtsMetadataReader = new DtsMetadataReader(typeChecker, reflector);
const resourceLoader = compiler['resourceManager'];
// Optional filter for testing. Allows for simulation of parallel execution
// even if some tsconfig's have overlap due to sharing of TS sources.
// (this is commonly not the case in g3 where deps are `.d.ts` files).
const limitToRootNamesOnly = process.env['LIMIT_TO_ROOT_NAMES_ONLY'] === '1';
if (limitToRootNamesOnly) {
assert(
programAbsoluteRootPaths !== undefined,
'Expected absolute root paths when limiting to root names.',
);
}
return {
metaRegistry: metaReader,

View file

@ -11,7 +11,11 @@ import ts from 'typescript';
import {getAngularDecorators} from '@angular/compiler-cli/src/ngtsc/annotations';
import {parseDecoratorInputTransformFunction} from '@angular/compiler-cli/src/ngtsc/annotations/directive';
import {FatalDiagnosticError} from '@angular/compiler-cli/src/ngtsc/diagnostics';
import {Reference, ReferenceEmitter} from '@angular/compiler-cli/src/ngtsc/imports';
import {
Reference,
ReferenceEmitKind,
ReferenceEmitter,
} from '@angular/compiler-cli/src/ngtsc/imports';
import {
DecoratorInputTransform,
DirectiveMeta,
@ -31,6 +35,7 @@ import {
import {CompilationMode} from '@angular/compiler-cli/src/ngtsc/transform';
import {MigrationHost} from '../migration_host';
import {InputNode, isInputContainerNode} from '../input_detection/input_node';
import {NULL_EXPR} from '../../../../../../compiler/src/output/output_ast';
/** Metadata extracted of an input declaration (in `.ts` or `.d.ts` files). */
export interface ExtractedInput extends InputMapping {
@ -46,10 +51,9 @@ export function extractDecoratorInput(
reflector: ReflectionHost,
metadataReader: DtsMetadataReader,
evaluator: PartialEvaluator,
refEmitter: ReferenceEmitter,
): ExtractedInput | null {
return (
extractSourceCodeInput(node, host, reflector, evaluator, refEmitter) ??
extractSourceCodeInput(node, host, reflector, evaluator) ??
extractDtsInput(node, metadataReader)
);
}
@ -116,7 +120,6 @@ function extractSourceCodeInput(
host: MigrationHost,
reflector: ReflectionHost,
evaluator: PartialEvaluator,
refEmitter: ReferenceEmitter,
): ExtractedInput | null {
if (
!isInputContainerNode(node) ||
@ -155,7 +158,7 @@ function extractSourceCodeInput(
isRequired = !!evaluatedInputOpts.get('required');
}
if (evaluatedInputOpts.has('transform') && evaluatedInputOpts.get('transform') != null) {
transformResult = parseTransformOfInput(evaluatedInputOpts, node, reflector, refEmitter);
transformResult = parseTransformOfInput(evaluatedInputOpts, node, reflector);
}
}
}
@ -180,20 +183,32 @@ function parseTransformOfInput(
evaluatedInputOpts: ResolvedValueMap,
node: InputNode,
reflector: ReflectionHost,
refEmitter: ReferenceEmitter,
): DecoratorInputTransform | null {
const transformValue = evaluatedInputOpts.get('transform');
if (!(transformValue instanceof DynamicValue) && !(transformValue instanceof Reference)) {
return null;
}
// For parsing the transform, we don't need a real reference emitter, as
// the emitter is only used for verifying that the transform type could be
// copied into e.g. an `ngInputAccept` class member.
const noopRefEmitter = new ReferenceEmitter([
{
emit: () => ({
kind: ReferenceEmitKind.Success as const,
expression: NULL_EXPR,
importedFile: null,
}),
},
]);
try {
return parseDecoratorInputTransformFunction(
node.parent as ClassDeclaration,
node.name.text,
transformValue,
reflector,
refEmitter,
noopRefEmitter,
CompilationMode.FULL,
);
} catch (e: unknown) {

View file

@ -30,7 +30,7 @@ import {
} from './passes/problematic_patterns/incompatibility';
import {MigrationConfig} from './migration_config';
import {ClassFieldUniqueKey} from './passes/reference_resolution/known_fields';
import {createNgtscProgram} from '../../../utils/tsurge/helpers/ngtsc_program';
import {createBaseProgramInfo} from '../../../utils/tsurge/helpers/create_program';
/**
* Tsurge migration for migrating Angular `@Input()` declarations to
@ -50,9 +50,9 @@ export class SignalInputMigration extends TsurgeComplexMigration<
super();
}
// Override the default ngtsc program creation, to add extra flags.
// Override the default program creation, to add extra flags.
override createProgram(tsconfigAbsPath: string, fs?: FileSystem): BaseProgramInfo {
return createNgtscProgram(tsconfigAbsPath, fs, {
return createBaseProgramInfo(tsconfigAbsPath, fs, {
_compilePoisonedComponents: true,
// We want to migrate non-exported classes too.
compileNonExportedClasses: true,
@ -85,7 +85,6 @@ export class SignalInputMigration extends TsurgeComplexMigration<
// Extend the program info with the analysis information we need in every phase.
prepareAnalysisDeps(info: ProgramInfo): AnalysisProgramInfo {
assert(info.ngCompiler !== null, 'Expected `NgCompiler` to be configured.');
const analysisInfo = {
...info,
...prepareAnalysisInfo(info.program, info.ngCompiler, info.programAbsoluteRootFileNames),

View file

@ -32,7 +32,6 @@ export function pass1__IdentifySourceFileAndDeclarationInputs(
reflector: TypeScriptReflectionHost,
dtsMetadataReader: DtsMetadataReader,
evaluator: PartialEvaluator,
refEmitter: ReferenceEmitter,
knownDecoratorInputs: KnownInputs,
result: MigrationResult,
) {
@ -43,7 +42,6 @@ export function pass1__IdentifySourceFileAndDeclarationInputs(
reflector,
dtsMetadataReader,
evaluator,
refEmitter,
);
if (decoratorInput !== null) {
assert(isInputContainerNode(node), 'Expected input to be declared on accessor or property.');

View file

@ -31,9 +31,9 @@ export function pass2_IdentifySourceFileReferences(
programInfo: ProgramInfo,
checker: ts.TypeChecker,
reflector: ReflectionHost,
resourceLoader: ResourceLoader,
resourceLoader: ResourceLoader | null,
evaluator: PartialEvaluator,
templateTypeChecker: TemplateTypeChecker,
templateTypeChecker: TemplateTypeChecker | null,
groupedTsAstVisitor: GroupedTsAstVisitor,
knownInputs: KnownInputs,
result: MigrationResult,

View file

@ -34,7 +34,7 @@ import {checkInheritanceOfKnownFields} from './problematic_patterns/check_inheri
*/
export function pass4__checkInheritanceOfInputs(
inheritanceGraph: InheritanceGraph,
metaRegistry: MetadataReader,
metaRegistry: MetadataReader | null,
knownInputs: KnownInputs,
) {
checkInheritanceOfKnownFields(inheritanceGraph, metaRegistry, knownInputs, {

View file

@ -43,7 +43,7 @@ export interface InheritanceTracker<D extends ClassFieldDescriptor> {
*/
export function checkInheritanceOfKnownFields<D extends ClassFieldDescriptor>(
inheritanceGraph: InheritanceGraph,
metaRegistry: MetadataReader,
metaRegistry: MetadataReader | null,
fields: KnownFields<D> & InheritanceTracker<D>,
opts: {
getFieldsForClass: (node: ts.ClassDeclaration) => D[];
@ -62,21 +62,23 @@ export function checkInheritanceOfKnownFields<D extends ClassFieldDescriptor>(
assert(ts.isClassDeclaration(inputClass), 'Expected input graph node to be always a class.');
const classFields = opts.getFieldsForClass(inputClass);
const inputFieldNamesFromMetadataArray = new Set<string>();
// Iterate through derived class chains and determine all inputs that are overridden
// via class metadata fields. e.g `@Component#inputs`. This is later used to mark a
// potential similar class input as incompatible— because those cannot be migrated.
const inputFieldNamesFromMetadataArray = new Set<string>();
for (const derivedClasses of inheritanceGraph.traceDerivedClasses(inputClass)) {
const derivedMeta =
ts.isClassDeclaration(derivedClasses) && derivedClasses.name !== undefined
? metaRegistry.getDirectiveMetadata(new Reference(derivedClasses as ClassDeclaration))
: null;
if (metaRegistry !== null) {
for (const derivedClasses of inheritanceGraph.traceDerivedClasses(inputClass)) {
const derivedMeta =
ts.isClassDeclaration(derivedClasses) && derivedClasses.name !== undefined
? metaRegistry.getDirectiveMetadata(new Reference(derivedClasses as ClassDeclaration))
: null;
if (derivedMeta !== null && derivedMeta.inputFieldNamesFromMetadataArray !== null) {
derivedMeta.inputFieldNamesFromMetadataArray.forEach((b) =>
inputFieldNamesFromMetadataArray.add(b),
);
if (derivedMeta !== null && derivedMeta.inputFieldNamesFromMetadataArray !== null) {
derivedMeta.inputFieldNamesFromMetadataArray.forEach((b) =>
inputFieldNamesFromMetadataArray.add(b),
);
}
}
}

View file

@ -40,9 +40,9 @@ export function createFindAllSourceFileReferencesVisitor<D extends ClassFieldDes
programInfo: ProgramInfo,
checker: ts.TypeChecker,
reflector: ReflectionHost,
resourceLoader: ResourceLoader,
resourceLoader: ResourceLoader | null,
evaluator: PartialEvaluator,
templateTypeChecker: TemplateTypeChecker,
templateTypeChecker: TemplateTypeChecker | null,
knownFields: KnownFields<D>,
fieldNamesToConsiderForReferenceLookup: Set<string> | null,
result: ReferenceResult<D>,
@ -67,7 +67,9 @@ export function createFindAllSourceFileReferencesVisitor<D extends ClassFieldDes
const visitor = (node: ts.Node) => {
let lastTime = currentTimeInMs();
if (ts.isClassDeclaration(node)) {
// Note: If there is no template type checker and resource loader, we aren't processing
// an Angular program, and can skip template detection.
if (ts.isClassDeclaration(node) && templateTypeChecker !== null && resourceLoader !== null) {
identifyTemplateReferences(
programInfo,
node,

View file

@ -45,7 +45,6 @@ export function executeAnalysisPhase(
templateTypeChecker,
resourceLoader,
evaluator,
refEmitter,
}: AnalysisProgramInfo,
) {
// Pass 1
@ -61,7 +60,6 @@ export function executeAnalysisPhase(
reflector,
dtsMetadataReader,
evaluator,
refEmitter,
knownInputs,
result,
),

View file

@ -100,12 +100,16 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
}
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
assert(info.ngCompiler !== null, 'Expected queries migration to have an Angular program.');
// Pre-Analyze the program and get access to the template type checker.
const {templateTypeChecker} = info.ngCompiler['ensureAnalyzed']();
// Generate all type check blocks.
templateTypeChecker.generateAllTypeCheckBlocks();
const {templateTypeChecker} = info.ngCompiler?.['ensureAnalyzed']() ?? {
templateTypeChecker: null,
};
const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
// Generate all type check blocks, if we have Angular template information.
if (templateTypeChecker !== null) {
templateTypeChecker.generateAllTypeCheckBlocks();
}
const {sourceFiles, program} = info;
const checker = program.getTypeChecker();
@ -226,7 +230,7 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
info,
checker,
reflector,
info.ngCompiler['resourceManager'],
resourceLoader,
evaluator,
templateTypeChecker,
allFieldsOrKnownQueries,
@ -391,10 +395,13 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
}
override async migrate(globalMetadata: GlobalUnitData, info: ProgramInfo) {
assert(info.ngCompiler !== null, 'Expected queries migration to have an Angular program.');
// Pre-Analyze the program and get access to the template type checker.
const {templateTypeChecker, metaReader} = info.ngCompiler['ensureAnalyzed']();
const {templateTypeChecker, metaReader} = info.ngCompiler?.['ensureAnalyzed']() ?? {
templateTypeChecker: null,
metaReader: null,
};
const resourceLoader = info.ngCompiler?.['resourceManager'] ?? null;
const {program, sourceFiles} = info;
const checker = program.getTypeChecker();
const reflector = new TypeScriptReflectionHost(checker);
@ -472,7 +479,7 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
info,
checker,
reflector,
info.ngCompiler['resourceManager'],
resourceLoader,
evaluator,
templateTypeChecker,
knownQueries,

View file

@ -8,11 +8,10 @@
import {absoluteFrom, FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {isShim} from '@angular/compiler-cli/src/ngtsc/shims';
import {createNgtscProgram} from './helpers/ngtsc_program';
import {BaseProgramInfo, ProgramInfo} from './program_info';
import {getRootDirs} from '@angular/compiler-cli/src/ngtsc/util/src/typescript';
import {Serializable} from './helpers/serializable';
import {createProgramInfo} from './helpers/create_program_info';
import {createBaseProgramInfo} from './helpers/create_program';
/**
* Type describing statistics that could be tracked
@ -42,7 +41,7 @@ export abstract class TsurgeBaseMigration<UnitAnalysisMetadata, CombinedGlobalMe
* - In 1P: Ngtsc or TS programs are created based on the Blaze target.
*/
createProgram(tsconfigAbsPath: string, fs?: FileSystem): BaseProgramInfo {
return createProgramInfo(tsconfigAbsPath, fs);
return createBaseProgramInfo(tsconfigAbsPath, fs);
}
// Optional function to prepare the base `ProgramInfo` even further,

View file

@ -19,8 +19,8 @@ import {createNgtscProgram} from './ngtsc_program';
import {parseTsconfigOrDie} from './ts_parse_config';
import {createPlainTsProgram} from './ts_program';
/** Creates the program info for the given tsconfig path. */
export function createProgramInfo(
/** Creates the base program info for the given tsconfig path. */
export function createBaseProgramInfo(
absoluteTsconfigPath: string,
fs?: FileSystem,
optionOverrides: NgCompilerOptions = {},