Skip to content

Commit 1eba57e

Browse files
alxhubjosephperrott
authored andcommitted
fix(language-service): show suggestion when type inference is suboptimal (angular#41072)
The Ivy Language Service uses the compiler's template type-checking engine, which honors the configuration in the user's tsconfig.json. We recommend that users upgrade to `strictTemplates` mode in their projects to take advantage of the best possible type inference, and thus to have the best experience in Language Service. If a project is not using `strictTemplates`, then the compiler will not leverage certain type inference options it has. One case where this is very noticeable is the inference of let- variables for structural directives that provide a template context guard (such as NgFor). Without `strictTemplates`, these guards will not be applied and such variables will be inferred as 'any', degrading the user experience within Language Service. This is working as designed, since the Language Service _should_ reflect types exactly as the compiler sees them. However, the View Engine Language Service used its own type system that _would_ infer these types even when the compiler did not. As a result, it's confusing to some users why the Ivy Language Service has "worse" type inference. To address this confusion, this commit implements a suggestion diagnostic which is shown in the Language Service for variables which could have been narrowed via a context guard, but the type checking configuration didn't allow it. This should make the reason why variables receive the 'any' type as well as the action needed to improve the typings much more obvious, improving the Language Service experience. Fixes angular/vscode-ng-language-service#1155 Closes angular#41042 PR Close angular#41072
1 parent 80f11a9 commit 1eba57e

File tree

11 files changed

+131
-14
lines changed

11 files changed

+131
-14
lines changed

goldens/public-api/compiler-cli/error_code.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@ export declare enum ErrorCode {
3838
INLINE_TCB_REQUIRED = 8900,
3939
INLINE_TYPE_CTOR_REQUIRED = 8901,
4040
INJECTABLE_DUPLICATE_PROV = 9001,
41-
SUGGEST_STRICT_TEMPLATES = 10001
41+
SUGGEST_STRICT_TEMPLATES = 10001,
42+
SUGGEST_SUBOPTIMAL_TYPE_INFERENCE = 10002
4243
}

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,10 @@ export class NgCompiler {
692692
strictLiteralTypes: true,
693693
enableTemplateTypeChecker: this.enableTemplateTypeChecker,
694694
useInlineTypeConstructors,
695+
// Warnings for suboptimal type inference are only enabled if in Language Service mode
696+
// (providing the full TemplateTypeChecker API) and if strict mode is not enabled. In strict
697+
// mode, the user is in full control of type inference.
698+
suggestionsForSuboptimalTypeInference: this.enableTemplateTypeChecker && !strictTemplates,
695699
};
696700
} else {
697701
typeCheckingConfig = {
@@ -717,6 +721,9 @@ export class NgCompiler {
717721
strictLiteralTypes: false,
718722
enableTemplateTypeChecker: this.enableTemplateTypeChecker,
719723
useInlineTypeConstructors,
724+
// In "basic" template type-checking mode, no warnings are produced since most things are
725+
// not checked anyways.
726+
suggestionsForSuboptimalTypeInference: false,
720727
};
721728
}
722729

packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,15 +171,22 @@ export enum ErrorCode {
171171
*/
172172
INJECTABLE_DUPLICATE_PROV = 9001,
173173

174-
// 10XXX error codes are reserved for diagnostics with category
175-
// `ts.DiagnosticCategory.Suggestion`. These diagnostics are generated by
176-
// language service.
174+
// 10XXX error codes are reserved for diagnostics with categories other than
175+
// `ts.DiagnosticCategory.Error`. These diagnostics are generated by the compiler when configured
176+
// to do so by a tool such as the Language Service, or by the Language Service itself.
177177

178178
/**
179179
* Suggest users to enable `strictTemplates` to make use of full capabilities
180180
* provided by Angular language service.
181181
*/
182182
SUGGEST_STRICT_TEMPLATES = 10001,
183+
184+
/**
185+
* Indicates that a particular structural directive provides advanced type narrowing
186+
* functionality, but the current template type-checking configuration does not allow its usage in
187+
* type inference.
188+
*/
189+
SUGGEST_SUBOPTIMAL_TYPE_INFERENCE = 10002,
183190
}
184191

185192
/**

packages/compiler-cli/src/ngtsc/typecheck/api/api.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,20 @@ export interface TypeCheckingConfig {
275275
* inlining, this must be set to `false`.
276276
*/
277277
useInlineTypeConstructors: boolean;
278+
279+
/**
280+
* Whether or not to produce diagnostic suggestions in cases where the compiler could have
281+
* inferred a better type for a construct, but was prevented from doing so by the current type
282+
* checking configuration.
283+
*
284+
* For example, if the compiler could have used a template context guard to infer a better type
285+
* for a structural directive's context and `let-` variables, but the user is in
286+
* `fullTemplateTypeCheck` mode and such guards are therefore disabled.
287+
*
288+
* This mode is useful for clients like the Language Service which want to inform users of
289+
* opportunities to improve their own developer experience.
290+
*/
291+
suggestionsForSuboptimalTypeInference: boolean;
278292
}
279293

280294

packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export interface OutOfBandDiagnosticRecorder {
6868

6969
requiresInlineTypeConstructors(
7070
templateId: TemplateId, node: ClassDeclaration, directives: ClassDeclaration[]): void;
71+
72+
/**
73+
* Report a warning when structural directives support context guards, but the current
74+
* type-checking configuration prohibits their usage.
75+
*/
76+
suboptimalTypeInference(templateId: TemplateId, variables: TmplAstVariable[]): void;
7177
}
7278

7379
export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder {
@@ -174,6 +180,37 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor
174180
directives.map(
175181
dir => makeRelatedInformation(dir.name, `Requires an inline type constructor.`))));
176182
}
183+
184+
suboptimalTypeInference(templateId: TemplateId, variables: TmplAstVariable[]): void {
185+
const mapping = this.resolver.getSourceMapping(templateId);
186+
187+
// Select one of the template variables that's most suitable for reporting the diagnostic. Any
188+
// variable will do, but prefer one bound to the context's $implicit if present.
189+
let diagnosticVar: TmplAstVariable|null = null;
190+
for (const variable of variables) {
191+
if (diagnosticVar === null || (variable.value === '' || variable.value === '$implicit')) {
192+
diagnosticVar = variable;
193+
}
194+
}
195+
if (diagnosticVar === null) {
196+
// There is no variable on which to report the diagnostic.
197+
return;
198+
}
199+
200+
let varIdentification = `'${diagnosticVar.name}'`;
201+
if (variables.length === 2) {
202+
varIdentification += ` (and 1 other)`;
203+
} else if (variables.length > 2) {
204+
varIdentification += ` (and ${variables.length - 1} others)`;
205+
}
206+
const message =
207+
`This structural directive supports advanced type inference, but the current compiler configuration prevents its usage. The variable ${
208+
varIdentification} will have type 'any' as a result.\n\nConsider enabling the 'strictTemplates' option in your tsconfig.json for better type inference within this template.`;
209+
210+
this._diagnostics.push(makeTemplateDiagnostic(
211+
templateId, mapping, diagnosticVar.keySpan, ts.DiagnosticCategory.Suggestion,
212+
ngErrorCode(ErrorCode.SUGGEST_SUBOPTIMAL_TYPE_INFERENCE), message));
213+
}
177214
}
178215

179216
function makeInlineDiagnostic(

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,20 @@ class TcbTemplateBodyOp extends TcbOp {
286286

287287
// The second kind of guard is a template context guard. This guard narrows the template
288288
// rendering context variable `ctx`.
289-
if (dir.hasNgTemplateContextGuard && this.tcb.env.config.applyTemplateContextGuards) {
290-
const ctx = this.scope.resolve(this.template);
291-
const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
292-
addParseSpanInfo(guardInvoke, this.template.sourceSpan);
293-
directiveGuards.push(guardInvoke);
289+
if (dir.hasNgTemplateContextGuard) {
290+
if (this.tcb.env.config.applyTemplateContextGuards) {
291+
const ctx = this.scope.resolve(this.template);
292+
const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
293+
addParseSpanInfo(guardInvoke, this.template.sourceSpan);
294+
directiveGuards.push(guardInvoke);
295+
} else if (
296+
this.template.variables.length > 0 &&
297+
this.tcb.env.config.suggestionsForSuboptimalTypeInference) {
298+
// The compiler could have inferred a better type for the variables in this template,
299+
// but was prevented from doing so by the type-checking configuration. Issue a warning
300+
// diagnostic.
301+
this.tcb.oobRecorder.suboptimalTypeInference(this.tcb.id, this.template.variables);
302+
}
294303
}
295304
}
296305
}

packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ export const ALL_ENABLED_CONFIG: Readonly<TypeCheckingConfig> = {
188188
useContextGenericType: true,
189189
strictLiteralTypes: true,
190190
enableTemplateTypeChecker: false,
191-
useInlineTypeConstructors: true
191+
useInlineTypeConstructors: true,
192+
suggestionsForSuboptimalTypeInference: false,
192193
};
193194

194195
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
@@ -272,6 +273,7 @@ export function tcb(
272273
strictLiteralTypes: true,
273274
enableTemplateTypeChecker: false,
274275
useInlineTypeConstructors: true,
276+
suggestionsForSuboptimalTypeInference: false,
275277
...config
276278
};
277279
options = options || {
@@ -690,4 +692,5 @@ export class NoopOobRecorder implements OutOfBandDiagnosticRecorder {
690692
duplicateTemplateVar(): void {}
691693
requiresInlineTcb(): void {}
692694
requiresInlineTypeConstructors(): void {}
695+
suboptimalTypeInference(): void {}
693696
}

packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,8 @@ describe('type check blocks', () => {
745745
useContextGenericType: true,
746746
strictLiteralTypes: true,
747747
enableTemplateTypeChecker: false,
748-
useInlineTypeConstructors: true
748+
useInlineTypeConstructors: true,
749+
suggestionsForSuboptimalTypeInference: false,
749750
};
750751

751752
describe('config.applyTemplateContextGuards', () => {

packages/language-service/ivy/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ts_library(
77
deps = [
88
"//packages/compiler",
99
"//packages/compiler-cli/src/ngtsc/core:api",
10+
"//packages/compiler-cli/src/ngtsc/diagnostics",
1011
"//packages/compiler-cli/src/ngtsc/file_system",
1112
"//packages/compiler-cli/src/ngtsc/file_system/testing",
1213
"//packages/compiler-cli/src/ngtsc/testing",

packages/language-service/ivy/test/diagnostic_spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
910
import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
1011
import * as ts from 'typescript';
1112

@@ -245,4 +246,40 @@ describe('getSemanticDiagnostics', () => {
245246
'component is missing a template',
246247
]);
247248
});
249+
250+
it('reports a warning when the project configuration prevents good type inference', () => {
251+
const files = {
252+
'app.ts': `
253+
import {Component, NgModule} from '@angular/core';
254+
import {CommonModule} from '@angular/common';
255+
256+
@Component({
257+
template: '<div *ngFor="let user of users">{{user}}</div>',
258+
})
259+
export class MyComponent {
260+
users = ['Alpha', 'Beta'];
261+
}
262+
`
263+
};
264+
265+
const project = createModuleAndProjectWithDeclarations(env, 'test', files, {
266+
// Disable `strictTemplates`.
267+
strictTemplates: false,
268+
// Use `fullTemplateTypeCheck` mode instead.
269+
fullTemplateTypeCheck: true,
270+
});
271+
const diags = project.getDiagnosticsForFile('app.ts');
272+
expect(diags.length).toBe(1);
273+
const diag = diags[0];
274+
expect(diag.code).toBe(ngErrorCode(ErrorCode.SUGGEST_SUBOPTIMAL_TYPE_INFERENCE));
275+
expect(diag.category).toBe(ts.DiagnosticCategory.Suggestion);
276+
expect(getTextOfDiagnostic(diag)).toBe('user');
277+
});
248278
});
279+
280+
function getTextOfDiagnostic(diag: ts.Diagnostic): string {
281+
expect(diag.file).not.toBeUndefined();
282+
expect(diag.start).not.toBeUndefined();
283+
expect(diag.length).not.toBeUndefined();
284+
return diag.file!.text.substring(diag.start!, diag.start! + diag.length!);
285+
}

0 commit comments

Comments
 (0)