From 1eb4066c2e50e71475e158108a727cfbc505dfe1 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 16 Nov 2020 11:22:44 -0800 Subject: [PATCH] refactor(compiler-cli): Expose API for mappping from TCB to template location (#39715) Consumers of the `TemplateTypeChecker` API could be interested in mapping from a shim location back to the original source location in the template. One concrete example of this use-case is for the "find references" action in the Language Service. This will return locations in the TypeScript shim file, and we will then need to be able to map the result back to the template. PR Close #39715 --- .../src/ngtsc/typecheck/api/api.ts | 20 ++- .../src/ngtsc/typecheck/api/checker.ts | 9 +- .../src/ngtsc/typecheck/src/checker.ts | 35 ++++- .../src/ngtsc/typecheck/src/context.ts | 3 +- .../src/ngtsc/typecheck/src/diagnostics.ts | 114 +-------------- .../src/ngtsc/typecheck/src/dom.ts | 2 +- .../src/ngtsc/typecheck/src/oob.ts | 2 +- .../src/ngtsc/typecheck/src/source.ts | 2 +- .../src/ngtsc/typecheck/src/tcb_util.ts | 135 ++++++++++++++++++ .../ngtsc/typecheck/src/type_check_block.ts | 17 +-- ...ecker__get_symbol_of_template_node_spec.ts | 18 +++ 11 files changed, 222 insertions(+), 135 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index 2ff9d963df..014fa84036 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {BoundTarget, DirectiveMeta, SchemaMetadata} from '@angular/compiler'; +import {AbsoluteSourceSpan, BoundTarget, DirectiveMeta, ParseSourceSpan, SchemaMetadata} from '@angular/compiler'; import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system'; @@ -306,6 +306,24 @@ export interface ExternalTemplateSourceMapping { templateUrl: string; } +/** + * A mapping of a TCB template id to a span in the corresponding template source. + */ +export interface SourceLocation { + id: TemplateId; + span: AbsoluteSourceSpan; +} + +/** + * A representation of all a node's template mapping information we know. Useful for producing + * diagnostics based on a TCB node or generally mapping from a TCB node back to a template location. + */ +export interface FullTemplateMapping { + sourceLocation: SourceLocation; + templateSourceMapping: TemplateSourceMapping; + span: ParseSourceSpan; +} + /** * Abstracts the operation of determining which shim file will host a particular component's * template type-checking code. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index 73be8bef38..366d059625 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -9,9 +9,10 @@ import {AST, ParseError, TmplAstNode, TmplAstTemplate} from '@angular/compiler'; import * as ts from 'typescript'; +import {FullTemplateMapping} from './api'; import {GlobalCompletion} from './completion'; import {DirectiveInScope, PipeInScope} from './scope'; -import {Symbol} from './symbols'; +import {ShimLocation, Symbol} from './symbols'; /** * Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the @@ -66,6 +67,12 @@ export interface TemplateTypeChecker { */ getDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[]; + /** + * Given a `shim` and position within the file, returns information for mapping back to a template + * location. + */ + getTemplateMappingAtShimLocation(shimLocation: ShimLocation): FullTemplateMapping|null; + /** * Get all `ts.Diagnostic`s currently available that pertain to the given component. * diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 4550807500..2e77230c88 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -6,24 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, ParseError, parseTemplate, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; +import {AST, ParseError, parseTemplate, TmplAstNode, TmplAstTemplate,} from '@angular/compiler'; import * as ts from 'typescript'; -import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; +import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; import {ReferenceEmitter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; import {isNamedClassDeclaration, ReflectionHost} from '../../reflection'; import {ComponentScopeReader} from '../../scope'; import {isShim} from '../../shims'; import {getSourceFileOrNull} from '../../util/src/typescript'; -import {CompletionKind, DirectiveInScope, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api'; +import {DirectiveInScope, FullTemplateMapping, GlobalCompletion, OptimizeFor, PipeInScope, ProgramTypeCheckAdapter, ShimLocation, Symbol, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api'; import {TemplateDiagnostic} from '../diagnostics'; -import {ExpressionIdentifier, findFirstMatchingNode} from './comments'; import {CompletionEngine} from './completion'; import {InliningMode, ShimTypeCheckingData, TemplateData, TypeCheckContextImpl, TypeCheckingHost} from './context'; -import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics'; +import {shouldReportDiagnostic, translateDiagnostic} from './diagnostics'; import {TemplateSourceManager} from './source'; +import {findTypeCheckBlock, getTemplateMapping, TemplateSourceResolver} from './tcb_util'; import {SymbolBuilder} from './template_symbol_builder'; /** @@ -172,6 +172,31 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return {nodes}; } + private getFileAndShimRecordsForPath(shimPath: AbsoluteFsPath): + {fileRecord: FileTypeCheckingData, shimRecord: ShimTypeCheckingData}|null { + for (const fileRecord of this.state.values()) { + if (fileRecord.shimData.has(shimPath)) { + return {fileRecord, shimRecord: fileRecord.shimData.get(shimPath)!}; + } + } + return null; + } + + getTemplateMappingAtShimLocation({shimPath, positionInShimFile}: ShimLocation): + FullTemplateMapping|null { + const records = this.getFileAndShimRecordsForPath(absoluteFrom(shimPath)); + if (records === null) { + return null; + } + const {fileRecord} = records; + + const shimSf = this.typeCheckingStrategy.getProgram().getSourceFile(absoluteFrom(shimPath)); + if (shimSf === undefined) { + return null; + } + return getTemplateMapping(shimSf, positionInShimFile, fileRecord.sourceManager); + } + /** * Retrieve type-checking diagnostics from the given `ts.SourceFile` using the most recent * type-checking program. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 30c15189bc..e122c3e94b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -20,7 +20,8 @@ import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom'; import {Environment} from './environment'; import {OutOfBandDiagnosticRecorder, OutOfBandDiagnosticRecorderImpl} from './oob'; import {TemplateSourceManager} from './source'; -import {generateTypeCheckBlock, requiresInlineTypeCheckBlock} from './type_check_block'; +import {requiresInlineTypeCheckBlock} from './tcb_util'; +import {generateTypeCheckBlock} from './type_check_block'; import {TypeCheckFile} from './type_check_file'; import {generateInlineTypeCtor, requiresInlineTypeCtor} from './type_constructor'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts index ff8be74968..02a22ef292 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts @@ -7,34 +7,10 @@ */ import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler'; import * as ts from 'typescript'; - -import {getTokenAtPosition} from '../../util/src/typescript'; -import {TemplateId, TemplateSourceMapping} from '../api'; +import {TemplateId} from '../api'; import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics'; +import {getTemplateMapping, TemplateSourceResolver} from './tcb_util'; -import {hasIgnoreMarker, readSpanComment} from './comments'; - - -/** - * Adapter interface which allows the template type-checking diagnostics code to interpret offsets - * in a TCB and map them back to original locations in the template. - */ -export interface TemplateSourceResolver { - getTemplateId(node: ts.ClassDeclaration): TemplateId; - - /** - * For the given template id, retrieve the original source mapping which describes how the offsets - * in the template should be interpreted. - */ - getSourceMapping(id: TemplateId): TemplateSourceMapping; - - /** - * Convert an absolute source span associated with the given template id into a full - * `ParseSourceSpan`. The returned parse span has line and column numbers in addition to only - * absolute offsets and gives access to the original template source. - */ - toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan|null; -} /** * Wraps the node in parenthesis such that inserted span comments become attached to the proper @@ -115,91 +91,13 @@ export function translateDiagnostic( if (diagnostic.file === undefined || diagnostic.start === undefined) { return null; } - - // Locate the node that the diagnostic is reported on and determine its location in the source. - const node = getTokenAtPosition(diagnostic.file, diagnostic.start); - const sourceLocation = findSourceLocation(node, diagnostic.file); - if (sourceLocation === null) { + const fullMapping = getTemplateMapping(diagnostic.file, diagnostic.start, resolver); + if (fullMapping === null) { return null; } - // Now use the external resolver to obtain the full `ParseSourceFile` of the template. - const span = resolver.toParseSourceSpan(sourceLocation.id, sourceLocation.span); - if (span === null) { - return null; - } - - const mapping = resolver.getSourceMapping(sourceLocation.id); + const {sourceLocation, templateSourceMapping, span} = fullMapping; return makeTemplateDiagnostic( - sourceLocation.id, mapping, span, diagnostic.category, diagnostic.code, + sourceLocation.id, templateSourceMapping, span, diagnostic.category, diagnostic.code, diagnostic.messageText); } - -export function findTypeCheckBlock(file: ts.SourceFile, id: TemplateId): ts.Node|null { - for (const stmt of file.statements) { - if (ts.isFunctionDeclaration(stmt) && getTemplateId(stmt, file) === id) { - return stmt; - } - } - return null; -} - -interface SourceLocation { - id: TemplateId; - span: AbsoluteSourceSpan; -} - -/** - * Traverses up the AST starting from the given node to extract the source location from comments - * that have been emitted into the TCB. If the node does not exist within a TCB, or if an ignore - * marker comment is found up the tree, this function returns null. - */ -function findSourceLocation(node: ts.Node, sourceFile: ts.SourceFile): SourceLocation|null { - // Search for comments until the TCB's function declaration is encountered. - while (node !== undefined && !ts.isFunctionDeclaration(node)) { - if (hasIgnoreMarker(node, sourceFile)) { - // There's an ignore marker on this node, so the diagnostic should not be reported. - return null; - } - - const span = readSpanComment(node, sourceFile); - if (span !== null) { - // Once the positional information has been extracted, search further up the TCB to extract - // the unique id that is attached with the TCB's function declaration. - const id = getTemplateId(node, sourceFile); - if (id === null) { - return null; - } - return {id, span}; - } - - node = node.parent; - } - - return null; -} - -function getTemplateId(node: ts.Node, sourceFile: ts.SourceFile): TemplateId|null { - // Walk up to the function declaration of the TCB, the file information is attached there. - while (!ts.isFunctionDeclaration(node)) { - if (hasIgnoreMarker(node, sourceFile)) { - // There's an ignore marker on this node, so the diagnostic should not be reported. - return null; - } - node = node.parent; - - // Bail once we have reached the root. - if (node === undefined) { - return null; - } - } - - const start = node.getFullStart(); - return ts.forEachLeadingCommentRange(sourceFile.text, start, (pos, end, kind) => { - if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { - return null; - } - const commentText = sourceFile.text.substring(pos + 2, end - 2); - return commentText; - }) as TemplateId || null; -} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts index 4d22fd92fb..3ce5ac5322 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts @@ -13,7 +13,7 @@ import {ErrorCode, ngErrorCode} from '../../diagnostics'; import {TemplateId} from '../api'; import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics'; -import {TemplateSourceResolver} from './diagnostics'; +import {TemplateSourceResolver} from './tcb_util'; const REGISTRY = new DomElementSchemaRegistry(); const REMOVE_XHTML_REGEX = /^:xhtml:/; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts index 75170a918a..aaec8d7927 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts @@ -14,7 +14,7 @@ import {ClassDeclaration} from '../../reflection'; import {TemplateId} from '../api'; import {makeTemplateDiagnostic, TemplateDiagnostic} from '../diagnostics'; -import {TemplateSourceResolver} from './diagnostics'; +import {TemplateSourceResolver} from './tcb_util'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts index 6d0c7486fe..8bd6877576 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts @@ -12,8 +12,8 @@ import * as ts from 'typescript'; import {TemplateId, TemplateSourceMapping} from '../api'; import {getTemplateId} from '../diagnostics'; -import {TemplateSourceResolver} from './diagnostics'; import {computeLineStartsMap, getLineAndCharacterFromPosition} from './line_mappings'; +import {TemplateSourceResolver} from './tcb_util'; /** * Represents the source of a template that was processed during type-checking. This information is diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts new file mode 100644 index 0000000000..16ac8b874f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts @@ -0,0 +1,135 @@ +/** + * @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 {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler'; +import {ClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection'; +import * as ts from 'typescript'; + +import {getTokenAtPosition} from '../../util/src/typescript'; +import {FullTemplateMapping, SourceLocation, TemplateId, TemplateSourceMapping} from '../api'; + +import {hasIgnoreMarker, readSpanComment} from './comments'; +import {checkIfClassIsExported, checkIfGenericTypesAreUnbound} from './ts_util'; + +/** + * Adapter interface which allows the template type-checking diagnostics code to interpret offsets + * in a TCB and map them back to original locations in the template. + */ +export interface TemplateSourceResolver { + getTemplateId(node: ts.ClassDeclaration): TemplateId; + + /** + * For the given template id, retrieve the original source mapping which describes how the offsets + * in the template should be interpreted. + */ + getSourceMapping(id: TemplateId): TemplateSourceMapping; + + /** + * Convert an absolute source span associated with the given template id into a full + * `ParseSourceSpan`. The returned parse span has line and column numbers in addition to only + * absolute offsets and gives access to the original template source. + */ + toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan|null; +} + +export function requiresInlineTypeCheckBlock(node: ClassDeclaration): boolean { + // In order to qualify for a declared TCB (not inline) two conditions must be met: + // 1) the class must be exported + // 2) it must not have constrained generic types + if (!checkIfClassIsExported(node)) { + // Condition 1 is false, the class is not exported. + return true; + } else if (!checkIfGenericTypesAreUnbound(node)) { + // Condition 2 is false, the class has constrained generic types + return true; + } else { + return false; + } +} + +/** Maps a shim position back to a template location. */ +export function getTemplateMapping( + shimSf: ts.SourceFile, position: number, resolver: TemplateSourceResolver): FullTemplateMapping| + null { + const node = getTokenAtPosition(shimSf, position); + const sourceLocation = findSourceLocation(node, shimSf); + if (sourceLocation === null) { + return null; + } + + const mapping = resolver.getSourceMapping(sourceLocation.id); + const span = resolver.toParseSourceSpan(sourceLocation.id, sourceLocation.span); + if (span === null) { + return null; + } + return {sourceLocation, templateSourceMapping: mapping, span}; +} + +export function findTypeCheckBlock(file: ts.SourceFile, id: TemplateId): ts.Node|null { + for (const stmt of file.statements) { + if (ts.isFunctionDeclaration(stmt) && getTemplateId(stmt, file) === id) { + return stmt; + } + } + return null; +} + +/** + * Traverses up the AST starting from the given node to extract the source location from comments + * that have been emitted into the TCB. If the node does not exist within a TCB, or if an ignore + * marker comment is found up the tree, this function returns null. + */ +export function findSourceLocation(node: ts.Node, sourceFile: ts.SourceFile): SourceLocation|null { + // Search for comments until the TCB's function declaration is encountered. + while (node !== undefined && !ts.isFunctionDeclaration(node)) { + if (hasIgnoreMarker(node, sourceFile)) { + // There's an ignore marker on this node, so the diagnostic should not be reported. + return null; + } + + const span = readSpanComment(node, sourceFile); + if (span !== null) { + // Once the positional information has been extracted, search further up the TCB to extract + // the unique id that is attached with the TCB's function declaration. + const id = getTemplateId(node, sourceFile); + if (id === null) { + return null; + } + return {id, span}; + } + + node = node.parent; + } + + return null; +} + +function getTemplateId(node: ts.Node, sourceFile: ts.SourceFile): TemplateId|null { + // Walk up to the function declaration of the TCB, the file information is attached there. + while (!ts.isFunctionDeclaration(node)) { + if (hasIgnoreMarker(node, sourceFile)) { + // There's an ignore marker on this node, so the diagnostic should not be reported. + return null; + } + node = node.parent; + + // Bail once we have reached the root. + if (node === undefined) { + return null; + } + } + + const start = node.getFullStart(); + return ts.forEachLeadingCommentRange(sourceFile.text, start, (pos, end, kind) => { + if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { + return null; + } + const commentText = sourceFile.text.substring(pos + 2, end - 2); + return commentText; + }) as TemplateId || null; +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 0410b72bc5..9b37884e49 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -21,7 +21,7 @@ import {Environment} from './environment'; import {astToTypescript, NULL_AS_ANY} from './expression'; import {OutOfBandDiagnosticRecorder} from './oob'; import {ExpressionSemanticVisitor} from './template_semantics'; -import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateTypeQueryForCoercedInput, tsCreateVariable, tsDeclareVariable} from './ts_util'; +import {tsCallMethod, tsCastToAny, tsCreateElement, tsCreateTypeQueryForCoercedInput, tsCreateVariable, tsDeclareVariable} from './ts_util'; /** * Given a `ts.ClassDeclaration` for a component, and metadata regarding that component, compose a @@ -1867,18 +1867,3 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator { return super.resolve(ast); } } - -export function requiresInlineTypeCheckBlock(node: ClassDeclaration): boolean { - // In order to qualify for a declared TCB (not inline) two conditions must be met: - // 1) the class must be exported - // 2) it must not have constrained generic types - if (!checkIfClassIsExported(node)) { - // Condition 1 is false, the class is not exported. - return true; - } else if (!checkIfGenericTypesAreUnbound(node)) { - // Condition 2 is false, the class has constrained generic types - return true; - } else { - return false; - } -} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index 326c9c6136..9343f6e6a5 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -99,6 +99,11 @@ runInEachFileSystem(() => { expect( (symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText()) .toEqual('name'); + + // Ensure we can go back to the original location using the shim location + const mapping = + templateTypeChecker.getTemplateMappingAtShimLocation(symbol.bindings[0].shimLocation)!; + expect(mapping.span.toString()).toEqual('name'); }); describe('templates', () => { @@ -155,6 +160,14 @@ runInEachFileSystem(() => { assertVariableSymbol(symbol); expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('any'); expect(symbol.declaration.name).toEqual('contextFoo'); + + // Ensure we can map the shim locations back to the template + const initializerMapping = + templateTypeChecker.getTemplateMappingAtShimLocation(symbol.initializerLocation)!; + expect(initializerMapping.span.toString()).toEqual('bar'); + const localVarMapping = + templateTypeChecker.getTemplateMappingAtShimLocation(symbol.localVarLocation)!; + expect(localVarMapping.span.toString()).toEqual('contextFoo'); }); it('should get a symbol for local ref which refers to a directive', () => { @@ -170,6 +183,11 @@ runInEachFileSystem(() => { assertReferenceSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol)).toEqual('TestDir'); assertDirectiveReference(symbol); + + // Ensure we can map the var shim location back to the template + const localVarMapping = + templateTypeChecker.getTemplateMappingAtShimLocation(symbol.referenceVarLocation); + expect(localVarMapping!.span.toString()).toEqual('ref1'); }); function assertDirectiveReference(symbol: ReferenceSymbol) {