Skip to content

Commit 43a0e28

Browse files
committed
fix(language-service): prevent external template inlay hints from appearing in TS files
Inlay hints from external templates were being incorrectly applied to TypeScript files because the compiler was processing all templates associated with components found in the TS file, regardless of whether the template was inline or external. This resulted in misplaced hints due to mismatched offsets. This change filters the templates and host bindings processed in getInlayHintsForTemplate to only include those that belong to the target file being queried. Fixes #69224 (cherry picked from commit 2e4ed80)
1 parent fe6c5d7 commit 43a0e28

3 files changed

Lines changed: 84 additions & 6 deletions

File tree

packages/language-service/src/inlay_hints.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,21 +210,33 @@ export function getInlayHintsForTemplate(
210210
compiler: NgCompiler,
211211
typeCheckInfo: TypeCheckInfo,
212212
span: ts.TextSpan,
213+
targetFileName: string,
213214
config: InlayHintsConfig = {},
214215
): AngularInlayHint[] {
215216
const mergedConfig = {...DEFAULT_CONFIG, ...config};
216217
const hints: AngularInlayHint[] = [];
217218
const ttc = compiler.getTemplateTypeChecker();
219+
220+
// Get the template AST (may be null for directives without templates)
221+
const template = ttc.getTemplate(typeCheckInfo.declaration);
222+
const templateFileUrl =
223+
template && template.length > 0 ? template[0].sourceSpan.start.file.url : null;
224+
225+
const shouldProcessTemplate = templateFileUrl !== null && templateFileUrl === targetFileName;
226+
const shouldProcessHostBindings =
227+
typeCheckInfo.declaration.getSourceFile().fileName === targetFileName;
228+
229+
if (!shouldProcessTemplate && !shouldProcessHostBindings) {
230+
return hints;
231+
}
232+
218233
const typeChecker = compiler.getCurrentProgram().getTypeChecker();
219234

220235
// Get the Type Check Block for accessing resolved types
221236
// The TCB has TypeScript's resolved types for all expressions, including
222237
// overloaded function calls and generic type inference
223238
const tcb = ttc.getTypeCheckBlock(typeCheckInfo.declaration);
224239

225-
// Get the template AST (may be null for directives without templates)
226-
const template = ttc.getTemplate(typeCheckInfo.declaration);
227-
228240
// Helper to check if a position is within our requested span
229241
const isInSpan = (pos: number): boolean => {
230242
return pos >= span.start && pos < span.start + span.length;
@@ -1805,7 +1817,7 @@ export function getInlayHintsForTemplate(
18051817
}
18061818

18071819
// Visit the template (if it exists - directives without templates will skip this)
1808-
if (template) {
1820+
if (template && shouldProcessTemplate) {
18091821
const visitor = new InlayHintVisitor();
18101822
tmplAstVisitAll(visitor, template);
18111823
}
@@ -1817,7 +1829,7 @@ export function getInlayHintsForTemplate(
18171829
// positions might be outside that range, but they should still be shown since they belong
18181830
// to this component. We create a helper function that checks if we're requesting the whole file.
18191831
const hostElement = ttc.getHostElement(typeCheckInfo.declaration);
1820-
if (hostElement) {
1832+
if (hostElement && shouldProcessHostBindings) {
18211833
// For host bindings, check if the position is within the requested span.
18221834
// Host binding keySpan positions are absolute TypeScript file positions.
18231835
const isHostBindingInSpan = (pos: number): boolean => {

packages/language-service/src/language_service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export class LanguageService {
282282
compiler,
283283
typeCheckInfo,
284284
span,
285+
fileName,
285286
config,
286287
);
287288
hints.push(...templateHints);
@@ -298,7 +299,13 @@ export class LanguageService {
298299
// For external template files (HTML), find the associated component
299300
const typeCheckInfo = getTypeCheckInfoAtPosition(fileName, span.start, compiler);
300301
if (typeCheckInfo) {
301-
const templateHints = getInlayHintsForTemplate(compiler, typeCheckInfo, span, config);
302+
const templateHints = getInlayHintsForTemplate(
303+
compiler,
304+
typeCheckInfo,
305+
span,
306+
fileName,
307+
config,
308+
);
302309
hints.push(...templateHints);
303310
}
304311
}

packages/language-service/test/inlay_hints_spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3724,4 +3724,63 @@ describe('inlay hints', () => {
37243724
});
37253725
});
37263726
});
3727+
3728+
describe('external templates', () => {
3729+
it('should not provide template inlay hints in the TS file', () => {
3730+
const files = {
3731+
'app.ts': `
3732+
import {Component} from '@angular/core';
3733+
3734+
@Component({
3735+
selector: 'app-cmp',
3736+
templateUrl: './app.html',
3737+
standalone: false,
3738+
})
3739+
export class AppCmp {
3740+
users = [{id: 1, name: 'Alice'}];
3741+
onClick(event: any) {}
3742+
}
3743+
`,
3744+
'app.html': `
3745+
@for (user of users; track user.id) {
3746+
<div (click)="onClick($event)">{{ user.name }}</div>
3747+
}
3748+
`,
3749+
};
3750+
project = createModuleAndProjectWithDeclarations(env, 'test', files);
3751+
const appFile = project.openFile('app.ts');
3752+
3753+
const hints = appFile.getInlayHints();
3754+
expect(hints).toEqual([]);
3755+
});
3756+
3757+
it('should provide template inlay hints in the HTML file', () => {
3758+
const files = {
3759+
'app.ts': `
3760+
import {Component} from '@angular/core';
3761+
3762+
@Component({
3763+
selector: 'app-cmp',
3764+
templateUrl: './app.html',
3765+
standalone: false,
3766+
})
3767+
export class AppCmp {
3768+
users = [{id: 1, name: 'Alice'}];
3769+
onClick(e: any) {}
3770+
}
3771+
`,
3772+
'app.html': `
3773+
@for (user of users; track user.id) {
3774+
<div (click)="onClick($event)">{{ user.name }}</div>
3775+
}
3776+
`,
3777+
};
3778+
project = createModuleAndProjectWithDeclarations(env, 'test', files);
3779+
const htmlFile = project.openFile('app.html');
3780+
3781+
const hints = htmlFile.getInlayHints();
3782+
const userHint = hints.find((h) => h.text.includes('id: number'));
3783+
expect(userHint).toBeDefined();
3784+
});
3785+
});
37273786
});

0 commit comments

Comments
 (0)