Skip to content

Commit 4db89f4

Browse files
zarendatscott
authored andcommitted
fix(compiler-cli): report non-template diagnostics (angular#40331)
Report non-template diagnotics when calling `getDiagnotics` function of the language service we only returned template diagnotics. This change causes it to return all diagnotics, not just diagnostics from the template type checker. PR Close angular#40331
1 parent 625d2c2 commit 4db89f4

File tree

10 files changed

+100
-44
lines changed

10 files changed

+100
-44
lines changed

packages/compiler-cli/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ ts_library(
3333
"//packages/compiler-cli/src/ngtsc/shims",
3434
"//packages/compiler-cli/src/ngtsc/translator",
3535
"//packages/compiler-cli/src/ngtsc/typecheck",
36+
"//packages/compiler-cli/src/ngtsc/typecheck/api",
3637
"@npm//@bazel/typescript",
3738
"@npm//@types/node",
3839
"@npm//chokidar",

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

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {ivySwitchTransform} from '../../switch';
3030
import {aliasTransformFactory, CompilationMode, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
3131
import {TemplateTypeCheckerImpl} from '../../typecheck';
3232
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api';
33-
import {isTemplateDiagnostic} from '../../typecheck/diagnostics';
3433
import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript';
3534
import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api';
3635

@@ -84,11 +83,12 @@ export class NgCompiler {
8483
private constructionDiagnostics: ts.Diagnostic[] = [];
8584

8685
/**
87-
* Semantic diagnostics related to the program itself.
86+
* Non-template diagnostics related to the program itself. Does not include template
87+
* diagnostics because the template type checker memoizes them itself.
8888
*
89-
* This is set by (and memoizes) `getDiagnostics`.
89+
* This is set by (and memoizes) `getNonTemplateDiagnostics`.
9090
*/
91-
private diagnostics: ts.Diagnostic[]|null = null;
91+
private nonTemplateDiagnostics: ts.Diagnostic[]|null = null;
9292

9393
private closureCompilerEnabled: boolean;
9494
private nextProgram: ts.Program;
@@ -175,38 +175,23 @@ export class NgCompiler {
175175
return this.incrementalDriver.depGraph.getResourceDependencies(file);
176176
}
177177

178+
/**
179+
* Get all Angular-related diagnostics for this compilation.
180+
*/
181+
getDiagnostics(): ts.Diagnostic[] {
182+
return [...this.getNonTemplateDiagnostics(), ...this.getTemplateDiagnostics()];
183+
}
184+
178185
/**
179186
* Get all Angular-related diagnostics for this compilation.
180187
*
181188
* If a `ts.SourceFile` is passed, only diagnostics related to that file are returned.
182189
*/
183-
getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[] {
184-
if (this.diagnostics === null) {
185-
const compilation = this.ensureAnalyzed();
186-
this.diagnostics =
187-
[...compilation.traitCompiler.diagnostics, ...this.getTemplateDiagnostics()];
188-
if (this.entryPoint !== null && compilation.exportReferenceGraph !== null) {
189-
this.diagnostics.push(...checkForPrivateExports(
190-
this.entryPoint, this.tsProgram.getTypeChecker(), compilation.exportReferenceGraph));
191-
}
192-
}
193-
194-
if (file === undefined) {
195-
return this.diagnostics;
196-
} else {
197-
return this.diagnostics.filter(diag => {
198-
if (diag.file === file) {
199-
return true;
200-
} else if (isTemplateDiagnostic(diag) && diag.componentFile === file) {
201-
// Template diagnostics are reported when diagnostics for the component file are
202-
// requested (since no consumer of `getDiagnostics` would ever ask for diagnostics from
203-
// the fake ts.SourceFile for templates).
204-
return true;
205-
} else {
206-
return false;
207-
}
208-
});
209-
}
190+
getDiagnosticsForFile(file: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[] {
191+
return [
192+
...this.getNonTemplateDiagnostics().filter(diag => diag.file === file),
193+
...this.getTemplateDiagnosticsForFile(file, optimizeFor)
194+
];
210195
}
211196

212197
/**
@@ -582,6 +567,37 @@ export class NgCompiler {
582567
return diagnostics;
583568
}
584569

570+
private getTemplateDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor):
571+
ReadonlyArray<ts.Diagnostic> {
572+
const compilation = this.ensureAnalyzed();
573+
574+
// Get the diagnostics.
575+
const typeCheckSpan = this.perfRecorder.start('typeCheckDiagnostics');
576+
const diagnostics: ts.Diagnostic[] = [];
577+
if (!sf.isDeclarationFile && !this.adapter.isShim(sf)) {
578+
diagnostics.push(...compilation.templateTypeChecker.getDiagnosticsForFile(sf, optimizeFor));
579+
}
580+
581+
const program = this.typeCheckingProgramStrategy.getProgram();
582+
this.perfRecorder.stop(typeCheckSpan);
583+
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program);
584+
this.nextProgram = program;
585+
586+
return diagnostics;
587+
}
588+
589+
private getNonTemplateDiagnostics(): ts.Diagnostic[] {
590+
if (this.nonTemplateDiagnostics === null) {
591+
const compilation = this.ensureAnalyzed();
592+
this.nonTemplateDiagnostics = [...compilation.traitCompiler.diagnostics];
593+
if (this.entryPoint !== null && compilation.exportReferenceGraph !== null) {
594+
this.nonTemplateDiagnostics.push(...checkForPrivateExports(
595+
this.entryPoint, this.tsProgram.getTypeChecker(), compilation.exportReferenceGraph));
596+
}
597+
}
598+
return this.nonTemplateDiagnostics;
599+
}
600+
585601
/**
586602
* Reifies the inter-dependencies of NgModules and the components within their compilation scopes
587603
* into the `IncrementalDriver`'s dependency graph.

packages/compiler-cli/src/ngtsc/core/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ts_library(
1717
"//packages/compiler-cli/src/ngtsc/incremental",
1818
"//packages/compiler-cli/src/ngtsc/reflection",
1919
"//packages/compiler-cli/src/ngtsc/typecheck",
20+
"//packages/compiler-cli/src/ngtsc/typecheck/api",
2021
"@npm//typescript",
2122
],
2223
)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {runInEachFileSystem} from '../../file_system/testing';
1313
import {NoopIncrementalBuildStrategy} from '../../incremental';
1414
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
1515
import {ReusedProgramStrategy} from '../../typecheck';
16+
import {OptimizeFor} from '../../typecheck/api';
1617

1718
import {NgCompilerOptions} from '../api';
1819

@@ -54,7 +55,8 @@ runInEachFileSystem(() => {
5455
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
5556
/* usePoisonedData */ false);
5657

57-
const diags = compiler.getDiagnostics(getSourceFileOrError(program, COMPONENT));
58+
const diags = compiler.getDiagnosticsForFile(
59+
getSourceFileOrError(program, COMPONENT), OptimizeFor.SingleFile);
5860
expect(diags.length).toBe(1);
5961
expect(diags[0].messageText).toContain('does_not_exist');
6062
});

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
2020
import {DeclarationNode} from './reflection';
2121
import {retagAllTsFiles, untagAllTsFiles} from './shims';
2222
import {ReusedProgramStrategy} from './typecheck';
23+
import {OptimizeFor} from './typecheck/api';
2324

2425

2526

@@ -182,7 +183,9 @@ export class NgtscProgram implements api.Program {
182183
}
183184
}
184185

185-
const diagnostics = this.compiler.getDiagnostics(sf);
186+
const diagnostics = sf === undefined ?
187+
this.compiler.getDiagnostics() :
188+
this.compiler.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram);
186189
this.reuseTsProgram = this.compiler.getNextProgram();
187190
return diagnostics;
188191
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {NodeJSFileSystem, setFileSystem} from './file_system';
1414
import {PatchedProgramIncrementalBuildStrategy} from './incremental';
1515
import {NOOP_PERF_RECORDER} from './perf';
1616
import {untagAllTsFiles} from './shims';
17+
import {OptimizeFor} from './typecheck/api';
1718
import {ReusedProgramStrategy} from './typecheck/src/augmented_program';
1819

1920
// The following is needed to fix a the chicken-and-egg issue where the sync (into g3) script will
@@ -111,7 +112,10 @@ export class NgTscPlugin implements TscPlugin {
111112
}
112113

113114
getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[] {
114-
return this.compiler.getDiagnostics(file);
115+
if (file === undefined) {
116+
return this.compiler.getDiagnostics();
117+
}
118+
return this.compiler.getDiagnosticsForFile(file, OptimizeFor.WholeProgram);
115119
}
116120

117121
getOptionDiagnostics(): ts.Diagnostic[] {

packages/language-service/ivy/language_service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class LanguageService {
5151
const program = compiler.getNextProgram();
5252
const sourceFile = program.getSourceFile(fileName);
5353
if (sourceFile) {
54-
diagnostics.push(...ttc.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile));
54+
diagnostics.push(...compiler.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile));
5555
}
5656
} else {
5757
const components = compiler.getComponentsWithTemplateFile(fileName);

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {absoluteFrom, getSourceFileOrError} from '@angular/compiler-cli/src/ngtsc/file_system';
1010
import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
11+
import {OptimizeFor} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
1112

1213
import {LanguageServiceTestEnvironment} from './env';
1314

@@ -68,9 +69,15 @@ describe('language-service/compiler integration', () => {
6869
// Expect that this program is clean diagnostically.
6970
const ngCompiler = env.ngLS.compilerFactory.getOrCreate();
7071
const program = ngCompiler.getNextProgram();
71-
expect(ngCompiler.getDiagnostics(getSourceFileOrError(program, appCmpFile))).toEqual([]);
72-
expect(ngCompiler.getDiagnostics(getSourceFileOrError(program, appModuleFile))).toEqual([]);
73-
expect(ngCompiler.getDiagnostics(getSourceFileOrError(program, testFile))).toEqual([]);
72+
expect(ngCompiler.getDiagnosticsForFile(
73+
getSourceFileOrError(program, appCmpFile), OptimizeFor.WholeProgram))
74+
.toEqual([]);
75+
expect(ngCompiler.getDiagnosticsForFile(
76+
getSourceFileOrError(program, appModuleFile), OptimizeFor.WholeProgram))
77+
.toEqual([]);
78+
expect(ngCompiler.getDiagnosticsForFile(
79+
getSourceFileOrError(program, testFile), OptimizeFor.WholeProgram))
80+
.toEqual([]);
7481
});
7582

7683
it('should show type-checking errors from components with poisoned scopes', () => {
@@ -153,9 +160,9 @@ describe('language-service/compiler integration', () => {
153160
name: moduleFile,
154161
contents: `
155162
import {NgModule} from '@angular/core';
156-
163+
157164
import {Cmp} from './cmp';
158-
165+
159166
@NgModule({
160167
declarations: [Cmp],
161168
})
@@ -173,7 +180,8 @@ describe('language-service/compiler integration', () => {
173180
// Angular should be complaining about the module not being understandable.
174181
const programBefore = env.tsLS.getProgram()!;
175182
const moduleSfBefore = programBefore.getSourceFile(moduleFile)!;
176-
const ngDiagsBefore = env.ngLS.compilerFactory.getOrCreate().getDiagnostics(moduleSfBefore);
183+
const ngDiagsBefore = env.ngLS.compilerFactory.getOrCreate().getDiagnosticsForFile(
184+
moduleSfBefore, OptimizeFor.SingleFile);
177185
expect(ngDiagsBefore.length).toBe(1);
178186

179187
// Fix the import.
@@ -182,7 +190,8 @@ describe('language-service/compiler integration', () => {
182190
// Angular should stop complaining about the NgModule.
183191
const programAfter = env.tsLS.getProgram()!;
184192
const moduleSfAfter = programAfter.getSourceFile(moduleFile)!;
185-
const ngDiagsAfter = env.ngLS.compilerFactory.getOrCreate().getDiagnostics(moduleSfAfter);
193+
const ngDiagsAfter = env.ngLS.compilerFactory.getOrCreate().getDiagnosticsForFile(
194+
moduleSfAfter, OptimizeFor.SingleFile);
186195
expect(ngDiagsAfter.length).toBe(0);
187196
});
188197
});

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,22 @@ describe('getSemanticDiagnostics', () => {
174174
'Parser Error: Bindings cannot contain assignments at column 8 in [{{nope = true}}] in /app2.html@0:0'
175175
]);
176176
});
177+
178+
it('reports a diagnostic for a component without a template', () => {
179+
const appFile = {
180+
name: absoluteFrom('/app.ts'),
181+
contents: `
182+
import {Component} from '@angular/core';
183+
@Component({})
184+
export class MyComponent {}
185+
`
186+
};
187+
188+
const env = createModuleWithDeclarations([appFile]);
189+
const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts'));
190+
191+
expect(diags.map(x => x.messageText)).toEqual([
192+
'component is missing a template',
193+
]);
194+
});
177195
});

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
1111
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, getSourceFileOrError} from '@angular/compiler-cli/src/ngtsc/file_system';
1212
import {MockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
1313
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
14-
import {TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
14+
import {OptimizeFor, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
1515
import * as ts from 'typescript/lib/tsserverlibrary';
1616

1717
import {LanguageService} from '../language_service';
@@ -172,7 +172,9 @@ export class LanguageServiceTestEnvironment {
172172
continue;
173173
}
174174

175-
const ngDiagnostics = ngCompiler.getDiagnostics(sf);
175+
// It's more efficient to optimize for WholeProgram since we call this with every file in the
176+
// program.
177+
const ngDiagnostics = ngCompiler.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram);
176178
expect(ngDiagnostics.map(diag => diag.messageText)).toEqual([]);
177179
}
178180

0 commit comments

Comments
 (0)