UNPKG

@typespec/compiler

Version:

TypeSpec Compiler Preview

252 lines 12.2 kB
import { DiagnosticSeverity, Range } from "vscode-languageserver"; import { defaultConfig, findTypeSpecConfigPath, loadTypeSpecConfigFile, } from "../config/config-loader.js"; import { resolveOptionsFromConfig } from "../config/config-to-options.js"; import { compilerAssert } from "../core/diagnostics.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 { formatDiagnostic } from "../core/logger/console-sink.js"; import { parse } from "../core/parser.js"; import { getDirectoryPath, joinPaths } from "../core/path-utils.js"; import { compile as compileProgram } from "../core/program.js"; import { doIO, loadFile } from "../utils/io.js"; import { resolveTspMain } from "../utils/misc.js"; import { getLocationInYamlScript } from "../yaml/diagnostics.js"; import { parseYaml } from "../yaml/parser.js"; import { serverOptions } from "./constants.js"; import { trackActionFunc } from "./server-track-action-task.js"; import { UpdateManger } from "./update-manager.js"; export function createCompileService({ compilerHost, serverHost, fileService, fileSystemCache, log, }) { const oldPrograms = new Map(); const eventListeners = new Map(); const updated = new UpdateManger((document) => compile(document)); let configFilePath; 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) { listener(...args); } } function notifyChange(document) { updated.scheduleUpdate(document); } /** * 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, runOptions) { const path = await fileService.getPath(document); const mainFile = await getMainFileForDocument(path); const config = await getConfig(mainFile); configFilePath = config.filename; log({ level: "debug", message: `config resolved`, detail: config }); const [optionsFromConfig, _] = resolveOptionsFromConfig(config, { cwd: getDirectoryPath(path), }); const options = { ...optionsFromConfig, ...serverOptions, ...(additionalOptions ?? {}), }; // 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; } log({ level: "debug", message: `compiler options resolved`, detail: options }); if (!fileService.upToDate(document)) { return undefined; } let program; try { program = await compileProgram(runOptions?.trackAction ? { ...compilerHost, logSink: { log: compilerHost.logSink.log, getPath: compilerHost.logSink.getPath, trackAction: (message, finalMessage, action) => trackActionFunc(serverHost.log, message, finalMessage, action), }, } : compilerHost, mainFile, options, runOptions?.bypassCache ? undefined : oldPrograms.get(mainFile)); oldPrograms.set(mainFile, program); if (!fileService.upToDate(document)) { return undefined; } if (mainFile !== path && !program.sourceFiles.has(path)) { // If the file that changed wasn't imported by anything from the main // file, retry using the file itself as the main file. log({ level: "debug", message: `target file was not included in compiling, try to compile ${path} as main file directly`, }); program = await compileProgram(compilerHost, path, options, oldPrograms.get(path)); oldPrograms.set(path, program); } if (!fileService.upToDate(document)) { return undefined; } const doc = "version" in document ? document : serverHost.getOpenDocumentByURL(document.uri); const script = program.sourceFiles.get(path); compilerAssert(script, "Failed to get script."); const result = { program, document: doc, script, optionsFromConfig }; 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) { log({ 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) { log({ level: "debug", message: `can't find path with config file, try to use default config`, }); return { ...defaultConfig, projectRoot: getDirectoryPath(mainFile) }; } const cached = await fileSystemCache.get(configPath); const deepCopy = (obj) => JSON.parse(JSON.stringify(obj)); if (cached?.data) { return deepCopy(cached.data); } const config = await loadTypeSpecConfigFile(compilerHost, configPath); return deepCopy(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. * * 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:")) { log({ level: "debug", message: `untitled document treated as its own main file: ${path}` }); return path; } let dir = getDirectoryPath(path); const options = { allowFileNotFound: true }; while (true) { let mainFile = "main.tsp"; let pkg; const pkgPath = joinPaths(dir, "package.json"); const cached = await fileSystemCache.get(pkgPath); if (cached?.data) { pkg = cached.data; } else { [pkg] = await loadFile(compilerHost, pkgPath, JSON.parse, logMainFileSearchDiagnostic, options); await fileSystemCache.setData(pkgPath, pkg ?? {}); } const tspMain = resolveTspMain(pkg); if (typeof tspMain === "string") { log({ level: "debug", message: `tspMain resolved from package.json (${pkgPath}) as ${tspMain}`, }); mainFile = tspMain; } const candidate = joinPaths(dir, mainFile); const stat = await doIO(() => compilerHost.stat(candidate), candidate, logMainFileSearchDiagnostic, options); if (stat?.isFile()) { log({ level: "debug", message: `main file found as ${candidate}` }); return candidate; } const parentDir = getDirectoryPath(dir); if (parentDir === dir) { break; } log({ level: "debug", message: `main file not found in ${dir}, search in parent directory ${parentDir}`, }); dir = parentDir; } log({ level: "debug", message: `reached directory root, using ${path} as main file` }); return path; function logMainFileSearchDiagnostic(diagnostic) { log({ level: `error`, message: `Unexpected diagnostic while looking for main file of ${path}`, detail: formatDiagnostic(diagnostic), }); } } } //# sourceMappingURL=compile-service.js.map