mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(compiler-cli): index bound directive inputs and outputs in templates
Update the template indexer to discover and record bound directive inputs (property bindings, static text attributes) and outputs (event bindings). This associates template binding identifiers with their target directive or component class declarations, enabling indexers and language tooling to properly resolve and cross-reference bound directive inputs and outputs.
This commit is contained in:
committed by
Matthew Beck
parent
6f91982c0d
commit
4fc45a9b38
@@ -1187,6 +1187,13 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
|
||||
getReferenceTarget(node) {
|
||||
return boundTemplate.getReferenceTarget(node);
|
||||
},
|
||||
getConsumerOfBinding(binding) {
|
||||
const consumer = boundTemplate.getConsumerOfBinding(binding);
|
||||
if (consumer && 'ref' in consumer && consumer.ref) {
|
||||
return {ref: {node: consumer.ref.node}};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getExpressionTarget(ast) {
|
||||
return boundTemplate.getExpressionTarget(ast);
|
||||
},
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import {
|
||||
AST,
|
||||
TmplAstBoundAttribute,
|
||||
TmplAstBoundEvent,
|
||||
TmplAstComponent,
|
||||
TmplAstDirective,
|
||||
TmplAstElement,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
TmplAstNode,
|
||||
TmplAstReference,
|
||||
TmplAstTemplate,
|
||||
TmplAstTextAttribute,
|
||||
TmplAstVariable,
|
||||
} from '@angular/compiler';
|
||||
import {DeclarationNode} from '../../reflection';
|
||||
@@ -33,6 +36,8 @@ export enum IdentifierKind {
|
||||
LetDeclaration,
|
||||
Component,
|
||||
Directive,
|
||||
Input,
|
||||
Output,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,11 +143,19 @@ export interface VariableIdentifier extends TemplateIdentifier {
|
||||
kind: IdentifierKind.Variable;
|
||||
}
|
||||
|
||||
/** Describes a `@let` declaration in a template. */
|
||||
/** Describes an `@let` declaration in a template. */
|
||||
export interface LetDeclarationIdentifier extends TemplateIdentifier {
|
||||
kind: IdentifierKind.LetDeclaration;
|
||||
}
|
||||
|
||||
/** Describes a bound attribute or event in a template targeting an Angular input/output. */
|
||||
export interface BoundAttributeIdentifier<T = DeclarationNode> extends TemplateIdentifier {
|
||||
kind: IdentifierKind.Input | IdentifierKind.Output;
|
||||
target: {
|
||||
node: T;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifiers recorded at the top level of the template, without any context about the HTML nodes
|
||||
* they were discovered in.
|
||||
@@ -156,7 +169,8 @@ export type TopLevelIdentifier<T = DeclarationNode> =
|
||||
| MethodIdentifier<T>
|
||||
| LetDeclarationIdentifier
|
||||
| ComponentNodeIdentifier<T>
|
||||
| DirectiveNodeIdentifier<T>;
|
||||
| DirectiveNodeIdentifier<T>
|
||||
| BoundAttributeIdentifier<T>;
|
||||
|
||||
/** Identifiers that can bring in directives to the template. */
|
||||
export type DirectiveHostIdentifier<T = DeclarationNode> =
|
||||
@@ -207,6 +221,9 @@ export interface AbstractBoundTemplate<T> {
|
||||
directive: {ref: {node: T}};
|
||||
}
|
||||
| null;
|
||||
getConsumerOfBinding?(
|
||||
binding: TmplAstBoundAttribute | TmplAstBoundEvent | TmplAstTextAttribute,
|
||||
): {ref: {node: T}} | TmplAstElement | TmplAstTemplate | null;
|
||||
getExpressionTarget(ast: AST): TmplAstReference | TmplAstVariable | TmplAstLetDeclaration | null;
|
||||
getUsedDirectives(): Array<{ref: {node: T}; isComponent: boolean}>;
|
||||
getTemplateAst(): TmplAstNode[] | undefined;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PropertyRead,
|
||||
ThisReceiver,
|
||||
TmplAstBoundAttribute,
|
||||
TmplAstBoundEvent,
|
||||
TmplAstComponent,
|
||||
TmplAstDirective,
|
||||
TmplAstElement,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
TmplAstNode,
|
||||
TmplAstReference,
|
||||
TmplAstTemplate,
|
||||
TmplAstTextAttribute,
|
||||
TmplAstVariable,
|
||||
tmplAstVisitAll,
|
||||
} from '@angular/compiler';
|
||||
@@ -31,6 +33,7 @@ import {
|
||||
AbsoluteSourceSpan,
|
||||
AbstractBoundTemplate,
|
||||
AttributeIdentifier,
|
||||
BoundAttributeIdentifier,
|
||||
ComponentNodeIdentifier,
|
||||
DirectiveHostIdentifier,
|
||||
DirectiveNodeIdentifier,
|
||||
@@ -48,9 +51,7 @@ import {
|
||||
type ExpressionIdentifier<T = DeclarationNode> = PropertyIdentifier<T> | MethodIdentifier<T>;
|
||||
type TmplTarget = TmplAstReference | TmplAstVariable | TmplAstLetDeclaration;
|
||||
type TargetIdentifier<T = DeclarationNode> =
|
||||
| ReferenceIdentifier<T>
|
||||
| VariableIdentifier
|
||||
| LetDeclarationIdentifier;
|
||||
ReferenceIdentifier<T> | VariableIdentifier | LetDeclarationIdentifier;
|
||||
type TargetIdentifierMap<T = DeclarationNode> = Map<TmplTarget, TargetIdentifier<T>>;
|
||||
|
||||
/**
|
||||
@@ -148,6 +149,10 @@ class TemplateVisitor<T = DeclarationNode> extends CombinedRecursiveAstVisitor {
|
||||
}
|
||||
|
||||
override visitBoundAttribute(attribute: TmplAstBoundAttribute): void {
|
||||
const identifier = this.bindingToIdentifier(attribute, IdentifierKind.Input);
|
||||
if (identifier !== null) {
|
||||
this.identifiers.add(identifier);
|
||||
}
|
||||
const previous = this.currentAstWithSource;
|
||||
this.currentAstWithSource = {
|
||||
source: attribute.valueSpan?.toString() || null,
|
||||
@@ -157,6 +162,53 @@ class TemplateVisitor<T = DeclarationNode> extends CombinedRecursiveAstVisitor {
|
||||
this.currentAstWithSource = previous;
|
||||
}
|
||||
|
||||
override visitBoundEvent(event: TmplAstBoundEvent): void {
|
||||
const identifier = this.bindingToIdentifier(event, IdentifierKind.Output);
|
||||
if (identifier !== null) {
|
||||
this.identifiers.add(identifier);
|
||||
}
|
||||
super.visitBoundEvent(event);
|
||||
}
|
||||
|
||||
override visitTextAttribute(attribute: TmplAstTextAttribute): void {
|
||||
const identifier = this.bindingToIdentifier(attribute, IdentifierKind.Input);
|
||||
if (identifier !== null) {
|
||||
this.identifiers.add(identifier);
|
||||
}
|
||||
super.visitTextAttribute(attribute);
|
||||
}
|
||||
|
||||
private bindingToIdentifier(
|
||||
node: TmplAstBoundAttribute | TmplAstBoundEvent | TmplAstTextAttribute,
|
||||
kind: IdentifierKind.Input | IdentifierKind.Output,
|
||||
): BoundAttributeIdentifier<T> | null {
|
||||
if (!this.boundTemplate.getConsumerOfBinding) {
|
||||
return null;
|
||||
}
|
||||
const consumer = this.boundTemplate.getConsumerOfBinding(node);
|
||||
if (!consumer || consumer instanceof TmplAstElement || consumer instanceof TmplAstTemplate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keySpan = node.keySpan ?? (node instanceof TmplAstTextAttribute ? node.sourceSpan : null);
|
||||
if (!keySpan) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const span = new AbsoluteSourceSpan(
|
||||
keySpan.start.offset,
|
||||
keySpan.start.offset + node.name.length,
|
||||
);
|
||||
return {
|
||||
name: node.name,
|
||||
span,
|
||||
kind,
|
||||
target: {
|
||||
node: consumer.ref.node,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Creates an identifier for a template element or template node. */
|
||||
private directiveHostToIdentifier(
|
||||
node: TmplAstElement | TmplAstTemplate | TmplAstComponent | TmplAstDirective,
|
||||
|
||||
@@ -1196,5 +1196,135 @@ runInEachFileSystem(() => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should discover bound attribute inputs on components', () => {
|
||||
const comp = `
|
||||
export class MyComp {
|
||||
myInput: string;
|
||||
}
|
||||
`;
|
||||
const compDecl = util.getComponentDeclaration(comp, 'MyComp');
|
||||
const template = '<my-comp [myInput]="foo"></my-comp>';
|
||||
const boundTemplate = util.getBoundTemplate(template, {}, [
|
||||
{
|
||||
selector: 'my-comp',
|
||||
declaration: compDecl,
|
||||
inputs: {myInput: 'myInput'},
|
||||
},
|
||||
]);
|
||||
const refs = Array.from(getTemplateIdentifiers(boundTemplate));
|
||||
|
||||
expect(refs).toContain({
|
||||
name: 'myInput',
|
||||
kind: IdentifierKind.Input,
|
||||
span: new AbsoluteSourceSpan(10, 17),
|
||||
target: {
|
||||
node: compDecl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should discover bound event outputs on components', () => {
|
||||
const comp = `
|
||||
export class MyComp {
|
||||
myOutput: any;
|
||||
}
|
||||
`;
|
||||
const compDecl = util.getComponentDeclaration(comp, 'MyComp');
|
||||
const template = '<my-comp (myOutput)="handle()"></my-comp>';
|
||||
const boundTemplate = util.getBoundTemplate(template, {}, [
|
||||
{
|
||||
selector: 'my-comp',
|
||||
declaration: compDecl,
|
||||
outputs: {myOutput: 'myOutput'},
|
||||
},
|
||||
]);
|
||||
const refs = Array.from(getTemplateIdentifiers(boundTemplate));
|
||||
|
||||
expect(refs).toContain({
|
||||
name: 'myOutput',
|
||||
kind: IdentifierKind.Output,
|
||||
span: new AbsoluteSourceSpan(10, 18),
|
||||
target: {
|
||||
node: compDecl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should discover static text attribute inputs on components', () => {
|
||||
const comp = `
|
||||
export class MyComp {
|
||||
myInput: string;
|
||||
}
|
||||
`;
|
||||
const compDecl = util.getComponentDeclaration(comp, 'MyComp');
|
||||
const template = '<my-comp myInput="staticVal"></my-comp>';
|
||||
const boundTemplate = util.getBoundTemplate(template, {}, [
|
||||
{
|
||||
selector: 'my-comp',
|
||||
declaration: compDecl,
|
||||
inputs: {myInput: 'myInput'},
|
||||
},
|
||||
]);
|
||||
const refs = Array.from(getTemplateIdentifiers(boundTemplate));
|
||||
|
||||
expect(refs).toContain({
|
||||
name: 'myInput',
|
||||
kind: IdentifierKind.Input,
|
||||
span: new AbsoluteSourceSpan(9, 16),
|
||||
target: {
|
||||
node: compDecl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should discover bound attribute inputs and event outputs on directives applied to elements', () => {
|
||||
const dir = `
|
||||
export class MyDir {
|
||||
dirInput: string;
|
||||
dirOutput: any;
|
||||
}
|
||||
`;
|
||||
const dirDecl = util.getComponentDeclaration(dir, 'MyDir');
|
||||
const template = '<div myDir [dirInput]="foo" (dirOutput)="handle()"></div>';
|
||||
const boundTemplate = util.getBoundTemplate(template, {}, [
|
||||
{
|
||||
selector: '[myDir]',
|
||||
declaration: dirDecl,
|
||||
inputs: {dirInput: 'dirInput'},
|
||||
outputs: {dirOutput: 'dirOutput'},
|
||||
},
|
||||
]);
|
||||
const refs = Array.from(getTemplateIdentifiers(boundTemplate));
|
||||
|
||||
expect(refs).toContain({
|
||||
name: 'dirInput',
|
||||
kind: IdentifierKind.Input,
|
||||
span: new AbsoluteSourceSpan(12, 20),
|
||||
target: {
|
||||
node: dirDecl,
|
||||
},
|
||||
});
|
||||
expect(refs).toContain({
|
||||
name: 'dirOutput',
|
||||
kind: IdentifierKind.Output,
|
||||
span: new AbsoluteSourceSpan(29, 38),
|
||||
target: {
|
||||
node: dirDecl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not discover input or output identifiers for native element bindings and events', () => {
|
||||
const template =
|
||||
'<button [disabled]="isDisabled" (click)="handleClick()" title="nativeTitle"></button>';
|
||||
const boundTemplate = util.getBoundTemplate(template);
|
||||
const refs = Array.from(getTemplateIdentifiers(boundTemplate));
|
||||
|
||||
const inputOrOutputRefs = refs.filter(
|
||||
(ref) => ref.kind === IdentifierKind.Input || ref.kind === IdentifierKind.Output,
|
||||
);
|
||||
expect(inputOrOutputRefs).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,15 +57,20 @@ export function getComponentDeclaration(componentStr: string, className: string)
|
||||
export function getBoundTemplate(
|
||||
template: string,
|
||||
options: ParseTemplateOptions = {},
|
||||
components: Array<{selector: string | null; declaration: ClassDeclaration}> = [],
|
||||
components: Array<{
|
||||
selector: string | null;
|
||||
declaration: ClassDeclaration;
|
||||
inputs?: Record<string, string>;
|
||||
outputs?: Record<string, string>;
|
||||
}> = [],
|
||||
): AbstractBoundTemplate<DeclarationNode> {
|
||||
const componentsMeta = components.map(({selector, declaration}) => ({
|
||||
const componentsMeta = components.map(({selector, declaration, inputs = {}, outputs = {}}) => ({
|
||||
ref: new Reference(declaration),
|
||||
selector,
|
||||
name: declaration.name.getText(),
|
||||
isComponent: true,
|
||||
inputs: ClassPropertyMapping.fromMappedObject({}),
|
||||
outputs: ClassPropertyMapping.fromMappedObject({}),
|
||||
inputs: ClassPropertyMapping.fromMappedObject(inputs),
|
||||
outputs: ClassPropertyMapping.fromMappedObject(outputs),
|
||||
exportAs: null,
|
||||
isStructural: false,
|
||||
animationTriggerNames: null,
|
||||
@@ -106,6 +111,13 @@ export function getBoundTemplate(
|
||||
getReferenceTarget(node) {
|
||||
return boundTemplate.getReferenceTarget(node);
|
||||
},
|
||||
getConsumerOfBinding(binding) {
|
||||
const consumer = boundTemplate.getConsumerOfBinding(binding);
|
||||
if (consumer && 'ref' in consumer && consumer.ref) {
|
||||
return {ref: {node: consumer.ref.node}};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getExpressionTarget(ast) {
|
||||
return boundTemplate.getExpressionTarget(ast);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user