#!/usr/bin/env node import cp from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import url from "node:url"; import which from "which"; import type { MetaModel, Notification, OrType, Property, Request, Structure, Type, } from "./metaModelSchema.mts"; const __filename = url.fileURLToPath(new URL(import.meta.url)); const __dirname = path.dirname(__filename); 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")); // Preprocess the model to inline extends/mixins contents function preprocessModel() { 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; } } // Preprocess the model before proceeding preprocessModel(); 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 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; } // 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${titleCase(type.value)}`; 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 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 `${titleCase(type.element.name)}s`; } 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 }], ]); /** * 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", "NotebookDocumentIdentifier", "PreviousResultId", "VersionedNotebookDocumentIdentifier", "VersionedTextDocumentIdentifier", "OptionalVersionedTextDocumentIdentifier", ]); // 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"); 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 => v === "$" ? "" : titleCase(v)).join(""); } /** * Generate the Go code */ function generateCode() { const parts: string[] = []; function write(s: string) { parts.push(s); } function writeLine(s = "") { parts.push(s + "\n"); } // File header writeLine("// Code generated by generate.mts; DO NOT EDIT."); writeLine(""); writeLine("package lsproto"); writeLine(""); writeLine(`import (`); writeLine(`\t"fmt"`); writeLine(""); writeLine(`\t"github.com/go-json-experiment/json"`); writeLine(`\t"github.com/go-json-experiment/json/jsontext"`); 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); const goType = prop.optional || type.needsPointer ? `*${type.name}` : type.name; writeLine(`\t${titleCase(prop.name)} ${goType} \`json:"${prop.name}${prop.optional ? ",omitzero" : ""}"\``); if (includeDocumentation) { writeLine(""); } } writeLine("}"); writeLine(""); } generateStructFields(structure.name, true); writeLine(""); if (hasTextDocumentURI(structure)) { // Generate TextDocumentURI method writeLine(`func (s *${structure.name}) TextDocumentURI() DocumentUri {`); writeLine(`\treturn s.TextDocument.Uri`); writeLine(`}`); writeLine(""); } // Generate UnmarshalJSONFrom method for structure validation const requiredProps = structure.properties?.filter(p => !p.optional) || []; if (requiredProps.length > 0) { writeLine(`\tvar _ json.UnmarshalerFrom = (*${structure.name})(nil)`); writeLine(""); writeLine(`func (s *${structure.name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`); writeLine(`\tvar (`); for (const prop of requiredProps) { writeLine(`\t\tseen${titleCase(prop.name)} bool`); } writeLine(`\t)`); writeLine(""); writeLine(`\tif k := dec.PeekKind(); k != '{' {`); writeLine(`\t\treturn fmt.Errorf("expected object start, but encountered %v", k)`); writeLine(`\t}`); writeLine(`\tif _, err := dec.ReadToken(); err != nil {`); writeLine(`\t\treturn err`); writeLine(`\t}`); writeLine(""); writeLine(`\tfor dec.PeekKind() != '}' {`); writeLine("name, err := dec.ReadValue()"); writeLine(`\t\tif err != nil {`); writeLine(`\t\t\treturn err`); writeLine(`\t\t}`); writeLine(`\t\tswitch string(name) {`); for (const prop of structure.properties) { writeLine(`\t\tcase \`"${prop.name}"\`:`); if (!prop.optional) { writeLine(`\t\t\tseen${titleCase(prop.name)} = true`); } writeLine(`\t\t\tif err := json.UnmarshalDecode(dec, &s.${titleCase(prop.name)}); err != nil {`); writeLine(`\t\t\t\treturn err`); writeLine(`\t\t\t}`); } writeLine(`\t\tdefault:`); writeLine(`\t\t// Ignore unknown properties.`); writeLine(`\t\t}`); writeLine(`\t}`); writeLine(""); writeLine(`\tif _, err := dec.ReadToken(); err != nil {`); writeLine(`\t\treturn err`); writeLine(`\t}`); writeLine(""); for (const prop of requiredProps) { writeLine(`\tif !seen${titleCase(prop.name)} {`); writeLine(`\t\treturn fmt.Errorf("required property '${prop.name}' is missing")`); writeLine(`\t}`); } writeLine(""); writeLine(`\treturn nil`); writeLine(`}`); writeLine(""); } } // 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), identifier: `${enumeration.name}${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(""); } const requestsAndNotifications: (Request | Notification)[] = [...model.requests, ...model.notifications]; // Generate unmarshalParams function writeLine("func unmarshalParams(method Method, data []byte) (any, error) {"); writeLine("\tswitch method {"); // Requests and notifications for (const request of requestsAndNotifications) { const methodName = methodNameIdentifier(request.method); if (!request.params) { writeLine(`\tcase Method${methodName}:`); writeLine(`\t\treturn unmarshalEmpty(data)`); continue; } if (Array.isArray(request.params)) { throw new Error("Unexpected array type for request params: " + JSON.stringify(request.params)); } const resolvedType = resolveType(request.params); writeLine(`\tcase Method${methodName}:`); if (resolvedType.name === "any") { writeLine(`\t\treturn unmarshalAny(data)`); } else { writeLine(`\t\treturn unmarshalPtrTo[${resolvedType.name}](data)`); } } writeLine("\tdefault:"); writeLine(`\t\treturn unmarshalAny(data)`); writeLine("\t}"); writeLine("}"); writeLine(""); writeLine("// Methods"); writeLine("const ("); for (const request of requestsAndNotifications) { write(formatDocumentation(request.documentation)); const methodName = methodNameIdentifier(request.method); writeLine(`\tMethod${methodName} Method = "${request.method}"`); } 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) : "any"; 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 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 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); writeLine(`\t${fieldName} *${memberType}`); } } writeLine(`}`); writeLine(""); // Get the field names and types for marshal/unmarshal methods const fieldEntries = Array.from(uniqueTypeFields.entries()).map(([typeName, fieldName]) => ({ fieldName, typeName })); // Marshal method writeLine(`var _ json.MarshalerTo = (*${name})(nil)`); writeLine(""); writeLine(`func (o *${name}) MarshalJSONTo(enc *jsontext.Encoder) error {`); // Determine if this union contained null (check if any member has containedNull = true) const unionContainedNull = members.some(member => member.containedNull); if (unionContainedNull) { write(`\tassertAtMostOne("more than one element of ${name} is set", `); } else { write(`\tassertOnlyOne("exactly one element of ${name} should be set", `); } // Create assertion to ensure at most one field is set at a time // Write the assertion conditions for (let i = 0; i < fieldEntries.length; i++) { if (i > 0) write(", "); write(`o.${fieldEntries[i].fieldName} != nil`); } writeLine(`)`); writeLine(""); for (const entry of fieldEntries) { writeLine(`\tif o.${entry.fieldName} != nil {`); writeLine(`\t\treturn json.MarshalEncode(enc, o.${entry.fieldName})`); writeLine(`\t}`); } // If all fields are nil, marshal as null (only for unions that can contain null) if (unionContainedNull) { writeLine(`\treturn enc.WriteToken(jsontext.Null)`); } else { writeLine(`\tpanic("unreachable")`); } writeLine(`}`); writeLine(""); // Unmarshal method writeLine(`var _ json.UnmarshalerFrom = (*${name})(nil)`); writeLine(""); writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`); writeLine(`\t*o = ${name}{}`); writeLine(""); 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(""); } 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}`); } // Match the error format from the original script writeLine(`\treturn fmt.Errorf("invalid ${name}: %s", data)`); 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 *jsontext.Encoder) error {`); writeLine(`\treturn enc.WriteValue(jsontext.Value(\`${jsonValue}\`))`); writeLine(`}`); writeLine(""); writeLine(`var _ json.UnmarshalerFrom = &${name}{}`); writeLine(""); writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *jsontext.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 fmt.Errorf("expected ${name} value %s, got %s", \`${jsonValue}\`, v)`); writeLine(`\t}`); writeLine(`\treturn nil`); writeLine(`}`); writeLine(""); } return parts.join(""); } function hasTextDocumentURI(structure: Structure) { return structure.properties?.some(p => !p.optional && p.name === "textDocument" && p.type.kind === "reference" && p.type.name === "TextDocumentIdentifier" ); } /** * Main function */ function main() { try { collectTypeDefinitions(); const generatedCode = generateCode(); fs.writeFileSync(out, generatedCode); // Format with gofmt const gofmt = which.sync("go"); cp.execFileSync(gofmt, ["tool", "mvdan.cc/gofumpt", "-lang=go1.25", "-w", out]); console.log(`Successfully generated ${out}`); } catch (error) { console.error("Error generating code:", error); process.exit(1); } } main();