diff --git a/__tests__/api-writer/glua-api-writer.spec.ts b/__tests__/api-writer/glua-api-writer.spec.ts index 721fff9..3bf768c 100644 --- a/__tests__/api-writer/glua-api-writer.spec.ts +++ b/__tests__/api-writer/glua-api-writer.spec.ts @@ -421,6 +421,64 @@ describe('GLua API Writer', () => { } }); + it('should emit distinct direct and class overrides once across output files', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gluals-aliased-overrides-')); + const structuresOutputPath = path.join(tmpDir, 'structures.lua'); + const toolOutputPath = path.join(tmpDir, 'tool.lua'); + const classOverride = [ + '---@class ToolObjectSlot', + '---@field Ent Entity', + '---@class Tool', + 'Tool = Tool or {}', + ].join('\n'); + const directOverride = [ + '---@type boolean?', + 'TOOL.AddToMenu = true', + ].join('\n'); + const toolClass = { + type: 'class', + address: 'Tool', + name: 'Tool', + description: 'Sandbox tool methods.', + realm: 'shared', + url: 'https://wiki.facepunch.com/gmod/Tool', + parent: '', + }; + const toolStruct = { + type: 'struct', + address: 'TOOL', + name: 'TOOL', + description: 'Sandbox tool definition.', + realm: 'shared', + url: 'https://wiki.facepunch.com/gmod/Structures/TOOL', + fields: [{ + name: 'ScrapedOnly', + type: 'string', + description: 'A scraped field that the direct override replaces.', + }], + }; + const writer = new GluaApiWriter(tmpDir); + + try { + writer.addOverride('class.Tool', classOverride); + writer.addOverride('TOOL', directOverride); + writer.writePages([toolClass], toolOutputPath); + writer.writePages([toolStruct], structuresOutputPath); + writer.writeToDisk(); + + const api = [structuresOutputPath, toolOutputPath] + .filter(filePath => fs.existsSync(filePath)) + .map(filePath => fs.readFileSync(filePath, 'utf8')) + .join('\n'); + expect(api.match(/---@class ToolObjectSlot/g)).toHaveLength(1); + expect(api.match(/---@class Tool\n/g)).toHaveLength(1); + expect(api.match(/TOOL\.AddToMenu = true/g)).toHaveLength(1); + expect(api).not.toContain('ScrapedOnly'); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it('should allow overriding specific class declarations', () => { const writer = new GluaApiWriter(); const overrideStart = `---@class Custom_Entity_Fields : Parent`; diff --git a/src/api-writer/glua-api-writer.ts b/src/api-writer/glua-api-writer.ts index 0d09693..9577001 100644 --- a/src/api-writer/glua-api-writer.ts +++ b/src/api-writer/glua-api-writer.ts @@ -63,11 +63,15 @@ type PlannedClass = ClassMetadata & { name: string; outputFilePath: string; fields: StructField[]; - directOverride?: string; + directOverride?: { + pageAddress: string; + content: string; + }; }; export class GluaApiWriter { private readonly writtenClasses: Set = new Set(); + private readonly plannedPageOverrides: Set = new Set(); private readonly writtenLibraryGlobals: Set = new Set(); private readonly pageOverrides: Map = new Map(); private readonly plannedClasses: Map = new Map(); @@ -196,7 +200,7 @@ export class GluaApiWriter { const fileSafeAddress = safeFileName(page.address, '.'); if (this.pageOverrides.has(fileSafeAddress)) { if ((isClass(page) || isStruct(page) || isPanel(page)) - && this.writtenClasses.has(this.resolveToCanonicalClassName(page.name))) + && this.plannedPageOverrides.has(fileSafeAddress)) return ''; let api = ''; @@ -550,6 +554,7 @@ export class GluaApiWriter { private collectClassPlans() { this.plannedClasses.clear(); + this.plannedPageOverrides.clear(); const entries = [...this.files.entries()] .flatMap(([filePath, pages]) => pages.map(page => ({ ...page, filePath }))) @@ -596,8 +601,15 @@ export class GluaApiWriter { }); const metadataPages = metadataEntries.map(({ page }) => page); const directOverride = metadataEntries - .map(({ page }) => this.pageOverrides.get(safeFileName(page.address, '.'))) + .map(({ page }) => { + const pageAddress = safeFileName(page.address, '.'); + const content = this.pageOverrides.get(pageAddress); + return content === undefined ? undefined : { pageAddress, content }; + }) .find(override => override !== undefined); + if (directOverride) + this.plannedPageOverrides.add(directOverride.pageAddress); + const firstMetadataValue = (select: (page: WikiPage) => T | undefined) => { for (const page of metadataPages) { const value = select(page); @@ -656,8 +668,26 @@ export class GluaApiWriter { for (const plan of plans) { if (plan.directOverride !== undefined) { - this.writtenClasses.add(plan.name); - api += `${plan.directOverride.replace(/\n+$/g, '')}\n\n`; + const classOverrideAddress = `class.${plan.name}`; + // A direct page override replaces scraped fields, not a separate + // canonical class override (for example, TOOL and class.Tool). + if (plan.directOverride.pageAddress !== classOverrideAddress + && this.pageOverrides.has(classOverrideAddress)) { + api += this.writeClassStart( + plan.name, + plan.realm, + plan.url, + plan.parent, + plan.deprecated, + plan.description, + '', + true, + ); + } else { + this.writtenClasses.add(plan.name); + } + + api += `${plan.directOverride.content.replace(/\n+$/g, '')}\n\n`; continue; }