UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

239 lines 12.1 kB
import { DiagnosticSeverity, Range } from "vscode-languageserver"; import { defaultConfig, findTypeSpecConfigPath, loadTypeSpecConfigFile, TypeSpecConfigFilename, } from "../config/config-loader.js"; import { resolveOptionsFromConfig } from "../config/config-to-options.js"; import { builtInLinterRule_UnusedTemplateParameter } from "../core/linter-rules/unused-template-parameter.rule.js"; import { builtInLinterRule_UnusedUsing } from "../core/linter-rules/unused-using.rule.js"; import { builtInLinterLibraryName } from "../core/linter.js"; import { parse } from "../core/parser.js"; import { getBaseFileName, getDirectoryPath } from "../core/path-utils.js"; import { distinctArray } from "../utils/misc.js"; import { getLocationInYamlScript } from "../yaml/diagnostics.js"; import { parseYaml } from "../yaml/parser.js"; import { serverOptions } from "./constants.js"; import { debugLoggers } from "./debug.js"; import { resolveEntrypointFile } from "./entrypoint-resolver.js"; import { ServerCompileManager, } from "./server-compile-manager.js"; export function createCompileService({ compilerHost, serverHost, fileService, fileSystemCache, updateManager, log, clientConfigsProvider, }) { const eventListeners = new Map(); const compileManager = new ServerCompileManager(updateManager, compilerHost, log); let configFilePath; const debug = debugLoggers.compileConfig; const logDebug = debug.enabled ? log : () => { }; return { compile, getScript, on, notifyChange, getMainFileForDocument }; function on(event, listener) { eventListeners.set(event, listener); } function notify(event, ...args) { const listener = eventListeners.get(event); if (listener) { void listener(...args); } } function notifyChange(document, updateType) { void updateManager.scheduleUpdate(document, updateType); } /** * Compile the given document. * First, the main.tsp file will be obtained for compilation. * If the current document is not the main.tsp file or not included in the compilation starting from the main file found, * the current document will be recompiled and returned as part of the result. * Otherwise, the compilation of main.tsp will be returned as part of the result. * @param document The document to compile. tsp file that is open or not opened in workspace. * @returns see {@link CompileResult} for more details. */ async function compile(document, additionalOptions, serverCompileOptions) { const path = await fileService.getPath(document); const pathBaseName = getBaseFileName(path); if (!path.endsWith(".tsp") && pathBaseName !== TypeSpecConfigFilename) { return undefined; } const mainFile = await getMainFileForDocument(path); if (mainFile === undefined) { logDebug({ level: "debug", message: `failed to resolve main file for ${path}` }); return undefined; } if (!mainFile.endsWith(".tsp")) { return undefined; } const config = await getConfig(mainFile); configFilePath = config.filename; logDebug({ level: "debug", message: `config resolved`, detail: config }); const [optionsFromConfig, _] = resolveOptionsFromConfig(config, { cwd: getDirectoryPath(path), }); // we need to keep the optionsFromConfig unchanged which will be returned in CompileResult const clone = structuredClone(optionsFromConfig); const options = { ...clone, ...serverOptions, ...(additionalOptions ?? {}), }; // If emit is set in additionalOptions, use this setting first // otherwise, obtain the `typespec.lsp.emit` configuration from clientConfigsProvider if (additionalOptions?.emit === undefined) { const configEmits = clientConfigsProvider?.config?.lsp?.emit; const CONFIG_DEFAULTS = "<config:defaults>"; if (configEmits) { if (configEmits.find((e) => e === CONFIG_DEFAULTS)) { // keep the emits in tspconfig only when user configs "<config:defaults>", and append other emits from vscode settings if there is any options.emit = distinctArray([...(options.emit ?? []), ...configEmits.filter((e) => e !== CONFIG_DEFAULTS)], (s) => s); } else { // use the configured emits if no "<config:defaults>" is found options.emit = configEmits; } } else { // by default, exclude emits from compile which are not useful in most case but may cause perf issue // User can set ['<config:defaults>'] to opt-in options.emit = []; } } // add linter rule for unused using if user didn't configure it explicitly const unusedUsingRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedUsing}`; if (options.linterRuleSet?.enable?.[unusedUsingRule] === undefined && options.linterRuleSet?.disable?.[unusedUsingRule] === undefined) { options.linterRuleSet ??= {}; options.linterRuleSet.enable ??= {}; options.linterRuleSet.enable[unusedUsingRule] = true; } // add linter rule for unused template parameter if user didn't configure it explicitly const unusedTemplateParameterRule = `${builtInLinterLibraryName}/${builtInLinterRule_UnusedTemplateParameter}`; if (options.linterRuleSet?.enable?.[unusedTemplateParameterRule] === undefined && options.linterRuleSet?.disable?.[unusedTemplateParameterRule] === undefined) { options.linterRuleSet ??= {}; options.linterRuleSet.enable ??= {}; options.linterRuleSet.enable[unusedTemplateParameterRule] = true; } const isCancelledOrOutOfDate = () => { return serverCompileOptions.isCancelled?.() || !fileService.upToDate(document); }; if (isCancelledOrOutOfDate()) { return undefined; } let tracker; try { tracker = await compileManager.compile(mainFile, options, serverCompileOptions); let program = await tracker.getCompileResult(); if (isCancelledOrOutOfDate()) { return undefined; } if (mainFile !== path && !program.sourceFiles.has(path) && pathBaseName !== TypeSpecConfigFilename) { // If the file that changed wasn't imported by anything from the main // file, retry using the file itself as the main file. logDebug({ level: "debug", message: `target file was not included in compiling, try to compile ${path} as main file directly`, }); tracker = await compileManager.compile(path, options, serverCompileOptions); program = await tracker.getCompileResult(); } if (isCancelledOrOutOfDate()) { return undefined; } const doc = "version" in document ? document : serverHost.getOpenDocumentByURL(document.uri); const script = program.sourceFiles.get(path); const result = { program, document: doc, script, optionsFromConfig, tracker }; notify("compileEnd", result); return result; } catch (err) { if (serverHost.throwInternalErrors) { throw err; } let uri = document.uri; let range = Range.create(0, 0, 0, 0); if (err.name === "ExternalError" && err.info.kind === "emitter" && configFilePath) { const emitterName = err.info.metadata.name; const [yamlScript] = parseYaml(await serverHost.compilerHost.readFile(configFilePath)); const target = getLocationInYamlScript(yamlScript, ["emit", emitterName], "key"); if (target.pos === 0) { logDebug({ level: "debug", message: `Unexpected situation, can't find emitter '${emitterName}' in config file '${configFilePath}'`, }); } uri = fileService.getURL(configFilePath); const lineAndChar = target.file.getLineAndCharacterOfPosition(target.pos); range = Range.create(lineAndChar.line, lineAndChar.character, lineAndChar.line, lineAndChar.character + emitterName.length); } serverHost.sendDiagnostics({ uri, diagnostics: [ { severity: DiagnosticSeverity.Error, range, message: (err.name === "ExternalError" ? "External compiler error!\n" : `Internal compiler error!\nFile issue at https://github.com/microsoft/typespec\n\n`) + err.stack, }, ], }); return undefined; } } async function getConfig(mainFile) { const entrypointStat = await compilerHost.stat(mainFile); const lookupDir = entrypointStat.isDirectory() ? mainFile : getDirectoryPath(mainFile); const configPath = await findTypeSpecConfigPath(compilerHost, lookupDir, true); if (!configPath) { logDebug({ level: "debug", message: `can't find path with config file, try to use default config`, }); return { ...defaultConfig, projectRoot: getDirectoryPath(mainFile) }; } // JSON round-trip intentionally strips non-serializable values (functions) from the config const cached = await fileSystemCache.get(configPath); if (cached?.data) { return JSON.parse(JSON.stringify(cached.data)); } const config = await loadTypeSpecConfigFile(compilerHost, configPath); return JSON.parse(JSON.stringify(config)); } async function getScript(document) { const file = await compilerHost.readFile(await fileService.getPath(document)); const cached = compilerHost.parseCache?.get(file); if (cached === undefined) { const parsed = parse(file, { docs: true, comments: true }); compilerHost.parseCache?.set(file, parsed); return parsed; } else { return cached; } } /** * Infer the appropriate entry point (a.k.a. "main file") for analyzing a * change to the file at the given path. This is necessary because different * results can be obtained from compiling the same file with different entry * points. * * Priority is given to processing user-defined files as the entry point, * and it has the highest priority. * * Walk directory structure upwards looking for package.json with tspMain or * main.tsp file. Stop search when reaching a workspace root. If a root is * reached without finding an entry point, use the given path as its own * entry point. * * Untitled documents are always treated as their own entry points as they * do not exist in a directory that could pull them in via another entry * point. */ async function getMainFileForDocument(path) { if (path.startsWith("untitled:")) { logDebug({ level: "debug", message: `untitled document treated as its own main file: ${path}`, }); return path; } const entrypoints = clientConfigsProvider?.config?.entrypoint; return resolveEntrypointFile(compilerHost, entrypoints, path, fileSystemCache, log); } } //# sourceMappingURL=compile-service.js.map