Files
plainleaf/client/space_lua/api_metadata_coverage.test.ts
T

181 lines
5.6 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import yaml from "js-yaml";
import ts from "typescript";
import { expect, test } from "vitest";
import { isILuaFunction, type LuaEnv, LuaTable } from "./runtime.ts";
import { luaBuildStandardEnv } from "./stdlib.ts";
function collectUndocumentedBuiltins(
value: unknown,
path: string,
seen: WeakSet<object>,
undocumented: string[],
): void {
if (isILuaFunction(value)) {
if (!value.info?.description?.trim()) undocumented.push(path);
return;
}
if (!(value instanceof LuaTable) || seen.has(value)) return;
seen.add(value);
for (const key of value.keys()) {
if (typeof key !== "string") continue;
collectUndocumentedBuiltins(
value.rawGet(key),
`${path}.${key}`,
seen,
undocumented,
);
}
}
test("every standard-environment function has a description", () => {
const env: LuaEnv = luaBuildStandardEnv();
const undocumented: string[] = [];
const seen = new WeakSet<object>();
for (const name of env.keys()) {
if (name === "_G") continue;
collectUndocumentedBuiltins(env.get(name), name, seen, undocumented);
}
expect(undocumented.sort()).toEqual([]);
});
test("standard-library function names are derived from their environment path", () => {
const stdlibDir = join(import.meta.dirname, "stdlib");
const paths = [
join(import.meta.dirname, "stdlib.ts"),
...readdirSync(stdlibDir)
.filter((name) => name.endsWith(".ts"))
.map((name) => join(stdlibDir, name)),
];
const explicitNames: string[] = [];
for (const path of paths) {
const source = ts.createSourceFile(
path,
readFileSync(path, "utf8"),
ts.ScriptTarget.Latest,
true,
);
const visit = (node: ts.Node): void => {
if (
ts.isNewExpression(node) &&
["LuaBuiltinFunction", "LuaNativeJSFunction"].includes(
node.expression.getText(source),
)
) {
const definition = node.arguments?.[0];
if (definition && ts.isObjectLiteralExpression(definition)) {
const name = definition.properties.find(
(property) =>
ts.isPropertyAssignment(property) &&
propertyName(property.name, source) === "name",
);
if (name) {
const line =
source.getLineAndCharacterOfPosition(name.getStart()).line + 1;
explicitNames.push(
`${relative(import.meta.dirname, path)}:${line}`,
);
}
}
}
ts.forEachChild(node, visit);
};
visit(source);
}
expect(explicitNames).toEqual([]);
});
function propertyName(node: ts.PropertyName, source: ts.SourceFile): string {
return node.getText(source).replace(/^["']|["']$/g, "");
}
function hasInlineDescription(
initializer: ts.Expression,
source: ts.SourceFile,
): boolean {
if (!ts.isObjectLiteralExpression(initializer)) return false;
return initializer.properties.some(
(property) =>
ts.isPropertyAssignment(property) &&
propertyName(property.name, source) === "description" &&
(ts.isStringLiteral(property.initializer) ||
ts.isNoSubstitutionTemplateLiteral(property.initializer)),
);
}
test("every static syscall registration has an inline description", () => {
const syscallDir = join(import.meta.dirname, "..", "plugos", "syscalls");
const paths = [
...readdirSync(syscallDir)
.filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts"))
.map((name) => join(syscallDir, name)),
join(import.meta.dirname, "syscalls.ts"),
];
const undocumented: string[] = [];
let syscallCount = 0;
for (const path of paths) {
const filename = relative(import.meta.dirname, path);
const source = ts.createSourceFile(
path,
readFileSync(path, "utf8"),
ts.ScriptTarget.Latest,
true,
);
const visit = (node: ts.Node): void => {
if (ts.isPropertyAssignment(node)) {
const name = propertyName(node.name, source);
if (/^(?:lua:)?[A-Za-z][\w-]*(?:\.[\w-]+)+$/.test(name)) {
syscallCount++;
if (!hasInlineDescription(node.initializer, source)) {
undocumented.push(`${filename}: ${name}`);
}
}
}
ts.forEachChild(node, visit);
};
visit(source);
}
expect(syscallCount).toBeGreaterThan(0);
expect(undocumented.sort()).toEqual([]);
});
test("every built-in plug syscall has an inline description", () => {
const plugsDir = join(import.meta.dirname, "..", "..", "plugs");
const undocumented: string[] = [];
let syscallCount = 0;
for (const directory of readdirSync(plugsDir, { withFileTypes: true }).filter(
(entry) => entry.isDirectory(),
)) {
const directoryPath = join(plugsDir, directory.name);
for (const filename of readdirSync(directoryPath).filter((name) =>
name.endsWith(".plug.yaml"),
)) {
const manifest = yaml.load(
readFileSync(join(directoryPath, filename), "utf8"),
) as {
functions?: Record<
string,
{ syscall?: string | { name?: string; description?: string } }
>;
};
for (const [functionName, definition] of Object.entries(
manifest.functions ?? {},
)) {
if (!definition.syscall) continue;
syscallCount++;
if (
typeof definition.syscall === "string" ||
!definition.syscall.description?.trim()
) {
undocumented.push(`${filename}: ${functionName}`);
}
}
}
}
expect(syscallCount).toBeGreaterThan(0);
expect(undocumented.sort()).toEqual([]);
});