Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions __tests__/api-writer/glua-api-writer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <WikiPage>{
type: 'class',
address: 'Tool',
name: 'Tool',
description: 'Sandbox tool methods.',
realm: 'shared',
url: 'https://wiki.facepunch.com/gmod/Tool',
parent: '',
};
const toolStruct = <WikiPage>{
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`;
Expand Down
40 changes: 35 additions & 5 deletions src/api-writer/glua-api-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = new Set();
private readonly plannedPageOverrides: Set<string> = new Set();
private readonly writtenLibraryGlobals: Set<string> = new Set();
private readonly pageOverrides: Map<string, string> = new Map();
private readonly plannedClasses: Map<string, PlannedClass> = new Map();
Expand Down Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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 })))
Expand Down Expand Up @@ -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 = <T>(select: (page: WikiPage) => T | undefined) => {
for (const page of metadataPages) {
const value = select(page);
Expand Down Expand Up @@ -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;
}

Expand Down
Loading