/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/language-service/testing/src/project.ts
342 строки
11 KB
Matthieu Riegler
refactor(compiler): remove `fullTemplateTypeCheck` compiler option.
06 апр 2026, 21:55
06 апр 2026, 21:55
f3c471e
Код
Авторство
О чём код?
/** * @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.dev/license */ import {InternalOptions, TypeCheckingOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; import { absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, getSourceFileOrError, } from '@angular/compiler-cli/src/ngtsc/file_system'; import {OptimizeFor, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import ts from 'typescript'; import {ApplyRefactoringProgressFn, ApplyRefactoringResult} from '../../api'; import {LanguageService} from '../../src/language_service'; import {OpenBuffer} from './buffer'; import {patchLanguageServiceProjectsWithTestHost} from './language_service_test_cache'; export type ProjectFiles = { [fileName: string]: string; }; function writeTsconfig( fs: FileSystem, tsConfigPath: AbsoluteFsPath, entryFiles: AbsoluteFsPath[], angularCompilerOptions: TestableOptions, tsCompilerOptions: any = {}, ): void { const basePaths = { '@angular/core': ['./node_modules/@angular/core'], '@angular/core/rxjs-interop': ['./node_modules/@angular/core/rxjs-interop'], '@angular/forms': ['./node_modules/@angular/forms'], '@angular/animations': ['./node_modules/@angular/animations'], '@angular/common': ['./node_modules/@angular/common'], rxjs: ['./node_modules/rxjs'], 'rxjs/operators': ['./node_modules/rxjs/operators'], }; const mergedPaths = { ...basePaths, ...(tsCompilerOptions.paths || {}), }; fs.writeFile( tsConfigPath, JSON.stringify( { compilerOptions: { strict: true, experimentalDecorators: true, moduleResolution: 'bundler', target: 'es2015', rootDir: '.', lib: ['dom', 'es2015'], ...tsCompilerOptions, paths: mergedPaths, }, files: entryFiles, angularCompilerOptions: { strictTemplates: true, ...angularCompilerOptions, }, }, null, 2, ), ); } export type TestableOptions = TypeCheckingOptions & InternalOptions & { // This already exists in `InternalOptions`, but it's `internal` so it's stripped away. _enableSelectorless?: boolean; }; export class Project { private tsProject: ts.server.Project; private tsLS: ts.LanguageService; readonly ngLS: LanguageService; private buffers = new Map<string, OpenBuffer>(); static initialize( projectName: string, projectService: ts.server.ProjectService, files: ProjectFiles, angularCompilerOptions: TestableOptions = {}, tsCompilerOptions = {}, ): Project { const fs = getFileSystem(); const tsConfigPath = absoluteFrom(`/${projectName}/tsconfig.json`); const entryFiles: AbsoluteFsPath[] = []; for (const projectFilePath of Object.keys(files)) { const contents = files[projectFilePath]; const filePath = absoluteFrom(`/${projectName}/${projectFilePath}`); const dirPath = fs.dirname(filePath); fs.ensureDir(dirPath); fs.writeFile(filePath, contents); if (projectFilePath.endsWith('.ts') && !projectFilePath.endsWith('.d.ts')) { entryFiles.push(filePath); } // Force TypeScript to load the new file content immediately const scriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath( ts.server.toNormalizedPath(filePath), true, // openedByClient contents, ts.ScriptKind.Unknown, // infer false, // hasMixedContent projectService.host, ); if (scriptInfo) { scriptInfo.reloadFromFile(); } } writeTsconfig(fs, tsConfigPath, entryFiles, angularCompilerOptions, tsCompilerOptions); const tsConfigInfo = projectService.getScriptInfo(tsConfigPath); if (tsConfigInfo) { tsConfigInfo.reloadFromFile(); } patchLanguageServiceProjectsWithTestHost(); projectService.openClientFile(entryFiles[0]); projectService.closeClientFile(entryFiles[0]); return new Project(projectName, projectService, tsConfigPath); } private constructor( readonly name: string, private projectService: ts.server.ProjectService, private tsConfigPath: AbsoluteFsPath, ) { const tsProject = projectService.findProject(tsConfigPath); if (tsProject === undefined) { throw new Error(`Failed to create project for ${tsConfigPath}`); } this.tsProject = tsProject; this.tsLS = tsProject.getLanguageService(); this.ngLS = new LanguageService(tsProject, this.tsLS, {}); } getAbsFileName(projectFileName: string): string { return absoluteFrom(`/${this.name}/${projectFileName}`); } openFile(projectFileName: string): OpenBuffer { if (!this.buffers.has(projectFileName)) { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); let scriptInfo = this.tsProject.getScriptInfo(fileName); this.projectService.openClientFile( // By attempting to open the file, the compiler is going to try to parse it as // TS which will throw an error. We pass in JSX which is more permissive. fileName, undefined, fileName.endsWith('.html') ? ts.ScriptKind.JSX : ts.ScriptKind.TS, ); scriptInfo = this.tsProject.getScriptInfo(fileName); if (scriptInfo === undefined) { throw new Error( `Unable to open ScriptInfo for ${projectFileName} in project ${this.tsConfigPath}`, ); } this.buffers.set( projectFileName, new OpenBuffer(this.ngLS, this.tsProject, projectFileName, scriptInfo), ); } return this.buffers.get(projectFileName)!; } getSourceFile(projectFileName: string): ts.SourceFile | undefined { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); return this.tsProject.getSourceFile(this.projectService.toPath(fileName)); } getTypeChecker(): ts.TypeChecker { return this.ngLS.compilerFactory.getOrCreate().getCurrentProgram().getTypeChecker(); } getDiagnosticsForFile(projectFileName: string): ts.Diagnostic[] { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); const diagnostics: ts.Diagnostic[] = []; if (fileName.endsWith('.ts')) { diagnostics.push(...this.tsLS.getSyntacticDiagnostics(fileName)); diagnostics.push(...this.tsLS.getSemanticDiagnostics(fileName)); } diagnostics.push(...this.ngLS.getSemanticDiagnostics(fileName)); return diagnostics; } getSuggestionDiagnosticsForFile(projectFileName: string): ts.Diagnostic[] { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); const diagnostics: ts.Diagnostic[] = []; diagnostics.push(...this.ngLS.getSuggestionDiagnostics(fileName)); return diagnostics; } getCodeFixesAtPosition( projectFileName: string, start: number, end: number, errorCodes: readonly number[], ): readonly ts.CodeFixAction[] { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); return this.ngLS.getCodeFixesAtPosition( fileName, start, end, errorCodes, {}, { includeCompletionsForModuleExports: true, }, ); } getRefactoringsAtPosition( projectFileName: string, positionOrRange: number | ts.TextRange, ): readonly ts.ApplicableRefactorInfo[] { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); return this.ngLS.getPossibleRefactorings(fileName, positionOrRange); } applyRefactoring( projectFileName: string, positionOrRange: number | ts.TextRange, refactorName: string, reportProgress: ApplyRefactoringProgressFn, ): Promise<ApplyRefactoringResult | undefined> { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); return this.ngLS.applyRefactoring(fileName, positionOrRange, refactorName, reportProgress); } getCombinedCodeFix(projectFileName: string, fixId: string): ts.CombinedCodeActions { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); return this.ngLS.getCombinedCodeFix( { type: 'file', fileName, }, fixId, {}, {}, ); } expectNoSourceDiagnostics(): void { const program = this.tsLS.getProgram(); if (program === undefined) { throw new Error(`Expected to get a ts.Program`); } const ngCompiler = this.ngLS.compilerFactory.getOrCreate(); for (const sf of program.getSourceFiles()) { if ( sf.isDeclarationFile || sf.fileName.endsWith('.ngtypecheck.ts') || !sf.fileName.endsWith('.ts') ) { continue; } const syntactic = program.getSyntacticDiagnostics(sf); expect(syntactic.map((diag) => diag.messageText)).toEqual([]); if (syntactic.length > 0) { continue; } const semantic = program.getSemanticDiagnostics(sf); expect(semantic.map((diag) => diag.messageText)).toEqual([]); if (semantic.length > 0) { continue; } // It's more efficient to optimize for WholeProgram since we call this with every file in the // program. const ngDiagnostics = ngCompiler.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram); expect(ngDiagnostics.map((diag) => diag.messageText)).toEqual([]); } } expectNoTemplateDiagnostics(projectFileName: string, className: string): void { const program = this.tsLS.getProgram(); if (program === undefined) { throw new Error(`Expected to get a ts.Program`); } const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); const sf = getSourceFileOrError(program, fileName); const component = getClassOrError(sf, className); const diags = this.getTemplateTypeChecker().getDiagnosticsForComponent(component); expect(diags.map((diag) => diag.messageText)).toEqual([]); } getTemplateTypeChecker(): TemplateTypeChecker { return this.ngLS.compilerFactory.getOrCreate().getTemplateTypeChecker(); } getLogger(): ts.server.Logger { return this.tsProject.projectService.logger; } getLinkedEditingRangeAtPosition( projectFileName: string, position: number, ): {ranges: ts.TextSpan[]; wordPattern?: string} | null { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); return this.ngLS.getLinkedEditingRangeAtPosition(fileName, position) ?? null; } getSemanticDiagnostics(projectFileName: string): ts.Diagnostic[] { const fileName = absoluteFrom(`/${this.name}/${projectFileName}`); return [...this.ngLS.getSemanticDiagnostics(fileName)]; } } function getClassOrError(sf: ts.SourceFile, name: string): ts.ClassDeclaration { for (const stmt of sf.statements) { if (ts.isClassDeclaration(stmt) && stmt.name !== undefined && stmt.name.text === name) { return stmt; } } throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`); }