diff --git a/packages/language-service/ivy/BUILD.bazel b/packages/language-service/ivy/BUILD.bazel index f71c4b7141ca71..3519eedd136e06 100644 --- a/packages/language-service/ivy/BUILD.bazel +++ b/packages/language-service/ivy/BUILD.bazel @@ -7,7 +7,13 @@ ts_library( srcs = glob(["*.ts"]), deps = [ "//packages/compiler-cli", - "//packages/language-service/ivy/compiler", + "//packages/compiler-cli/src/ngtsc/core", + "//packages/compiler-cli/src/ngtsc/core:api", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/incremental", + "//packages/compiler-cli/src/ngtsc/shims", + "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "@npm//typescript", ], ) diff --git a/packages/language-service/ivy/compiler/BUILD.bazel b/packages/language-service/ivy/compiler/BUILD.bazel deleted file mode 100644 index 22c25e3ba19b92..00000000000000 --- a/packages/language-service/ivy/compiler/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("//tools:defaults.bzl", "ts_library") - -package(default_visibility = ["//packages/language-service/ivy:__pkg__"]) - -ts_library( - name = "compiler", - srcs = glob(["*.ts"]), - deps = [ - "//packages/compiler-cli", - "//packages/compiler-cli/src/ngtsc/core", - "//packages/compiler-cli/src/ngtsc/file_system", - "//packages/compiler-cli/src/ngtsc/incremental", - "//packages/compiler-cli/src/ngtsc/typecheck", - "//packages/compiler-cli/src/ngtsc/typecheck/api", - "@npm//typescript", - ], -) diff --git a/packages/language-service/ivy/compiler/README.md b/packages/language-service/ivy/compiler/README.md deleted file mode 100644 index 2945bc7dac0ef8..00000000000000 --- a/packages/language-service/ivy/compiler/README.md +++ /dev/null @@ -1,2 +0,0 @@ -All files in this directory are temporary. This is created to simulate the final -form of the Ivy compiler that supports language service. diff --git a/packages/language-service/ivy/compiler/compiler.ts b/packages/language-service/ivy/compiler/compiler.ts deleted file mode 100644 index 9454666ffb2017..00000000000000 --- a/packages/language-service/ivy/compiler/compiler.ts +++ /dev/null @@ -1,124 +0,0 @@ - -/** - * @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} from '@angular/compiler-cli'; -import {NgCompiler, NgCompilerHost} 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 {TypeCheckingProgramStrategy, UpdateMode} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; -import * as ts from 'typescript/lib/tsserverlibrary'; - -import {makeCompilerHostFromProject} from './compiler_host'; - -interface AnalysisResult { - compiler: NgCompiler; - program: ts.Program; -} - -export class Compiler { - private tsCompilerHost: ts.CompilerHost; - private lastKnownProgram: ts.Program|null = null; - private readonly strategy: TypeCheckingProgramStrategy; - - constructor(private readonly project: ts.server.Project, private options: CompilerOptions) { - this.tsCompilerHost = makeCompilerHostFromProject(project); - this.strategy = createTypeCheckingProgramStrategy(project); - // Do not retrieve the program in constructor because project is still in - // the process of loading, and not all data members have been initialized. - } - - setCompilerOptions(options: CompilerOptions) { - this.options = options; - } - - analyze(): AnalysisResult|undefined { - const inputFiles = this.project.getRootFiles(); - const ngCompilerHost = - NgCompilerHost.wrap(this.tsCompilerHost, inputFiles, this.options, this.lastKnownProgram); - const program = this.strategy.getProgram(); - const compiler = new NgCompiler( - ngCompilerHost, this.options, program, this.strategy, - new PatchedProgramIncrementalBuildStrategy(), this.lastKnownProgram); - try { - // This is the only way to force the compiler to update the typecheck file - // in the program. We have to do try-catch because the compiler immediately - // throws if it fails to parse any template in the entire program! - const d = compiler.getDiagnostics(); - if (d.length) { - // There could be global compilation errors. It's useful to print them - // out in development. - console.error(d.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))); - } - } catch (e) { - console.error('Failed to analyze program', e.message); - return; - } - this.lastKnownProgram = compiler.getNextProgram(); - return { - compiler, - program: this.lastKnownProgram, - }; - } -} - -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, updateMode: UpdateMode) { - if (updateMode !== UpdateMode.Complete) { - throw new Error(`Incremental update mode is currently not supported`); - } - 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; -} diff --git a/packages/language-service/ivy/compiler/compiler_host.ts b/packages/language-service/ivy/compiler/compiler_host.ts deleted file mode 100644 index f417ed2541436e..00000000000000 --- a/packages/language-service/ivy/compiler/compiler_host.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @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 * as ts from 'typescript/lib/tsserverlibrary'; - -export function makeCompilerHostFromProject(project: ts.server.Project): ts.CompilerHost { - const compilerHost: ts.CompilerHost = { - fileExists(fileName: string): boolean { - return project.fileExists(fileName); - }, - readFile(fileName: string): string | - undefined { - return project.readFile(fileName); - }, - directoryExists(directoryName: string): boolean { - return project.directoryExists(directoryName); - }, - getCurrentDirectory(): string { - return project.getCurrentDirectory(); - }, - getDirectories(path: string): string[] { - return project.getDirectories(path); - }, - getSourceFile( - fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void, - shouldCreateNewSourceFile?: boolean): ts.SourceFile | - undefined { - const path = project.projectService.toPath(fileName); - return project.getSourceFile(path); - }, - getSourceFileByPath( - fileName: string, path: ts.Path, languageVersion: ts.ScriptTarget, - onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): ts.SourceFile | - undefined { - return project.getSourceFile(path); - }, - getCancellationToken(): ts.CancellationToken { - return { - isCancellationRequested() { - return project.getCancellationToken().isCancellationRequested(); - }, - throwIfCancellationRequested() { - if (this.isCancellationRequested()) { - throw new ts.OperationCanceledException(); - } - }, - }; - }, - getDefaultLibFileName(options: ts.CompilerOptions): string { - return project.getDefaultLibFileName(); - }, - writeFile( - fileName: string, data: string, writeByteOrderMark: boolean, - onError?: (message: string) => void, sourceFiles?: readonly ts.SourceFile[]) { - return project.writeFile(fileName, data); - }, - getCanonicalFileName(fileName: string): string { - return project.projectService.toCanonicalFileName(fileName); - }, - useCaseSensitiveFileNames(): boolean { - return project.useCaseSensitiveFileNames(); - }, - getNewLine(): string { - return project.getNewLine(); - }, - readDirectory( - rootDir: string, extensions: readonly string[], excludes: readonly string[]|undefined, - includes: readonly string[], depth?: number): string[] { - return project.readDirectory(rootDir, extensions, excludes, includes, depth); - }, - resolveModuleNames( - moduleNames: string[], containingFile: string, reusedNames: string[]|undefined, - redirectedReference: ts.ResolvedProjectReference|undefined, options: ts.CompilerOptions): - (ts.ResolvedModule | undefined)[] { - return project.resolveModuleNames( - moduleNames, containingFile, reusedNames, redirectedReference); - }, - resolveTypeReferenceDirectives( - typeReferenceDirectiveNames: string[], containingFile: string, - redirectedReference: ts.ResolvedProjectReference|undefined, options: ts.CompilerOptions): - (ts.ResolvedTypeReferenceDirective | undefined)[] { - return project.resolveTypeReferenceDirectives( - typeReferenceDirectiveNames, containingFile, redirectedReference); - }, - }; - - if (project.trace) { - compilerHost.trace = function trace(s: string) { - project.trace!(s); - }; - } - if (project.realpath) { - compilerHost.realpath = function realpath(path: string): string { - return project.realpath!(path); - }; - } - return compilerHost; -} diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index b0b66997841697..1841f5e66b4aad 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -7,30 +7,53 @@ */ import {CompilerOptions, createNgCompilerOptions} from '@angular/compiler-cli'; +import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; +import {NgCompilerAdapter} from '@angular/compiler-cli/src/ngtsc/core/api'; +import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {PatchedProgramIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental'; +import {isShim} from '@angular/compiler-cli/src/ngtsc/shims'; +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 {Compiler} from './compiler/compiler'; export class LanguageService { private options: CompilerOptions; - private readonly compiler: Compiler; + private lastKnownProgram: ts.Program|null = null; + private readonly strategy: TypeCheckingProgramStrategy; + private readonly adapter: NgCompilerAdapter; constructor(project: ts.server.Project, private readonly tsLS: ts.LanguageService) { this.options = parseNgCompilerOptions(project); + this.strategy = createTypeCheckingProgramStrategy(project); + this.adapter = createNgCompilerAdapter(project); this.watchConfigFile(project); - this.compiler = new Compiler(project, this.options); } getSemanticDiagnostics(fileName: string): ts.Diagnostic[] { - const result = this.compiler.analyze(); - if (!result) { - return []; + const program = this.strategy.getProgram(); + const compiler = this.createCompiler(program); + if (fileName.endsWith('.ts')) { + const sourceFile = program.getSourceFile(fileName); + if (!sourceFile) { + return []; + } + const ttc = compiler.getTemplateTypeChecker(); + const diagnostics = ttc.getDiagnosticsForFile(sourceFile, OptimizeFor.SingleFile); + this.lastKnownProgram = compiler.getNextProgram(); + return diagnostics; } - const {compiler, program} = result; - const sourceFile = program.getSourceFile(fileName); - if (!sourceFile) { - return []; - } - return compiler.getDiagnostics(sourceFile); + throw new Error('Ivy LS currently does not support external template'); + } + + private createCompiler(program: ts.Program): NgCompiler { + return new NgCompiler( + this.adapter, + this.options, + program, + this.strategy, + new PatchedProgramIncrementalBuildStrategy(), + this.lastKnownProgram, + ); } private watchConfigFile(project: ts.server.Project) { @@ -47,7 +70,6 @@ export class LanguageService { project.log(`Config file changed: ${fileName}`); if (eventKind === ts.FileWatcherEventKind.Changed) { this.options = parseNgCompilerOptions(project); - this.compiler.setCompilerOptions(this.options); } }); } @@ -66,3 +88,80 @@ export function parseNgCompilerOptions(project: ts.server.Project): CompilerOpti const basePath = project.getCurrentDirectory(); return createNgCompilerOptions(basePath, config, project.getCompilationSettings()); } + +function createNgCompilerAdapter(project: ts.server.Project): NgCompilerAdapter { + return { + entryPoint: null, // entry point is only need if code is emitted + constructionDiagnostics: [], + ignoreForEmit: new Set(), + factoryTracker: null, // no .ngfactory shims + unifiedModulesHost: null, // only used in Bazel + rootDirs: project.getCompilationSettings().rootDirs?.map(absoluteFrom) || [], + isShim, + fileExists(fileName: string): boolean { + return project.fileExists(fileName); + }, + readFile(fileName: string): string | + undefined { + return project.readFile(fileName); + }, + getCurrentDirectory(): string { + return project.getCurrentDirectory(); + }, + getCanonicalFileName(fileName: string): string { + return project.projectService.toCanonicalFileName(fileName); + }, + }; +} + +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) { + 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; +} diff --git a/packages/language-service/ivy/test/BUILD.bazel b/packages/language-service/ivy/test/BUILD.bazel index 941062a1efae2e..7d539e9e6b1f5b 100644 --- a/packages/language-service/ivy/test/BUILD.bazel +++ b/packages/language-service/ivy/test/BUILD.bazel @@ -25,7 +25,6 @@ jasmine_node_test( ], tags = [ "ivy-only", - "manual", # do not run this on CI since compiler APIs are not yet stable ], deps = [ ":test_lib",