From 32fdf0be3c3ef58c4a8b4f67cf4bf7297d25da54 Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Wed, 25 Feb 2026 23:38:37 -0700 Subject: [PATCH] Add --table-mapping and --strip-helpers CLI options --table-mapping "OldName:NewName" remaps the first segment of qualified table names in function, table, field, enum, and global attributes. Useful for generating context-specific types (e.g. Spring -> SpringSynced). --strip-helpers removes standalone helper type definitions (classes, enums, aliases) from the output, keeping only function and table declarations. Prevents duplicate type definitions when generating context-specific outputs alongside per-file outputs. Bump version to 3.4.0. --- package.json | 2 +- src/cli.ts | 37 +++++++- src/index.ts | 17 +++- src/stripHelpers.ts | 15 +++ src/tableMapping.ts | 72 ++++++++++++++ src/test/stripHelpers.test.ts | 125 +++++++++++++++++++++++++ src/test/tableMapping.test.ts | 171 ++++++++++++++++++++++++++++++++++ src/test/utility/harness.ts | 15 ++- 8 files changed, 446 insertions(+), 8 deletions(-) create mode 100644 src/stripHelpers.ts create mode 100644 src/tableMapping.ts create mode 100644 src/test/stripHelpers.test.ts create mode 100644 src/test/tableMapping.test.ts diff --git a/package.json b/package.json index 278ac91..f04278b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lua-doc-extractor", - "version": "3.3.3", + "version": "3.4.0", "description": "Extracts lua documentation from C-style comments", "main": "dist/src/index.js", "homepage": "https://github.com/rhys-vdw/lua-doc-extractor", diff --git a/src/cli.ts b/src/cli.ts index 982cbbc..596d410 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,6 +12,7 @@ import { addHeader, formatDocs, getDocs, processDocs } from "."; import project from "../package.json"; import { Doc } from "./doc"; import { toResultAsync } from "./result"; +import { parseTableMappings } from "./tableMapping"; interface Options { src: string[]; @@ -21,6 +22,8 @@ interface Options { error?: boolean; repo?: string; file?: string; + "table-mapping"?: string[]; + "strip-helpers"?: boolean; } const optionList = [ { @@ -71,6 +74,22 @@ const optionList = [ `, }, + { + name: "table-mapping", + type: String, + multiple: true, + defaultValue: [], + typeLabel: "{underline from:to} ...", + description: + 'Remap table names in the output. Format: "OldName:NewName". Can be specified multiple times.\n', + }, + { + name: "strip-helpers", + type: Boolean, + defaultValue: false, + description: + "{white (Default: false)} Strip standalone helper types (classes, enums, aliases) from the output. Keeps only function and table declarations.\n", + }, { name: "error", type: Boolean, @@ -135,6 +154,8 @@ async function runAsync() { repo, file, error: enableErrorCode, + "table-mapping": tableMappingRaw, + "strip-helpers": stripHelpers, } = options; if (version) { @@ -147,6 +168,8 @@ async function runAsync() { process.exit(0); } + const tableMapping = parseTableMappings(tableMappingRaw ?? []); + const srcFiles = await glob(src); if (srcFiles.length === 0) { @@ -203,7 +226,7 @@ async function runAsync() { const rel = relative(cwd(), path); const outPath = join(dest, `${rel}.lua`); if (ds.length > 0) { - await writeLibraryFile(ds, outPath, repo, [path]); + await writeLibraryFile(ds, outPath, repo, [path], tableMapping, stripHelpers); } }) ); @@ -215,7 +238,9 @@ async function runAsync() { valid.flatMap(([, ds]) => ds), outPath, repo, - sources + sources, + tableMapping, + stripHelpers ); } @@ -233,10 +258,14 @@ async function writeLibraryFile( docs: Doc[], outPath: string, repo?: string, - sources: string[] = [] + sources: string[] = [], + tableMapping?: ReadonlyMap, + stripHelpers?: boolean ) { try { - const formattedDocs = formatDocs(processDocs(docs, repo ?? null)); + const formattedDocs = formatDocs( + processDocs(docs, repo ?? null, { tableMapping, stripHelpers }) + ); await mkdir(dirname(outPath), { recursive: true }); await writeFile(outPath, addHeader(formattedDocs, sources)); console.log(chalk`{bold.blue ►} '{white ${outPath}}'`); diff --git a/src/index.ts b/src/index.ts index bd8f21d..8840cd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,8 @@ import { header } from "./header"; import { fail, Result, success } from "./result"; import { applyRules } from "./rules"; import { appendSourceLinks } from "./source"; +import { stripHelperTypes } from "./stripHelpers"; +import { applyTableMapping } from "./tableMapping"; import { addTables, mergeTables } from "./tables"; import { trimTrailingWhitespace } from "./utility"; @@ -41,10 +43,23 @@ function runProcessors(docs: Doc[], processors: readonly DocProcessor[]) { return processors.reduce((acc, processor) => processor(acc), docs); } -export function processDocs(docs: Doc[], repoUrl: string | null): Doc[] { +export interface ProcessDocsOptions { + tableMapping?: ReadonlyMap | null; + stripHelpers?: boolean; +} + +export function processDocs( + docs: Doc[], + repoUrl: string | null, + options?: ProcessDocsOptions +): Doc[] { + const opts = options ?? {}; + return runProcessors(docs, [ removeEmptyDocs, appendSourceLinks(repoUrl), + applyTableMapping(opts.tableMapping ?? null), + ...(opts.stripHelpers ? [stripHelperTypes] : []), processGlobals, addTables, addTableToEnumFields, diff --git a/src/stripHelpers.ts b/src/stripHelpers.ts new file mode 100644 index 0000000..4574397 --- /dev/null +++ b/src/stripHelpers.ts @@ -0,0 +1,15 @@ +import { isAttribute } from "./attribute"; +import { Doc } from "./doc"; + +/** + * Strip docs that only define helper types (classes, enums, aliases). + * Keep docs that define functions or tables -- these are the table-scoped + * API members that benefit from --table-mapping. + */ +export function stripHelperTypes(docs: Doc[]): Doc[] { + return docs.filter((doc) => + doc.attributes.some( + (a) => isAttribute(a, "function") || isAttribute(a, "table") + ) + ); +} diff --git a/src/tableMapping.ts b/src/tableMapping.ts new file mode 100644 index 0000000..9033d88 --- /dev/null +++ b/src/tableMapping.ts @@ -0,0 +1,72 @@ +import { Attribute, isAttribute } from "./attribute"; +import { Doc } from "./doc"; + +function mapName( + name: readonly string[], + mapping: ReadonlyMap +): readonly string[] { + if (name.length === 0) return name; + const mapped = mapping.get(name[0]); + if (mapped == null) return name; + return [mapped, ...name.slice(1)]; +} + +export const applyTableMapping = + (mapping: ReadonlyMap | null) => + (docs: Doc[]): Doc[] => { + if (mapping == null || mapping.size === 0) return docs; + + docs.forEach((doc) => { + doc.attributes = doc.attributes.map((attr) => { + if (isAttribute(attr, "function")) { + return { + ...attr, + args: { ...attr.args, name: mapName(attr.args.name, mapping) }, + }; + } + if (isAttribute(attr, "table")) { + return { + ...attr, + args: { ...attr.args, name: mapName(attr.args.name, mapping) }, + }; + } + if (isAttribute(attr, "field")) { + return { + ...attr, + args: { ...attr.args, name: mapName(attr.args.name, mapping) }, + }; + } + if (isAttribute(attr, "enum")) { + return { + ...attr, + args: { ...attr.args, name: mapName(attr.args.name, mapping) }, + }; + } + if (isAttribute(attr, "global")) { + return { + ...attr, + args: { ...attr.args, name: mapName(attr.args.name, mapping) }, + }; + } + return attr; + }); + }); + + return docs; + }; + +export function parseTableMappings( + raw: readonly string[] +): Map { + const mapping = new Map(); + for (const entry of raw) { + const colon = entry.indexOf(":"); + if (colon === -1) { + throw new Error( + `Invalid table-mapping "${entry}". Expected format: "OldName:NewName"` + ); + } + mapping.set(entry.slice(0, colon), entry.slice(colon + 1)); + } + return mapping; +} diff --git a/src/test/stripHelpers.test.ts b/src/test/stripHelpers.test.ts new file mode 100644 index 0000000..60cf356 --- /dev/null +++ b/src/test/stripHelpers.test.ts @@ -0,0 +1,125 @@ +import dedent from "dedent-js"; +import { testInput } from "./utility/harness"; + +const opts = { stripHelpers: true }; + +testInput( + "stripHelpers: keeps function declarations", + dedent` + /*** + * Get the current game frame. + * + * @function Spring.GetGameFrame + * @return integer frame + */ + `, + dedent` + ---Get the current game frame. + --- + ---@return integer frame + function Spring.GetGameFrame() end + `, + undefined, + opts +); + +testInput( + "stripHelpers: keeps table declarations", + dedent` + /*** @table Spring.MoveCtrl */ + `, + dedent` + Spring.MoveCtrl = {} + `, + undefined, + opts +); + +testInput( + "stripHelpers: strips standalone class", + dedent` + /*** + * @class losAccess + * @x_helper + * @field public private boolean? only readable by the ally (default) + * @field public allied boolean? readable by ally + ingame allied + */ + `, + ``, + undefined, + opts +); + +testInput( + "stripHelpers: strips standalone enum", + dedent` + /*** + * @enum LosMask + * @field LOS_INLOS integer + * @field LOS_INRADAR integer + */ + `, + ``, + undefined, + opts +); + +testInput( + "stripHelpers: strips standalone alias", + dedent` + /*** + * @alias Heading integer + */ + `, + ``, + undefined, + opts +); + +testInput( + "stripHelpers: keeps function, strips class from same input", + dedent` + /*** + * @class losAccess + * @x_helper + * @field public private boolean? only readable by the ally (default) + */ + + /*** + * Set game rules param. + * + * @function Spring.SetGameRulesParam + * @param name string + */ + `, + dedent` + ---Set game rules param. + --- + ---@param name string + function Spring.SetGameRulesParam(name) end + `, + undefined, + opts +); + +testInput( + "stripHelpers: combines with table-mapping", + dedent` + /*** + * @class losAccess + * @x_helper + * @field public private boolean? only readable by the ally (default) + */ + + /*** + * @function Spring.SetGameRulesParam + * @param name string + */ + `, + dedent` + ---@param name string + function SpringSynced.SetGameRulesParam(name) end + `, + undefined, + { ...opts, tableMapping: new Map([["Spring", "SpringSynced"]]) } +); diff --git a/src/test/tableMapping.test.ts b/src/test/tableMapping.test.ts new file mode 100644 index 0000000..f4431e9 --- /dev/null +++ b/src/test/tableMapping.test.ts @@ -0,0 +1,171 @@ +import dedent from "dedent-js"; +import { testInput } from "./utility/harness"; + +const springToSynced = new Map([["Spring", "SpringSynced"]]); + +testInput( + "Remaps function table name", + dedent` + /*** + * Get the current game frame. + * + * @function Spring.GetGameFrame + * @return integer frame + */ + `, + dedent` + ---Get the current game frame. + --- + ---@return integer frame + function SpringSynced.GetGameFrame() end + `, + undefined, + { tableMapping: springToSynced } +); + +testInput( + "Remaps method table name", + dedent` + /*** + * Does foo. + * + * @function Spring:Foo + * @param x integer + */ + `, + dedent` + ---Does foo. + --- + ---@param x integer + function SpringSynced:Foo(x) end + `, + undefined, + { tableMapping: springToSynced } +); + +testInput( + "Remaps table declaration", + dedent` + /*** @table Spring */ + `, + dedent` + SpringSynced = {} + `, + undefined, + { tableMapping: springToSynced } +); + +testInput( + "Remaps nested table declaration", + dedent` + /*** @table Spring.MoveCtrl */ + `, + dedent` + SpringSynced.MoveCtrl = {} + `, + undefined, + { tableMapping: springToSynced } +); + +testInput( + "Remaps field table name", + dedent` + /*** + * @field Spring.MoveCtrl MoveCtrl + */ + `, + dedent` + ---@type MoveCtrl + SpringSynced.MoveCtrl = nil + `, + undefined, + { tableMapping: springToSynced } +); + +testInput( + "Leaves unmapped tables unchanged", + dedent` + /*** + * Does bar. + * + * @function Other.Bar + * @param y number + */ + `, + dedent` + ---Does bar. + --- + ---@param y number + function Other.Bar(y) end + `, + undefined, + { tableMapping: springToSynced } +); + +testInput( + "No mapping passes through unchanged", + dedent` + /*** + * @function Spring.Foo + * @param x integer + */ + `, + dedent` + ---@param x integer + function Spring.Foo(x) end + ` +); + +testInput( + "Remaps function with multiple params and return", + dedent` + /*** + * Changes alliance. + * + * @function Spring.SetAlly + * @param firstAllyTeamID integer + * @param secondAllyTeamID integer + * @param ally boolean + * @return nil + */ + `, + dedent` + ---Changes alliance. + --- + ---@param firstAllyTeamID integer + ---@param secondAllyTeamID integer + ---@param ally boolean + ---@return nil + function SpringSynced.SetAlly(firstAllyTeamID, secondAllyTeamID, ally) end + `, + undefined, + { tableMapping: springToSynced } +); + +testInput( + "Remaps merged table with functions", + dedent` + /*** @table Spring */ + + /*** + * @function Spring.Foo + * @param x integer + */ + + /*** + * @function Spring.Bar + * @param y string + */ + `, + dedent` + SpringSynced = {} + + ---@param x integer + function SpringSynced.Foo(x) end + + ---@param y string + function SpringSynced.Bar(y) end + `, + undefined, + { tableMapping: springToSynced } +); diff --git a/src/test/utility/harness.ts b/src/test/utility/harness.ts index d3705ae..5a42ecf 100644 --- a/src/test/utility/harness.ts +++ b/src/test/utility/harness.ts @@ -12,6 +12,8 @@ export interface TestInputOptions { only?: boolean; path?: string; outputLex?: boolean; + tableMapping?: ReadonlyMap; + stripHelpers?: boolean; } function writeJson(json: {}, name: string) { @@ -25,7 +27,14 @@ export function testInput( input: string, expected?: string, expectedComments?: readonly Comment[], - { only, repoUrl, path = "PATH", outputLex }: TestInputOptions = {} + { + only, + repoUrl, + path = "PATH", + outputLex, + tableMapping, + stripHelpers, + }: TestInputOptions = {} ) { const testFn = only ? test.only : test; testFn(name, (t) => { @@ -75,7 +84,9 @@ export function testInput( t.error(e, `docErrors[${i}]`); }); - const actual = formatDocs(processDocs(docs, repoUrl || null)); + const actual = formatDocs( + processDocs(docs, repoUrl || null, { tableMapping, stripHelpers }) + ); if (expected !== undefined) { t.isEqual(actual, expected, "formatDocs has correct output");