diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts
index 9c367c4822..21908703a9 100644
--- a/packages/language-service/ivy/language_service.ts
+++ b/packages/language-service/ivy/language_service.ts
@@ -70,7 +70,28 @@ export class LanguageService {
const program = compiler.getNextProgram();
const sourceFile = program.getSourceFile(fileName);
if (sourceFile) {
- diagnostics.push(...compiler.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile));
+ const ngDiagnostics = compiler.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile);
+ // There are several kinds of diagnostics returned by `NgCompiler` for a source file:
+ //
+ // 1. Angular-related non-template diagnostics from decorated classes within that file.
+ // 2. Template diagnostics for components with direct inline templates (a string literal).
+ // 3. Template diagnostics for components with indirect inline templates (templates computed
+ // by expression).
+ // 4. Template diagnostics for components with external templates.
+ //
+ // When showing diagnostics for a TS source file, we want to only include kinds 1 and 2 -
+ // those diagnostics which are reported at a location within the TS file itself. Diagnostics
+ // for external templates will be shown when editing that template file (the `else` block)
+ // below.
+ //
+ // Currently, indirect inline template diagnostics (kind 3) are not shown at all by the
+ // Language Service, because there is no sensible location in the user's code for them. Such
+ // templates are an edge case, though, and should not be common.
+ //
+ // TODO(alxhub): figure out a good user experience for indirect template diagnostics and
+ // show them from within the Language Service.
+ diagnostics.push(...ngDiagnostics.filter(
+ diag => diag.file !== undefined && diag.file.fileName === sourceFile.fileName));
}
} else {
const components = compiler.getComponentsWithTemplateFile(fileName);
diff --git a/packages/language-service/ivy/test/compiler_spec.ts b/packages/language-service/ivy/test/compiler_spec.ts
index 027c32ff3a..33ba654ffb 100644
--- a/packages/language-service/ivy/test/compiler_spec.ts
+++ b/packages/language-service/ivy/test/compiler_spec.ts
@@ -30,11 +30,11 @@ describe('language-service/compiler integration', () => {
'test.html': `Test`
});
- expect(project.getDiagnosticsForFile('test.ts').length).toBeGreaterThan(0);
+ expect(project.getDiagnosticsForFile('test.html').length).toBeGreaterThan(0);
const tmplFile = project.openFile('test.html');
tmplFile.contents = '
Test
';
- expect(project.getDiagnosticsForFile('test.ts').length).toEqual(0);
+ expect(project.getDiagnosticsForFile('test.html').length).toEqual(0);
});
it('should not produce errors from inline test declarations mixing with those of the app', () => {
diff --git a/packages/language-service/ivy/test/diagnostic_spec.ts b/packages/language-service/ivy/test/diagnostic_spec.ts
index 5288e06814..97f7239d33 100644
--- a/packages/language-service/ivy/test/diagnostic_spec.ts
+++ b/packages/language-service/ivy/test/diagnostic_spec.ts
@@ -75,6 +75,44 @@ describe('getSemanticDiagnostics', () => {
expect(diags).toEqual([]);
});
+ it('should not report external template diagnostics on the TS file', () => {
+ const files = {
+ 'app.ts': `
+ import {Component, NgModule} from '@angular/core';
+
+ @Component({
+ templateUrl: './app.html'
+ })
+ export class AppComponent {}
+ `,
+ 'app.html': '{{nope}}'
+ };
+
+ const project = createModuleAndProjectWithDeclarations(env, 'test', files);
+ const diags = project.getDiagnosticsForFile('app.ts');
+ expect(diags).toEqual([]);
+ });
+
+ it('should report diagnostics in inline templates', () => {
+ const files = {
+ 'app.ts': `
+ import {Component, NgModule} from '@angular/core';
+
+ @Component({
+ template: '{{nope}}',
+ })
+ export class AppComponent {}
+ `
+ };
+ const project = createModuleAndProjectWithDeclarations(env, 'test', files);
+ const diags = project.getDiagnosticsForFile('app.ts');
+ expect(diags.length).toBe(1);
+ const {category, file, messageText} = diags[0];
+ expect(category).toBe(ts.DiagnosticCategory.Error);
+ expect(file?.fileName).toBe('/test/app.ts');
+ expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`);
+ });
+
it('should report member does not exist in external template', () => {
const files = {
'app.ts': `
@@ -179,11 +217,17 @@ describe('getSemanticDiagnostics', () => {
};
const project = env.addProject('test', files);
- const diags = project.getDiagnosticsForFile('app.ts');
- expect(diags.map(x => x.messageText).sort()).toEqual([
- 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = false}}] in /test/app1.html@0:0',
- 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}] in /test/app2.html@0:0'
- ]);
+ const diags1 = project.getDiagnosticsForFile('app1.html');
+ expect(diags1.length).toBe(1);
+ expect(diags1[0].messageText)
+ .toBe(
+ 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = false}}] in /test/app1.html@0:0');
+
+ const diags2 = project.getDiagnosticsForFile('app2.html');
+ expect(diags2.length).toBe(1);
+ expect(diags2[0].messageText)
+ .toBe(
+ 'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}] in /test/app2.html@0:0');
});
it('reports a diagnostic for a component without a template', () => {