Skip to content

Commit abeb525

Browse files
committed
☕ Implement return type generation
1 parent 033dcbf commit abeb525

File tree

4 files changed

+99
-21
lines changed

4 files changed

+99
-21
lines changed

scripts/gen-function/format.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ export function formatDocs(docs: string): string[] {
2626

2727
function formatVariants(fn: string, vars: Variant[]): string[] {
2828
if (vars.length === 0) {
29-
return formatVariants(fn, [[]]);
29+
return formatVariants(fn, [{ args: [], restype: "unknown" }]);
3030
}
31-
const lines = vars.map((variant) => {
32-
const args = variant.map(({ name, optional }) => {
31+
const lines = vars.map(({ args, restype }) => {
32+
const argTypes = args.map(({ name, optional }) => {
3333
name = translate[name] ?? name;
3434
return `${name}${optional ? "?" : ""}: unknown`;
3535
});
3636
return `export function ${fn}(denops: Denops, ${
37-
args.join(", ")
38-
}): Promise<unknown>;`;
37+
argTypes.join(", ")
38+
}): Promise<${restype}>;`;
3939
});
4040
return lines;
4141
}

scripts/gen-function/gen-function.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ for (const vimHelpDownloadUrl of vimHelpDownloadUrls) {
4545
console.log(`Download from ${vimHelpDownloadUrl}`);
4646
}
4747
const vimHelps = await Promise.all(vimHelpDownloadUrls.map(downloadString));
48-
const vimDefs = vimHelps.map(parse).flat();
48+
const vimDefs = parse(vimHelps.join("\n"));
4949
const vimFnSet = difference(new Set(vimDefs.map((def) => def.fn)), manualFnSet);
5050

5151
const nvimHelpDownloadUrls = [
@@ -57,7 +57,7 @@ for (const nvimHelpDownloadUrl of nvimHelpDownloadUrls) {
5757
console.log(`Download from ${nvimHelpDownloadUrl}`);
5858
}
5959
const nvimHelps = await Promise.all(nvimHelpDownloadUrls.map(downloadString));
60-
const nvimDefs = nvimHelps.map(parse).flat();
60+
const nvimDefs = parse(nvimHelps.join("\n"));
6161
const nvimFnSet = difference(
6262
new Set(nvimDefs.map((def) => def.fn)),
6363
manualFnSet,

scripts/gen-function/parse.ts

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@ import { Definition, Variant } from "./types.ts";
22
import { createMarkdownFromHelp } from "../markdown.ts";
33
import { Counter, regexIndexOf } from "../utils.ts";
44

5+
type FnDef = {
6+
restype: string;
7+
};
8+
9+
export const RESTYPE_MAP: Readonly<Record<string, string>> = {
10+
any: "unknown",
11+
blob: "unknown",
12+
bool: "boolean",
13+
boolean: "boolean",
14+
channel: "unknown",
15+
dict: "Record<string, unknown>",
16+
float: "number",
17+
funcref: "unknown",
18+
job: "unknown",
19+
list: "unknown[]",
20+
none: "void",
21+
number: "number",
22+
string: "string",
23+
};
24+
525
/**
626
* Parse Vim/Neovim help.
727
*
@@ -17,7 +37,9 @@ import { Counter, regexIndexOf } from "../utils.ts";
1737
*/
1838
export function parse(content: string): Definition[] {
1939
// Remove modeline
20-
content = content.replace(/\n vim:[^\n]*\s*$/, "");
40+
content = content.replace(/\n\s*vim:[^\n]*:\s*\n/g, "\n---\n");
41+
42+
const fnDefs = parseFunctionList(content);
2143

2244
const definitions: Definition[] = [];
2345
let last = -1;
@@ -29,7 +51,7 @@ export function parse(content: string): Definition[] {
2951
continue;
3052
}
3153
const { block, start, end } = extractBlock(content, index);
32-
const definition = parseBlock(fn, block);
54+
const definition = parseBlock(fn, block, fnDefs.get(fn));
3355
if (definition) {
3456
definitions.push(definition);
3557
last = end;
@@ -88,7 +110,11 @@ function extractBlock(content: string, index: number): {
88110
*
89111
* This function parse content like above and return `Definition`.
90112
*/
91-
function parseBlock(fn: string, body: string): Definition | undefined {
113+
function parseBlock(
114+
fn: string,
115+
body: string,
116+
def?: FnDef,
117+
): Definition | undefined {
92118
// Separate vars/docs blocks
93119
const reTags = /(?:[ \t]+\*[^*\s]+\*)+[ \t]*$/.source;
94120
const reArgs = /\([^()]*(?:\n[ \t][^()]*)?\)/.source;
@@ -115,9 +141,7 @@ function parseBlock(fn: string, body: string): Definition | undefined {
115141
() => varsBlock.split("\n").at(-1)!.replaceAll(/[^\t]/g, " "),
116142
);
117143

118-
const vars = varsBlock.split("\n")
119-
.map(parseVariant)
120-
.filter(<T>(x: T): x is NonNullable<T> => !!x);
144+
const vars = varsBlock.split("\n").map((s) => parseVariant(s, def));
121145
const docs = createMarkdownFromHelp(docsBlock);
122146

123147
return { fn, docs, vars };
@@ -139,16 +163,27 @@ function parseBlock(fn: string, body: string): Definition | undefined {
139163
*
140164
* This function parse content like above and return `Variant`.
141165
*/
142-
function parseVariant(variant: string): Variant | undefined {
166+
function parseVariant(variant: string, def?: FnDef): Variant {
143167
// Extract {args} part from {variant}
144-
const m = variant.match(/^\w+\((.+?)\)/);
145-
if (!m) {
146-
// The {variant} does not have {args}, probabliy it's not variant (ex. `strstr`)
147-
return undefined;
168+
const m = variant.match(/^\w+\((?<args>.*?)\)/)?.groups ?? {};
169+
const args = parseVariantArgs(m.args);
170+
171+
return {
172+
args,
173+
restype: "unknown",
174+
...def,
175+
};
176+
}
177+
178+
function parseVariantArgs(argsBody: string): Variant["args"] {
179+
if (argsBody.length === 0) {
180+
// The {variant} does not have {args}
181+
return [];
148182
}
149-
let optional = m[1].startsWith("[");
183+
184+
let optional = argsBody.startsWith("[");
150185
const counter = new Counter();
151-
const args = m[1].split(",").map((t) => {
186+
const args = argsBody.split(",").map((t) => {
152187
const name = t.replaceAll(/[{}\[\]\s]/g, "");
153188
const spread = name.endsWith("...");
154189
const arg = {
@@ -172,3 +207,43 @@ function parseVariant(variant: string): Variant | undefined {
172207
});
173208
return uniqueArgs;
174209
}
210+
211+
/**
212+
* Extract function definitions from `builtin-function-list`.
213+
*
214+
* `builtin-function-list` block is constructed with following parts
215+
*
216+
* ```text
217+
* fn restype
218+
* ~~~~~~~~~~~~ ~~~~~~
219+
* filewritable({file}) Number |TRUE| if {file} is a writable file
220+
*
221+
* fn restype
222+
* ~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
223+
* filter({expr1}, {expr2}) List/Dict/Blob/String
224+
* remove items from {expr1} where
225+
* {expr2} is 0
226+
* ```
227+
*
228+
* - `restype` may be preceded by TAB, space or newline.
229+
* - `restype` may be splited by "/", ", " or " or ".
230+
* - `restype` may not exist.
231+
*/
232+
function parseFunctionList(content: string): Map<string, FnDef> {
233+
const s = content.match(/\*builtin-function-list\*\s.*?\n===/s)?.[0] ?? "";
234+
return new Map([
235+
...s.matchAll(
236+
/^(?<fn>\w+)\(.*?\)\s+(?<restype>\w+(?:(?:\/|, | or )\w+)*)/gms,
237+
) as Iterable<{ groups: { fn: string; restype: string } }>,
238+
].map(({ groups: { fn, restype } }) => {
239+
const restypes = restype.split(/\/|, | or /g).map((t) =>
240+
RESTYPE_MAP[t.toLowerCase()] ?? "unknown"
241+
);
242+
return [
243+
fn,
244+
{
245+
restype: [...new Set(restypes)].join(" | "),
246+
},
247+
];
248+
}));
249+
}

scripts/gen-function/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ export type Arg = {
44
optional: boolean;
55
};
66

7-
export type Variant = Arg[];
7+
export type Variant = {
8+
args: Arg[];
9+
restype: string;
10+
};
811

912
export type Definition = {
1013
fn: string;

0 commit comments

Comments
 (0)