From 7ee39b1beddaa28f8f2454fb8f3be7ad384d296a Mon Sep 17 00:00:00 2001 From: thinknathan Date: Wed, 31 Jan 2024 17:49:06 -0800 Subject: [PATCH] Generate @returns comments --- package.json | 2 +- src/xtgen.ts | 278 ++++++++++++++++++++++++++------------------- tests/game.project | 20 ++-- xtgen.mjs | 269 +++++++++++++++++++++++++------------------ 4 files changed, 334 insertions(+), 235 deletions(-) diff --git a/package.json b/package.json index bf0bd72..0f8c3cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tsd-ext-type-gen", - "version": "1.0.2", + "version": "1.0.4", "description": "TypeScript definitions generator that parses script APIs from Defold extensions", "repository": { "type": "git", diff --git a/src/xtgen.ts b/src/xtgen.ts index 6870b0a..0d8f0cd 100644 --- a/src/xtgen.ts +++ b/src/xtgen.ts @@ -9,6 +9,105 @@ import { parse } from 'yaml'; import yargs from 'yargs'; const DEBUG = false; +// Definitions file starts with this string +const HEADER = `/** @noSelfInFile */ +/// +/// \n`; + +// Invalid names in TypeScript +const INVALID_NAMES = [ + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'new', + 'null', + 'return', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', +]; + +// All valid types are listed here +const KNOWN_TYPES: { [key: string]: string } = { + TBL: '{}', + TABLE: '{}', + NUM: 'number', + NUMBER: 'number', + NUMMBER: 'number', // intentional typo + INT: 'number', + INTEGER: 'number', + FLOAT: 'number', + STR: 'string', + STRING: 'string', + BOOL: 'boolean', + BOOLEAN: 'boolean', + FN: '(...args: any[]) => any', + FUNC: '(...args: any[]) => any', + FUNCTION: '(...args: any[]) => any', + HASH: 'hash', + URL: 'url', + NODE: 'node', + BUFFER: 'buffer', + BUFFERSTREAM: 'bufferstream', + VECTOR3: 'vmath.vector3', + VECTOR4: 'vmath.vector4', + MATRIX: 'vmath.matrix4', + MATRIX4: 'vmath.matrix4', + QUAT: 'vmath.quaternion', + QUATERNION: 'vmath.quaternion', + QUATERTION: 'vmath.quaternion', // intentional typo + LUAUSERDATA: 'LuaUserdata', + // TO-DO: Parse strings that have pipes instead of relying on a naive lookup + 'STRING|HASH|URL': 'string | hash | url', + 'STRING|URL|HASH': 'string | hash | url', + 'HASH|STRING|URL': 'string | hash | url', + 'HASH|URL|STRING': 'string | hash | url', + 'URL|STRING|HASH': 'string | hash | url', + 'URL|HASH|STRING': 'string | hash | url', + 'STRING | HASH | URL': 'string | hash | url', + 'STRING | URL | HASH': 'string | hash | url', + 'HASH | STRING | URL': 'string | hash | url', + 'HASH | URL | STRING': 'string | hash | url', + 'URL | STRING | HASH': 'string | hash | url', + 'URL | HASH | STRING': 'string | hash | url', + 'STRING|HASH': 'string | hash', + 'HASH|STRING': 'string | hash', + 'STRING | HASH': 'string | hash', + 'HASH | STRING': 'string | hash', +}; + +// We'll make default return types slightly stricter than default param types +const DEFAULT_PARAM_TYPE = 'any'; +const DEFAULT_RETURN_TYPE = 'unknown'; +// Theoretically, it's impossible not to have a name, but just in case +const DEFAULT_NAME_IF_BLANK = 'missingName'; async function main() { console.time('Done in'); @@ -40,7 +139,7 @@ async function main() { try { iniData = await fs.promises.readFile(absPath, 'utf8'); } catch (e) { - console.error(e); + console.error(e, absPath); return; } @@ -53,7 +152,7 @@ async function main() { }); if (deps.length === 0) { - console.error('Could not find dependencies in project file.'); + console.error('Could not find dependencies in project file.', absPath); return; } @@ -106,7 +205,7 @@ async function main() { return parse(entry.getData().toString('utf8')); })[0]; } catch (e) { - console.error(e); + console.error(e, dep); return; } @@ -130,7 +229,7 @@ async function main() { JSON.stringify(api), ); } catch (e) { - console.error(e); + console.error(e, dep); return; } } @@ -139,8 +238,11 @@ async function main() { const result = generateTypeScriptDefinitions(api, details); if (result) { + // Guess the URL by including only the first 6 strings split by slash + const depUrl = dep.split('/').slice(0, 5).join('/'); + // Append header - const final = HEADER + result; + const final = `${HEADER}/**\n * @url ${depUrl}\n * @noResolution\n */\n${result}`; // Save the definitions to file try { @@ -149,7 +251,7 @@ async function main() { final, ); } catch (e) { - console.error(e); + console.error(e, dep); return; } } @@ -160,107 +262,6 @@ async function main() { console.log(`Exported definitions to ${path.join(process.cwd(), outDir)}`); } -// Definitions file starts with this string -const HEADER = `/** @noSelfInFile */ -/// -/// -/** @noResolution */\n`; - -// Invalid names in TypeScript -const INVALID_NAMES = [ - 'break', - 'case', - 'catch', - 'class', - 'const', - 'continue', - 'debugger', - 'default', - 'delete', - 'do', - 'else', - 'export', - 'extends', - 'false', - 'finally', - 'for', - 'function', - 'if', - 'import', - 'in', - 'instanceof', - 'new', - 'null', - 'return', - 'super', - 'switch', - 'this', - 'throw', - 'true', - 'try', - 'typeof', - 'var', - 'void', - 'while', - 'with', - 'yield', -]; - -// All valid types are listed here -const KNOWN_TYPES: { [key: string]: string } = { - TBL: '{}', - TABLE: '{}', - NUM: 'number', - NUMBER: 'number', - NUMMBER: 'number', // intentional typo - INT: 'number', - INTEGER: 'number', - FLOAT: 'number', - STR: 'string', - STRING: 'string', - BOOL: 'boolean', - BOOLEAN: 'boolean', - FN: '(...args: any[]) => any', - FUNC: '(...args: any[]) => any', - FUNCTION: '(...args: any[]) => any', - HASH: 'hash', - URL: 'url', - NODE: 'node', - BUFFER: 'buffer', - BUFFERSTREAM: 'bufferstream', - VECTOR3: 'vmath.vector3', - VECTOR4: 'vmath.vector4', - MATRIX: 'vmath.matrix4', - MATRIX4: 'vmath.matrix4', - QUAT: 'vmath.quaternion', - QUATERNION: 'vmath.quaternion', - QUATERTION: 'vmath.quaternion', // intentional typo - LUAUSERDATA: 'LuaUserdata', - // TO-DO: Parse strings that have pipes instead of relying on a naive lookup - 'STRING|HASH|URL': 'string | hash | url', - 'STRING|URL|HASH': 'string | hash | url', - 'HASH|STRING|URL': 'string | hash | url', - 'HASH|URL|STRING': 'string | hash | url', - 'URL|STRING|HASH': 'string | hash | url', - 'URL|HASH|STRING': 'string | hash | url', - 'STRING | HASH | URL': 'string | hash | url', - 'STRING | URL | HASH': 'string | hash | url', - 'HASH | STRING | URL': 'string | hash | url', - 'HASH | URL | STRING': 'string | hash | url', - 'URL | STRING | HASH': 'string | hash | url', - 'URL | HASH | STRING': 'string | hash | url', - 'STRING|HASH': 'string | hash', - 'HASH|STRING': 'string | hash', - 'STRING | HASH': 'string | hash', - 'HASH | STRING': 'string | hash', -}; - -// We'll make default return types slightly stricter than default param types -const DEFAULT_PARAM_TYPE = 'any'; -const DEFAULT_RETURN_TYPE = 'unknown'; -// Theoretically, it's impossible not have a name, but just in case -const DEFAULT_NAME_IF_BLANK = 'missingName'; - // Utility Functions // Check if a string is all uppercase with optional underscores @@ -294,10 +295,14 @@ function getName(name: string) { let modifiedName = name.replace('...', 'args'); // Check against the reserved keywords in TypeScript if (INVALID_NAMES.includes(modifiedName)) { - console.warn(`Modifying invalid name ${modifiedName}`); modifiedName = modifiedName + '_'; } + // Sanitize type name, allow alpha-numeric and underscore modifiedName = modifiedName.replace(/[^a-zA-Z0-9_$]/g, '_'); + if (modifiedName !== name) { + console.warn(`Modifying invalid name "${name}" to "${modifiedName}"`); + } + return modifiedName; } @@ -333,18 +338,61 @@ function sanitizeForComment(str: string) { function getComments(entry: ScriptApiFunction) { // Make sure the description doesn't break out of the comment const desc = entry.desc || entry.description; - let newDesc = desc ? sanitizeForComment(desc) : ''; + let newDesc = desc ?? ''; // If params exist, let's create `@param`s in JSDoc format if (entry.parameters && Array.isArray(entry.parameters)) { newDesc = getParamComments(entry.parameters, newDesc); } + // Comments for `@returns` + const returnObj = entry.return || entry.returns; + if (returnObj) { + newDesc = getReturnComments(returnObj, newDesc); + } + + // Comments for `@example` if (entry.examples && Array.isArray(entry.examples)) { newDesc = getExampleComments(entry.examples, newDesc); } - return newDesc ? `/**\n * ${newDesc}\n */\n` : ''; + return newDesc ? `/**\n * ${sanitizeForComment(newDesc)}\n */\n` : ''; +} + +function getReturnComments( + returnObj: ScriptApiEntry | ScriptApiEntry[], + newDesc: string, +) { + let returnType = ''; + let comments = ''; + + if (Array.isArray(returnObj)) { + if (returnObj.length > 1) { + returnType = `LuaMultiReturn<[${returnObj.map((ret) => ret.type).join(', ')}]>`; + } else { + returnType = Array.isArray(returnObj[0].type) + ? returnObj[0].type.join(' | ') + : returnObj[0].type ?? ''; + } + // Add comments if they exist + if (returnObj.some((ret) => ret.desc || ret.description)) { + comments = `${returnObj.map((ret) => ret.desc || ret.description).join(' | ')}`; + } + } else if (returnObj.type) { + // Instead of getting a TS type here, use the raw Lua type + returnType = Array.isArray(returnObj.type) + ? returnObj.type.join(' | ') + : returnObj.type; + comments = getType(returnObj.type, 'return'); + } + + // If we've figured out a comment, but not a returnType + if (comments.length && !returnType.length) { + returnType = DEFAULT_RETURN_TYPE; + } + + newDesc += `\n * @returns {${returnType}} ${comments}`; + return newDesc; } function getParamComments(parameters: ScriptApiParameter[], newDesc: string) { @@ -361,7 +409,7 @@ function getParamComments(parameters: ScriptApiParameter[], newDesc: string) { } else { rawType = param.type; } - // Sanitize type name + // Sanitize type name, allow alpha-numeric, underscore and pipe rawType = rawType.replace(/[^a-zA-Z|0-9_$]/g, '_'); newDesc += ` {${rawType}}`; } @@ -369,7 +417,7 @@ function getParamComments(parameters: ScriptApiParameter[], newDesc: string) { const desc = param.desc || param.description; if (desc) { - newDesc += ` ${sanitizeForComment(desc)}`; + newDesc += ` ${desc}`; } if (param.fields && Array.isArray(param.fields)) { @@ -382,7 +430,7 @@ function getParamComments(parameters: ScriptApiParameter[], newDesc: string) { function getParamFields(fields: ScriptApiEntry[], newDesc: string) { fields.forEach((field) => { - newDesc += ` ${sanitizeForComment(JSON.stringify(field))}`; + newDesc += ` ${JSON.stringify(field)}`; }); return newDesc; } @@ -391,7 +439,7 @@ function getExampleComments(examples: ScriptApiExample[], newDesc: string) { examples.forEach((example) => { const desc = example.desc || example.description; if (desc) { - newDesc += `\n * @example ${sanitizeForComment(desc)}`; + newDesc += `\n * @example ${desc}`; } }); return newDesc; @@ -469,7 +517,7 @@ function getReturnType( if (returnObj.length > 1) { return `LuaMultiReturn<[${returnObj.map((ret) => getType(ret.type, 'return')).join(', ')}]>`; } else { - return `${returnObj.map((ret) => getType(ret.type, 'return')).join(', ')}`; + return `${getType(returnObj[0].type, 'return')}`; } } else if (returnObj.type) { return getType(returnObj.type, 'return'); diff --git a/tests/game.project b/tests/game.project index e9e5255..3bb8103 100644 --- a/tests/game.project +++ b/tests/game.project @@ -2,12 +2,14 @@ title = Test # Value from fuzz.script_api as a zip dependencies#0 = data:application/zip;base64,UEsDBBQACAAIADtMPlgAAAAAAAAAAJ8EAAAPACAAZnV6ei5zY3JpcHRfYXBpVVQNAAcCM7llAzO5ZQIzuWV1eAsAAQT1AQAABBQAAACllNFqgzAUhu/7FMeOeeFovdhdYawg29hg3UX7Aqk5tgGNkhxLJ334JWq7KNoVlitDzjnfn/w/zkCyDBcwfS2rypO7lyOh1F4upxMA+i7sES23Kdo9Rx2bfVMjcglJrqBthA92YOtYiYIgynldn2G2RaUX5hNgdiYlpiEKmgq7WkpSyphEA7arhdnxqIH2CIXKD4Ijd1GxQc3PLQVTBkEXZAcbO0gHq0kZ9e5BC94YYg8ElMMWwV4A+Rw+S012z+DAUsGhGXVRo5BKJYek2AHPPBoRFCgxKqhBjz4AHllWpNhhOu9obWKg65r+jOmk61IY7FD6ihF60lzP4++yKOk2z95Qou3UwCS07S7Ps6Pq8Jgact/sqoNheNXDk/Rt3E77pd4PPN9XYZWy9H+mOnLo8UHTkJqnbqJ+bb+vqmH1/qDhYQBBCGH4d6CGLOoQ5ldCvmvd4qNW3ZKv1nMT3aExolHWj1jkr9ab5WrTj9WdiVX9D/oBUEsHCN+ExiB1AQAAnwQAAFBLAQIUAxQACAAIADtMPljfhMYgdQEAAJ8EAAAPACAAAAAAAAAAAACkgQAAAABmdXp6LnNjcmlwdF9hcGlVVA0ABwIzuWUDM7llAjO5ZXV4CwABBPUBAAAEFAAAAFBLBQYAAAAAAQABAF0AAADSAQAAAAA= -dependencies#1 = https://github.com/thejustinwalsh/defold-xmath/archive/refs/heads/main.zip -dependencies#2 = https://github.com/britzl/extension-imgui/archive/refs/tags/1.3.1.zip -dependencies#3 = https://github.com/indiesoftby/defold-scene3d/archive/refs/tags/1.2.0.zip -dependencies#4 = https://github.com/selimanac/defold-random/archive/refs/tags/v1.2.6.zip -dependencies#5 = https://github.com/indiesoftby/defold-splitmix64/archive/refs/tags/1.0.1.zip -dependencies#6 = https://github.com/subsoap/defstring/archive/master.zip +# defstring doesn't have a script_api +dependencies#1 = https://github.com/subsoap/defstring/archive/master.zip +# The following deps all have script_api +dependencies#2 = https://github.com/thejustinwalsh/defold-xmath/archive/refs/heads/main.zip +dependencies#3 = https://github.com/britzl/extension-imgui/archive/refs/tags/1.3.1.zip +dependencies#4 = https://github.com/indiesoftby/defold-scene3d/archive/refs/tags/1.2.0.zip +dependencies#5 = https://github.com/selimanac/defold-random/archive/refs/tags/v1.2.6.zip +dependencies#6 = https://github.com/indiesoftby/defold-splitmix64/archive/refs/tags/1.0.1.zip dependencies#7 = https://github.com/britzl/boom/archive/refs/heads/main.zip dependencies#8 = https://github.com/AGulev/jstodef/archive/refs/tags/2.0.0.zip dependencies#9 = https://github.com/defold/extension-review/archive/refs/tags/3.2.0.zip @@ -16,4 +18,8 @@ dependencies#11 = https://github.com/indiesoftby/defold-yametrica/archive/refs/t dependencies#12 = https://github.com/indiesoftby/defold-webgl-memory/archive/main.zip dependencies#13 = https://github.com/britzl/defold-lfs/archive/refs/tags/1.1.0.zip dependencies#14 = https://github.com/defold/extension-ironsource/archive/refs/tags/1.0.0.zip -dependencies#15 = https://github.com/alchimystic/defold-rng/archive/refs/tags/v1.1.0.zip \ No newline at end of file +dependencies#15 = https://github.com/alchimystic/defold-rng/archive/refs/tags/v1.1.0.zip +dependencies#16 = https://github.com/thinknathan/def-setv/archive/refs/tags/v1.0.0.zip +dependencies#17 = https://github.com/thinknathan/defold-estring/archive/refs/tags/v2.0.1.zip +dependencies#18 = https://github.com/thinknathan/defold-etable/archive/refs/tags/v1.0.1.zip +# dependencies#19 = https://github.com/thinknathan/defold-prng/archive/refs/tags/v1.0.2.zip diff --git a/xtgen.mjs b/xtgen.mjs index a4dc69c..c0daa5e 100644 --- a/xtgen.mjs +++ b/xtgen.mjs @@ -7,6 +7,102 @@ import fetch from 'node-fetch'; import { parse } from 'yaml'; import yargs from 'yargs'; const DEBUG = false; +// Definitions file starts with this string +const HEADER = `/** @noSelfInFile */ +/// +/// \n`; +// Invalid names in TypeScript +const INVALID_NAMES = [ + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'new', + 'null', + 'return', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', +]; +// All valid types are listed here +const KNOWN_TYPES = { + TBL: '{}', + TABLE: '{}', + NUM: 'number', + NUMBER: 'number', + NUMMBER: 'number', // intentional typo + INT: 'number', + INTEGER: 'number', + FLOAT: 'number', + STR: 'string', + STRING: 'string', + BOOL: 'boolean', + BOOLEAN: 'boolean', + FN: '(...args: any[]) => any', + FUNC: '(...args: any[]) => any', + FUNCTION: '(...args: any[]) => any', + HASH: 'hash', + URL: 'url', + NODE: 'node', + BUFFER: 'buffer', + BUFFERSTREAM: 'bufferstream', + VECTOR3: 'vmath.vector3', + VECTOR4: 'vmath.vector4', + MATRIX: 'vmath.matrix4', + MATRIX4: 'vmath.matrix4', + QUAT: 'vmath.quaternion', + QUATERNION: 'vmath.quaternion', + QUATERTION: 'vmath.quaternion', // intentional typo + LUAUSERDATA: 'LuaUserdata', + // TO-DO: Parse strings that have pipes instead of relying on a naive lookup + 'STRING|HASH|URL': 'string | hash | url', + 'STRING|URL|HASH': 'string | hash | url', + 'HASH|STRING|URL': 'string | hash | url', + 'HASH|URL|STRING': 'string | hash | url', + 'URL|STRING|HASH': 'string | hash | url', + 'URL|HASH|STRING': 'string | hash | url', + 'STRING | HASH | URL': 'string | hash | url', + 'STRING | URL | HASH': 'string | hash | url', + 'HASH | STRING | URL': 'string | hash | url', + 'HASH | URL | STRING': 'string | hash | url', + 'URL | STRING | HASH': 'string | hash | url', + 'URL | HASH | STRING': 'string | hash | url', + 'STRING|HASH': 'string | hash', + 'HASH|STRING': 'string | hash', + 'STRING | HASH': 'string | hash', + 'HASH | STRING': 'string | hash', +}; +// We'll make default return types slightly stricter than default param types +const DEFAULT_PARAM_TYPE = 'any'; +const DEFAULT_RETURN_TYPE = 'unknown'; +// Theoretically, it's impossible not to have a name, but just in case +const DEFAULT_NAME_IF_BLANK = 'missingName'; async function main() { console.time('Done in'); // Command line args @@ -33,7 +129,7 @@ async function main() { try { iniData = await fs.promises.readFile(absPath, 'utf8'); } catch (e) { - console.error(e); + console.error(e, absPath); return; } // Locate dependencies @@ -44,7 +140,7 @@ async function main() { return dep.split('=')[1].trim(); }); if (deps.length === 0) { - console.error('Could not find dependencies in project file.'); + console.error('Could not find dependencies in project file.', absPath); return; } // Iterate over each dependency @@ -90,7 +186,7 @@ async function main() { return parse(entry.getData().toString('utf8')); })[0]; } catch (e) { - console.error(e); + console.error(e, dep); return; } // If we have no API to parse, exit early @@ -111,15 +207,17 @@ async function main() { JSON.stringify(api), ); } catch (e) { - console.error(e); + console.error(e, dep); return; } } // Turn our parsed object into definitions const result = generateTypeScriptDefinitions(api, details); if (result) { + // Guess the URL by including only the first 6 strings split by slash + const depUrl = dep.split('/').slice(0, 5).join('/'); // Append header - const final = HEADER + result; + const final = `${HEADER}/**\n * @url ${depUrl}\n * @noResolution\n */\n${result}`; // Save the definitions to file try { await fs.promises.writeFile( @@ -127,7 +225,7 @@ async function main() { final, ); } catch (e) { - console.error(e); + console.error(e, dep); return; } } @@ -136,103 +234,6 @@ async function main() { console.timeEnd('Done in'); console.log(`Exported definitions to ${path.join(process.cwd(), outDir)}`); } -// Definitions file starts with this string -const HEADER = `/** @noSelfInFile */ -/// -/// -/** @noResolution */\n`; -// Invalid names in TypeScript -const INVALID_NAMES = [ - 'break', - 'case', - 'catch', - 'class', - 'const', - 'continue', - 'debugger', - 'default', - 'delete', - 'do', - 'else', - 'export', - 'extends', - 'false', - 'finally', - 'for', - 'function', - 'if', - 'import', - 'in', - 'instanceof', - 'new', - 'null', - 'return', - 'super', - 'switch', - 'this', - 'throw', - 'true', - 'try', - 'typeof', - 'var', - 'void', - 'while', - 'with', - 'yield', -]; -// All valid types are listed here -const KNOWN_TYPES = { - TBL: '{}', - TABLE: '{}', - NUM: 'number', - NUMBER: 'number', - NUMMBER: 'number', // intentional typo - INT: 'number', - INTEGER: 'number', - FLOAT: 'number', - STR: 'string', - STRING: 'string', - BOOL: 'boolean', - BOOLEAN: 'boolean', - FN: '(...args: any[]) => any', - FUNC: '(...args: any[]) => any', - FUNCTION: '(...args: any[]) => any', - HASH: 'hash', - URL: 'url', - NODE: 'node', - BUFFER: 'buffer', - BUFFERSTREAM: 'bufferstream', - VECTOR3: 'vmath.vector3', - VECTOR4: 'vmath.vector4', - MATRIX: 'vmath.matrix4', - MATRIX4: 'vmath.matrix4', - QUAT: 'vmath.quaternion', - QUATERNION: 'vmath.quaternion', - QUATERTION: 'vmath.quaternion', // intentional typo - LUAUSERDATA: 'LuaUserdata', - // TO-DO: Parse strings that have pipes instead of relying on a naive lookup - 'STRING|HASH|URL': 'string | hash | url', - 'STRING|URL|HASH': 'string | hash | url', - 'HASH|STRING|URL': 'string | hash | url', - 'HASH|URL|STRING': 'string | hash | url', - 'URL|STRING|HASH': 'string | hash | url', - 'URL|HASH|STRING': 'string | hash | url', - 'STRING | HASH | URL': 'string | hash | url', - 'STRING | URL | HASH': 'string | hash | url', - 'HASH | STRING | URL': 'string | hash | url', - 'HASH | URL | STRING': 'string | hash | url', - 'URL | STRING | HASH': 'string | hash | url', - 'URL | HASH | STRING': 'string | hash | url', - 'STRING|HASH': 'string | hash', - 'HASH|STRING': 'string | hash', - 'STRING | HASH': 'string | hash', - 'HASH | STRING': 'string | hash', -}; -// We'll make default return types slightly stricter than default param types -const DEFAULT_PARAM_TYPE = 'any'; -const DEFAULT_RETURN_TYPE = 'unknown'; -// Theoretically, it's impossible not have a name, but just in case -const DEFAULT_NAME_IF_BLANK = 'missingName'; // Utility Functions // Check if a string is all uppercase with optional underscores function isAllUppercase(str) { @@ -258,10 +259,13 @@ function getName(name) { let modifiedName = name.replace('...', 'args'); // Check against the reserved keywords in TypeScript if (INVALID_NAMES.includes(modifiedName)) { - console.warn(`Modifying invalid name ${modifiedName}`); modifiedName = modifiedName + '_'; } + // Sanitize type name, allow alpha-numeric and underscore modifiedName = modifiedName.replace(/[^a-zA-Z0-9_$]/g, '_'); + if (modifiedName !== name) { + console.warn(`Modifying invalid name "${name}" to "${modifiedName}"`); + } return modifiedName; } // Transforms API type to TS type @@ -291,15 +295,50 @@ function sanitizeForComment(str) { function getComments(entry) { // Make sure the description doesn't break out of the comment const desc = entry.desc || entry.description; - let newDesc = desc ? sanitizeForComment(desc) : ''; + let newDesc = desc ?? ''; // If params exist, let's create `@param`s in JSDoc format if (entry.parameters && Array.isArray(entry.parameters)) { newDesc = getParamComments(entry.parameters, newDesc); } + // Comments for `@returns` + const returnObj = entry.return || entry.returns; + if (returnObj) { + newDesc = getReturnComments(returnObj, newDesc); + } + // Comments for `@example` if (entry.examples && Array.isArray(entry.examples)) { newDesc = getExampleComments(entry.examples, newDesc); } - return newDesc ? `/**\n * ${newDesc}\n */\n` : ''; + return newDesc ? `/**\n * ${sanitizeForComment(newDesc)}\n */\n` : ''; +} +function getReturnComments(returnObj, newDesc) { + let returnType = ''; + let comments = ''; + if (Array.isArray(returnObj)) { + if (returnObj.length > 1) { + returnType = `LuaMultiReturn<[${returnObj.map((ret) => ret.type).join(', ')}]>`; + } else { + returnType = Array.isArray(returnObj[0].type) + ? returnObj[0].type.join(' | ') + : returnObj[0].type ?? ''; + } + // Add comments if they exist + if (returnObj.some((ret) => ret.desc || ret.description)) { + comments = `${returnObj.map((ret) => ret.desc || ret.description).join(' | ')}`; + } + } else if (returnObj.type) { + // Instead of getting a TS type here, use the raw Lua type + returnType = Array.isArray(returnObj.type) + ? returnObj.type.join(' | ') + : returnObj.type; + comments = getType(returnObj.type, 'return'); + } + // If we've figured out a comment, but not a returnType + if (comments.length && !returnType.length) { + returnType = DEFAULT_RETURN_TYPE; + } + newDesc += `\n * @returns {${returnType}} ${comments}`; + return newDesc; } function getParamComments(parameters, newDesc) { parameters.forEach((param) => { @@ -315,14 +354,14 @@ function getParamComments(parameters, newDesc) { } else { rawType = param.type; } - // Sanitize type name + // Sanitize type name, allow alpha-numeric, underscore and pipe rawType = rawType.replace(/[^a-zA-Z|0-9_$]/g, '_'); newDesc += ` {${rawType}}`; } newDesc += ` ${name}`; const desc = param.desc || param.description; if (desc) { - newDesc += ` ${sanitizeForComment(desc)}`; + newDesc += ` ${desc}`; } if (param.fields && Array.isArray(param.fields)) { newDesc = getParamFields(param.fields, newDesc); @@ -333,7 +372,7 @@ function getParamComments(parameters, newDesc) { } function getParamFields(fields, newDesc) { fields.forEach((field) => { - newDesc += ` ${sanitizeForComment(JSON.stringify(field))}`; + newDesc += ` ${JSON.stringify(field)}`; }); return newDesc; } @@ -341,7 +380,7 @@ function getExampleComments(examples, newDesc) { examples.forEach((example) => { const desc = example.desc || example.description; if (desc) { - newDesc += `\n * @example ${sanitizeForComment(desc)}`; + newDesc += `\n * @example ${desc}`; } }); return newDesc; @@ -381,8 +420,14 @@ function getParameterDefinition(param) { const optional = param.optional ? '?' : ''; let type = getType(param.type, 'param'); if (type === KNOWN_TYPES['FUNCTION']) { + // Get a more specific function signature type = generateFunctionDefinition(param, true); - } else if (type === KNOWN_TYPES['TABLE'] && param.fields) { + } else if ( + type === KNOWN_TYPES['TABLE'] && + param.fields && + Array.isArray(param.fields) + ) { + // Try to get the exact parameters of a table type = `{ ${param.fields.map(getParameterDefinition).join('; ')} }`; } return `${name}${optional}: ${type}`; @@ -395,7 +440,7 @@ function getReturnType(returnObj) { if (returnObj.length > 1) { return `LuaMultiReturn<[${returnObj.map((ret) => getType(ret.type, 'return')).join(', ')}]>`; } else { - return `${returnObj.map((ret) => getType(ret.type, 'return')).join(', ')}`; + return `${getType(returnObj[0].type, 'return')}`; } } else if (returnObj.type) { return getType(returnObj.type, 'return');