#!/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([ "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(); // 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(); for (const structure of model.structures) { structureMap.set(structure.name, structure); } function collectInheritedProperties(structure: Structure, visited = new Set()): 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(); 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(); 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; literalTypes: Map; unionTypes: Map; typeAliasMap: Map; } 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(); 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; 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>(); 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 | null = null; for (const [fieldName, mapping] of fieldCandidates) { const validMapping = new Map(); 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; }>(); 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(); 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>, 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>, ): { 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>, 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()): 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(); // 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(); 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(); 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(); 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 => 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, b: Map, ): 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); });