mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(migrations): add initial helpers and registry for tracking inputs (#57082)
This commit adds the initial set of helpers and registries for tracking inputs discovered in the signal input migration. A few short summary notes: - Every input has an unique key. This key is used for global analysis that may be performed when the migration is executed on a per individual unit basis in Google3 via e.g. go/tsunami — The keys allow us to build a combined global metadata for e.g. references or figuring out which inputs are incompatible or not. - A known input may be _any_ input in an compilation unit/ the program. E.g. even inputs from `d.ts`. We keep track of these inputs so that we can later figure out if a reference `ts.Identifier` points to any of those. In addition it allows us to attach incompatibility metadata to those. I.e. incompatible for migration because "something writes to the input". PR Close #57082
This commit is contained in:
parent
521a8d287b
commit
a196c8d8db
6 changed files with 337 additions and 0 deletions
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 ts from 'typescript';
|
||||
import {ExtractedInput} from './input_decorator';
|
||||
import {InputDescriptor, InputUniqueKey} from '../utils/input_id';
|
||||
import {ClassIncompatibilityReason, InputMemberIncompatibility} from './incompatibility';
|
||||
|
||||
/**
|
||||
* Class that holds information about a given directive and its input fields.
|
||||
*/
|
||||
export class DirectiveInfo {
|
||||
/**
|
||||
* Map of inputs detected in the given class.
|
||||
* Maps string-based input ids to the detailed input metadata.
|
||||
*/
|
||||
inputFields = new Map<InputUniqueKey, {descriptor: InputDescriptor; metadata: ExtractedInput}>();
|
||||
|
||||
/** Map of input IDs and their incompatibilities. */
|
||||
memberIncompatibility = new Map<InputUniqueKey, InputMemberIncompatibility>();
|
||||
|
||||
/** Whether the whole class is incompatible. */
|
||||
incompatible: ClassIncompatibilityReason | null = null;
|
||||
|
||||
constructor(public clazz: ts.ClassDeclaration) {}
|
||||
|
||||
/**
|
||||
* Checks whether the class is skipped for migration because all of
|
||||
* the inputs are marked as incompatible, or the class itself.
|
||||
*/
|
||||
isClassSkippedForMigration(): boolean {
|
||||
return (
|
||||
this.incompatible !== null ||
|
||||
Array.from(this.inputFields.values()).every(({descriptor}) =>
|
||||
this.isInputMemberIncompatible(descriptor),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given input member is incompatible. If the class is incompatible,
|
||||
* then the member is as well.
|
||||
*/
|
||||
isInputMemberIncompatible(input: InputDescriptor): boolean {
|
||||
return this.incompatible !== null || this.memberIncompatibility.has(input.key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 ts from 'typescript';
|
||||
|
||||
/** Reasons why an input cannot be migrated. */
|
||||
export enum InputIncompatibilityReason {
|
||||
Accessor,
|
||||
WriteAssignment,
|
||||
OverriddenByDerivedClass,
|
||||
RedeclaredViaDerivedClassDecorator,
|
||||
TypeConflictWithBaseClass,
|
||||
ParentIsIncompatible,
|
||||
SpyOnThatOverwritesField,
|
||||
NarrowedInTemplateButNotSupportedYetTODO,
|
||||
IgnoredBecauseOfLanguageServiceRefactoringRange,
|
||||
RequiredInputButNoGoodExplicitTypeExtractable,
|
||||
}
|
||||
|
||||
/** Reasons why a whole class and its inputs cannot be migrated. */
|
||||
export enum ClassIncompatibilityReason {
|
||||
ClassManuallyInstantiated,
|
||||
ClassReferencedInPotentiallyBadLocation,
|
||||
}
|
||||
|
||||
/** Description of an input incompatibility and some helpful debug context. */
|
||||
export interface InputMemberIncompatibility {
|
||||
reason: InputIncompatibilityReason;
|
||||
context: ts.Node | null;
|
||||
}
|
||||
|
||||
/** Whether the given value refers to an input member incompatibility. */
|
||||
export function isInputMemberIncompatibility(value: unknown): value is InputMemberIncompatibility {
|
||||
return (
|
||||
(value as Partial<InputMemberIncompatibility>).reason !== undefined &&
|
||||
(value as Partial<InputMemberIncompatibility>).context !== undefined &&
|
||||
InputIncompatibilityReason.hasOwnProperty(
|
||||
(value as Partial<InputMemberIncompatibility>).reason!,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 ts from 'typescript';
|
||||
import {getMemberName} from '../utils/class_member_names';
|
||||
|
||||
/** Variants of input names supported by Angular compiler's input detection. */
|
||||
export type InputNameNode = ts.Identifier | ts.StringLiteral | ts.PrivateIdentifier;
|
||||
|
||||
/** Describes a TypeScript node that can be an Angular `@Input()` declaration. */
|
||||
export type InputNode = (ts.AccessorDeclaration | ts.PropertyDeclaration) & {
|
||||
name: InputNameNode;
|
||||
parent: ts.ClassDeclaration;
|
||||
};
|
||||
|
||||
/** Checks whether the given node can be an `@Input()` declaration node. */
|
||||
export function isInputContainerNode(node: ts.Node): node is InputNode {
|
||||
return (
|
||||
((ts.isAccessor(node) && ts.isClassDeclaration(node.parent)) ||
|
||||
ts.isPropertyDeclaration(node)) &&
|
||||
getMemberName(node) !== null
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 ts from 'typescript';
|
||||
import {InputDescriptor, InputUniqueKey} from '../utils/input_id';
|
||||
import {ExtractedInput} from './input_decorator';
|
||||
import {InputNode} from './input_node';
|
||||
import {DirectiveInfo} from './directive_info';
|
||||
import {ClassIncompatibilityReason, InputMemberIncompatibility} from './incompatibility';
|
||||
|
||||
/**
|
||||
* Public interface describing a single known `@Input()` in the
|
||||
* compilation.
|
||||
*
|
||||
* A known `@Input()` may be defined in sources, or inside some `d.ts` files
|
||||
* loaded into the program.
|
||||
*/
|
||||
export type KnownInputInfo = {
|
||||
metadata: ExtractedInput;
|
||||
descriptor: InputDescriptor;
|
||||
container: DirectiveInfo;
|
||||
isIncompatible: () => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registry keeping track of all known `@Input()`s in the compilation.
|
||||
*
|
||||
* A known `@Input()` may be defined in sources, or inside some `d.ts` files
|
||||
* loaded into the program.
|
||||
*/
|
||||
export class KnownInputs {
|
||||
/**
|
||||
* Known inputs from the whole program.
|
||||
*/
|
||||
knownInputIds = new Map<InputUniqueKey, KnownInputInfo>();
|
||||
|
||||
/** Known container classes of inputs. */
|
||||
private _allClasses = new Set<ts.ClassDeclaration>();
|
||||
/** Maps classes to their directive info. */
|
||||
private _classToDirectiveInfo = new Map<ts.ClassDeclaration, DirectiveInfo>();
|
||||
|
||||
/** Whether the given input exists. */
|
||||
has(descr: Pick<InputDescriptor, 'key'>): boolean {
|
||||
return this.knownInputIds.has(descr.key);
|
||||
}
|
||||
|
||||
/** Whether the given class contains `@Input`s. */
|
||||
isInputContainingClass(clazz: ts.ClassDeclaration): boolean {
|
||||
return this._classToDirectiveInfo.has(clazz);
|
||||
}
|
||||
|
||||
/** Gets precise `@Input()` information for the given class. */
|
||||
getDirectiveInfoForClass(clazz: ts.ClassDeclaration): DirectiveInfo | undefined {
|
||||
return this._classToDirectiveInfo.get(clazz);
|
||||
}
|
||||
|
||||
/** Gets known input information for the given `@Input()`. */
|
||||
get(descr: Pick<InputDescriptor, 'key'>): KnownInputInfo | undefined {
|
||||
return this.knownInputIds.get(descr.key);
|
||||
}
|
||||
|
||||
/** Gets all classes containing `@Input`s in the compilation. */
|
||||
getAllInputContainingClasses(): ts.ClassDeclaration[] {
|
||||
return Array.from(this._allClasses.values());
|
||||
}
|
||||
|
||||
/** Registers an `@Input()` in the registry. */
|
||||
register(data: {descriptor: InputDescriptor; node: InputNode; metadata: ExtractedInput}) {
|
||||
if (!this._classToDirectiveInfo.has(data.node.parent)) {
|
||||
this._classToDirectiveInfo.set(data.node.parent, new DirectiveInfo(data.node.parent));
|
||||
}
|
||||
const directiveInfo = this._classToDirectiveInfo.get(data.node.parent)!;
|
||||
|
||||
directiveInfo.inputFields.set(data.descriptor.key, {
|
||||
descriptor: data.descriptor,
|
||||
metadata: data.metadata,
|
||||
});
|
||||
this.knownInputIds.set(data.descriptor.key, {
|
||||
metadata: data.metadata,
|
||||
descriptor: data.descriptor,
|
||||
container: directiveInfo,
|
||||
isIncompatible: () => directiveInfo.isInputMemberIncompatible(data.descriptor),
|
||||
});
|
||||
this._allClasses.add(data.node.parent);
|
||||
}
|
||||
|
||||
/** Marks the given input as incompatible for migration. */
|
||||
markInputAsIncompatible(input: InputDescriptor, incompatibility: InputMemberIncompatibility) {
|
||||
if (!this.knownInputIds.has(input.key)) {
|
||||
throw new Error(`Input cannot be marked as incompatible because it's not registered.`);
|
||||
}
|
||||
this.knownInputIds
|
||||
.get(input.key)!
|
||||
.container.memberIncompatibility.set(input.key, incompatibility);
|
||||
}
|
||||
|
||||
/** Marks the given class as incompatible for migration. */
|
||||
markDirectiveAsIncompatible(
|
||||
clazz: ts.ClassDeclaration,
|
||||
incompatibility: ClassIncompatibilityReason,
|
||||
) {
|
||||
if (!this._classToDirectiveInfo.has(clazz)) {
|
||||
throw new Error(`Class cannot be marked as incompatible because it's not known.`);
|
||||
}
|
||||
this._classToDirectiveInfo.get(clazz)!.incompatible = incompatibility;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 ts from 'typescript';
|
||||
import {MigrationHost} from '../migration_host';
|
||||
import {isInputContainerNode} from '../input_detection/input_node';
|
||||
import {getInputDescriptor} from '../utils/input_id';
|
||||
import {KnownInputInfo, KnownInputs} from './known_inputs';
|
||||
|
||||
/**
|
||||
* Attempts to resolve the known `@Input` metadata for the given
|
||||
* type checking symbol. Returns `null` if it's not for an input.
|
||||
*/
|
||||
export function attemptRetrieveInputFromSymbol(
|
||||
host: MigrationHost,
|
||||
memberSymbol: ts.Symbol,
|
||||
knownInputs: KnownInputs,
|
||||
): KnownInputInfo | null {
|
||||
// Even for declared classes from `.d.ts`, the value declaration
|
||||
// should exist and point to the property declaration.
|
||||
if (
|
||||
memberSymbol.valueDeclaration !== undefined &&
|
||||
isInputContainerNode(memberSymbol.valueDeclaration)
|
||||
) {
|
||||
const member = memberSymbol.valueDeclaration;
|
||||
// If the member itself is an input that is being migrated, we
|
||||
// do not need to check, as overriding would be fine then— like before.
|
||||
const memberInputDescr = isInputContainerNode(member) ? getInputDescriptor(host, member) : null;
|
||||
return memberInputDescr !== null ? knownInputs.get(memberInputDescr) ?? null : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 ts from 'typescript';
|
||||
import {MigrationHost} from '../migration_host';
|
||||
import {InputNode} from '../input_detection/input_node';
|
||||
|
||||
/**
|
||||
* Unique key for an input in a project.
|
||||
*
|
||||
* This is the serializable variant, raw string form that
|
||||
* is serializable and allows for cross-target knowledge
|
||||
* needed for the batching capability (via e.g. go/tsunami).
|
||||
*/
|
||||
export type InputUniqueKey = {__inputUniqueKey: true};
|
||||
|
||||
/**
|
||||
* Interface that describes an input recognized in the
|
||||
* migration and project.
|
||||
*/
|
||||
export interface InputDescriptor {
|
||||
key: InputUniqueKey;
|
||||
node: InputNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the descriptor for the given input node.
|
||||
*
|
||||
* An input descriptor describes a recognized input in the
|
||||
* whole project (regardless of batching) and allows easy
|
||||
* access to the associated TypeScript declaration node, while
|
||||
* also providing a unique key for the input that can be used
|
||||
* for serializable communication between compilation units
|
||||
* (e.g. when running via batching; in e.g. go/tsunami).
|
||||
*/
|
||||
export function getInputDescriptor(host: MigrationHost, node: InputNode): InputDescriptor {
|
||||
let className: string;
|
||||
if (ts.isAccessor(node)) {
|
||||
className = node.parent.name?.text || '<anonymous>';
|
||||
} else {
|
||||
className = node.parent.name?.text ?? '<anonymous>';
|
||||
}
|
||||
|
||||
const fileId = host.fileToId(node.getSourceFile());
|
||||
return {
|
||||
key: `${fileId}@@${className}@@${node.name.text}` as unknown as InputUniqueKey,
|
||||
node,
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether the given value is an input descriptor. */
|
||||
export function isInputDescriptor(v: unknown): v is InputDescriptor {
|
||||
return (
|
||||
(v as Partial<InputDescriptor>).key !== undefined &&
|
||||
(v as Partial<InputDescriptor>).node !== undefined
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue