@rank-lang/lsp 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.ts ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import {
4
+ createConnection,
5
+ ProposedFeatures,
6
+ TextDocumentSyncKind,
7
+ TextDocuments,
8
+ } from "vscode-languageserver/node.js";
9
+ import { TextDocument } from "vscode-languageserver-textdocument";
10
+ import { fileURLToPath } from "node:url";
11
+ import { ProjectSession } from "./session.js";
12
+ import { publishDiagnostics } from "./features/diagnostics.js";
13
+ import { handleHover } from "./features/hover.js";
14
+ import { handleDefinition } from "./features/definition.js";
15
+ import { handleCompletion } from "./features/completion.js";
16
+
17
+ const connection = createConnection(ProposedFeatures.all);
18
+ const documents = new TextDocuments(TextDocument);
19
+ const session = new ProjectSession();
20
+
21
+ connection.onInitialize(() => ({
22
+ capabilities: {
23
+ textDocumentSync: TextDocumentSyncKind.Incremental,
24
+ hoverProvider: true,
25
+ definitionProvider: true,
26
+ completionProvider: {
27
+ triggerCharacters: [".", ":", " "],
28
+ },
29
+ },
30
+ }));
31
+
32
+ documents.onDidOpen(({ document }) => {
33
+ const filePath = fileURLToPath(document.uri);
34
+ void publishDiagnostics(connection, session, filePath);
35
+ });
36
+
37
+ documents.onDidChangeContent(({ document }) => {
38
+ const filePath = fileURLToPath(document.uri);
39
+ session.invalidate(filePath);
40
+ void publishDiagnostics(connection, session, filePath);
41
+ });
42
+
43
+ documents.onDidSave(({ document }) => {
44
+ const filePath = fileURLToPath(document.uri);
45
+ session.invalidate(filePath);
46
+ void publishDiagnostics(connection, session, filePath);
47
+ });
48
+
49
+ connection.onDidChangeWatchedFiles(({ changes }) => {
50
+ const manifestChanges = changes
51
+ .map(({ uri }) => fileURLToPath(uri))
52
+ .filter((filePath) => path.basename(filePath) === "rank.toml");
53
+
54
+ if (manifestChanges.length === 0) {
55
+ return;
56
+ }
57
+
58
+ for (const manifestPath of manifestChanges) {
59
+ session.invalidate(manifestPath);
60
+ }
61
+
62
+ for (const document of documents.all()) {
63
+ const filePath = fileURLToPath(document.uri);
64
+ void publishDiagnostics(connection, session, filePath);
65
+ }
66
+ });
67
+
68
+ connection.onHover((params) => handleHover(params, session, documents.get(params.textDocument.uri)));
69
+ connection.onDefinition((params) => handleDefinition(params, session, documents.get(params.textDocument.uri)));
70
+ connection.onCompletion((params) => handleCompletion(params, session, documents.get(params.textDocument.uri)));
71
+
72
+ documents.listen(connection);
73
+ connection.listen();
package/src/session.ts ADDED
@@ -0,0 +1,224 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import {
4
+ loadNearestRankManifestIfValid,
5
+ loadProjectModuleGraphAsync,
6
+ resolveSecurityOptions,
7
+ type RootModuleGraph,
8
+ type LoadedModule,
9
+ type TypeCheckOptions,
10
+ } from "@rank-lang/compiler";
11
+
12
+ interface CachedGraph {
13
+ graph: RootModuleGraph;
14
+ invalidated: boolean;
15
+ }
16
+
17
+ // Manages a per-project-root module graph cache. The cache is invalidated on
18
+ // textDocument/didChange and textDocument/didSave for any file in the project.
19
+ export class ProjectSession {
20
+ private readonly cache = new Map<string, CachedGraph>();
21
+
22
+ async getModuleGraph(filePath: string): Promise<RootModuleGraph | null> {
23
+ const resolvedFilePath = path.resolve(filePath);
24
+ const cacheKey = this.cacheKeyForFile(resolvedFilePath);
25
+
26
+ const cached = this.cache.get(cacheKey);
27
+
28
+ if (cached && !cached.invalidated && this.getLoadedModule(cached.graph, resolvedFilePath)) {
29
+ return cached.graph;
30
+ }
31
+
32
+ try {
33
+ const graph = await this.loadGraphForFile(resolvedFilePath);
34
+ this.cache.set(cacheKey, { graph, invalidated: false });
35
+ return graph;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ getLoadedModule(graph: RootModuleGraph, filePath: string): LoadedModule | null {
42
+ const resolved = path.resolve(filePath);
43
+
44
+ for (const mod of Object.values(graph.modules)) {
45
+ if (path.resolve(mod.filePath) === resolved) {
46
+ return mod;
47
+ }
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ async getTypeCheckOptions(filePath: string, graph?: RootModuleGraph): Promise<TypeCheckOptions | undefined> {
54
+ const resolvedFilePath = path.resolve(filePath);
55
+ const securityOptions = resolveSecurityOptions(loadNearestRankManifestIfValid(resolvedFilePath)?.security);
56
+ const testManifestPath = this.nearestTestManifestPathForFile(resolvedFilePath);
57
+
58
+ const providerCapabilities = new Set(securityOptions.compileTime.providerCapabilities);
59
+
60
+ if (testManifestPath && this.testManifestUsesProviderStubs(testManifestPath)) {
61
+ const loadedGraph = graph ?? await this.getModuleGraph(resolvedFilePath);
62
+
63
+ if (loadedGraph) {
64
+ for (const providerExport of Object.values(loadedGraph.providerExports)) {
65
+ if (providerExport.kind !== "backend") {
66
+ continue;
67
+ }
68
+
69
+ for (const capability of providerExport.capabilities) {
70
+ providerCapabilities.add(capability);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ if (providerCapabilities.size === 0) {
77
+ if (securityOptions.compileTime.providerMutationExports.length > 0 || securityOptions.compileTime.allowedHttpHosts) {
78
+ return {
79
+ ...(securityOptions.compileTime.providerMutationExports.length > 0
80
+ ? { providerMutationExports: securityOptions.compileTime.providerMutationExports }
81
+ : {}),
82
+ ...(securityOptions.compileTime.allowedHttpHosts ? { allowedHttpHosts: securityOptions.compileTime.allowedHttpHosts } : {}),
83
+ };
84
+ }
85
+
86
+ return undefined;
87
+ }
88
+
89
+ return {
90
+ providerCapabilities: [...providerCapabilities].sort((left, right) => left.localeCompare(right)),
91
+ ...(securityOptions.compileTime.providerMutationExports.length > 0
92
+ ? { providerMutationExports: securityOptions.compileTime.providerMutationExports }
93
+ : {}),
94
+ ...(securityOptions.compileTime.allowedHttpHosts ? { allowedHttpHosts: securityOptions.compileTime.allowedHttpHosts } : {}),
95
+ };
96
+ }
97
+
98
+ invalidate(filePath: string): void {
99
+ const cacheKey = this.cacheKeyForFile(filePath);
100
+ const cached = this.cache.get(cacheKey);
101
+ if (cached) cached.invalidated = true;
102
+ }
103
+
104
+ private async loadGraphForFile(filePath: string): Promise<RootModuleGraph> {
105
+ const preferredEntryPath = this.preferredEntryPathForFile(filePath);
106
+ const securityOptions = resolveSecurityOptions(loadNearestRankManifestIfValid(filePath)?.security);
107
+ const graphOptions = securityOptions.compileTime.offline
108
+ ? { offline: true as const }
109
+ : undefined;
110
+
111
+ if (preferredEntryPath && preferredEntryPath !== filePath) {
112
+ const graph = await loadProjectModuleGraphAsync(preferredEntryPath, graphOptions);
113
+
114
+ if (this.getLoadedModule(graph, filePath)) {
115
+ return graph;
116
+ }
117
+ }
118
+
119
+ return loadProjectModuleGraphAsync(filePath, graphOptions);
120
+ }
121
+
122
+ private cacheKeyForFile(filePath: string): string {
123
+ return this.findProjectRoot(filePath) ?? path.resolve(filePath);
124
+ }
125
+
126
+ private preferredEntryPathForFile(filePath: string): string | null {
127
+ const resolvedFilePath = path.resolve(filePath);
128
+
129
+ if (path.basename(resolvedFilePath) === "main.rank") {
130
+ return resolvedFilePath;
131
+ }
132
+
133
+ let dir = path.dirname(resolvedFilePath);
134
+ const projectRoot = this.findProjectRoot(resolvedFilePath);
135
+ const stopDir = projectRoot ? path.resolve(projectRoot) : null;
136
+
137
+ for (let i = 0; i < 20; i++) {
138
+ const candidatePath = path.join(dir, "main.rank");
139
+
140
+ if (fs.existsSync(candidatePath)) {
141
+ return candidatePath;
142
+ }
143
+
144
+ if (stopDir && dir === stopDir) {
145
+ break;
146
+ }
147
+
148
+ const parent = path.dirname(dir);
149
+
150
+ if (parent === dir) {
151
+ break;
152
+ }
153
+
154
+ dir = parent;
155
+ }
156
+
157
+ return null;
158
+ }
159
+
160
+ private nearestTestManifestPathForFile(filePath: string): string | null {
161
+ let dir = path.dirname(path.resolve(filePath));
162
+ const projectRoot = this.findProjectRoot(filePath);
163
+ const stopDir = projectRoot ? path.resolve(projectRoot) : null;
164
+
165
+ for (let i = 0; i < 20; i++) {
166
+ const candidatePath = path.join(dir, "rank-test.json");
167
+
168
+ if (fs.existsSync(candidatePath)) {
169
+ return candidatePath;
170
+ }
171
+
172
+ if (stopDir && dir === stopDir) {
173
+ break;
174
+ }
175
+
176
+ const parent = path.dirname(dir);
177
+
178
+ if (parent === dir) {
179
+ break;
180
+ }
181
+
182
+ dir = parent;
183
+ }
184
+
185
+ return null;
186
+ }
187
+
188
+ private testManifestUsesProviderStubs(manifestPath: string): boolean {
189
+ try {
190
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as unknown;
191
+
192
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
193
+ return false;
194
+ }
195
+
196
+ const manifest = parsed as Record<string, unknown>;
197
+ const providerStubs = manifest.providerStubs;
198
+
199
+ if (manifest.kind !== "rank/test-case" || manifest.version !== 1 || typeof providerStubs !== "string" || providerStubs.length === 0) {
200
+ return false;
201
+ }
202
+
203
+ return fs.existsSync(path.resolve(path.dirname(manifestPath), providerStubs));
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ private findProjectRoot(filePath: string): string | null {
210
+ let dir = path.dirname(path.resolve(filePath));
211
+
212
+ for (let i = 0; i < 20; i++) {
213
+ if (fs.existsSync(path.join(dir, "rank.toml"))) {
214
+ return dir;
215
+ }
216
+
217
+ const parent = path.dirname(dir);
218
+ if (parent === dir) break;
219
+ dir = parent;
220
+ }
221
+
222
+ return null;
223
+ }
224
+ }
@@ -0,0 +1,54 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ import type { SourcePosition, SourceSpan, Diagnostic as CompilerDiagnostic } from "@rank-lang/compiler";
4
+ import type { TextDocument } from "vscode-languageserver-textdocument";
5
+ import {
6
+ type DiagnosticRelatedInformation,
7
+ type Position as LspPosition,
8
+ type Range as LspRange,
9
+ type Diagnostic as LspDiagnostic,
10
+ DiagnosticSeverity,
11
+ } from "vscode-languageserver/node.js";
12
+
13
+ // Compiler uses 1-indexed lines/columns; LSP uses 0-indexed.
14
+ export function toSourcePosition(pos: LspPosition, document?: TextDocument): SourcePosition {
15
+ return {
16
+ offset: document?.offsetAt(pos) ?? 0,
17
+ line: pos.line + 1,
18
+ column: pos.character + 1,
19
+ };
20
+ }
21
+
22
+ export function toLspRange(span: SourceSpan): LspRange {
23
+ return {
24
+ start: { line: span.start.line - 1, character: span.start.column - 1 },
25
+ end: { line: span.end.line - 1, character: span.end.column - 1 },
26
+ };
27
+ }
28
+
29
+ export function toLspDiagnostic(d: CompilerDiagnostic): LspDiagnostic {
30
+ const range: LspRange = d.span
31
+ ? toLspRange(d.span)
32
+ : { start: { line: 0, character: 0 }, end: { line: 0, character: Number.MAX_SAFE_INTEGER } };
33
+
34
+ const diagnostic: LspDiagnostic = {
35
+ range,
36
+ severity: d.severity === "error" ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
37
+ message: d.message,
38
+ source: "rank",
39
+ code: d.code,
40
+ ...(d.labels && d.labels.length > 0
41
+ ? {
42
+ relatedInformation: d.labels.map<DiagnosticRelatedInformation>((label) => ({
43
+ location: {
44
+ uri: pathToFileURL(label.sourcePath).href,
45
+ range: toLspRange(label.span),
46
+ },
47
+ message: label.message,
48
+ })),
49
+ }
50
+ : {}),
51
+ };
52
+
53
+ return diagnostic;
54
+ }
@@ -0,0 +1,178 @@
1
+ import {
2
+ formatStdlibHoverMarkdown,
3
+ lookupStdlibBuiltinHoverInfo,
4
+ lookupStdlibBuiltinValue,
5
+ lookupStdlibBuiltinValueByQualifiedName,
6
+ lookupStdlibModuleHoverInfo,
7
+ lookupStdlibModuleValue,
8
+ lookupStdlibTypeExportHoverInfo,
9
+ stdModuleNamespacePath,
10
+ type LoadedModule,
11
+ } from "@rank-lang/compiler";
12
+ import type { Position as LspPosition } from "vscode-languageserver/node.js";
13
+ import type { TextDocument } from "vscode-languageserver-textdocument";
14
+
15
+ import { identifierAtPosition } from "./type-symbols.js";
16
+
17
+ function importedStdNamespaceModulePath(
18
+ loadedModule: LoadedModule,
19
+ name: string,
20
+ ): string | null {
21
+ for (const importNode of loadedModule.program.imports) {
22
+ if (importNode.modulePath !== "std" || importNode.clause.kind !== "NamedImportClause") {
23
+ continue;
24
+ }
25
+
26
+ if (!importNode.clause.names.includes(name)) {
27
+ continue;
28
+ }
29
+
30
+ return stdModuleNamespacePath(name) ?? null;
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function importedStdMemberModulePath(
37
+ loadedModule: LoadedModule,
38
+ name: string,
39
+ ): string | null {
40
+ for (const importNode of loadedModule.program.imports) {
41
+ if (!importNode.modulePath.startsWith("std::") || importNode.clause.kind !== "NamedImportClause") {
42
+ continue;
43
+ }
44
+
45
+ if (!importNode.clause.names.includes(name)) {
46
+ continue;
47
+ }
48
+
49
+ return importNode.modulePath;
50
+ }
51
+
52
+ return null;
53
+ }
54
+
55
+ function directStdlibHover(identifier: string): string | null {
56
+ const builtinId = lookupStdlibBuiltinValueByQualifiedName(identifier);
57
+
58
+ if (builtinId) {
59
+ const hover = lookupStdlibBuiltinHoverInfo(builtinId);
60
+ return hover ? formatStdlibHoverMarkdown(hover) : null;
61
+ }
62
+
63
+ const segments = identifier.split("::");
64
+
65
+ if (segments.length >= 3) {
66
+ const exportName = segments[segments.length - 1];
67
+ const modulePath = segments.slice(0, -1).join("::");
68
+
69
+ if (exportName) {
70
+ const typeHover = lookupStdlibTypeExportHoverInfo(modulePath, exportName);
71
+
72
+ if (typeHover) {
73
+ return formatStdlibHoverMarkdown(typeHover);
74
+ }
75
+ }
76
+ }
77
+
78
+ const moduleValue = lookupStdlibModuleValue(identifier);
79
+
80
+ if (moduleValue) {
81
+ const hover = lookupStdlibBuiltinHoverInfo(moduleValue);
82
+ return hover ? formatStdlibHoverMarkdown(hover) : null;
83
+ }
84
+
85
+ const moduleHover = lookupStdlibModuleHoverInfo(identifier);
86
+ return moduleHover ? formatStdlibHoverMarkdown(moduleHover) : null;
87
+ }
88
+
89
+ function importedQualifiedStdlibHover(
90
+ loadedModule: LoadedModule,
91
+ identifier: string,
92
+ ): string | null {
93
+ const segments = identifier.split("::");
94
+
95
+ if (segments.length !== 2) {
96
+ return null;
97
+ }
98
+
99
+ const qualifier = segments[0];
100
+ const exportName = segments[1];
101
+
102
+ if (!qualifier || !exportName) {
103
+ return null;
104
+ }
105
+
106
+ const modulePath = importedStdNamespaceModulePath(loadedModule, qualifier);
107
+
108
+ if (!modulePath) {
109
+ return null;
110
+ }
111
+
112
+ const builtinId = lookupStdlibBuiltinValue(modulePath, exportName);
113
+
114
+ if (builtinId) {
115
+ const hover = lookupStdlibBuiltinHoverInfo(builtinId);
116
+ return hover ? formatStdlibHoverMarkdown(hover) : null;
117
+ }
118
+
119
+ const typeHover = lookupStdlibTypeExportHoverInfo(modulePath, exportName);
120
+ return typeHover ? formatStdlibHoverMarkdown(typeHover) : null;
121
+ }
122
+
123
+ function importedBareStdlibHover(
124
+ loadedModule: LoadedModule,
125
+ identifier: string,
126
+ ): string | null {
127
+ const namespaceModulePath = importedStdNamespaceModulePath(loadedModule, identifier);
128
+
129
+ if (namespaceModulePath) {
130
+ const moduleValue = lookupStdlibModuleValue(namespaceModulePath);
131
+
132
+ if (moduleValue) {
133
+ const hover = lookupStdlibBuiltinHoverInfo(moduleValue);
134
+ return hover ? formatStdlibHoverMarkdown(hover) : null;
135
+ }
136
+
137
+ const moduleHover = lookupStdlibModuleHoverInfo(namespaceModulePath);
138
+ return moduleHover ? formatStdlibHoverMarkdown(moduleHover) : null;
139
+ }
140
+
141
+ const memberModulePath = importedStdMemberModulePath(loadedModule, identifier);
142
+
143
+ if (!memberModulePath) {
144
+ return null;
145
+ }
146
+
147
+ const builtinId = lookupStdlibBuiltinValue(memberModulePath, identifier);
148
+
149
+ if (builtinId) {
150
+ const hover = lookupStdlibBuiltinHoverInfo(builtinId);
151
+ return hover ? formatStdlibHoverMarkdown(hover) : null;
152
+ }
153
+
154
+ const typeHover = lookupStdlibTypeExportHoverInfo(memberModulePath, identifier);
155
+ return typeHover ? formatStdlibHoverMarkdown(typeHover) : null;
156
+ }
157
+
158
+ export function findStdlibHoverAtPosition(
159
+ document: TextDocument,
160
+ position: LspPosition,
161
+ loadedModule: LoadedModule,
162
+ ): string | null {
163
+ const identifier = identifierAtPosition(document, position);
164
+
165
+ if (!identifier) {
166
+ return null;
167
+ }
168
+
169
+ if (identifier.startsWith("std::")) {
170
+ return directStdlibHover(identifier);
171
+ }
172
+
173
+ if (identifier.includes("::")) {
174
+ return importedQualifiedStdlibHover(loadedModule, identifier);
175
+ }
176
+
177
+ return importedBareStdlibHover(loadedModule, identifier);
178
+ }
@@ -0,0 +1,165 @@
1
+ import type {
2
+ LoadedModule,
3
+ RootModuleGraph,
4
+ SourceSpan,
5
+ TypeAliasDeclarationNode,
6
+ TypeNode,
7
+ } from "@rank-lang/compiler";
8
+ import type { Position as LspPosition } from "vscode-languageserver/node.js";
9
+ import type { TextDocument } from "vscode-languageserver-textdocument";
10
+
11
+ interface TypeSymbolTarget {
12
+ filePath: string;
13
+ name: string;
14
+ span: SourceSpan;
15
+ type: TypeNode;
16
+ }
17
+
18
+ function isIdentifierCharacter(char: string | undefined): boolean {
19
+ return char !== undefined && /[A-Za-z0-9_:]/.test(char);
20
+ }
21
+
22
+ export function identifierAtPosition(document: TextDocument, position: LspPosition): string | null {
23
+ const text = document.getText();
24
+ const offset = document.offsetAt(position);
25
+ let start = offset;
26
+ let end = offset;
27
+
28
+ if (isIdentifierCharacter(text[offset])) {
29
+ end += 1;
30
+ } else if (isIdentifierCharacter(text[offset - 1])) {
31
+ start -= 1;
32
+ } else {
33
+ return null;
34
+ }
35
+
36
+ while (start > 0 && isIdentifierCharacter(text[start - 1])) {
37
+ start -= 1;
38
+ }
39
+
40
+ while (end < text.length && isIdentifierCharacter(text[end])) {
41
+ end += 1;
42
+ }
43
+
44
+ return text.slice(start, end);
45
+ }
46
+
47
+ function typeAliasDeclarationInModule(
48
+ loadedModule: LoadedModule,
49
+ name: string,
50
+ ): TypeAliasDeclarationNode | undefined {
51
+ return loadedModule.program.declarations.find(
52
+ (declaration): declaration is TypeAliasDeclarationNode =>
53
+ declaration.kind === "TypeAliasDeclaration" && declaration.name === name,
54
+ );
55
+ }
56
+
57
+ function localTypeSymbolTarget(
58
+ loadedModule: LoadedModule,
59
+ name: string,
60
+ ): TypeSymbolTarget | null {
61
+ const declaration = typeAliasDeclarationInModule(loadedModule, name);
62
+
63
+ if (!declaration?.nameSpan) {
64
+ return null;
65
+ }
66
+
67
+ return {
68
+ filePath: loadedModule.filePath,
69
+ name,
70
+ span: declaration.nameSpan,
71
+ type: declaration.type,
72
+ };
73
+ }
74
+
75
+ export function findTypeSymbolAtPosition(
76
+ document: TextDocument,
77
+ position: LspPosition,
78
+ moduleGraph: RootModuleGraph,
79
+ loadedModule: LoadedModule,
80
+ ): TypeSymbolTarget | null {
81
+ const name = identifierAtPosition(document, position);
82
+
83
+ if (!name || name.includes("::")) {
84
+ return null;
85
+ }
86
+
87
+ const local = localTypeSymbolTarget(loadedModule, name);
88
+ if (local) {
89
+ return local;
90
+ }
91
+
92
+ for (const importNode of loadedModule.program.imports) {
93
+ if (importNode.clause.kind !== "NamedImportClause") {
94
+ continue;
95
+ }
96
+
97
+ if (!importNode.clause.names.includes(name)) {
98
+ continue;
99
+ }
100
+
101
+ for (const mod of Object.values(moduleGraph.modules)) {
102
+ if (mod === loadedModule || !mod.exportedTypeNames.includes(name)) {
103
+ continue;
104
+ }
105
+
106
+ const imported = localTypeSymbolTarget(mod, name);
107
+ if (imported) {
108
+ return imported;
109
+ }
110
+ }
111
+ }
112
+
113
+ return null;
114
+ }
115
+
116
+ export function formatTypeNode(typeNode: TypeNode): string {
117
+ switch (typeNode.kind) {
118
+ case "NamedType":
119
+ return typeNode.name;
120
+ case "NeverType":
121
+ return "never";
122
+ case "ListType":
123
+ return `[${formatTypeNode(typeNode.itemType)}]`;
124
+ case "LiteralType":
125
+ return typeNode.lexeme;
126
+ case "ParseTemplateType":
127
+ return `parse${typeNode.raw}`;
128
+ case "RoutePathType":
129
+ return `match\`${typeNode.raw}\``;
130
+ case "GenericType":
131
+ return `${typeNode.name}<${typeNode.arguments.map((argument) => formatTypeNode(argument)).join(", ")}>`;
132
+ case "ConfiguredType":
133
+ return `${formatTypeNode(typeNode.base)} { ... }`;
134
+ case "MappedType": {
135
+ const optionalSuffix = typeNode.optional === "add" ? "?" : typeNode.optional === "remove" ? "-?" : "";
136
+ return `Object { [${typeNode.keyName} in ${formatTypeNode(typeNode.domainType)}]${optionalSuffix}: ${formatTypeNode(typeNode.valueType)} }`;
137
+ }
138
+ case "IndexedAccessType":
139
+ return `${formatTypeNode(typeNode.objectType)}[${formatTypeNode(typeNode.indexType)}]`;
140
+ case "KeyofType":
141
+ return `keyof ${formatTypeNode(typeNode.operand)}`;
142
+ case "ConditionalType":
143
+ return `${formatTypeNode(typeNode.checkType)} extends ${formatTypeNode(typeNode.extendsType)} ? ${formatTypeNode(typeNode.trueType)} : ${formatTypeNode(typeNode.falseType)}`;
144
+ case "ObjectType": {
145
+ const fields = typeNode.fields.map((field) => `${field.name}${field.optional ? "?" : ""}: ${formatTypeNode(field.valueType)}`);
146
+ if (typeNode.restField) {
147
+ fields.push(`...: ${formatTypeNode(typeNode.restField.valueType)}`);
148
+ }
149
+ return `Object { ${fields.join(", ")} }`;
150
+ }
151
+ case "RefinementType": {
152
+ const fields = typeNode.fields.map((field) => `${field.name}${field.optional ? "?" : ""}: ${formatTypeNode(field.valueType)}`);
153
+ if (typeNode.restField) {
154
+ fields.push(`...: ${formatTypeNode(typeNode.restField.valueType)}`);
155
+ }
156
+ return `${formatTypeNode(typeNode.base)} { ${fields.join(", ")} }`;
157
+ }
158
+ case "FunctionType":
159
+ return `(${typeNode.parameterTypes.map((parameterType) => formatTypeNode(parameterType)).join(", ")}) => ${formatTypeNode(typeNode.returnType)}`;
160
+ case "UnionType":
161
+ return typeNode.members.map((member) => formatTypeNode(member)).join(" | ");
162
+ case "IntersectionType":
163
+ return typeNode.members.map((member) => formatTypeNode(member)).join(" & ");
164
+ }
165
+ }