refactor(language-service): [Ivy] remove temporary compiler (#38310)
Now that Ivy compiler has a proper `TemplateTypeChecker` interface (see https://github.com/angular/angular/pull/38105) we no longer need to keep the temporary compiler implementation. The temporary compiler was created to enable testing infrastructure to be developed for the Ivy language service. This commit removes the whole `ivy/compiler` directory and moves two functions `createTypeCheckingProgramStrategy` and `getOrCreateTypeCheckScriptInfo` to the `LanguageService` class. Also re-enable the Ivy LS test since it's no longer blocking development. PR Close #38310
This commit is contained in:
		
							parent
							
								
									3b9c802dee
								
							
						
					
					
						commit
						cfe424e875
					
				| @ -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", | ||||
|     ], | ||||
| ) | ||||
|  | ||||
| @ -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", | ||||
|     ], | ||||
| ) | ||||
| @ -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. | ||||
| @ -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<AbsoluteFsPath, string>, 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; | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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 needed 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<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; | ||||
| } | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user