Skip to content

Commit cc7fca4

Browse files
ayazhafizAndrewKushnir
authored andcommitted
feat(language-service): specific suggestions for template context diags (#34751)
This commit elaborates diagnostics produced for invalid template contexts by including the name of the embedded template type using the template context, and in the common case that the implicity property is being referenced (e.g. in a `for .. of ..` expression), suggesting to refine the type of the context. This suggestion is provided because users will sometimes use a base class as the type of the context in the embedded view, and a more specific context later on (e.g. in an `ngOnChanges` method). Closes angular/vscode-ng-language-service#251 PR Close #34751
1 parent 6a4186c commit cc7fca4

File tree

4 files changed

+55
-13
lines changed

4 files changed

+55
-13
lines changed

packages/language-service/src/expression_diagnostics.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,12 @@ class ExpressionDiagnosticsVisitor extends RecursiveTemplateAstVisitor {
269269
if (context && !context.has(ast.value)) {
270270
if (ast.value === '$implicit') {
271271
this.reportError(
272-
'The template context does not have an implicit value', spanOf(ast.sourceSpan));
272+
`The template context of '${directive.type.reference.name}' does not define an implicit value.\n` +
273+
`If the context type is a base type, consider refining it to a more specific type.`,
274+
spanOf(ast.sourceSpan));
273275
} else {
274276
this.reportError(
275-
`The template context does not define a member called '${ast.value}'`,
277+
`The template context of '${directive.type.reference.name}' does not define a member called '${ast.value}'`,
276278
spanOf(ast.sourceSpan));
277279
}
278280
}

packages/language-service/test/diagnostics_spec.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {MockTypescriptHost} from './test_utils';
2525

2626
const EXPRESSION_CASES = '/app/expression-cases.ts';
2727
const NG_FOR_CASES = '/app/ng-for-cases.ts';
28-
const NG_IF_CASES = '/app/ng-if-cases.ts';
2928
const TEST_TEMPLATE = '/app/test.ng';
3029
const APP_COMPONENT = '/app/app.component.ts';
3130

@@ -250,11 +249,6 @@ describe('diagnostics', () => {
250249
`and element references do not contain such a member`);
251250
});
252251

253-
it('should report an unknown context reference', () => {
254-
const diags = ngLS.getSemanticDiagnostics(NG_FOR_CASES).map(d => d.messageText);
255-
expect(diags).toContain(`The template context does not define a member called 'even_1'`);
256-
});
257-
258252
it('should report an unknown value in a key expression', () => {
259253
const diags = ngLS.getSemanticDiagnostics(NG_FOR_CASES).map(d => d.messageText);
260254
expect(diags).toContain(
@@ -264,10 +258,37 @@ describe('diagnostics', () => {
264258
});
265259
});
266260

267-
describe('in ng-if-cases.ts', () => {
268-
it('should report an implicit context reference', () => {
269-
const diags = ngLS.getSemanticDiagnostics(NG_IF_CASES).map(d => d.messageText);
270-
expect(diags).toContain(`The template context does not define a member called 'unknown'`);
261+
describe('embedded templates', () => {
262+
it('should suggest refining a template context missing a property', () => {
263+
mockHost.override(
264+
TEST_TEMPLATE,
265+
`<button type="button" ~{start-emb}*counter="let hero of heroes"~{end-emb}></button>`);
266+
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
267+
expect(diags.length).toBe(1);
268+
const {messageText, start, length} = diags[0];
269+
expect(messageText)
270+
.toBe(
271+
`The template context of 'CounterDirective' does not define an implicit value.\n` +
272+
`If the context type is a base type, consider refining it to a more specific type.`, );
273+
274+
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
275+
expect(start).toBe(span.start);
276+
expect(length).toBe(span.length);
277+
});
278+
279+
it('should report an unknown context reference', () => {
280+
mockHost.override(
281+
TEST_TEMPLATE,
282+
`<div ~{start-emb}*ngFor="let hero of heroes; let e = even_1"~{end-emb}></div>`);
283+
const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE);
284+
expect(diags.length).toBe(1);
285+
const {messageText, start, length} = diags[0];
286+
expect(messageText)
287+
.toBe(`The template context of 'NgForOf' does not define a member called 'even_1'`);
288+
289+
const span = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'emb');
290+
expect(start).toBe(span.start);
291+
expect(length).toBe(span.length);
271292
});
272293
});
273294

packages/language-service/test/project/app/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import * as ParsingCases from './parsing-cases';
3333
ParsingCases.CaseIncompleteOpen,
3434
ParsingCases.CaseMissingClosing,
3535
ParsingCases.CaseUnknown,
36+
ParsingCases.CounterDirective,
3637
ParsingCases.EmptyInterpolation,
3738
ParsingCases.HintModel,
3839
ParsingCases.NoValueAttribute,

packages/language-service/test/project/app/parsing-cases.ts

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

9-
import {Component, Directive, EventEmitter, Input, Output} from '@angular/core';
9+
import {Component, Directive, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
1010

1111
import {Hero} from './app.component';
1212

@@ -109,6 +109,24 @@ export class AsyncForUsingComponent {
109109
export class References {
110110
}
111111

112+
class CounterDirectiveContext<T> {
113+
constructor(public $implicit: T) {}
114+
}
115+
116+
@Directive({selector: '[counterOf]'})
117+
export class CounterDirective implements OnChanges {
118+
// Object does not have an "$implicit" property.
119+
constructor(private container: ViewContainerRef, private template: TemplateRef<Object>) {}
120+
121+
@Input('counterOf') counter: number = 0;
122+
ngOnChanges(_changes: SimpleChanges) {
123+
this.container.clear();
124+
for (let i = 0; i < this.counter; ++i) {
125+
this.container.createEmbeddedView(this.template, new CounterDirectiveContext<number>(i + 1));
126+
}
127+
}
128+
}
129+
112130
/**
113131
* This Component provides the `test-comp` selector.
114132
*/

0 commit comments

Comments
 (0)