This PR enables `getSemanticDiagnostics()` to be called on external templates. Several changes are needed to land this feature: 1. The adapter needs to implement two additional methods: a. `readResource()` Load the template from snapshot instead of reading from disk b. `getModifiedResourceFiles()` Inform the compiler that external templates have changed so that the loader could invalidate its internal cache. 2. Create `ScriptInfo` for external templates in MockHost. Prior to this, MockHost only track changes in TypeScript files. Now it needs to create `ScriptInfo` for external templates as well. For (1), in order to make sure we don't reload the template if it hasn't changed, we need to keep track of its version. Since the complexity has increased, the adapter is refactored into its own class. PR Close #39065
175 lines
6.9 KiB
TypeScript
175 lines
6.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC 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 {CompilerOptions, createNgCompilerOptions} from '@angular/compiler-cli';
|
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
|
import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
import {PatchedProgramIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental';
|
|
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
|
|
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
|
|
|
import {DefinitionBuilder} from './definitions';
|
|
import {isExternalTemplate, isTypeScriptFile, LanguageServiceAdapter} from './language_service_adapter';
|
|
import {QuickInfoBuilder} from './quick_info';
|
|
|
|
export class LanguageService {
|
|
private options: CompilerOptions;
|
|
private lastKnownProgram: ts.Program|null = null;
|
|
private readonly strategy: TypeCheckingProgramStrategy;
|
|
private readonly adapter: LanguageServiceAdapter;
|
|
|
|
constructor(project: ts.server.Project, private readonly tsLS: ts.LanguageService) {
|
|
this.options = parseNgCompilerOptions(project);
|
|
this.strategy = createTypeCheckingProgramStrategy(project);
|
|
this.adapter = new LanguageServiceAdapter(project);
|
|
this.watchConfigFile(project);
|
|
}
|
|
|
|
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
|
|
const program = this.strategy.getProgram();
|
|
const compiler = this.createCompiler(program, fileName);
|
|
const ttc = compiler.getTemplateTypeChecker();
|
|
const diagnostics: ts.Diagnostic[] = [];
|
|
if (isTypeScriptFile(fileName)) {
|
|
const sourceFile = program.getSourceFile(fileName);
|
|
if (sourceFile) {
|
|
diagnostics.push(...ttc.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile));
|
|
}
|
|
} else {
|
|
const components = compiler.getComponentsWithTemplateFile(fileName);
|
|
for (const component of components) {
|
|
if (ts.isClassDeclaration(component)) {
|
|
diagnostics.push(...ttc.getDiagnosticsForComponent(component));
|
|
}
|
|
}
|
|
}
|
|
this.lastKnownProgram = compiler.getNextProgram();
|
|
return diagnostics;
|
|
}
|
|
|
|
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|
|
|undefined {
|
|
const program = this.strategy.getProgram();
|
|
const compiler = this.createCompiler(program, fileName);
|
|
return new DefinitionBuilder(this.tsLS, compiler).getDefinitionAndBoundSpan(fileName, position);
|
|
}
|
|
|
|
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo|undefined {
|
|
const program = this.strategy.getProgram();
|
|
const compiler = this.createCompiler(program, fileName);
|
|
return new QuickInfoBuilder(this.tsLS, compiler).get(fileName, position);
|
|
}
|
|
|
|
/**
|
|
* Create a new instance of Ivy compiler.
|
|
* If the specified `fileName` refers to an external template, check if it has
|
|
* changed since the last time it was read. If it has changed, signal the
|
|
* compiler to reload the file via the adapter.
|
|
*/
|
|
private createCompiler(program: ts.Program, fileName: string): NgCompiler {
|
|
if (isExternalTemplate(fileName)) {
|
|
this.adapter.registerTemplateUpdate(fileName);
|
|
}
|
|
return new NgCompiler(
|
|
this.adapter,
|
|
this.options,
|
|
program,
|
|
this.strategy,
|
|
new PatchedProgramIncrementalBuildStrategy(),
|
|
/** enableTemplateTypeChecker */ true,
|
|
this.lastKnownProgram,
|
|
/** perfRecorder (use default) */ undefined,
|
|
);
|
|
}
|
|
|
|
private watchConfigFile(project: ts.server.Project) {
|
|
// TODO: Check the case when the project is disposed. An InferredProject
|
|
// could be disposed when a tsconfig.json is added to the workspace,
|
|
// in which case it becomes a ConfiguredProject (or vice-versa).
|
|
// We need to make sure that the FileWatcher is closed.
|
|
if (!(project instanceof ts.server.ConfiguredProject)) {
|
|
return;
|
|
}
|
|
const {host} = project.projectService;
|
|
host.watchFile(
|
|
project.getConfigFilePath(), (fileName: string, eventKind: ts.FileWatcherEventKind) => {
|
|
project.log(`Config file changed: ${fileName}`);
|
|
if (eventKind === ts.FileWatcherEventKind.Changed) {
|
|
this.options = parseNgCompilerOptions(project);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export function parseNgCompilerOptions(project: ts.server.Project): CompilerOptions {
|
|
let config = {};
|
|
if (project instanceof ts.server.ConfiguredProject) {
|
|
const configPath = project.getConfigFilePath();
|
|
const result = ts.readConfigFile(configPath, path => project.readFile(path));
|
|
if (result.error) {
|
|
project.error(ts.flattenDiagnosticMessageText(result.error.messageText, '\n'));
|
|
}
|
|
config = result.config || config;
|
|
}
|
|
const basePath = project.getCurrentDirectory();
|
|
return createNgCompilerOptions(basePath, config, project.getCompilationSettings());
|
|
}
|
|
|
|
function createTypeCheckingProgramStrategy(project: ts.server.Project):
|
|
TypeCheckingProgramStrategy {
|
|
return {
|
|
supportsInlineOperations: false,
|
|
shimPathForComponent(component: ts.ClassDeclaration): AbsoluteFsPath {
|
|
return TypeCheckShimGenerator.shimFor(absoluteFromSourceFile(component.getSourceFile()));
|
|
},
|
|
getProgram(): ts.Program {
|
|
const program = project.getLanguageService().getProgram();
|
|
if (!program) {
|
|
throw new Error('Language service does not have a program!');
|
|
}
|
|
return program;
|
|
},
|
|
updateFiles(contents: Map<AbsoluteFsPath, string>) {
|
|
for (const [fileName, newText] of contents) {
|
|
const scriptInfo = getOrCreateTypeCheckScriptInfo(project, fileName);
|
|
const snapshot = scriptInfo.getSnapshot();
|
|
const length = snapshot.getLength();
|
|
scriptInfo.editContent(0, length, newText);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function getOrCreateTypeCheckScriptInfo(
|
|
project: ts.server.Project, tcf: string): ts.server.ScriptInfo {
|
|
// First check if there is already a ScriptInfo for the tcf
|
|
const {projectService} = project;
|
|
let scriptInfo = projectService.getScriptInfo(tcf);
|
|
if (!scriptInfo) {
|
|
// ScriptInfo needs to be opened by client to be able to set its user-defined
|
|
// content. We must also provide file content, otherwise the service will
|
|
// attempt to fetch the content from disk and fail.
|
|
scriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(
|
|
ts.server.toNormalizedPath(tcf),
|
|
true, // openedByClient
|
|
'', // fileContent
|
|
ts.ScriptKind.TS, // scriptKind
|
|
);
|
|
if (!scriptInfo) {
|
|
throw new Error(`Failed to create script info for ${tcf}`);
|
|
}
|
|
}
|
|
// Add ScriptInfo to project if it's missing. A ScriptInfo needs to be part of
|
|
// the project so that it becomes part of the program.
|
|
if (!project.containsScriptInfo(scriptInfo)) {
|
|
project.addRoot(scriptInfo);
|
|
}
|
|
return scriptInfo;
|
|
}
|