diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index be30ec0566d..b1c873d9ab6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -1160,6 +1160,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler< if (analysis.isPoisoned && !this.usePoisonedData) { return null; } + const typeCheckScope = this.typeCheckScopeRegistry.getTypeCheckScope(new Reference(node)); const scope = this.scopeReader.getScopeForComponent(node); const selector = analysis.meta.selector; let matcher: DirectiveMatcher | null = null; @@ -1210,6 +1211,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler< getTemplateAst() { return boundTemplate.target.template; }, + getPipe(name) { + const pipe = typeCheckScope.pipes.get(name); + return pipe ? {ref: {node: pipe.ref.node}} : null; + }, }; context.addComponent({ diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts index fe3e866838d..ef7cade1f33 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts @@ -38,6 +38,7 @@ export enum IdentifierKind { Directive, Input, Output, + Pipe, } /** @@ -156,6 +157,14 @@ export interface BoundAttributeIdentifier extends TemplateI } | null; } +/** Describes a pipe used in a template expression. */ +export interface PipeIdentifier extends TemplateIdentifier { + kind: IdentifierKind.Pipe; + target: { + node: T; + } | null; +} + /** * Identifiers recorded at the top level of the template, without any context about the HTML nodes * they were discovered in. @@ -170,7 +179,8 @@ export type TopLevelIdentifier = | LetDeclarationIdentifier | ComponentNodeIdentifier | DirectiveNodeIdentifier - | BoundAttributeIdentifier; + | BoundAttributeIdentifier + | PipeIdentifier; /** Identifiers that can bring in directives to the template. */ export type DirectiveHostIdentifier = @@ -227,6 +237,7 @@ export interface AbstractBoundTemplate { getExpressionTarget(ast: AST): TmplAstReference | TmplAstVariable | TmplAstLetDeclaration | null; getUsedDirectives(): Array<{ref: {node: T}; isComponent: boolean}>; getTemplateAst(): TmplAstNode[] | undefined; + getPipe(name: string): {ref: {node: T}} | null; } /** diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts index f402aad5178..0dbcb9cc4c3 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts @@ -8,6 +8,7 @@ import { AST, ASTWithSource, + BindingPipe, CombinedRecursiveAstVisitor, ImplicitReceiver, ParseSourceSpan, @@ -41,6 +42,7 @@ import { IdentifierKind, LetDeclarationIdentifier, MethodIdentifier, + PipeIdentifier, PropertyIdentifier, ReferenceIdentifier, TemplateNodeIdentifier, @@ -148,6 +150,41 @@ class TemplateVisitor extends CombinedRecursiveAstVisitor { super.visitPropertyRead(ast, null); } + override visitPipe(ast: BindingPipe): void { + this.visitPipeIdentifier(ast); + super.visitPipe(ast, null); + } + + private visitPipeIdentifier(ast: BindingPipe): void { + if (this.currentAstWithSource === null || this.currentAstWithSource.source === null) { + return; + } + + const {absoluteOffset, source: expressionStr} = this.currentAstWithSource; + const identifierStart = ast.nameSpan.start - absoluteOffset; + + if (!expressionStr.startsWith(ast.name, identifierStart)) { + this.errors.push( + new Error( + `Impossible state: "${ast.name}" not found in "${expressionStr}" at location ${identifierStart}`, + ), + ); + return; + } + + const absoluteStart = absoluteOffset + identifierStart; + const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length); + const target = this.boundTemplate.getPipe(ast.name); + const identifier: PipeIdentifier = { + name: ast.name, + span, + kind: IdentifierKind.Pipe, + target: target ? {node: target.ref.node} : null, + }; + + this.identifiers.add(identifier); + } + override visitBoundAttribute(attribute: TmplAstBoundAttribute): void { const identifier = this.bindingToIdentifier(attribute, IdentifierKind.Input); if (identifier !== null) { diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts index 05d682f5711..e9f5ed4a30f 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts @@ -1326,5 +1326,49 @@ runInEachFileSystem(() => { ); expect(inputOrOutputRefs).toEqual([]); }); + + it('should discover pipes in template expressions', () => { + const pipe = ` + export class MyPipe { + transform(v: any) { return v; } + } + `; + const pipeDecl = util.getComponentDeclaration(pipe, 'MyPipe'); + const template = '{{ foo | myPipe }}'; + const boundTemplate = util.getBoundTemplate( + template, + {}, + [], + [ + { + name: 'myPipe', + declaration: pipeDecl, + }, + ], + ); + const refs = Array.from(getTemplateIdentifiers(boundTemplate)); + + expect(refs).toContain({ + name: 'myPipe', + kind: IdentifierKind.Pipe, + span: new AbsoluteSourceSpan(9, 15), + target: { + node: pipeDecl, + }, + }); + }); + + it('should handle unresolvable pipes gracefully', () => { + const template = '{{ foo | unknownPipe }}'; + const boundTemplate = util.getBoundTemplate(template); + const refs = Array.from(getTemplateIdentifiers(boundTemplate)); + + expect(refs).toContain({ + name: 'unknownPipe', + kind: IdentifierKind.Pipe, + span: new AbsoluteSourceSpan(9, 20), + target: null, + }); + }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts index d7f98cea161..9dfcf78880d 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts @@ -63,6 +63,10 @@ export function getBoundTemplate( inputs?: Record; outputs?: Record; }> = [], + pipes: Array<{ + name: string; + declaration: ClassDeclaration; + }> = [], ): AbstractBoundTemplate { const componentsMeta = components.map(({selector, declaration, inputs = {}, outputs = {}}) => ({ ref: new Reference(declaration), @@ -130,6 +134,10 @@ export function getBoundTemplate( getTemplateAst() { return boundTemplate.target.template; }, + getPipe(name) { + const pipe = pipes.find((p) => p.name === name); + return pipe ? {ref: {node: pipe.declaration}} : null; + }, }; return abstractBoundTemplate; }