Files
2026-07-09 16:50:43 -04:00

3437 lines
147 KiB
TypeScript

#!/usr/bin/env -S node --experimental-strip-types
// Usage: node --experimental-strip-types generate.mts
import { $ } from "execa";
import fs from "node:fs";
import path from "node:path";
import url from "node:url";
import type {
Enumeration,
MetaModel,
Notification,
OrType,
Property,
ReferenceType,
Request,
Structure,
Type,
TypeAlias,
} from "./metaModelSchema.mts";
const __filename = url.fileURLToPath(new URL(import.meta.url));
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "../../../..");
const out = path.resolve(__dirname, "../lsp_generated.go");
const metaModelPath = path.resolve(__dirname, "metaModel.json");
if (!fs.existsSync(metaModelPath)) {
console.error("Meta model file not found; did you forget to run fetchModel.mjs?");
process.exit(1);
}
const model: MetaModel = JSON.parse(fs.readFileSync(metaModelPath, "utf-8"));
// Custom structures to add to the model
const customStructures: Structure[] = [
{
name: "InitializationOptions",
properties: [
{
name: "disablePushDiagnostics",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "DisablePushDiagnostics disables automatic pushing of diagnostics to the client.",
},
{
name: "codeLensShowLocationsCommandName",
type: { kind: "base", name: "string" },
optional: true,
documentation: "The client-side command name that resolved references/implementations `CodeLens` should trigger. Arguments passed will be `(DocumentUri, Position, Location[])`.",
},
{
name: "userPreferences",
type: { kind: "reference", name: "any" },
optional: true,
documentation: "userPreferences and/or formatting options if provided at initialization.",
},
{
name: "enableTelemetry",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "EnableTelemetry enables sending telemetry events from the server to the client.",
},
{
name: "logVerbosity",
type: { kind: "reference", name: "LogVerbosity" },
optional: true,
documentation: "The initial log verbosity level, matching the client's output channel log level at startup. Subsequent changes are sent via custom/setLogVerbosity.",
},
],
documentation: "InitializationOptions contains user-provided initialization options.",
},
{
name: "AutoImportFix",
properties: [
{
name: "kind",
type: { kind: "reference", name: "AutoImportFixKind" },
omitzeroValue: true,
},
{
name: "name",
type: { kind: "base", name: "string" },
omitzeroValue: true,
},
{
name: "importKind",
type: { kind: "reference", name: "ImportKind" },
},
{
name: "useRequire",
type: { kind: "base", name: "boolean" },
omitzeroValue: true,
},
{
name: "addAsTypeOnly",
type: { kind: "reference", name: "AddAsTypeOnly" },
},
{
name: "moduleSpecifier",
type: { kind: "base", name: "string" },
documentation: "The module specifier for this auto-import.",
omitzeroValue: true,
},
{
name: "importIndex",
type: { kind: "base", name: "integer" },
documentation: "Index of the import to modify when adding to an existing import declaration.",
},
{
name: "usagePosition",
type: { kind: "reference", name: "Position" },
optional: true,
},
{
name: "namespacePrefix",
type: { kind: "base", name: "string" },
omitzeroValue: true,
},
],
documentation: "AutoImportFix contains information about an auto-import suggestion.",
},
{
name: "CompletionItemData",
properties: [
{
name: "fileName",
type: { kind: "base", name: "string" },
documentation: "The file name where the completion was requested.",
omitzeroValue: true,
},
{
name: "position",
type: { kind: "base", name: "integer" },
documentation: "The position where the completion was requested.",
omitzeroValue: true,
},
{
name: "source",
type: { kind: "base", name: "string" },
documentation: "Special source value for disambiguation.",
omitzeroValue: true,
},
{
name: "name",
type: { kind: "base", name: "string" },
documentation: "The name of the completion item.",
omitzeroValue: true,
},
{
name: "autoImport",
type: { kind: "reference", name: "AutoImportFix" },
optional: true,
documentation: "Auto-import data for this completion item.",
},
],
documentation: "CompletionItemData is preserved on a CompletionItem between CompletionRequest and CompletionResolveRequest.",
},
{
name: "CodeLensData",
properties: [
{
name: "kind",
type: { kind: "reference", name: "CodeLensKind" },
documentation: `The kind of the code lens ("references" or "implementations").`,
},
{
name: "uri",
type: { kind: "base", name: "DocumentUri" },
documentation: `The document in which the code lens and its range are located.`,
},
],
},
{
name: "ExperimentalServerCapabilities",
properties: [
{
name: "customSourceDefinitionProvider",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "The server provides source definition support via custom/textDocument/sourceDefinition.",
},
{
name: "customMultiDocumentHighlightProvider",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "The server provides multi-document highlight support via custom/textDocument/multiDocumentHighlight.",
},
],
documentation: "ExperimentalServerCapabilities contains experimental capabilities under development.",
},
{
name: "ExperimentalClientCapabilities",
properties: [
{
name: "hoverVerbosityLevel",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "The client supports hover verbosityLevel requests and canIncreaseVerbosity responses.",
},
],
documentation: "ExperimentalClientCapabilities contains experimental capabilities under development.",
},
{
name: "VSOnAutoInsertOptions",
properties: [
{
name: "_vs_triggerCharacters",
type: { kind: "array", element: { kind: "base", name: "string" } },
documentation: "List of trigger characters that trigger auto-insert.",
},
],
documentation: "Options for the textDocument/_vs_onAutoInsert provider capability.",
},
{
name: "VSReferenceItem",
properties: [
{
name: "_vs_id",
type: { kind: "base", name: "integer" },
documentation: "Unique identifier for this reference item.",
},
{
name: "_vs_definitionId",
type: { kind: "base", name: "integer" },
optional: true,
documentation: "The ID of the definition item this reference belongs to. Absent for definition items themselves.",
},
{
name: "_vs_kind",
type: { kind: "array", element: { kind: "reference", name: "VSReferenceKind" } },
optional: true,
documentation: "The kind(s) of this reference (read, write, etc.).",
},
{
name: "_vs_location",
type: { kind: "reference", name: "Location" },
documentation: "The location of this reference.",
},
{
name: "_vs_definitionText",
type: { kind: "reference", name: "VSClassifiedTextElement" },
optional: true,
documentation: "Classified display text for the definition (used for grouping headers in the UI).",
},
{
name: "_vs_projectName",
type: { kind: "base", name: "string" },
optional: true,
documentation: "The project name for this reference.",
},
{
name: "_vs_containingType",
type: { kind: "base", name: "string" },
optional: true,
documentation: "The containing type for this reference.",
},
],
documentation: "A VS-specific reference item with grouping support for Find All References.",
},
{
name: "VSOnAutoInsertParams",
properties: [
{
name: "_vs_textDocument",
type: { kind: "reference", name: "TextDocumentIdentifier" },
documentation: "The text document.",
},
{
name: "_vs_position",
type: { kind: "reference", name: "Position" },
documentation: "The position inside the text document.",
},
{
name: "_vs_ch",
type: { kind: "base", name: "string" },
documentation: "The character that triggered the auto-insert.",
},
],
documentation: "Parameters for the textDocument/_vs_onAutoInsert request.",
},
{
name: "VSOnAutoInsertResponseItem",
properties: [
{
name: "_vs_textEditFormat",
type: { kind: "reference", name: "InsertTextFormat" },
documentation: "The format of the text edit (plaintext or snippet).",
},
{
name: "_vs_textEdit",
type: { kind: "reference", name: "TextEdit" },
documentation: "The text edit to apply for the auto-insertion.",
},
],
documentation: "Response item for the textDocument/_vs_onAutoInsert request.",
},
{
name: "RequestFailureTelemetryEvent",
properties: [
{
name: "eventName",
type: { kind: "stringLiteral", value: "languageServer.errorResponse" },
documentation: "The name of the telemetry event.",
},
{
name: "telemetryPurpose",
type: { kind: "stringLiteral", value: "error" },
documentation: "Indicates whether the reason for generating the event (e.g. general usage telemetry or errors).",
},
{
name: "properties",
type: { kind: "reference", name: "RequestFailureTelemetryProperties" },
documentation: "The properties associated with the event.",
},
],
documentation: "A RequestFailureTelemetryEvent is sent when a request fails and the server recovers.",
},
{
name: "RequestFailureTelemetryProperties",
properties: [
{
name: "errorCode",
type: { kind: "base", name: "string" },
documentation: "The error code associated with the event.",
},
{
name: "requestMethod",
type: { kind: "base", name: "string" },
documentation: "The method of the request that caused the event.",
},
{
name: "stack",
type: { kind: "base", name: "string" },
documentation: "The stack trace associated with the event.",
},
],
documentation: "RequestFailureTelemetryProperties contains failure information when an LSP request manages to recover.",
},
{
name: "ProfileParams",
properties: [
{
name: "dir",
type: { kind: "base", name: "string" },
documentation: "The directory path where the profile should be saved.",
},
],
documentation: "Parameters for profiling requests.",
},
{
name: "ProfileResult",
properties: [
{
name: "file",
type: { kind: "base", name: "string" },
documentation: "The file path where the profile was saved.",
},
],
documentation: "Result of a profiling request.",
},
{
name: "InitializeAPISessionParams",
properties: [
{
name: "pipe",
type: { kind: "base", name: "string" },
optional: true,
documentation: "Optional path to use for the named pipe or Unix domain socket. If not provided, a unique path will be generated.",
},
],
documentation: "Parameters for the initializeAPISession request.",
},
{
name: "InitializeAPISessionResult",
properties: [
{
name: "sessionId",
type: { kind: "base", name: "string" },
documentation: "The unique identifier for this API session.",
},
{
name: "pipe",
type: { kind: "base", name: "string" },
documentation: "The path to the named pipe or Unix domain socket for API communication.",
},
],
documentation: "Result for the initializeAPISession request.",
},
{
name: "ProjectInfoParams",
properties: [
{
name: "textDocument",
type: { kind: "reference", name: "TextDocumentIdentifier" },
documentation: "The text document to get project info for.",
},
],
documentation: "Parameters for the custom/projectInfo request.",
},
{
name: "ProjectInfoResult",
properties: [
{
name: "configFilePath",
type: { kind: "base", name: "string" },
documentation: "The absolute path to the config file (e.g. /path/to/tsconfig.json) for the project that contains this file, or an empty string if the file is in an inferred project.",
},
],
documentation: "Result for the custom/projectInfo request.",
},
{
name: "SetLogVerbosityParams",
properties: [
{
name: "verbosity",
type: { kind: "reference", name: "LogVerbosity" },
documentation: "The log verbosity level.",
},
],
documentation: "Parameters for the custom/setLogVerbosity notification.",
},
{
name: "PerformanceStatsTelemetryEvent",
properties: [
{
name: "eventName",
type: { kind: "stringLiteral", value: "languageServer.performanceStats" },
documentation: "The name of the telemetry event.",
},
{
name: "telemetryPurpose",
type: { kind: "stringLiteral", value: "usage" },
documentation: "Indicates this is a usage telemetry event.",
},
{
name: "measurements",
type: { kind: "reference", name: "PerformanceStatsTelemetryMeasurements" },
documentation: "Numeric measurements for this telemetry event.",
},
],
documentation: "A PerformanceStatsTelemetryEvent is sent periodically with performance and resource usage statistics.",
},
{
name: "PerformanceStatsTelemetryMeasurements",
properties: [
{ name: "openFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of files currently open in the editor." },
{ name: "uptimeSeconds", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Seconds since the session was initialized." },
{ name: "projectCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of loaded projects." },
{ name: "configCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of loaded config files." },
{ name: "cachedDiskFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of files cached from disk." },
{ name: "memoryUsedBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total memory mapped by the Go runtime in bytes." },
{ name: "goMemLimit", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "GOMEMLIMIT value in bytes, or 0 if not set." },
{ name: "goGCPercent", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "GOGC percentage value configured for the GC." },
{ name: "heapGoalBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap size target the GC is working toward in bytes." },
{ name: "heapLiveBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Bytes of live (reachable) heap objects." },
{ name: "heapObjectCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of live or unswept objects occupying heap memory." },
{ name: "heapStackBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap memory reserved for goroutine stacks." },
{ name: "heapReleasedBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap memory returned to the OS." },
{ name: "heapFreeBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap memory that is free and eligible to be returned to the OS." },
{ name: "gcScanHeapBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total scannable heap bytes — how much the GC must traverse." },
{ name: "goMaxProcs", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "The current GOMAXPROCS value." },
{ name: "goroutineCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Current number of goroutines." },
{ name: "gcCyclesTotal", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total completed GC cycles." },
{ name: "gcCPUSeconds", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Cumulative CPU time spent in GC in seconds." },
{ name: "userCPUSeconds", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Cumulative CPU time spent in user Go code in seconds." },
{ name: "systemMemTotal", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total physical memory on the system in bytes." },
{ name: "systemMemUsed", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Used physical memory on the system in bytes." },
{ name: "autoImportProjectBucketCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of auto-import project buckets." },
{ name: "autoImportNodeModulesBucketCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of auto-import node_modules buckets." },
{ name: "autoImportUniquePackageCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Unique packages across all node_modules buckets." },
{ name: "autoImportProjectExportCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total indexed exports from project files." },
{ name: "autoImportNodeModulesExportCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total indexed exports from node_modules." },
{ name: "autoImportProjectFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total files tracked across project buckets." },
{ name: "autoImportNodeModulesFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total files tracked across node_modules buckets." },
{ name: "autoImportNodeModulesUnfilteredBucketCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of node_modules buckets with no package.json filter." },
],
documentation: "Numeric measurements for PerformanceStatsTelemetryEvent.",
},
{
name: "ProjectInfoTelemetryEvent",
properties: [
{
name: "eventName",
type: { kind: "stringLiteral", value: "languageServer.projectInfo" },
documentation: "The name of the telemetry event.",
},
{
name: "telemetryPurpose",
type: { kind: "stringLiteral", value: "usage" },
documentation: "Indicates this is a usage telemetry event.",
},
{
name: "properties",
type: { kind: "map", key: { kind: "base", name: "string" }, value: { kind: "base", name: "string" } },
documentation: "String properties for this telemetry event. Complex values (compilerOptions, fileStats) are JSON-stringified.",
},
{
name: "measurements",
type: { kind: "reference", name: "ProjectInfoTelemetryMeasurements" },
documentation: "Numeric measurements for this telemetry event.",
},
],
documentation: "A ProjectInfoTelemetryEvent is sent once per project when it is first loaded.",
},
{
name: "ProjectInfoTelemetryMeasurements",
properties: [
{ name: "jsFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "jsFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "jsxFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "jsxFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "tsFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "tsFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "tsxFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "tsxFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "dtsFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
{ name: "dtsFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
],
documentation: "Numeric measurements for ProjectInfoTelemetryEvent.",
},
{
name: "MultiDocumentHighlight",
properties: [
{
name: "uri",
type: { kind: "base", name: "DocumentUri" },
documentation: "The URI of the document containing the highlights.",
},
{
name: "highlights",
type: { kind: "array", element: { kind: "reference", name: "DocumentHighlight" } },
documentation: "The highlights for the document.",
},
],
documentation: "Represents a collection of document highlights from a single document, used in multi-document highlight responses.",
},
{
name: "MultiDocumentHighlightParams",
properties: [
{
name: "textDocument",
type: { kind: "reference", name: "TextDocumentIdentifier" },
documentation: "The text document.",
},
{
name: "position",
type: { kind: "reference", name: "Position" },
documentation: "The position inside the text document.",
},
{
name: "filesToSearch",
type: { kind: "array", element: { kind: "base", name: "DocumentUri" } },
documentation: "The list of file URIs to search for highlights across.",
},
],
documentation: "Parameters for the custom/textDocument/multiDocumentHighlight request.",
},
{
name: "VSClassifiedTextRun",
properties: [
{
name: "ClassificationTypeName",
type: { kind: "base", name: "string" },
documentation: "The classification type name (e.g. 'keyword', 'class name', 'parameter name').",
},
{
name: "Text",
type: { kind: "base", name: "string" },
documentation: "The text content of this run.",
},
{
name: "MarkerTagType",
type: { kind: "base", name: "string" },
optional: true,
documentation: "Optional marker tag type.",
},
{
name: "Style",
type: { kind: "base", name: "integer" },
optional: true,
omitzeroValue: true,
documentation: "The style of this text run.",
},
{
name: "_vs_type",
type: { kind: "stringLiteral", value: "ClassifiedTextRun" },
documentation: "VS type discriminator required by ObjectContentConverter for deserialization.",
},
],
documentation: "A classified text run with text and classification type, used for colorized display in VS.",
},
{
name: "VSClassifiedTextElement",
properties: [
{
name: "Runs",
type: { kind: "array", element: { kind: "reference", name: "VSClassifiedTextRun" } },
documentation: "The classified text runs that make up this element.",
},
{
name: "_vs_type",
type: { kind: "stringLiteral", value: "ClassifiedTextElement" },
documentation: "VS type discriminator required by ObjectContentConverter for deserialization.",
},
],
documentation: "A classified text element containing an array of classified text runs, used for colorized labels in VS.",
},
];
const customEnumerations: Enumeration[] = [
{
name: "LogVerbosity",
type: { kind: "base", name: "integer" },
values: [
{ name: "Off", value: 0, documentation: "All logging disabled." },
{ name: "Trace", value: 1, documentation: "Most verbose; includes LSP request/response traces." },
{ name: "Debug", value: 2, documentation: "Verbose server logs." },
{ name: "Info", value: 3, documentation: "Normal server logs." },
{ name: "Warning", value: 4, documentation: "Warnings only." },
{ name: "Error", value: 5, documentation: "Errors only." },
],
documentation: "Log verbosity level, mirroring the VS Code LogLevel enum values.",
},
{
name: "VSReferenceKind",
type: { kind: "base", name: "integer" },
values: [
{ name: "Inactive", value: 0 },
{ name: "Comment", value: 1 },
{ name: "String", value: 2 },
{ name: "Read", value: 3 },
{ name: "Write", value: 4 },
{ name: "Reference", value: 5 },
{ name: "Name", value: 6 },
{ name: "Qualified", value: 7 },
{ name: "TypeArgument", value: 8 },
{ name: "TypeConstraint", value: 9 },
{ name: "BaseType", value: 10 },
{ name: "Constructor", value: 11 },
{ name: "Destructor", value: 12 },
{ name: "Import", value: 13 },
{ name: "Declaration", value: 14 },
{ name: "AddressOf", value: 15 },
{ name: "NotReference", value: 16 },
{ name: "Unknown", value: 17 },
],
},
{
name: "CodeLensKind",
type: {
kind: "base",
name: "string",
},
values: [
{
name: "References",
value: "references",
},
{
name: "Implementations",
value: "implementations",
},
],
},
{
name: "AutoImportFixKind",
type: { kind: "base", name: "integer" },
values: [
{ name: "UseNamespace", value: 0, documentation: "Augment an existing namespace import." },
{ name: "JsdocTypeImport", value: 1, documentation: "Add a JSDoc-only type import." },
{ name: "AddToExisting", value: 2, documentation: "Insert into an existing import declaration." },
{ name: "AddNew", value: 3, documentation: "Create a fresh import statement." },
{ name: "PromoteTypeOnly", value: 4, documentation: "Promote a type-only import when necessary." },
],
},
{
name: "ImportKind",
type: { kind: "base", name: "integer" },
values: [
{ name: "Named", value: 0, documentation: "Adds a named import." },
{ name: "Default", value: 1, documentation: "Adds a default import." },
{ name: "Namespace", value: 2, documentation: "Adds a namespace import." },
{ name: "CommonJS", value: 3, documentation: "Adds a CommonJS import assignment." },
],
},
{
name: "AddAsTypeOnly",
type: { kind: "base", name: "integer" },
values: [
{ name: "Allowed", value: 1, documentation: "Import may be marked type-only if needed." },
{ name: "Required", value: 2, documentation: "Import must be marked type-only." },
{ name: "NotAllowed", value: 4, documentation: "Import cannot be marked type-only." },
],
},
{
name: "ClassificationTypeName",
type: { kind: "base", name: "string" },
values: [
{ name: "Keyword", value: "keyword", documentation: "Language keyword (e.g., function, const, class)." },
{ name: "Punctuation", value: "punctuation", documentation: "Punctuation characters (e.g., parentheses, commas, semicolons)." },
{ name: "Operator", value: "operator", documentation: "Operators (e.g., =, +, ?)." },
{ name: "WhiteSpace", value: "whitespace", documentation: "Whitespace including spaces and line breaks." },
{ name: "Text", value: "text", documentation: "Plain text with no special classification." },
{ name: "String", value: "string", documentation: "String and literal values." },
{ name: "Number", value: "number", documentation: "Numeric literal values." },
{ name: "Comment", value: "comment", documentation: "Comment text." },
{ name: "ClassName", value: "class name", documentation: "Class names." },
{ name: "InterfaceName", value: "interface name", documentation: "Interface names." },
{ name: "EnumName", value: "enum name", documentation: "Enum names." },
{ name: "ModuleName", value: "module name", documentation: "Module/namespace names." },
{ name: "MethodName", value: "method name", documentation: "Method and function names." },
{ name: "ParameterName", value: "parameter name", documentation: "Parameter names." },
{ name: "PropertyName", value: "property name", documentation: "Property and accessor names." },
{ name: "FieldName", value: "field name", documentation: "Field names (e.g., enum members)." },
{ name: "LocalName", value: "local name", documentation: "Local variable names." },
{ name: "TypeParameterName", value: "type parameter name", documentation: "Type parameter names." },
{ name: "Identifier", value: "identifier", documentation: "General identifiers (e.g., type aliases, imports)." },
],
documentation: "Roslyn classification type names used by VS for syntax coloring in tooltips and other UI elements.",
},
];
const customRequests: Request[] = [
{
method: "custom/runGC",
typeName: "RunGCRequest",
messageDirection: "clientToServer",
result: { kind: "base", name: "null" },
documentation: "Triggers garbage collection in the language server.",
},
{
method: "custom/saveHeapProfile",
typeName: "SaveHeapProfileRequest",
params: { kind: "reference", name: "ProfileParams" },
messageDirection: "clientToServer",
result: { kind: "reference", name: "ProfileResult" },
documentation: "Saves a heap profile to the specified directory.",
},
{
method: "custom/saveAllocProfile",
typeName: "SaveAllocProfileRequest",
params: { kind: "reference", name: "ProfileParams" },
messageDirection: "clientToServer",
result: { kind: "reference", name: "ProfileResult" },
documentation: "Saves an allocation profile to the specified directory.",
},
{
method: "custom/startCPUProfile",
typeName: "StartCPUProfileRequest",
params: { kind: "reference", name: "ProfileParams" },
messageDirection: "clientToServer",
result: { kind: "base", name: "null" },
documentation: "Starts CPU profiling, writing to the specified directory when stopped.",
},
{
method: "custom/stopCPUProfile",
typeName: "StopCPUProfileRequest",
messageDirection: "clientToServer",
result: { kind: "reference", name: "ProfileResult" },
documentation: "Stops CPU profiling and saves the profile.",
},
{
method: "custom/initializeAPISession",
typeName: "CustomInitializeAPISessionRequest",
params: { kind: "reference", name: "InitializeAPISessionParams" },
result: { kind: "reference", name: "InitializeAPISessionResult" },
messageDirection: "clientToServer",
documentation: "Custom request to initialize an API session.",
},
{
method: "custom/projectInfo",
typeName: "CustomProjectInfoRequest",
params: { kind: "reference", name: "ProjectInfoParams" },
result: { kind: "reference", name: "ProjectInfoResult" },
messageDirection: "clientToServer",
documentation: "Returns project information (e.g. the tsconfig.json path) for a given text document.",
},
{
method: "custom/textDocument/sourceDefinition",
typeName: "CustomTextDocumentSourceDefinitionRequest",
params: { kind: "reference", name: "TextDocumentPositionParams" },
result: { kind: "reference", name: "LocationOrLocationsOrDefinitionLinksOrNull" },
messageDirection: "clientToServer",
documentation: "Request to get source definitions for a position.",
},
{
method: "custom/textDocument/multiDocumentHighlight",
typeName: "CustomMultiDocumentHighlightRequest",
params: { kind: "reference", name: "MultiDocumentHighlightParams" },
result: {
kind: "or",
items: [
{ kind: "array", element: { kind: "reference", name: "MultiDocumentHighlight" } },
{ kind: "base", name: "null" },
],
},
messageDirection: "clientToServer",
documentation: "Request to get document highlights across multiple files.",
},
{
method: "textDocument/_vs_onAutoInsert",
typeName: "VSOnAutoInsertRequest",
params: { kind: "reference", name: "VSOnAutoInsertParams" },
result: {
kind: "or",
items: [
{ kind: "reference", name: "VSOnAutoInsertResponseItem" },
{ kind: "base", name: "null" },
],
},
messageDirection: "clientToServer",
documentation: "Request for auto-insert when a trigger character is typed (VS-specific).",
},
{
method: "textDocument/_vs_references",
typeName: "VSReferencesRequest",
params: { kind: "reference", name: "ReferenceParams" },
result: {
kind: "or",
items: [
{ kind: "array", element: { kind: "reference", name: "VSReferenceItem" } },
{ kind: "base", name: "null" },
],
},
messageDirection: "clientToServer",
documentation: "VS-specific request for Find All References with grouped reference items.",
},
];
const customNotifications: Notification[] = [
{
method: "custom/setLogVerbosity",
typeName: "CustomSetLogVerbosityNotification",
params: { kind: "reference", name: "SetLogVerbosityParams" },
messageDirection: "clientToServer",
documentation: "Notification to set the server's log verbosity level based on the output channel's log level.",
},
];
// compareStructures is the set of generated structures for which a Compare method should be emitted.
// The Compare method defines a total ordering by comparing fields in declaration order.
// All listed structures (and any structure-typed fields they reference) must contain only
// comparable fields: base scalar types, or other structures that are themselves in this set.
const compareStructures = new Set<string>([
"Position",
"Range",
"TextEdit",
]);
const customTypeAliases: TypeAlias[] = [
{
name: "TelemetryEvent",
type: {
kind: "or",
items: [
{ kind: "reference", name: "RequestFailureTelemetryEvent" },
{ kind: "reference", name: "PerformanceStatsTelemetryEvent" },
{ kind: "reference", name: "ProjectInfoTelemetryEvent" },
{ kind: "base", name: "null" },
],
},
},
];
// Track which custom Data structures were declared explicitly
const explicitDataStructures = new Set(customStructures.map(s => s.name));
// Map from registration method → { fieldName, optionsTypeName }
// Built during patchAndPreprocessModel, used during code generation.
interface RegistrationMethodInfo {
registrationMethod: string;
fieldName: string;
optionsTypeName: string;
isRegistrationOnly?: boolean;
}
let registrationMethods: RegistrationMethodInfo[] = [];
// Patch and preprocess the model
function patchAndPreprocessModel() {
// Track which Data types we need to create as placeholders
const neededDataStructures = new Set<string>();
// Collect all registration option types from requests and notifications
const registrationOptionTypes: Type[] = [];
for (const request of [...model.requests, ...model.notifications]) {
if (request.registrationOptions) {
registrationOptionTypes.push(request.registrationOptions);
}
}
// Create synthetic structures for "and" types in registration options
const syntheticStructures: Structure[] = [];
for (let i = 0; i < registrationOptionTypes.length; i++) {
const regOptType = registrationOptionTypes[i];
if (regOptType.kind === "and") {
// Find which request/notification this registration option belongs to
const owner = [...model.requests, ...model.notifications].find(r => r.registrationOptions === regOptType);
if (!owner) {
throw new Error("Could not find owner for 'and' type registration option");
}
// Determine the proper name based on the typeName or method
let structureName: string;
if (owner.typeName) {
// Use typeName as base: "ColorPresentationRequest" -> "ColorPresentationRegistrationOptions"
structureName = owner.typeName.replace(/Request$/, "").replace(/Notification$/, "") + "RegistrationOptions";
}
else {
// Fall back to method: "textDocument/colorPresentation" -> "ColorPresentationRegistrationOptions"
const methodParts = owner.method.split("/");
const lastPart = methodParts[methodParts.length - 1];
structureName = titleCase(lastPart) + "RegistrationOptions";
}
// Extract all reference types from the "and"
const refTypes = regOptType.items.filter((item): item is ReferenceType => item.kind === "reference");
// Create a synthetic structure that combines all the referenced structures
syntheticStructures.push({
name: structureName,
properties: [],
extends: refTypes,
documentation: `Registration options for ${owner.method}.`,
});
// Replace the "and" type with a reference to the synthetic structure
registrationOptionTypes[i] = { kind: "reference", name: structureName };
// Also update the model so the request/notification has the resolved type
owner.registrationOptions = registrationOptionTypes[i];
}
}
for (const structure of model.structures) {
// Patch ServerCapabilities to add custom tsgo capability flags
if (structure.name === "ServerCapabilities") {
structure.properties.push({
name: "_vs_onAutoInsertProvider",
type: { kind: "reference", name: "VSOnAutoInsertOptions" },
optional: true,
documentation: "Provider options for the VS auto-insert feature via textDocument/_vs_onAutoInsert.",
});
structure.properties.push({
name: "_vs_referencesProvider",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "The server provides VS-specific grouped references via textDocument/_vs_references.",
});
}
// Patch HoverParams to add verbosityLevel
if (structure.name === "HoverParams") {
structure.properties.push({
name: "verbosityLevel",
type: { kind: "base", name: "integer" },
optional: true,
documentation: "Controls how many levels of type definitions will be expanded. Default is 0.",
});
}
// Patch Hover to add canIncreaseVerbosity
if (structure.name === "Hover") {
structure.properties.push(
{
name: "canIncreaseVerbosity",
type: { kind: "base", name: "boolean" },
omitzeroValue: true,
documentation: "Whether the verbosity level can be increased for this hover.",
},
);
}
// Patch ClientCapabilities to add VS-specific client capabilities
if (structure.name === "ClientCapabilities") {
structure.properties.push(
{
name: "_vs_supportsVisualStudioExtensions",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "Whether the client supports Visual Studio extensions.",
},
{
name: "_vs_supportedSnippetVersion",
type: { kind: "base", name: "integer" },
optional: true,
documentation: "The snippet version supported by the client.",
},
{
name: "_vs_supportsNotIncludingTextInTextDocumentDidOpen",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "Whether the client supports not including text in textDocument/didOpen notifications.",
},
{
name: "_vs_supportsIconExtensions",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "Whether the client supports icon extensions.",
},
{
name: "_vs_supportsDiagnosticRequests",
type: { kind: "base", name: "boolean" },
optional: true,
documentation: "Whether the client supports diagnostic requests.",
},
);
}
// Patch SignatureInformation to add VS-specific colorized label
if (structure.name === "SignatureInformation") {
structure.properties.push({
name: "_vs_colorizedLabel",
type: { kind: "reference", name: "VSClassifiedTextElement" },
optional: true,
documentation: "A colorized label for the signature, providing classified text runs for VS syntax coloring.",
});
}
for (const prop of structure.properties) {
// Replace initializationOptions type with custom InitializationOptions.
// The spec types this field as LSPAny?, which includes null, so keep
// it nullable so a null value sent by loose clients is accepted.
if (prop.name === "initializationOptions" && prop.type.kind === "reference" && prop.type.name === "LSPAny") {
prop.type = {
kind: "or",
items: [
{ kind: "reference", name: "InitializationOptions" },
{ kind: "base", name: "null" },
],
};
}
// Replace Data *any fields with custom typed Data fields
if (prop.name === "data" && prop.type.kind === "reference" && prop.type.name === "LSPAny") {
const customDataType = `${structure.name}Data`;
prop.type = { kind: "reference", name: customDataType };
// If we haven't explicitly declared this Data structure, we'll need a placeholder
if (!explicitDataStructures.has(customDataType)) {
neededDataStructures.add(customDataType);
}
}
// Registration.registerOptions and Registration.method are handled specially:
// registerOptions becomes a custom struct, and method is derived from it.
// Remove both from the structure so the normal generator skips them.
if (structure.name === "Registration" && (prop.name === "registerOptions" || prop.name === "method")) {
// Will be filtered out below
}
// Replace ProgressParams.value with a proper union type
if (structure.name === "ProgressParams" && prop.name === "value" && prop.type.kind === "reference" && prop.type.name === "LSPAny") {
prop.type = {
kind: "or",
items: [
{ kind: "reference", name: "WorkDoneProgressBegin" },
{ kind: "reference", name: "WorkDoneProgressReport" },
{ kind: "reference", name: "WorkDoneProgressEnd" },
],
};
}
}
}
for (const notification of model.notifications) {
if (notification.typeName === "TelemetryEventNotification") {
notification.params = {
kind: "reference",
name: "TelemetryEvent",
};
}
}
// Create placeholder structures for Data types that weren't explicitly declared
for (const dataTypeName of neededDataStructures) {
const baseName = dataTypeName.replace(/Data$/, "");
customStructures.push({
name: dataTypeName,
properties: [],
documentation: `${dataTypeName} is a placeholder for custom data preserved on a ${baseName}.`,
});
}
// Add custom enumerations, custom structures, custom requests, and synthetic structures to the model
model.enumerations.push(...customEnumerations);
model.structures.push(...customStructures, ...syntheticStructures);
model.requests.push(...customRequests);
model.notifications.push(...customNotifications);
// Build structure map for preprocessing
const structureMap = new Map<string, Structure>();
for (const structure of model.structures) {
structureMap.set(structure.name, structure);
}
function collectInheritedProperties(structure: Structure, visited = new Set<string>()): Property[] {
if (visited.has(structure.name)) {
return []; // Avoid circular dependencies
}
visited.add(structure.name);
const properties: Property[] = [];
const inheritanceTypes = [...(structure.extends || []), ...(structure.mixins || [])];
for (const type of inheritanceTypes) {
if (type.kind === "reference") {
const inheritedStructure = structureMap.get(type.name);
if (inheritedStructure) {
properties.push(
...collectInheritedProperties(inheritedStructure, new Set(visited)),
...inheritedStructure.properties,
);
}
}
}
return properties;
}
// Inline inheritance for each structure
for (const structure of model.structures) {
const inheritedProperties = collectInheritedProperties(structure);
// Merge properties with structure's own properties taking precedence
const propertyMap = new Map<string, Property>();
inheritedProperties.forEach(prop => propertyMap.set(prop.name, prop));
structure.properties.forEach(prop => propertyMap.set(prop.name, prop));
structure.properties = Array.from(propertyMap.values());
structure.extends = undefined;
structure.mixins = undefined;
// Replace experimental LSPAny with typed ExperimentalClientCapabilities in ClientCapabilities
if (structure.name === "ClientCapabilities") {
const expProp = structure.properties.find(p => p.name === "experimental");
if (expProp) {
expProp.type = { kind: "reference", name: "ExperimentalClientCapabilities" };
expProp.optional = true;
}
}
// Replace experimental LSPAny with typed ExperimentalServerCapabilities in ServerCapabilities
if (structure.name === "ServerCapabilities") {
const expProp = structure.properties.find(p => p.name === "experimental");
if (expProp) {
expProp.type = { kind: "reference", name: "ExperimentalServerCapabilities" };
expProp.optional = true;
}
}
// Remove method and registerOptions from Registration (handled by custom codegen)
if (structure.name === "Registration") {
structure.properties = structure.properties.filter(p => p.name !== "method" && p.name !== "registerOptions");
}
}
// Remove _InitializeParams structure after flattening (it was only needed for inheritance)
model.structures = model.structures.filter(s => s.name !== "_InitializeParams");
// Remove all notebook-related features from the model
function isNotebookRelatedName(name: string): boolean {
const lower = name.toLowerCase();
return lower.includes("notebook");
}
function isNotebookRelatedMethod(method: string): boolean {
return method.toLowerCase().startsWith("notebookdocument/");
}
function typeReferencesNotebook(type: Type): boolean {
if (type.kind === "reference") return isNotebookRelatedName(type.name);
if (type.kind === "array") return typeReferencesNotebook(type.element);
if (type.kind === "or" || type.kind === "and") return type.items.some(typeReferencesNotebook);
if (type.kind === "map") return typeReferencesNotebook(type.key) || typeReferencesNotebook(type.value);
return false;
}
function isEntirelyNotebookType(type: Type): boolean {
if (type.kind === "reference") return isNotebookRelatedName(type.name);
if (type.kind === "array") return isEntirelyNotebookType(type.element);
if (type.kind === "or" || type.kind === "and") return type.items.every(isEntirelyNotebookType);
return false;
}
function removeNotebookFromType(type: Type): Type {
if (type.kind === "or") {
const filtered = type.items.filter(item => !typeReferencesNotebook(item)).map(removeNotebookFromType);
if (filtered.length === 1) return filtered[0];
if (filtered.length < type.items.length) {
return { ...type, items: filtered };
}
}
if (type.kind === "and") {
const filtered = type.items.filter(item => !typeReferencesNotebook(item)).map(removeNotebookFromType);
if (filtered.length === 1) return filtered[0];
if (filtered.length < type.items.length) {
return { ...type, items: filtered };
}
}
return type;
}
// Filter out notebook structures (and notebook-only structures like ExecutionSummary)
const notebookOnlyStructures = new Set(["ExecutionSummary"]);
model.structures = model.structures.filter(s => !isNotebookRelatedName(s.name) && !notebookOnlyStructures.has(s.name));
// Remove notebook properties from remaining structures
for (const structure of model.structures) {
structure.properties = structure.properties.filter(p => {
if (isNotebookRelatedName(p.name)) return false;
// Only remove properties whose type is entirely notebook-related
if (isEntirelyNotebookType(p.type)) return false;
return true;
});
// Clean up union types in remaining properties to remove notebook members
for (const prop of structure.properties) {
prop.type = removeNotebookFromType(prop.type);
}
}
// Filter out notebook notifications and requests
model.notifications = model.notifications.filter(n => !isNotebookRelatedMethod(n.method));
model.requests = model.requests.filter(r => !isNotebookRelatedMethod(r.method));
// Filter out notebook enumerations
model.enumerations = model.enumerations.filter(e => !isNotebookRelatedName(e.name));
// Remove notebook-related values from remaining enumerations
for (const enumeration of model.enumerations) {
enumeration.values = enumeration.values.filter(v => !isNotebookRelatedName(v.name));
}
// Filter out notebook type aliases
model.typeAliases = model.typeAliases.filter(ta => !isNotebookRelatedName(ta.name));
// Clean up type aliases that reference notebook types (e.g., DocumentFilter)
for (const ta of model.typeAliases) {
if (ta.type.kind === "or") {
ta.type.items = ta.type.items.filter(item => !typeReferencesNotebook(item));
// If only one item remains, unwrap the union
if (ta.type.items.length === 1) {
ta.type = ta.type.items[0];
}
}
}
// Build the registration method map (after notebook filtering).
// Each unique registration method gets a field in the generated RegisterOptions struct.
const regMethodSeen = new Set<string>();
for (const request of [...model.requests, ...model.notifications]) {
if (!request.registrationOptions) continue;
const regMethod = (request as any).registrationMethod || request.method;
if (regMethodSeen.has(regMethod)) continue;
regMethodSeen.add(regMethod);
// Resolve the options type name
const ro = request.registrationOptions;
let optionsTypeName: string;
if (ro.kind === "reference") {
optionsTypeName = ro.name;
}
else {
throw new Error(`Unexpected registrationOptions kind '${ro.kind}' for ${request.method}; expected all to be resolved to references`);
}
registrationMethods.push({
registrationMethod: regMethod,
fieldName: methodNameIdentifier(regMethod),
optionsTypeName,
});
}
// Identify registration-only methods (not also a request/notification method).
// These need their own Method constant emitted.
const allRequestMethods = new Set([...model.requests, ...model.notifications].map(r => r.method));
for (const reg of registrationMethods) {
(reg as any).isRegistrationOnly = !allRequestMethods.has(reg.registrationMethod);
}
// Merge LSPErrorCodes into ErrorCodes and remove LSPErrorCodes
const errorCodesEnum = model.enumerations.find(e => e.name === "ErrorCodes");
const lspErrorCodesEnum = model.enumerations.find(e => e.name === "LSPErrorCodes");
if (errorCodesEnum && lspErrorCodesEnum) {
// Merge LSPErrorCodes values into ErrorCodes
errorCodesEnum.values.push(...lspErrorCodesEnum.values);
// Remove LSPErrorCodes from the model
model.enumerations = model.enumerations.filter(e => e.name !== "LSPErrorCodes");
}
// Singularize plural enum names (e.g., "ErrorCodes" -> "ErrorCode")
for (const enumeration of model.enumerations) {
if (enumeration.name.endsWith("Codes")) {
enumeration.name = enumeration.name.slice(0, -1); // "Codes" -> "Code"
}
else if (enumeration.name.endsWith("Modifiers")) {
enumeration.name = enumeration.name.slice(0, -1); // "Modifiers" -> "Modifier"
}
else if (enumeration.name.endsWith("Types")) {
enumeration.name = enumeration.name.slice(0, -1); // "Types" -> "Type"
}
}
}
patchAndPreprocessModel();
// Validate that telemetry events in the TelemetryEvent union have properly shaped
// measurements and properties fields. measurements struct fields must only contain
// numeric types (decimal/integer/uinteger).
function validateTelemetryEvents() {
const telemetryAlias = customTypeAliases.find(a => a.name === "TelemetryEvent");
if (!telemetryAlias || telemetryAlias.type.kind !== "or") return;
const structureMap = new Map(model.structures.map(s => [s.name, s]));
for (const item of telemetryAlias.type.items) {
if (item.kind !== "reference") continue;
const eventStruct = structureMap.get(item.name);
if (!eventStruct) continue;
for (const prop of eventStruct.properties) {
if (prop.name === "measurements" && prop.type.kind === "reference") {
const measurementsStruct = structureMap.get(prop.type.name);
if (!measurementsStruct) continue;
for (const mp of measurementsStruct.properties) {
if (mp.type.kind !== "base" || !["decimal", "integer", "uinteger"].includes(mp.type.name)) {
throw new Error(
`Telemetry measurements struct ${prop.type.name}.${mp.name} must be a numeric type ` +
`(decimal/integer/uinteger), got ${mp.type.kind === "base" ? mp.type.name : mp.type.kind}`,
);
}
}
}
}
}
}
validateTelemetryEvents();
interface GoType {
name: string;
needsPointer: boolean;
}
interface TypeInfo {
types: Map<string, GoType>;
literalTypes: Map<string, string>;
unionTypes: Map<string, { name: string; type: Type; containedNull: boolean; }[]>;
typeAliasMap: Map<string, Type>;
}
const typeInfo: TypeInfo = {
types: new Map(),
literalTypes: new Map(),
unionTypes: new Map(),
typeAliasMap: new Map(),
};
function titleCase(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function goFieldName(prop: Property): string {
if (prop.name.startsWith("_vs_")) {
return "VS" + titleCase(prop.name.slice(4));
}
return titleCase(prop.name);
}
function resolveType(type: Type): GoType {
switch (type.kind) {
case "base":
switch (type.name) {
case "integer":
return { name: "int32", needsPointer: false };
case "uinteger":
return { name: "uint32", needsPointer: false };
case "string":
return { name: "string", needsPointer: false };
case "boolean":
return { name: "bool", needsPointer: false };
case "URI":
return { name: "URI", needsPointer: false };
case "DocumentUri":
return { name: "DocumentUri", needsPointer: false };
case "decimal":
return { name: "float64", needsPointer: false };
case "null":
return { name: "any", needsPointer: false };
default:
throw new Error(`Unsupported base type: ${type.name}`);
}
case "reference":
const typeAliasOverride = typeAliasOverrides.get(type.name);
if (typeAliasOverride) {
return typeAliasOverride;
}
const nonResolved = nonResolvedAliases.has(type.name);
if (nonResolved) {
return { name: type.name, needsPointer: false };
}
// Check if this is a type alias that resolves to a union type
const aliasedType = typeInfo.typeAliasMap.get(type.name);
if (aliasedType) {
return resolveType(aliasedType);
}
let refType = typeInfo.types.get(type.name);
if (!refType) {
refType = { name: type.name, needsPointer: true };
typeInfo.types.set(type.name, refType);
}
return refType;
case "array": {
const elementType = resolveType(type.element);
const arrayTypeName = elementType.needsPointer
? `[]*${elementType.name}`
: `[]${elementType.name}`;
return {
name: arrayTypeName,
needsPointer: false,
};
}
case "map": {
const keyType = resolveType(type.key);
const valueType = resolveType(type.value);
const valueTypeName = valueType.needsPointer ? `*${valueType.name}` : valueType.name;
return {
name: `map[${keyType.name}]${valueTypeName}`,
needsPointer: false,
};
}
case "tuple": {
if (
type.items.length === 2 &&
type.items[0].kind === "base" && type.items[0].name === "uinteger" &&
type.items[1].kind === "base" && type.items[1].name === "uinteger"
) {
return { name: "[2]uint32", needsPointer: false };
}
throw new Error("Unsupported tuple type: " + JSON.stringify(type));
}
case "stringLiteral": {
const typeName = `StringLiteral${type.value.split(".").map(titleCase).join("")}`;
typeInfo.literalTypes.set(String(type.value), typeName);
return { name: typeName, needsPointer: false };
}
case "integerLiteral": {
const typeName = `IntegerLiteral${type.value}`;
typeInfo.literalTypes.set(String(type.value), typeName);
return { name: typeName, needsPointer: false };
}
case "booleanLiteral": {
const typeName = `BooleanLiteral${type.value ? "True" : "False"}`;
typeInfo.literalTypes.set(String(type.value), typeName);
return { name: typeName, needsPointer: false };
}
case "literal":
if (type.value.properties.length === 0) {
return { name: "struct{}", needsPointer: false };
}
throw new Error("Unexpected non-empty literal object: " + JSON.stringify(type.value));
case "or": {
return handleOrType(type);
}
default:
throw new Error(`Unsupported type kind: ${type.kind}`);
}
}
function flattenOrTypes(types: Type[]): Type[] {
const flattened = new Set<Type>();
for (const rawType of types) {
let type = rawType;
// Dereference reference types that point to OR types
if (rawType.kind === "reference") {
const aliasedType = typeInfo.typeAliasMap.get(rawType.name);
if (aliasedType && aliasedType.kind === "or") {
type = aliasedType;
}
}
if (type.kind === "or") {
// Recursively flatten OR types
for (const subType of flattenOrTypes(type.items)) {
flattened.add(subType);
}
}
else {
flattened.add(rawType);
}
}
return Array.from(flattened);
}
function pluralize(name: string): string {
// Handle common irregular plurals and special cases
if (
name.endsWith("s") || name.endsWith("x") || name.endsWith("z") ||
name.endsWith("ch") || name.endsWith("sh")
) {
return name + "es";
}
if (name.endsWith("y") && name.length > 1 && !"aeiou".includes(name[name.length - 2])) {
return name.slice(0, -1) + "ies";
}
return name + "s";
}
function handleOrType(orType: OrType): GoType {
// First, flatten any nested OR types
const types = flattenOrTypes(orType.items);
// Check for nullable types (OR with null)
const nullIndex = types.findIndex(item => item.kind === "base" && item.name === "null");
let containedNull = nullIndex !== -1;
// If it's nullable, remove the null type from the list
let nonNullTypes = types;
if (containedNull) {
nonNullTypes = types.filter((_, i) => i !== nullIndex);
}
// If no types remain after filtering null, this shouldn't happen
if (nonNullTypes.length === 0) {
throw new Error("Union type with only null is not supported: " + JSON.stringify(types));
}
// Even if only one type remains after filtering null, we still need to create a union type
// to preserve the nullable behavior (all fields nil = null)
let memberNames = nonNullTypes.map(type => {
if (type.kind === "reference") {
return type.name;
}
else if (type.kind === "base") {
return titleCase(type.name);
}
else if (
type.kind === "array" &&
(type.element.kind === "reference" || type.element.kind === "base")
) {
return pluralize(titleCase(type.element.name));
}
else if (type.kind === "array") {
// Handle more complex array types
const elementType = resolveType(type.element);
return `${elementType.name}Array`;
}
else if (type.kind === "literal" && type.value.properties.length === 0) {
return "EmptyObject";
}
else if (type.kind === "tuple") {
return "Tuple";
}
else {
throw new Error(`Unsupported type kind in union: ${type.kind}`);
}
});
// Find longest common prefix of member names chunked by PascalCase
function findLongestCommonPrefix(names: string[]): string {
if (names.length === 0) return "";
if (names.length === 1) return "";
// Split each name into PascalCase chunks
function splitPascalCase(name: string): string[] {
const chunks: string[] = [];
let currentChunk = "";
for (let i = 0; i < name.length; i++) {
const char = name[i];
if (char >= "A" && char <= "Z" && currentChunk.length > 0) {
// Start of a new chunk
chunks.push(currentChunk);
currentChunk = char;
}
else {
currentChunk += char;
}
}
if (currentChunk.length > 0) {
chunks.push(currentChunk);
}
return chunks;
}
const allChunks = names.map(splitPascalCase);
const minChunkLength = Math.min(...allChunks.map(chunks => chunks.length));
// Find the longest common prefix of chunks
let commonChunks: string[] = [];
for (let i = 0; i < minChunkLength; i++) {
const chunk = allChunks[0][i];
if (allChunks.every(chunks => chunks[i] === chunk)) {
commonChunks.push(chunk);
}
else {
break;
}
}
return commonChunks.join("");
}
const commonPrefix = findLongestCommonPrefix(memberNames);
let unionTypeName = "";
if (commonPrefix.length > 0) {
const trimmedMemberNames = memberNames.map(name => name.slice(commonPrefix.length));
if (trimmedMemberNames.every(name => name)) {
unionTypeName = commonPrefix + trimmedMemberNames.join("Or");
memberNames = trimmedMemberNames;
}
else {
unionTypeName = memberNames.join("Or");
}
}
else {
unionTypeName = memberNames.join("Or");
}
if (containedNull) {
unionTypeName += "OrNull";
}
else {
containedNull = false;
}
const union = memberNames.map((name, i) => ({ name, type: nonNullTypes[i], containedNull }));
typeInfo.unionTypes.set(unionTypeName, union);
return {
name: unionTypeName,
needsPointer: false,
};
}
const typeAliasOverrides = new Map([
["LSPAny", { name: "any", needsPointer: false }],
["LSPArray", { name: "[]any", needsPointer: false }],
["LSPObject", { name: "map[string]any", needsPointer: false }],
["uint64", { name: "uint64", needsPointer: false }],
]);
// These type aliases are intentionally not resolved to their underlying types.
// It means that we can end up with non-normalized union types in some places.
// Also, unlike other type aliases, these will get a type alias in the generated source code.
// We may want to eventually do this for all type aliases though.
const nonResolvedAliases = new Set(customTypeAliases.map(ta => ta.name));
/**
* First pass: Resolve all type information
*/
function collectTypeDefinitions() {
// Process all enumerations first to make them available for struct fields
for (const enumeration of model.enumerations) {
typeInfo.types.set(enumeration.name, {
name: enumeration.name,
needsPointer: false,
});
}
const valueTypes = new Set([
"Position",
"Range",
"Location",
"Color",
"TextDocumentIdentifier",
"PreviousResultId",
"VersionedTextDocumentIdentifier",
"OptionalVersionedTextDocumentIdentifier",
"ExportInfoMapKey",
]);
// Process all structures
for (const structure of model.structures) {
typeInfo.types.set(structure.name, {
name: structure.name,
needsPointer: !valueTypes.has(structure.name),
});
}
// Process all type aliases
for (const typeAlias of model.typeAliases) {
if (typeAliasOverrides.has(typeAlias.name)) {
continue;
}
// Store the alias mapping so we can resolve it later
typeInfo.typeAliasMap.set(typeAlias.name, typeAlias.type);
}
}
function formatDocumentation(s: string | undefined): string {
if (!s) return "";
let lines: string[] = [];
for (let line of s.split("\n")) {
line = line.trimEnd();
line = line.replace(/(\w ) +/g, "$1");
// Some upstream docs include dangling block comment delimiters; remove them
// so they don't leak into generated `//` comments.
line = line.replace(/\s*\/\*+\s*/g, " ");
line = line.replace(/\s*\*+\/\s*/g, " ");
line = line.replace(/\s{2,}/g, " ").trimEnd();
line = line.replace(/\{@link(?:code)?.*?([^} ]+)\}/g, "$1");
line = line.replace(/^@(since|proposed|deprecated)(.*)/, (_, tag, rest) => {
lines.push("");
return `${titleCase(tag)}${rest ? ":" + rest : "."}`;
});
lines.push(line);
}
// filter out contiguous empty lines
while (true) {
const toRemove = lines.findIndex((line, index) => {
if (line) return false;
if (index === 0) return true;
if (index === lines.length - 1) return true;
return !(lines[index - 1] && lines[index + 1]);
});
if (toRemove === -1) break;
lines.splice(toRemove, 1);
}
return lines.length > 0 ? "// " + lines.join("\n// ") + "\n" : "";
}
function methodNameIdentifier(name: string) {
return name.split("/").map(v => {
if (v === "$") return "";
// Mirror goFieldName: "_vs_foo" -> "VSFoo".
if (v.startsWith("_vs_")) return "VS" + titleCase(v.slice(4));
return titleCase(v);
}).join("");
}
/**
* Returns the JSON token kind ("string", "number", "object", "array", "boolean")
* for a given meta model Type, or undefined if the kind cannot be statically determined.
*/
function jsonKindForType(type: Type): string | undefined {
switch (type.kind) {
case "base":
switch (type.name) {
case "integer":
case "uinteger":
case "decimal":
return "number";
case "string":
case "URI":
case "DocumentUri":
return "string";
case "boolean":
return "boolean";
default:
return undefined;
}
case "reference": {
if (typeAliasOverrides.has(type.name)) {
return undefined;
}
if (model.structures.some(s => s.name === type.name)) {
return "object";
}
const enumeration = model.enumerations.find(e => e.name === type.name);
if (enumeration) {
switch (enumeration.type.name) {
case "string":
return "string";
case "integer":
case "uinteger":
return "number";
default:
return undefined;
}
}
const aliasType = typeInfo.typeAliasMap.get(type.name);
if (aliasType) return jsonKindForType(aliasType);
return undefined;
}
case "array":
return "array";
case "map":
return "object";
case "tuple":
return "array";
case "stringLiteral":
return "string";
case "integerLiteral":
return "number";
case "booleanLiteral":
return "boolean";
case "literal":
return "object";
case "or": {
const kinds = new Set(type.items.map(item => jsonKindForType(item)).filter(Boolean));
return kinds.size === 1 ? kinds.values().next().value : undefined;
}
default:
return undefined;
}
}
function goKindCasesForJsonKind(kind: string): string {
switch (kind) {
case "string":
return `case '"':`;
case "number":
return `case '0':`;
case "object":
return `case '{':`;
case "array":
return `case '[':`;
case "boolean":
return `case 't', 'f':`;
default:
return "";
}
}
/**
* Checks if a meta model Type can represent a JSON null value.
* Used to determine whether to reject explicit JSON `null` for any field
* that can otherwise decode `null` without a type error.
*/
function typeCanBeNull(type: Type): boolean {
switch (type.kind) {
case "base":
return type.name === "null";
case "reference": {
const override = typeAliasOverrides.get(type.name);
if (override) {
return override.name === "any";
}
// A bare "any" reference resolves to Go's `any` (interface), which can hold null.
if (type.name === "any") {
return true;
}
if (nonResolvedAliases.has(type.name)) {
const customAlias = customTypeAliases.find(t => t.name === type.name);
if (customAlias) return typeCanBeNull(customAlias.type);
return false;
}
const aliased = typeInfo.typeAliasMap.get(type.name);
if (aliased) return typeCanBeNull(aliased);
return false;
}
case "or":
return type.items.some(item => typeCanBeNull(item));
default:
return false;
}
}
/**
* For a group of union entries that share the same JSON kind (e.g., all objects),
* find a discriminator field — a JSON property whose string literal type differs
* across variants — enabling efficient O(1) dispatch instead of try-each.
*/
function findDiscriminatorField(entries: { fieldName: string; typeName: string; originalType: Type; }[]): {
fieldName: string;
mapping: Map<string, { fieldName: string; typeName: string; originalType: Type; }>;
unmapped: { fieldName: string; typeName: string; originalType: Type; }[];
} | null {
// For each entry, find string literal fields and build candidate discriminators.
// A valid discriminator is a field name that appears on multiple variants with
// different string literal values.
const fieldCandidates = new Map<string, Map<string, typeof entries[0] | undefined>>();
for (const entry of entries) {
if (entry.originalType.kind !== "reference") continue;
const structure = model.structures.find(s => s.name === (entry.originalType as ReferenceType).name);
if (!structure) continue;
for (const prop of structure.properties) {
if (prop.type.kind === "stringLiteral") {
if (!fieldCandidates.has(prop.name)) {
fieldCandidates.set(prop.name, new Map());
}
const mapping = fieldCandidates.get(prop.name)!;
if (!mapping.has(prop.type.value)) {
mapping.set(prop.type.value, entry);
}
else {
// Two entries share the same literal value; invalidate this candidate.
mapping.set(prop.type.value, undefined);
}
}
}
}
// Pick the discriminator field that covers the most entries.
let bestField: string | null = null;
let bestMapping: Map<string, typeof entries[0]> | null = null;
for (const [fieldName, mapping] of fieldCandidates) {
const validMapping = new Map<string, typeof entries[0]>();
for (const [value, entry] of mapping) {
if (entry !== undefined) validMapping.set(value, entry);
}
if (validMapping.size >= 2 && (!bestMapping || validMapping.size > bestMapping.size)) {
bestField = fieldName;
bestMapping = validMapping;
}
}
if (!bestField || !bestMapping) return null;
const mappedEntries = new Set(bestMapping.values());
const unmapped = entries.filter(e => !mappedEntries.has(e));
return { fieldName: bestField, mapping: bestMapping, unmapped };
}
/**
* For a group of union entries that share the same JSON kind, find fields whose
* presence/absence in the JSON uniquely identifies a variant. A "presence discriminator"
* for variant X is a required field on X that does not appear in any other variant's
* property set at all.
*/
function findPresenceDiscriminator(entries: { fieldName: string; typeName: string; originalType: Type; }[]): {
checks: { jsonFieldName: string; entry: { fieldName: string; typeName: string; originalType: Type; }; }[];
unmapped: { fieldName: string; typeName: string; originalType: Type; }[];
} | null {
// Collect all property names for each variant
const variantProps = new Map<typeof entries[0], { required: Property[]; allNames: Set<string>; }>();
for (const entry of entries) {
if (entry.originalType.kind !== "reference") continue;
const structure = model.structures.find(s => s.name === (entry.originalType as ReferenceType).name);
if (!structure) continue;
const required = structure.properties.filter(p => !p.optional && !p.omitzeroValue);
const allNames = new Set(structure.properties.map(p => p.name));
variantProps.set(entry, { required, allNames });
}
const checks: { jsonFieldName: string; entry: typeof entries[0]; }[] = [];
const handled = new Set<typeof entries[0]>();
for (const entry of entries) {
const info = variantProps.get(entry);
if (!info) continue;
const otherEntries = entries.filter(e => e !== entry);
for (const field of info.required) {
const absentFromAllOthers = otherEntries.every(other => {
const otherInfo = variantProps.get(other);
if (!otherInfo) return false;
return !otherInfo.allNames.has(field.name);
});
if (absentFromAllOthers) {
checks.push({ jsonFieldName: field.name, entry });
handled.add(entry);
break;
}
}
}
if (checks.length === 0) return null;
const unmapped = entries.filter(e => !handled.has(e));
return { checks, unmapped };
}
/**
* Generate the Go code
*/
function generateCode() {
const parts: string[] = [];
function write(s: string) {
parts.push(s);
}
function writeLine(s = "") {
parts.push(s + "\n");
}
/**
* Generate Go code for discriminator-based union dispatch.
* Assumes a variable named `data` of type `json.Value` is in scope.
* Returns true if all switch branches return (exhaustive).
*/
function generateDiscriminatorDispatch(
disc: NonNullable<ReturnType<typeof findDiscriminatorField>>,
indent: string,
): boolean {
writeLine(`${indent}switch string(jsonObjectRawField(data, ${JSON.stringify(disc.fieldName)})) {`);
for (const [value, entry] of disc.mapping) {
writeLine(`${indent}case \`"${value}"\`:`);
writeLine(`${indent}\to.${entry.fieldName} = new(${entry.typeName})`);
writeLine(`${indent}\treturn json.Unmarshal(data, o.${entry.fieldName})`);
}
let exhaustive = false;
if (disc.unmapped.length > 0) {
writeLine(`${indent}default:`);
exhaustive = generateUnmappedFallback(disc.unmapped, indent + "\t");
}
writeLine(`${indent}}`);
return exhaustive;
}
/**
* Generate try-each fallback code for unmapped entries, chaining into
* presence dispatch if possible before falling back to raw try-each.
* Assumes a variable named `data` of type `json.Value` is in scope.
* Returns true if all generated paths return (exhaustive).
*/
function generateUnmappedFallback(
unmapped: { fieldName: string; typeName: string; originalType: Type; }[],
indent: string,
): boolean {
if (unmapped.length <= 1) {
// Exactly 1 entry: it's the only remaining variant after dispatch,
// so use a hard error return instead of speculative err == nil.
for (const entry of unmapped) {
writeLine(`${indent}o.${entry.fieldName} = new(${entry.typeName})`);
writeLine(`${indent}return json.Unmarshal(data, o.${entry.fieldName})`);
}
return unmapped.length === 1;
}
// Try chaining presence dispatch on the remaining subset
const pres = findPresenceDiscriminator(unmapped);
if (pres) {
return generatePresenceDispatch(pres, indent);
}
else {
for (const entry of unmapped) {
writeLine(`${indent}var v${entry.fieldName} ${entry.typeName}`);
writeLine(`${indent}if err := json.Unmarshal(data, &v${entry.fieldName}); err == nil {`);
writeLine(`${indent}\to.${entry.fieldName} = &v${entry.fieldName}`);
writeLine(`${indent}\treturn nil`);
writeLine(`${indent}}`);
}
return false;
}
}
/**
* Iteratively collect all presence discriminator checks across multiple
* passes, so they can be emitted as a single flat switch with one scan.
*/
function collectAllPresenceChecks(
pres: NonNullable<ReturnType<typeof findPresenceDiscriminator>>,
): {
allChecks: { jsonFieldName: string; entry: { fieldName: string; typeName: string; originalType: Type; }; }[];
finalUnmapped: { fieldName: string; typeName: string; originalType: Type; }[];
} {
const allChecks = [...pres.checks];
let remaining = pres.unmapped;
while (remaining.length > 1) {
const next = findPresenceDiscriminator(remaining);
if (!next) break;
allChecks.push(...next.checks);
remaining = next.unmapped;
}
return { allChecks, finalUnmapped: remaining };
}
/**
* Generate Go code for presence-based union dispatch.
* Assumes a variable named `data` of type `json.Value` is in scope.
* Collects all presence checks iteratively, then emits a single flat
* switch jsonObjectHasKey(data, key1, key2, ...) so data is scanned once.
* Returns true if all switch branches return (exhaustive).
*/
function generatePresenceDispatch(
pres: NonNullable<ReturnType<typeof findPresenceDiscriminator>>,
indent: string,
): boolean {
const { allChecks, finalUnmapped } = collectAllPresenceChecks(pres);
const args = allChecks.map(c => JSON.stringify(c.jsonFieldName)).join(", ");
writeLine(`${indent}switch jsonObjectHasKey(data, ${args}) {`);
for (let i = 0; i < allChecks.length; i++) {
writeLine(`${indent}case ${i}: // ${allChecks[i].jsonFieldName}`);
writeLine(`${indent}\to.${allChecks[i].entry.fieldName} = new(${allChecks[i].entry.typeName})`);
writeLine(`${indent}\treturn json.Unmarshal(data, o.${allChecks[i].entry.fieldName})`);
}
if (finalUnmapped.length > 0) {
writeLine(`${indent}default:`);
if (finalUnmapped.length === 1) {
// Only one variant left after dispatch — use hard error return.
const entry = finalUnmapped[0];
writeLine(`${indent}\to.${entry.fieldName} = new(${entry.typeName})`);
writeLine(`${indent}\treturn json.Unmarshal(data, o.${entry.fieldName})`);
}
else {
for (const entry of finalUnmapped) {
writeLine(`${indent}\tvar v${entry.fieldName} ${entry.typeName}`);
writeLine(`${indent}\tif err := json.Unmarshal(data, &v${entry.fieldName}); err == nil {`);
writeLine(`${indent}\t\to.${entry.fieldName} = &v${entry.fieldName}`);
writeLine(`${indent}\t\treturn nil`);
writeLine(`${indent}\t}`);
}
}
}
writeLine(`${indent}}`);
// Exhaustive if the default case has a single hard-returning entry
return finalUnmapped.length === 1;
}
function generateResolvedStruct(structure: Structure, indent: string = "\t"): string[] {
const lines: string[] = [];
for (const prop of structure.properties) {
// Add property documentation if it exists
if (prop.documentation) {
const propDoc = formatDocumentation(prop.documentation);
if (propDoc) {
// Add the documentation with proper indentation
for (const line of propDoc.split("\n").filter(l => l)) {
lines.push(`${indent}${line}`);
}
}
}
const type = resolveType(prop.type);
// For reference types that are structures, use a named resolved type
if (prop.type.kind === "reference") {
const refStructure = model.structures.find(s => s.name === type.name);
if (refStructure) {
// Use a named type for the resolved version
lines.push(`${indent}${goFieldName(prop)} Resolved${type.name} \`json:"${prop.name},omitzero"\``);
continue;
}
}
// For other types (primitives, enums, arrays, etc.), use the type directly (no pointer)
const goType = type.name;
lines.push(`${indent}${goFieldName(prop)} ${goType} \`json:"${prop.name},omitzero"\``);
}
return lines;
}
function generateResolveConversion(structure: Structure, varName: string, indent: string): string[] {
const lines: string[] = [];
for (const prop of structure.properties) {
const type = resolveType(prop.type);
const fieldName = goFieldName(prop);
const accessPath = `${varName}.${fieldName}`;
// For reference types that are structures, call the resolve method
if (prop.type.kind === "reference") {
const refStructure = model.structures.find(s => s.name === type.name);
if (refStructure) {
lines.push(`${indent}${fieldName}: ${accessPath}.resolve(),`);
continue;
}
}
// For other types, dereference if pointer
if (prop.optional || type.needsPointer) {
lines.push(`${indent}${fieldName}: derefOr(${accessPath}),`);
}
else {
lines.push(`${indent}${fieldName}: ${accessPath},`);
}
}
return lines;
}
function collectStructureDependencies(structure: Structure, visited = new Set<string>()): Structure[] {
if (visited.has(structure.name)) {
return [];
}
visited.add(structure.name);
const deps: Structure[] = [];
for (const prop of structure.properties) {
if (prop.type.kind === "reference") {
const refStructure = model.structures.find(s => s.name === (prop.type as ReferenceType).name);
if (refStructure) {
deps.push(...collectStructureDependencies(refStructure, new Set(visited)));
deps.push(refStructure);
}
}
}
return deps;
}
function generateResolvedTypeAndHelper(structure: Structure, isMain: boolean = false): string[] {
const lines: string[] = [];
const typeName = `Resolved${structure.name}`;
// Main method is exported (Resolve), helpers are unexported (resolve)
const methodName = isMain ? `Resolve` : `resolve`;
// Generate the resolved type with documentation
if (!isMain) {
// For non-main types, add standard documentation header
if (structure.documentation) {
const typeDoc = formatDocumentation(structure.documentation);
if (typeDoc) {
// Prepend comment explaining this is the resolved version
lines.push(`// ${typeName} is a resolved version of ${structure.name} with all optional fields`);
lines.push(`// converted to non-pointer values for easier access.`);
lines.push(`//`);
// Add the original structure documentation
for (const line of typeDoc.split("\n").filter(l => l)) {
lines.push(line);
}
}
}
else {
// If no documentation, just add a basic comment
lines.push(`// ${typeName} is a resolved version of ${structure.name} with all optional fields`);
lines.push(`// converted to non-pointer values for easier access.`);
}
}
// For main type, documentation is added separately before calling this function
lines.push(`type ${typeName} struct {`);
lines.push(...generateResolvedStruct(structure, "\t"));
lines.push(`}`);
lines.push(``);
// Generate the conversion method on the pointer receiver
lines.push(`func (v *${structure.name}) ${methodName}() ${typeName} {`);
lines.push(`\tif v == nil {`);
lines.push(`\t\treturn ${typeName}{}`);
lines.push(`\t}`);
lines.push(`\treturn ${typeName}{`);
lines.push(...generateResolveConversion(structure, "v", "\t\t"));
lines.push(`\t}`);
lines.push(`}`);
lines.push(``);
return lines;
}
// File header
writeLine("// Code generated by generate.mts; DO NOT EDIT.");
writeLine("");
writeLine("package lsproto");
writeLine("");
writeLine(`import (`);
writeLine(`\t"cmp"`);
writeLine(`\t"fmt"`);
writeLine(`\t"strings"`);
writeLine("");
writeLine(`\t"github.com/microsoft/typescript-go/internal/json"`);
writeLine(`)`);
writeLine("");
writeLine("// Meta model version " + model.metaData.version);
writeLine("");
// Generate structures
writeLine("// Structures\n");
for (const structure of model.structures) {
function generateStructFields(name: string, includeDocumentation: boolean) {
if (includeDocumentation) {
write(formatDocumentation(structure.documentation));
}
writeLine(`type ${name} struct {`);
// Properties are now inlined, no need to embed extends/mixins
for (const prop of structure.properties) {
if (includeDocumentation) {
write(formatDocumentation(prop.documentation));
}
const type = resolveType(prop.type);
// For properties marked with omitzeroValue, use value type with omitzero instead of pointer
const useOmitzero = prop.optional || prop.omitzeroValue;
const goType = (prop.optional || type.needsPointer) && !prop.omitzeroValue ? `*${type.name}` : type.name;
// Strictness markers for the shared unmarshalStruct interpreter:
// required = must be present. A nilable field (pointer/slice/map,
// not omitzero) rejects an explicit JSON null by default; mark the
// rare spec-nullable ones with `nullable` so the interpreter allows it.
const required = !prop.optional && !prop.omitzeroValue;
const nilable = prop.optional || type.needsPointer || type.name.startsWith("[]") || type.name.startsWith("map[") || !!prop.omitzeroValue;
const nullable = nilable && (typeCanBeNull(prop.type) || !!prop.omitzeroValue);
const lspMarkers = [required ? "required" : "", nullable ? "nullable" : ""].filter(Boolean).join(",");
const lspTag = lspMarkers ? ` lsp:"${lspMarkers}"` : "";
writeLine(`\t${goFieldName(prop)} ${goType} \`json:"${prop.name}${useOmitzero ? ",omitzero" : ""}"${lspTag}\``);
if (includeDocumentation) {
writeLine("");
}
}
// Special: add RegisterOptions field to Registration
if (structure.name === "Registration") {
writeLine("");
writeLine(`\t// Options necessary for the registration. Determines the method.`);
writeLine(`\tRegisterOptions *RegisterOptions \`json:"-"\``);
}
writeLine("}");
writeLine("");
}
generateStructFields(structure.name, true);
writeLine("");
if (hasTextDocumentURI(structure)) {
// Generate TextDocumentURI method
const textDocProp = structure.properties?.find(p => (p.name === "textDocument" || p.name === "_vs_textDocument") && p.type.kind === "reference" && p.type.name === "TextDocumentIdentifier");
const textDocFieldName = textDocProp ? goFieldName(textDocProp) : "TextDocument";
writeLine(`func (s *${structure.name}) TextDocumentURI() DocumentUri {`);
writeLine(`\treturn s.${textDocFieldName}.Uri`);
writeLine(`}`);
writeLine("");
if (hasTextDocumentPosition(structure)) {
// Generate TextDocumentPosition method
const posProp = structure.properties?.find(p => (p.name === "position" || p.name === "_vs_position") && p.type.kind === "reference" && p.type.name === "Position");
const posFieldName = posProp ? goFieldName(posProp) : "Position";
writeLine(`func (s *${structure.name}) TextDocumentPosition() Position {`);
writeLine(`\treturn s.${posFieldName}`);
writeLine(`}`);
writeLine("");
}
}
const locationUriProperty = getLocationUriProperty(structure);
if (locationUriProperty) {
// Generate Location method
writeLine(`func (s ${structure.name}) GetLocation() Location {`);
if (locationUriProperty === "Uri" && structure.name === "Location") {
writeLine(`\treturn s`);
}
else {
writeLine(`\treturn Location{`);
writeLine(`\t\tUri: s.${locationUriProperty},`);
writeLine(`\t\tRange: s.${locationUriProperty.replace(/Uri$/, "Range")},`);
writeLine(`\t}`);
}
writeLine(`}`);
writeLine("");
}
// Generate UnmarshalJSONFrom method for structure validation
// Skip Registration (has custom marshal/unmarshal generated separately)
// Skip properties marked with omitzeroValue since they're optional by nature
const requiredProps = structure.properties?.filter(p => {
if (p.optional) return false;
if (p.omitzeroValue) return false;
return true;
}) || [];
// Check if any fields need null rejection
const hasNullRejectableFields = structure.properties?.some(p => {
if (p.omitzeroValue) return false;
if (typeCanBeNull(p.type)) return false;
const resolved = resolveType(p.type);
return p.optional || resolved.needsPointer || resolved.name.startsWith("[]") || resolved.name.startsWith("map[");
}) || false;
if ((requiredProps.length > 0 || hasNullRejectableFields) && structure.name !== "Registration") {
writeLine(`\tvar _ json.UnmarshalerFrom = (*${structure.name})(nil)`);
writeLine("");
writeLine(`func (s *${structure.name}) UnmarshalJSONFrom(dec *json.Decoder) error {`);
writeLine(`\treturn unmarshalStruct(s, dec)`);
writeLine(`}`);
writeLine("");
}
// Generate RegisterOptions struct and custom Registration marshal/unmarshal
// right after the Registration struct definition.
if (structure.name === "Registration") {
// RegisterOptions struct
writeLine(`// RegisterOptions is an externally-tagged union representing the options for a capability registration.`);
writeLine(`// Exactly one field should be set. The set field determines the method for the registration.`);
writeLine(`type RegisterOptions struct {`);
for (const reg of registrationMethods) {
writeLine(`\t${reg.fieldName} *${reg.optionsTypeName}`);
}
writeLine(`}`);
writeLine("");
// MarshalJSONTo for Registration
writeLine(`var _ json.MarshalerTo = (*Registration)(nil)`);
writeLine("");
writeLine(`func (s *Registration) MarshalJSONTo(enc *json.Encoder) error {`);
// Assert RegisterOptions is set and exactly one field is set
writeLine(`\tif s.RegisterOptions == nil {`);
writeLine(`\t\tpanic("RegisterOptions must be set")`);
writeLine(`\t}`);
writeLine(`\tassertOnlyOne("exactly one element of RegisterOptions should be set", countNonNil(s.RegisterOptions))`);
writeLine("");
writeLine(`\tif err := enc.WriteToken(json.BeginObject); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif err := enc.WriteValue(json.Value(\`"id"\`)); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif err := json.MarshalEncode(enc, s.Id); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif err := enc.WriteValue(json.Value(\`"method"\`)); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tvar method json.Value`);
writeLine(`\tvar opts any`);
writeLine(`\tswitch {`);
for (const reg of registrationMethods) {
writeLine(`\tcase s.RegisterOptions.${reg.fieldName} != nil:`);
writeLine(`\t\tmethod = json.Value(\`"${reg.registrationMethod}"\`)`);
writeLine(`\t\topts = s.RegisterOptions.${reg.fieldName}`);
}
writeLine(`\t}`);
writeLine(`\tif err := enc.WriteValue(method); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif err := enc.WriteValue(json.Value(\`"registerOptions"\`)); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif err := json.MarshalEncode(enc, opts); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\treturn enc.WriteToken(json.EndObject)`);
writeLine(`}`);
writeLine("");
// UnmarshalJSONFrom for Registration
writeLine(`var _ json.UnmarshalerFrom = (*Registration)(nil)`);
writeLine("");
writeLine(`func (s *Registration) UnmarshalJSONFrom(dec *json.Decoder) error {`);
writeLine(`\t*s = Registration{}`);
writeLine(`\tconst (`);
writeLine(`\t\tmissingId uint = 1 << iota`);
writeLine(`\t\tmissingMethod`);
writeLine(`\t\t_missingLast`);
writeLine(`\t)`);
writeLine(`\tmissing := _missingLast - 1`);
writeLine("");
writeLine(`\tif k := dec.PeekKind(); k != '{' {`);
writeLine(`\t\treturn errNotObject(k)`);
writeLine(`\t}`);
writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine("");
writeLine(`\tvar method string`);
writeLine(`\tvar rawRegisterOptions json.Value`);
writeLine("");
writeLine(`\tfor dec.PeekKind() != '}' {`);
writeLine(`\t\tname, err := dec.ReadValue()`);
writeLine(`\t\tif err != nil {`);
writeLine(`\t\t\treturn err`);
writeLine(`\t\t}`);
writeLine(`\t\tswitch string(name) {`);
writeLine(`\t\tcase \`"id"\`:`);
writeLine(`\t\t\tmissing &^= missingId`);
writeLine(`\t\t\tif err := json.UnmarshalDecode(dec, &s.Id); err != nil {`);
writeLine(`\t\t\t\treturn err`);
writeLine(`\t\t\t}`);
writeLine(`\t\tcase \`"method"\`:`);
writeLine(`\t\t\tmissing &^= missingMethod`);
writeLine(`\t\t\tif err := json.UnmarshalDecode(dec, &method); err != nil {`);
writeLine(`\t\t\t\treturn err`);
writeLine(`\t\t\t}`);
writeLine(`\t\tcase \`"registerOptions"\`:`);
writeLine(`\t\t\tv, err := dec.ReadValue()`);
writeLine(`\t\t\tif err != nil {`);
writeLine(`\t\t\t\treturn err`);
writeLine(`\t\t\t}`);
writeLine(`\t\t\trawRegisterOptions = v`);
writeLine(`\t\tdefault:`);
writeLine(`\t\t\tif err := dec.SkipValue(); err != nil {`);
writeLine(`\t\t\t\treturn err`);
writeLine(`\t\t\t}`);
writeLine(`\t\t}`);
writeLine(`\t}`);
writeLine("");
writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine("");
writeLine(`\tif missing != 0 {`);
writeLine(`\t\tvar missingProps []string`);
writeLine(`\t\tif missing&missingId != 0 {`);
writeLine(`\t\t\tmissingProps = append(missingProps, "id")`);
writeLine(`\t\t}`);
writeLine(`\t\tif missing&missingMethod != 0 {`);
writeLine(`\t\t\tmissingProps = append(missingProps, "method")`);
writeLine(`\t\t}`);
writeLine(`\t\treturn errMissing(missingProps)`);
writeLine(`\t}`);
writeLine("");
writeLine(`\tif len(rawRegisterOptions) > 0 {`);
writeLine(`\t\ts.RegisterOptions = &RegisterOptions{}`);
writeLine(`\t\tswitch Method(method) {`);
for (const reg of registrationMethods) {
writeLine(`\t\tcase Method${reg.fieldName}:`);
writeLine(`\t\t\tvar v ${reg.optionsTypeName}`);
writeLine(`\t\t\tif err := json.Unmarshal(rawRegisterOptions, &v); err != nil {`);
writeLine(`\t\t\t\treturn err`);
writeLine(`\t\t\t}`);
writeLine(`\t\t\ts.RegisterOptions.${reg.fieldName} = &v`);
}
writeLine(`\t\tdefault:`);
writeLine(`\t\t\treturn fmt.Errorf("unknown registration method: %s", method)`);
writeLine(`\t\t}`);
writeLine(`\t} else {`);
writeLine(`\t\treturn fmt.Errorf("missing registerOptions for method: %s", method)`);
writeLine(`\t}`);
writeLine("");
writeLine(`\treturn nil`);
writeLine(`}`);
writeLine("");
}
if (compareStructures.has(structure.name)) {
generateCompareMethod(structure);
}
}
function generateCompareMethod(structure: Structure) {
const props = structure.properties ?? [];
writeLine(`func (s *${structure.name}) Compare(other *${structure.name}) int {`);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
const isLast = i === props.length - 1;
const fieldName = goFieldName(prop);
const expr = compareExpressionForProperty(structure.name, prop, fieldName);
if (isLast) {
writeLine(`\treturn ${expr}`);
}
else {
writeLine(`\tif c := ${expr}; c != 0 {`);
writeLine(`\t\treturn c`);
writeLine(`\t}`);
}
}
writeLine(`}`);
writeLine("");
}
function compareExpressionForProperty(structName: string, prop: Property, fieldName: string): string {
const resolved = resolveType(prop.type);
const isPointerField = (prop.optional || resolved.needsPointer) && !prop.omitzeroValue;
if (prop.type.kind === "reference") {
const refName = prop.type.name;
if (compareStructures.has(refName)) {
if (isPointerField) {
return `s.${fieldName}.Compare(other.${fieldName})`;
}
return `s.${fieldName}.Compare(&other.${fieldName})`;
}
}
if (prop.type.kind === "base") {
switch (prop.type.name) {
case "string":
case "URI":
case "DocumentUri":
case "integer":
case "uinteger":
case "decimal":
return `cmp.Compare(s.${fieldName}, other.${fieldName})`;
}
}
throw new Error(`Cannot generate Compare for ${structName}.${fieldName}: unsupported field type ${JSON.stringify(prop.type)}. Add support in compareExpressionForProperty.`);
}
// Helper function to detect if an enum is a bitflag enum
// Hardcoded list of bitflag enums
const bitflagEnums = new Set(["WatchKind"]);
function isBitflagEnum(enumeration: any): boolean {
return bitflagEnums.has(enumeration.name);
}
// Generate enumerations
writeLine("// Enumerations\n");
for (const enumeration of model.enumerations) {
write(formatDocumentation(enumeration.documentation));
let baseType;
switch (enumeration.type.name) {
case "string":
baseType = "string";
break;
case "integer":
baseType = "int32";
break;
case "uinteger":
baseType = "uint32";
break;
default:
throw new Error(`Unsupported enum type: ${enumeration.type.name}`);
}
writeLine(`type ${enumeration.name} ${baseType}`);
writeLine("");
// Get the pre-processed enum entries map that avoids duplicates
const enumValues = enumeration.values.map(value => ({
value: String(value.value),
numericValue: Number(value.value),
name: value.name,
identifier: `${enumeration.name}${titleCase(value.name)}`,
documentation: value.documentation,
deprecated: value.deprecated,
}));
writeLine("const (");
// Process entries with unique identifiers
for (const entry of enumValues) {
write(formatDocumentation(entry.documentation));
let valueLiteral;
// Handle string values
if (enumeration.type.name === "string") {
valueLiteral = `"${entry.value.replace(/^"|"$/g, "")}"`;
}
else {
valueLiteral = entry.value;
}
writeLine(`\t${entry.identifier} ${enumeration.name} = ${valueLiteral}`);
}
writeLine(")");
writeLine("");
// Generate String() method for non-string enums
if (enumeration.type.name !== "string") {
const isBitflag = isBitflagEnum(enumeration);
if (isBitflag) {
// Generate bitflag-aware String() method using stringer-style efficiency
const sortedValues = [...enumValues].sort((a, b) => a.numericValue - b.numericValue);
const names = sortedValues.map(v => v.name);
const values = sortedValues.map(v => v.numericValue);
const nameConst = `_${enumeration.name}_name`;
const indexVar = `_${enumeration.name}_index`;
const combinedNames = names.join("");
writeLine(`const ${nameConst} = "${combinedNames}"`);
write(`var ${indexVar} = [...]uint16{0`);
let offset = 0;
for (const name of names) {
offset += name.length;
write(`, ${offset}`);
}
writeLine(`}`);
writeLine("");
writeLine(`func (e ${enumeration.name}) String() string {`);
writeLine(`\tif e == 0 {`);
writeLine(`\t\treturn "0"`);
writeLine(`\t}`);
writeLine(`\tvar parts []string`);
for (let i = 0; i < values.length; i++) {
writeLine(`\tif e&${values[i]} != 0 {`);
writeLine(`\t\tparts = append(parts, ${nameConst}[${indexVar}[${i}]:${indexVar}[${i + 1}]])`);
writeLine(`\t}`);
}
writeLine(`\tif len(parts) == 0 {`);
writeLine(`\t\treturn fmt.Sprintf("${enumeration.name}(%d)", e)`);
writeLine(`\t}`);
writeLine(`\treturn strings.Join(parts, "|")`);
writeLine(`}`);
writeLine("");
}
else {
// Generate regular String() method using stringer-style approach
// Split values into runs of contiguous values
const sortedValues = [...enumValues].sort((a, b) => a.numericValue - b.numericValue);
// Split into runs
const runs: Array<{ names: string[]; values: number[]; }> = [];
let currentRun = { names: [sortedValues[0].name], values: [sortedValues[0].numericValue] };
for (let i = 1; i < sortedValues.length; i++) {
if (sortedValues[i].numericValue === sortedValues[i - 1].numericValue + 1) {
currentRun.names.push(sortedValues[i].name);
currentRun.values.push(sortedValues[i].numericValue);
}
else {
runs.push(currentRun);
currentRun = { names: [sortedValues[i].name], values: [sortedValues[i].numericValue] };
}
}
runs.push(currentRun);
const nameConst = `_${enumeration.name}_name`;
const indexVar = `_${enumeration.name}_index`;
if (runs.length === 1) {
// Single contiguous run - simple case
const combinedNames = runs[0].names.join("");
writeLine(`const ${nameConst} = "${combinedNames}"`);
write(`var ${indexVar} = [...]uint16{0`);
let offset = 0;
for (const name of runs[0].names) {
offset += name.length;
write(`, ${offset}`);
}
writeLine(`}`);
writeLine("");
const minVal = runs[0].values[0];
writeLine(`func (e ${enumeration.name}) String() string {`);
writeLine(`\ti := int(e) - ${minVal}`);
// For unsigned types, i can still be negative if e < minVal (due to underflow in conversion)
// So we always need to check both bounds
writeLine(`\tif i < 0 || i >= len(${indexVar})-1 {`);
writeLine(`\t\treturn fmt.Sprintf("${enumeration.name}(%d)", e)`);
writeLine(`\t}`);
writeLine(`\treturn ${nameConst}[${indexVar}[i]:${indexVar}[i+1]]`);
writeLine(`}`);
writeLine("");
}
else if (runs.length <= 10) {
// Multiple runs - use switch statement
let allNames = "";
const runInfo: Array<{ startOffset: number; endOffset: number; minVal: number; maxVal: number; }> = [];
for (const run of runs) {
const startOffset = allNames.length;
allNames += run.names.join("");
const endOffset = allNames.length;
runInfo.push({
startOffset,
endOffset,
minVal: run.values[0],
maxVal: run.values[run.values.length - 1],
});
}
writeLine(`const ${nameConst} = "${allNames}"`);
writeLine("");
// Generate index variables for each run
for (let i = 0; i < runs.length; i++) {
write(`var ${indexVar}_${i} = [...]uint16{0`);
let offset = 0;
for (const name of runs[i].names) {
offset += name.length;
write(`, ${offset}`);
}
writeLine(`}`);
}
writeLine("");
writeLine(`func (e ${enumeration.name}) String() string {`);
writeLine(`\tswitch {`);
for (let i = 0; i < runs.length; i++) {
const run = runs[i];
const info = runInfo[i];
if (run.values.length === 1) {
writeLine(`\tcase e == ${run.values[0]}:`);
writeLine(`\t\treturn ${nameConst}[${info.startOffset}:${info.endOffset}]`);
}
else {
if (info.minVal === 0 && baseType.startsWith("uint")) {
writeLine(`\tcase e <= ${info.maxVal}:`);
}
else if (info.minVal === 0) {
writeLine(`\tcase 0 <= e && e <= ${info.maxVal}:`);
}
else {
writeLine(`\tcase ${info.minVal} <= e && e <= ${info.maxVal}:`);
}
writeLine(`\t\ti := int(e) - ${info.minVal}`);
writeLine(`\t\treturn ${nameConst}[${info.startOffset}+${indexVar}_${i}[i]:${info.startOffset}+${indexVar}_${i}[i+1]]`);
}
}
writeLine(`\tdefault:`);
writeLine(`\t\treturn fmt.Sprintf("${enumeration.name}(%d)", e)`);
writeLine(`\t}`);
writeLine(`}`);
writeLine("");
}
else {
// Too many runs - use a map
let allNames = "";
const valueMap: Array<{ value: number; startOffset: number; endOffset: number; }> = [];
for (const run of runs) {
for (let i = 0; i < run.names.length; i++) {
const startOffset = allNames.length;
allNames += run.names[i];
const endOffset = allNames.length;
valueMap.push({ value: run.values[i], startOffset, endOffset });
}
}
writeLine(`const ${nameConst} = "${allNames}"`);
writeLine("");
writeLine(`var ${enumeration.name}_map = map[${enumeration.name}]string{`);
for (const entry of valueMap) {
writeLine(`\t${entry.value}: ${nameConst}[${entry.startOffset}:${entry.endOffset}],`);
}
writeLine(`}`);
writeLine("");
writeLine(`func (e ${enumeration.name}) String() string {`);
writeLine(`\tif str, ok := ${enumeration.name}_map[e]; ok {`);
writeLine(`\t\treturn str`);
writeLine(`\t}`);
writeLine(`\treturn fmt.Sprintf("${enumeration.name}(%d)", e)`);
writeLine(`}`);
writeLine("");
}
}
}
// Generate Error() method for ErrorCode to implement the error interface
if (enumeration.name === "ErrorCode") {
writeLine(`func (e ${enumeration.name}) Error() string {`);
writeLine(`\treturn e.String()`);
writeLine(`}`);
writeLine("");
}
}
const requestsAndNotifications: (Request | Notification)[] = [...model.requests, ...model.notifications];
writeLine("// Methods");
writeLine("const (");
for (const request of requestsAndNotifications) {
write(formatDocumentation(request.documentation));
const methodName = methodNameIdentifier(request.method);
writeLine(`\tMethod${methodName} Method = "${request.method}"`);
}
// Emit constants for registration-only methods (not also a request/notification)
for (const reg of registrationMethods) {
if (reg.isRegistrationOnly) {
writeLine(`\t// Registration-only method for ${reg.registrationMethod}.`);
writeLine(`\tMethod${reg.fieldName} Method = "${reg.registrationMethod}"`);
}
}
writeLine(")");
writeLine("");
// Generate request response types
writeLine("// Request response types");
writeLine("");
for (const request of requestsAndNotifications) {
const methodName = methodNameIdentifier(request.method);
let responseTypeName: string | undefined;
if ("result" in request) {
if (request.typeName && request.typeName.endsWith("Request")) {
responseTypeName = request.typeName.replace(/Request$/, "Response");
}
else {
responseTypeName = `${methodName}Response`;
}
writeLine(`// Response type for \`${request.method}\``);
// Special case for response types that are explicitly base type "null"
if (request.result.kind === "base" && request.result.name === "null") {
writeLine(`type ${responseTypeName} = Null`);
}
else {
const resultType = resolveType(request.result);
const goType = resultType.needsPointer ? `*${resultType.name}` : resultType.name;
writeLine(`type ${responseTypeName} = ${goType}`);
}
writeLine("");
}
if (Array.isArray(request.params)) {
throw new Error("Unexpected request params for " + methodName + ": " + JSON.stringify(request.params));
}
const paramType = request.params ? resolveType(request.params) : undefined;
const paramGoType = paramType ? (paramType.needsPointer ? `*${paramType.name}` : paramType.name) : "NoParams";
writeLine(`// Type mapping info for \`${request.method}\``);
if (responseTypeName) {
writeLine(`var ${methodName}Info = RequestInfo[${paramGoType}, ${responseTypeName}]{Method: Method${methodName}}`);
}
else {
writeLine(`var ${methodName}Info = NotificationInfo[${paramGoType}]{Method: Method${methodName}}`);
}
writeLine("");
}
// Generate type aliases
writeLine("// Type aliases\n");
for (const aliasName of customTypeAliases) {
const resolvedType = resolveType(aliasName.type);
const goType = resolvedType.needsPointer ? `*${resolvedType.name}` : resolvedType.name;
writeLine(`type ${aliasName.name} = ${goType}`);
writeLine("");
}
// Generate union types
writeLine("// Union types\n");
for (const [name, members] of typeInfo.unionTypes.entries()) {
writeLine(`type ${name} struct {`);
const uniqueTypeFields = new Map(); // Maps type name -> field name
const uniqueTypeToOriginal = new Map<string, Type>(); // Maps type name -> original meta model Type
let hasLocations = false;
for (const member of members) {
const type = resolveType(member.type);
const memberType = type.name;
// If this type name already exists in our map, skip it
if (!uniqueTypeFields.has(memberType)) {
const fieldName = titleCase(member.name);
uniqueTypeFields.set(memberType, fieldName);
uniqueTypeToOriginal.set(memberType, member.type);
writeLine(`\t${fieldName} *${memberType}`);
if (fieldName === "Locations" && memberType === "[]Location") {
hasLocations = true;
}
}
}
writeLine(`}`);
writeLine("");
// Get the field names and types for marshal/unmarshal methods
const fieldEntries = Array.from(uniqueTypeFields.entries()).map(([typeName, fieldName]) => ({
fieldName,
typeName,
originalType: uniqueTypeToOriginal.get(typeName)!,
}));
// Marshal method
writeLine(`var _ json.MarshalerTo = (*${name})(nil)`);
writeLine("");
const unionContainedNull = members.some(member => member.containedNull);
writeLine(`func (o *${name}) MarshalJSONTo(enc *json.Encoder) error {`);
writeLine(`\treturn marshalUnion(o, enc, "${name}", ${unionContainedNull})`);
writeLine(`}`);
writeLine("");
// Unmarshal method
writeLine(`var _ json.UnmarshalerFrom = (*${name})(nil)`);
writeLine("");
writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *json.Decoder) error {`);
writeLine(`\t*o = ${name}{}`);
writeLine("");
// Group field entries by their expected JSON token kind for optimized dispatch.
const kindMap = new Map<string, typeof fieldEntries>();
const unknownKindEntries: typeof fieldEntries = [];
for (const entry of fieldEntries) {
const kind = jsonKindForType(entry.originalType);
if (!kind) {
unknownKindEntries.push(entry);
}
else {
if (!kindMap.has(kind)) kindMap.set(kind, []);
kindMap.get(kind)!.push(entry);
}
}
// Sort ambiguous variants (same JSON kind) by number of required fields
// descending, so more specific variants are tried first. This prevents
// a less specific variant from greedily matching inputs intended for
// a more specific one.
function countRequiredFields(entry: typeof fieldEntries[0]): number {
if (entry.originalType.kind !== "reference") return 0;
const structure = model.structures.find(s => s.name === (entry.originalType as ReferenceType).name);
if (!structure) return 0;
return structure.properties.filter(p => !p.optional && !p.omitzeroValue).length;
}
for (const [, entries] of kindMap) {
if (entries.length > 1) {
entries.sort((a, b) => countRequiredFields(b) - countRequiredFields(a));
}
}
// Also sort the flat fieldEntries to match (for the fallback path)
// We need to sort only within groups of the same kind.
{
const sorted: typeof fieldEntries = [];
const seen = new Set<string>();
for (const [, entries] of kindMap) {
for (const entry of entries) {
sorted.push(entry);
seen.add(entry.fieldName);
}
}
for (const entry of unknownKindEntries) {
if (!seen.has(entry.fieldName)) {
sorted.push(entry);
}
}
// Replace fieldEntries contents with sorted order
fieldEntries.length = 0;
fieldEntries.push(...sorted);
}
// Validate that ambiguous union variants (same JSON kind) don't have
// order-dependent overlap. Two struct variants overlap if one's required
// fields are a subset of the other's, meaning any valid input for the
// superset also successfully parses as the subset (since unknown properties
// are ignored). This would make the unmarshal result depend on try order.
//
// Exception: variants discriminated by literal field values (e.g., a "kind"
// field with different string literal types) are safe because the literal
// unmarshaler rejects mismatched values.
for (const [kind, entries] of kindMap) {
if (entries.length <= 1) continue;
// Get required fields with their types for each variant
const variantInfo = entries.map(entry => {
if (entry.originalType.kind !== "reference") return null;
const structure = model.structures.find(s => s.name === (entry.originalType as ReferenceType).name);
if (!structure) return null;
const requiredFields = new Map<string, Type>();
for (const p of structure.properties) {
if (!p.optional && !p.omitzeroValue) {
requiredFields.set(p.name, p.type);
}
}
return { entry, requiredFields };
}).filter((v): v is NonNullable<typeof v> => v !== null);
// Check if two variants are discriminated by literal field values.
// Returns true if they share a field where both sides have different
// literal types (stringLiteral, integerLiteral, booleanLiteral).
function isDiscriminatedByLiteral(
a: Map<string, Type>,
b: Map<string, Type>,
): boolean {
for (const [fieldName, aType] of a) {
const bType = b.get(fieldName);
if (!bType) continue;
const aLiteral = aType.kind === "stringLiteral" || aType.kind === "integerLiteral" || aType.kind === "booleanLiteral";
const bLiteral = bType.kind === "stringLiteral" || bType.kind === "integerLiteral" || bType.kind === "booleanLiteral";
if (aLiteral && bLiteral) {
// Both are literals for the same field — check if values differ
if (aType.kind === bType.kind && (aType as any).value !== (bType as any).value) {
return true;
}
// Different literal kinds on same field also discriminates
if (aType.kind !== bType.kind) {
return true;
}
}
}
return false;
}
// Check each pair for subset relationships
for (let i = 0; i < variantInfo.length; i++) {
for (let j = 0; j < variantInfo.length; j++) {
if (i === j) continue;
const a = variantInfo[i];
const b = variantInfo[j];
const aNames = new Set(a.requiredFields.keys());
const bNames = new Set(b.requiredFields.keys());
const aSubsetOfB = [...aNames].every(f => bNames.has(f));
if (!aSubsetOfB) continue;
// Skip if discriminated by literal values
if (isDiscriminatedByLiteral(a.requiredFields, b.requiredFields)) continue;
if (aNames.size < bNames.size) {
// a is a strict subset of b
const aIdx = entries.indexOf(a.entry);
const bIdx = entries.indexOf(b.entry);
if (aIdx < bIdx) {
console.warn(
`Warning: In union ${name} (${kind} variants), ` +
`${a.entry.fieldName} (required: [${[...aNames]}]) is tried before ` +
`${b.entry.fieldName} (required: [${[...bNames]}]), but ` +
`${a.entry.fieldName}'s required fields are a strict subset — ` +
`it will greedily match inputs intended for ${b.entry.fieldName}. ` +
`Reorder so the more specific variant is tried first.`,
);
}
}
else if (aNames.size === bNames.size && i < j) {
// Identical required fields — truly ambiguous
console.warn(
`Warning: In union ${name} (${kind} variants), ` +
`${a.entry.fieldName} and ${b.entry.fieldName} have identical ` +
`required fields [${[...aNames]}] — they are structurally ` +
`indistinguishable and the unmarshal result is order-dependent.`,
);
}
}
}
}
// Determine if we can use PeekKind-based dispatch:
// - Every entry must have a known kind (no `any` etc.)
// - There must be at least 2 distinct cases (kind groups + null) for a switch to be worthwhile
const hasUnknownKinds = unknownKindEntries.length > 0;
const distinctKinds = kindMap.size + (unionContainedNull ? 1 : 0);
const canDispatch = !hasUnknownKinds && distinctKinds >= 2;
// Check if all kind groups are unambiguous (exactly 1 entry each).
// When unambiguous, we can UnmarshalDecode directly without buffering.
const allUnambiguous = canDispatch && Array.from(kindMap.values()).every(entries => entries.length === 1);
let fallbackExhaustive = false;
const hasBooleanKind = kindMap.has("boolean");
if (canDispatch && allUnambiguous) {
// Best case: PeekKind + UnmarshalDecode directly, no ReadValue buffer needed.
if (hasBooleanKind) {
writeLine(`\tswitch kind := dec.PeekKind(); kind {`);
}
else {
writeLine(`\tswitch dec.PeekKind() {`);
}
if (unionContainedNull) {
writeLine(`\tcase 'n':`);
writeLine(`\t\t_, err := dec.ReadToken()`);
writeLine(`\t\treturn err`);
}
for (const [kind, entries] of kindMap) {
writeLine(`\t${goKindCasesForJsonKind(kind)}`);
const entry = entries[0];
if (kind === "boolean") {
writeLine(`\t\to.${entry.fieldName} = new(kind == 't')`);
writeLine(`\t\t_, err := dec.ReadToken()`);
writeLine(`\t\treturn err`);
}
else {
writeLine(`\t\to.${entry.fieldName} = new(${entry.typeName})`);
writeLine(`\t\treturn json.UnmarshalDecode(dec, o.${entry.fieldName})`);
}
}
writeLine(`\tdefault:`);
writeLine(`\t\treturn errInvalidKind("${name}", dec.PeekKind())`);
writeLine(`\t}`);
}
else if (canDispatch) {
// Mixed case: some kind groups have multiple entries.
// Use PeekKind to dispatch, then ReadValue + try-each within ambiguous groups,
// or UnmarshalDecode directly for unambiguous groups.
if (hasBooleanKind) {
writeLine(`\tswitch kind := dec.PeekKind(); kind {`);
}
else {
writeLine(`\tswitch dec.PeekKind() {`);
}
if (unionContainedNull) {
writeLine(`\tcase 'n':`);
writeLine(`\t\t_, err := dec.ReadToken()`);
writeLine(`\t\treturn err`);
}
for (const [kind, entries] of kindMap) {
writeLine(`\t${goKindCasesForJsonKind(kind)}`);
if (entries.length === 1) {
// Unambiguous: decode directly
const entry = entries[0];
if (kind === "boolean") {
writeLine(`\t\to.${entry.fieldName} = new(kind == 't')`);
writeLine(`\t\t_, err := dec.ReadToken()`);
writeLine(`\t\treturn err`);
}
else {
writeLine(`\t\to.${entry.fieldName} = new(${entry.typeName})`);
writeLine(`\t\treturn json.UnmarshalDecode(dec, o.${entry.fieldName})`);
}
}
else {
// Ambiguous: buffer and dispatch
writeLine(`\t\tdata, err := dec.ReadValue()`);
writeLine(`\t\tif err != nil {`);
writeLine(`\t\t\treturn err`);
writeLine(`\t\t}`);
let exhaustive = false;
const disc = findDiscriminatorField(entries);
if (disc) {
exhaustive = generateDiscriminatorDispatch(disc, "\t\t");
}
else {
const pres = findPresenceDiscriminator(entries);
if (pres) {
exhaustive = generatePresenceDispatch(pres, "\t\t");
}
else {
for (const entry of entries) {
writeLine(`\t\tvar v${entry.fieldName} ${entry.typeName}`);
writeLine(`\t\tif err := json.Unmarshal(data, &v${entry.fieldName}); err == nil {`);
writeLine(`\t\t\to.${entry.fieldName} = &v${entry.fieldName}`);
writeLine(`\t\t\treturn nil`);
writeLine(`\t\t}`);
}
}
}
if (!exhaustive) {
writeLine(`\t\treturn errInvalidValue("${name}", data)`);
}
}
}
writeLine(`\tdefault:`);
writeLine(`\t\treturn errInvalidKind("${name}", dec.PeekKind())`);
writeLine(`\t}`);
}
else {
// Fallback: unknown kinds present (e.g. `any`), use ReadValue + try-each.
writeLine("\tdata, err := dec.ReadValue()");
writeLine("\tif err != nil {");
writeLine("\t\treturn err");
writeLine("\t}");
if (unionContainedNull) {
writeLine(`\tif string(data) == "null" {`);
writeLine(`\t\treturn nil`);
writeLine(`\t}`);
writeLine("");
}
let exhaustive = false;
const disc = findDiscriminatorField(fieldEntries);
if (disc) {
exhaustive = generateDiscriminatorDispatch(disc, "\t");
}
else {
const pres = findPresenceDiscriminator(fieldEntries);
if (pres) {
exhaustive = generatePresenceDispatch(pres, "\t");
}
else {
for (const entry of fieldEntries) {
writeLine(`\tvar v${entry.fieldName} ${entry.typeName}`);
writeLine(`\tif err := json.Unmarshal(data, &v${entry.fieldName}); err == nil {`);
writeLine(`\t\to.${entry.fieldName} = &v${entry.fieldName}`);
writeLine(`\t\treturn nil`);
writeLine(`\t}`);
}
}
}
fallbackExhaustive = exhaustive;
}
if (canDispatch) {
// Dispatch paths have an exhaustive switch with default, nothing after the switch.
}
else if (!fallbackExhaustive) {
// Fallback paths: the final error references `data` which is in scope.
writeLine(`\treturn errInvalidValue("${name}", data)`);
}
writeLine(`}`);
writeLine("");
// Generate GetLocations method
if (hasLocations) {
writeLine(`func (o ${name}) GetLocations() *[]Location {`);
writeLine(`\treturn o.Locations`);
writeLine(`}`);
writeLine("");
}
}
// Generate literal types
writeLine("// Literal types\n");
for (const [value, name] of typeInfo.literalTypes.entries()) {
const jsonValue = JSON.stringify(value);
writeLine(`// ${name} is a literal type for ${jsonValue}`);
writeLine(`type ${name} struct{}`);
writeLine("");
writeLine(`var _ json.MarshalerTo = ${name}{}`);
writeLine("");
writeLine(`func (o ${name}) MarshalJSONTo(enc *json.Encoder) error {`);
writeLine(`\treturn enc.WriteValue(json.Value(\`${jsonValue}\`))`);
writeLine(`}`);
writeLine("");
writeLine(`var _ json.UnmarshalerFrom = &${name}{}`);
writeLine("");
writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *json.Decoder) error {`);
writeLine(`\tv, err := dec.ReadValue();`);
writeLine(`\tif err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif string(v) != \`${jsonValue}\` {`);
writeLine(`\t\treturn errLiteralMismatch("${name}", \`${jsonValue}\`, v)`);
writeLine(`\t}`);
writeLine(`\treturn nil`);
writeLine(`}`);
writeLine("");
}
// Generate resolved capabilities
const clientCapsStructure = model.structures.find(s => s.name === "ClientCapabilities");
if (clientCapsStructure) {
writeLine("// Helper function for dereferencing pointers with zero value fallback");
writeLine("func derefOr[T any](v *T) T {");
writeLine("\tif v != nil {");
writeLine("\t\treturn *v");
writeLine("\t}");
writeLine("\tvar zero T");
writeLine("\treturn zero");
writeLine("}");
writeLine("");
// Collect all dependent structures and generate their resolved types
const deps = collectStructureDependencies(clientCapsStructure);
const uniqueDeps = Array.from(new Map(deps.map(d => [d.name, d])).values());
for (const dep of uniqueDeps) {
const depLines = generateResolvedTypeAndHelper(dep, false);
for (const line of depLines) {
writeLine(line);
}
}
// Generate the main ResolvedClientCapabilities type and function
writeLine("// ResolvedClientCapabilities is a version of ClientCapabilities where all nested");
writeLine("// fields are values (not pointers), making it easier to access deeply nested capabilities.");
writeLine("// Use (*ClientCapabilities).Resolve() to convert from ClientCapabilities.");
if (clientCapsStructure.documentation) {
writeLine("//");
const typeDoc = formatDocumentation(clientCapsStructure.documentation);
for (const line of typeDoc.split("\n").filter(l => l)) {
writeLine(line);
}
}
const mainLines = generateResolvedTypeAndHelper(clientCapsStructure, true);
for (const line of mainLines) {
writeLine(line);
}
}
return parts.join("");
}
function hasSomeProp(structure: Structure, propName: string, propTypeName: string) {
return structure.properties?.some(p =>
!p.optional &&
p.name === propName &&
p.type.kind === "reference" &&
p.type.name === propTypeName
);
}
function hasTextDocumentURI(structure: Structure) {
return hasSomeProp(structure, "textDocument", "TextDocumentIdentifier") ||
hasSomeProp(structure, "_vs_textDocument", "TextDocumentIdentifier");
}
function hasTextDocumentPosition(structure: Structure) {
return hasSomeProp(structure, "position", "Position") ||
hasSomeProp(structure, "_vs_position", "Position");
}
function getLocationUriProperty(structure: Structure) {
const prop = structure.properties?.find(p =>
!p.optional &&
titleCase(p.name).endsWith("Uri") &&
p.type.kind === "base" &&
p.type.name === "DocumentUri"
);
if (
prop &&
structure.properties.some(p =>
!p.optional &&
titleCase(p.name) === titleCase(prop.name).replace(/Uri$/, "Range") &&
p.type.kind === "reference" &&
p.type.name === "Range"
)
) {
return titleCase(prop.name);
}
}
/**
* Main function
*/
async function main() {
collectTypeDefinitions();
const generatedCode = generateCode();
fs.writeFileSync(out, generatedCode);
await $({ cwd: repoRoot })`dprint fmt ${out}`;
console.log(`Successfully generated ${out}`);
}
main().catch(e => {
console.error(e);
process.exit(1);
});