fix(language-service): get quick info at local var location to align with TS semantics and support type narrowing

Previously, the Language Service fetched Quick Info and definitions for template variables (such as `@let` declarations) using mapping to their `initializerLocation` (the right-hand side expression). This aggressively bubbled the type, JSDoc, and definition identity of the initializer backwards onto the variable itself.

This approach had two flaws:
1. It broke type narrowing because the LS read the original un-narrowed type from the source expression rather than the type of the narrowed intermediate variable in the Type Check Block.
2. It deviated from native TypeScript semantics, where a local `let` binding (`let address = hero.address`) does not inherit the docstrings or `(property)` kind of its initializer, acting solely as a local inferred variable.

By using `localVarLocation` rather than `initializerLocation` for LetDeclaration Quick Info and Type Definitions, these intermediate variables now properly preserve type narrowing within templates and flawlessly match the standard behavior expected of TypeScript block variables. `VariableSymbol.initializerLocation` is retained solely to map the value spans of structural directive contexts (e.g., `exportAs` strings).

fixes #65491
This commit is contained in:
Andrew Scott
2026-03-29 10:28:58 -07:00
committed by Andrew Scott
parent c15e3a005d
commit 75ac120493
7 changed files with 42 additions and 40 deletions
@@ -274,11 +274,6 @@ export interface LetDeclarationSymbol {
* The location in the shim file for the identifier of the `@let` declaration.
*/
localVarLocation: TcbLocation;
/**
* The location in the shim file of the `@let` declaration's initializer expression.
*/
initializerLocation: TcbLocation;
}
/**
@@ -646,9 +646,9 @@ export class SymbolBuilder {
return {
tsType: nodeValueSymbol.tsType,
tsSymbol: nodeValueSymbol.tsSymbol,
initializerLocation: nodeValueSymbol.tcbLocation,
kind: SymbolKind.Variable,
declaration: variable,
initializerLocation: nodeValueSymbol.tcbLocation,
localVarLocation: {
tcbPath: this.tcbPath,
isShimFile: this.tcbIsShim,
@@ -727,7 +727,7 @@ export class SymbolBuilder {
return null;
}
const nodeValueSymbol = this.getSymbolOfTsNode(node.initializer!);
const nodeValueSymbol = this.getSymbolOfTsNode(node.name);
if (nodeValueSymbol === null) {
return null;
@@ -736,7 +736,6 @@ export class SymbolBuilder {
return {
tsType: nodeValueSymbol.tsType,
tsSymbol: nodeValueSymbol.tsSymbol,
initializerLocation: nodeValueSymbol.tcbLocation,
kind: SymbolKind.LetDeclaration,
declaration: decl,
localVarLocation: {
@@ -201,12 +201,6 @@ 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.getSourceMappingAtTcbLocation(
symbol.initializerLocation,
)!;
expect(initializerMapping.span.toString()).toEqual('bar');
const localVarMapping = templateTypeChecker.getSourceMappingAtTcbLocation(
symbol.localVarLocation,
)!;
@@ -2025,11 +2019,6 @@ runInEachFileSystem(() => {
expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('string');
expect(symbol.declaration.name).toEqual('message');
// Ensure we can map the shim locations back to the template
const initializerMapping = templateTypeChecker.getSourceMappingAtTcbLocation(
symbol.initializerLocation,
)!;
expect(initializerMapping.span.toString()).toEqual(`'The value is ' + value`);
const localVarMapping = templateTypeChecker.getSourceMappingAtTcbLocation(
symbol.localVarLocation,
)!;