feat(compiler-cli): produce template diagnostics error messages (#17125)
Refactoring the compiler to use transformers moves the code generation after type-checking which suppresses the errors TypeScript would generate in the user code. `TypeChecker` currently produces the same factory code that was generated prior the switch to transfomers, getting back the same diagnostics as before. The refactoring will allow the code to diverge from the factory code and allow better diagnostic error messages than was previously possible by type-checking the factories.
This commit is contained in:

committed by
Victor Berchet

parent
1338995ee9
commit
230255f887
225
packages/compiler-cli/src/diagnostics/check_types.ts
Normal file
225
packages/compiler-cli/src/diagnostics/check_types.ts
Normal file
@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, NgAnalyzedModules, ParseSourceSpan, Statement, StaticReflector, TypeScriptEmitter, createAotCompiler} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
interface FactoryInfo {
|
||||
source: ts.SourceFile;
|
||||
context: EmitterVisitorContext;
|
||||
}
|
||||
|
||||
type FactoryInfoMap = Map<string, FactoryInfo>;
|
||||
|
||||
export enum DiagnosticKind {
|
||||
Message,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
export interface Diagnostic {
|
||||
message: string;
|
||||
kind: DiagnosticKind;
|
||||
span: ParseSourceSpan;
|
||||
}
|
||||
|
||||
export class TypeChecker {
|
||||
private _aotCompiler: AotCompiler|undefined;
|
||||
private _reflector: StaticReflector|undefined;
|
||||
private _factories: Map<string, FactoryInfo>|undefined;
|
||||
private _factoryNames: string[]|undefined;
|
||||
private _diagnosticProgram: ts.Program|undefined;
|
||||
private _diagnosticsByFile: Map<string, Diagnostic[]>|undefined;
|
||||
|
||||
constructor(
|
||||
private program: ts.Program, private tsOptions: ts.CompilerOptions,
|
||||
private compilerHost: ts.CompilerHost, private aotCompilerHost: AotCompilerHost,
|
||||
private aotOptions: AotCompilerOptions, private _analyzedModules?: NgAnalyzedModules,
|
||||
private _generatedFiles?: GeneratedFile[]) {}
|
||||
|
||||
getDiagnostics(fileName?: string): Diagnostic[] {
|
||||
return fileName ?
|
||||
this.diagnosticsByFileName.get(fileName) || [] :
|
||||
([] as Diagnostic[]).concat(...Array.from(this.diagnosticsByFileName.values()));
|
||||
}
|
||||
|
||||
private get analyzedModules(): NgAnalyzedModules {
|
||||
return this._analyzedModules || (this._analyzedModules = this.aotCompiler.analyzeModulesSync(
|
||||
this.program.getSourceFiles().map(sf => sf.fileName)));
|
||||
}
|
||||
|
||||
private get diagnosticsByFileName(): Map<string, Diagnostic[]> {
|
||||
return this._diagnosticsByFile || this.createDiagnosticsByFile();
|
||||
}
|
||||
|
||||
private get diagnosticProgram(): ts.Program {
|
||||
return this._diagnosticProgram || this.createDiagnosticProgram();
|
||||
}
|
||||
|
||||
private get generatedFiles(): GeneratedFile[] {
|
||||
let result = this._generatedFiles;
|
||||
if (!result) {
|
||||
this._generatedFiles = result = this.aotCompiler.emitAllImpls(this.analyzedModules);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private get aotCompiler(): AotCompiler {
|
||||
return this._aotCompiler || this.createCompilerAndReflector();
|
||||
}
|
||||
|
||||
private get reflector(): StaticReflector {
|
||||
let result = this._reflector;
|
||||
if (!result) {
|
||||
this.createCompilerAndReflector();
|
||||
result = this._reflector !;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private get factories(): Map<string, FactoryInfo> {
|
||||
return this._factories || this.createFactories();
|
||||
}
|
||||
|
||||
private get factoryNames(): string[] {
|
||||
return this._factoryNames || (this.createFactories() && this._factoryNames !);
|
||||
}
|
||||
|
||||
private createCompilerAndReflector() {
|
||||
const {compiler, reflector} = createAotCompiler(this.aotCompilerHost, this.aotOptions);
|
||||
this._reflector = reflector;
|
||||
return this._aotCompiler = compiler;
|
||||
}
|
||||
|
||||
private createDiagnosticProgram() {
|
||||
// Create a program that is all the files from the original program plus the factories.
|
||||
const existingFiles = this.program.getSourceFiles().map(source => source.fileName);
|
||||
const host = new TypeCheckingHost(this.compilerHost, this.program, this.factories);
|
||||
return this._diagnosticProgram =
|
||||
ts.createProgram([...existingFiles, ...this.factoryNames], this.tsOptions, host);
|
||||
}
|
||||
|
||||
private createFactories() {
|
||||
// Create all the factory files with enough information to map the diagnostics reported for the
|
||||
// created file back to the original source.
|
||||
const emitter = new TypeScriptEmitter();
|
||||
const factorySources =
|
||||
this.generatedFiles.filter(file => file.stmts != null && file.stmts.length)
|
||||
.map<[string, FactoryInfo]>(
|
||||
file => [file.genFileUrl, createFactoryInfo(emitter, file)]);
|
||||
this._factories = new Map(factorySources);
|
||||
this._factoryNames = Array.from(this._factories.keys());
|
||||
return this._factories;
|
||||
}
|
||||
|
||||
private createDiagnosticsByFile() {
|
||||
// Collect all the diagnostics binned by original source file name.
|
||||
const result = new Map<string, Diagnostic[]>();
|
||||
const diagnosticsFor = (fileName: string) => {
|
||||
let r = result.get(fileName);
|
||||
if (!r) {
|
||||
r = [];
|
||||
result.set(fileName, r);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
const program = this.diagnosticProgram;
|
||||
for (const factoryName of this.factoryNames) {
|
||||
const sourceFile = program.getSourceFile(factoryName);
|
||||
for (const diagnostic of this.diagnosticProgram.getSemanticDiagnostics(sourceFile)) {
|
||||
const span = this.sourceSpanOf(diagnostic.file, diagnostic.start, diagnostic.length);
|
||||
if (span) {
|
||||
const fileName = span.start.file.url;
|
||||
const diagnosticsList = diagnosticsFor(fileName);
|
||||
diagnosticsList.push({
|
||||
message: diagnosticMessageToString(diagnostic.messageText),
|
||||
kind: diagnosticKindConverter(diagnostic.category), span
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private sourceSpanOf(source: ts.SourceFile, start: number, length: number): ParseSourceSpan|null {
|
||||
// Find the corresponding TypeScript node
|
||||
const info = this.factories.get(source.fileName);
|
||||
if (info) {
|
||||
const {line, character} = ts.getLineAndCharacterOfPosition(source, start);
|
||||
return info.context.spanOf(line, character);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {
|
||||
return typeof message === 'string' ? message : diagnosticMessageToString(message.messageText);
|
||||
}
|
||||
|
||||
function diagnosticKindConverter(kind: ts.DiagnosticCategory) {
|
||||
// The diagnostics kind matches ts.DiagnosticCategory. Review this code if this changes.
|
||||
return kind as any as DiagnosticKind;
|
||||
}
|
||||
|
||||
function createFactoryInfo(emitter: TypeScriptEmitter, file: GeneratedFile): FactoryInfo {
|
||||
const {sourceText, context} =
|
||||
emitter.emitStatementsAndContext(file.srcFileUrl, file.genFileUrl, file.stmts !);
|
||||
const source = ts.createSourceFile(
|
||||
file.genFileUrl, sourceText, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
||||
return {source, context};
|
||||
}
|
||||
|
||||
class TypeCheckingHost implements ts.CompilerHost {
|
||||
constructor(
|
||||
private host: ts.CompilerHost, private originalProgram: ts.Program,
|
||||
private factories: Map<string, FactoryInfo>) {}
|
||||
|
||||
getSourceFile(
|
||||
fileName: string, languageVersion: ts.ScriptTarget,
|
||||
onError?: ((message: string) => void)): ts.SourceFile {
|
||||
const originalSource = this.originalProgram.getSourceFile(fileName);
|
||||
if (originalSource) {
|
||||
return originalSource;
|
||||
}
|
||||
const factoryInfo = this.factories.get(fileName);
|
||||
if (factoryInfo) {
|
||||
return factoryInfo.source;
|
||||
}
|
||||
return this.host.getSourceFile(fileName, languageVersion, onError);
|
||||
}
|
||||
|
||||
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
||||
return this.host.getDefaultLibFileName(options);
|
||||
}
|
||||
|
||||
writeFile: ts.WriteFileCallback =
|
||||
() => { throw new Error('Unexpected write in diagnostic program'); }
|
||||
|
||||
getCurrentDirectory(): string {
|
||||
return this.host.getCurrentDirectory();
|
||||
}
|
||||
|
||||
getDirectories(path: string): string[] { return this.host.getDirectories(path); }
|
||||
|
||||
getCanonicalFileName(fileName: string): string {
|
||||
return this.host.getCanonicalFileName(fileName);
|
||||
}
|
||||
|
||||
useCaseSensitiveFileNames(): boolean { return this.host.useCaseSensitiveFileNames(); }
|
||||
|
||||
getNewLine(): string { return this.host.getNewLine(); }
|
||||
|
||||
fileExists(fileName: string): boolean {
|
||||
return this.factories.has(fileName) || this.host.fileExists(fileName);
|
||||
}
|
||||
|
||||
readFile(fileName: string): string {
|
||||
const factoryInfo = this.factories.get(fileName);
|
||||
return (factoryInfo && factoryInfo.source.text) || this.host.readFile(fileName);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user