Skip to content

Commit

Permalink
Merge pull request #3 from thinknathan/more-checks
Browse files Browse the repository at this point in the history
More tests, better safety, more detailed table and function definitions
  • Loading branch information
thinknathan authored Jan 31, 2024
2 parents 60518cf + ca8967c commit 4521ad9
Show file tree
Hide file tree
Showing 4 changed files with 348 additions and 136 deletions.
27 changes: 18 additions & 9 deletions @types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,29 @@ declare type ScriptApi = Array<
>;

declare interface ScriptApiEntry {
name: string;
type: string | string[];
name?: string;
type?: string | string[];
desc?: string;
description?: string; // This is a typo, but some people use it
}

declare interface ScriptApiTable extends ScriptApiEntry {
members: Array<ScriptApiEntry | ScriptApiFunction | ScriptApiTable>;
members?: Array<ScriptApiEntry | ScriptApiFunction | ScriptApiTable>;
}

declare interface ScriptApiFunction extends ScriptApiEntry {
parameters?: Array<ScriptApiEntry & { optional?: boolean }>;
return?: Partial<ScriptApiEntry>[];
returns?: Partial<ScriptApiEntry>[];
examples?: {
desc: string;
}[];
parameters?: ScriptApiParameter[];
return?: ScriptApiEntry | ScriptApiEntry[]; // This is a typo, but some people use it
returns?: ScriptApiEntry | ScriptApiEntry[];
examples?: ScriptApiExample[];
}

declare interface ScriptApiExample {
desc?: string;
description?: string; // This is a typo, but some people use it
}

declare interface ScriptApiParameter extends ScriptApiEntry {
optional?: boolean;
fields?: ScriptApiEntry[];
}
235 changes: 172 additions & 63 deletions src/xtgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,46 @@ const HEADER = `/** @noSelfInFile */
/// <reference types="@ts-defold/types" />
/** @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: '{}',
Expand Down Expand Up @@ -218,6 +258,8 @@ const KNOWN_TYPES: { [key: string]: string } = {
// 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

Expand Down Expand Up @@ -250,6 +292,11 @@ function isApiFunc(
// Sanitizes name
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 + '_';
}
modifiedName = modifiedName.replace(/[^a-zA-Z0-9_$]/g, '_');
return modifiedName;
}
Expand Down Expand Up @@ -278,40 +325,78 @@ function getType(
return defaultType;
}

function sanitizeForComment(str: string) {
return str.replace(/\*\//g, '');
}

// Transforms and sanitizes descriptions
function getComments(entry: ScriptApiFunction) {
// Make sure the description doesn't break out of the comment
let newDesc = entry.desc ? entry.desc.replace(/\*\//g, '') : '';
const desc = entry.desc || entry.description;
let newDesc = desc ? sanitizeForComment(desc) : '';

// If params exist, let's create `@param`s in JSDoc format
if (entry.parameters) {
entry.parameters.forEach((param) => {
const name = getName(param.name);
if (name) {
newDesc += `\n * @param `;
if (param.type) {
// Instead of getting a TS type here, use the raw Lua type
let rawType = '';
if (Array.isArray(param.type)) {
// If multiple types, join them into a string
rawType = param.type.join('|');
} else {
rawType = param.type;
}
// Sanitize type name
rawType = rawType.replace(/[^a-zA-Z|0-9_$]/g, '_');
newDesc += `{${rawType}} `;
}
newDesc += `${name} `;
if (param.desc) {
const sanitizedDesc = param.desc.replace(/\*\//g, '');
newDesc += `${sanitizedDesc}`;
}
}
});
if (entry.parameters && Array.isArray(entry.parameters)) {
newDesc = getParamComments(entry.parameters, newDesc);
}

if (entry.examples && Array.isArray(entry.examples)) {
newDesc = getExampleComments(entry.examples, newDesc);
}

return newDesc ? `/**\n * ${newDesc}\n */\n` : '';
}

function getParamComments(parameters: ScriptApiParameter[], newDesc: string) {
parameters.forEach((param) => {
const name = param.name ? getName(param.name) : '';
if (name) {
newDesc += `\n * @param`;
if (param.type) {
// Instead of getting a TS type here, use the raw Lua type
let rawType = '';
if (Array.isArray(param.type)) {
// If multiple types, join them into a string
rawType = param.type.join('|');
} else {
rawType = param.type;
}
// Sanitize type name
rawType = rawType.replace(/[^a-zA-Z|0-9_$]/g, '_');
newDesc += ` {${rawType}}`;
}
newDesc += ` ${name}`;

const desc = param.desc || param.description;
if (desc) {
newDesc += ` ${sanitizeForComment(desc)}`;
}

if (param.fields && Array.isArray(param.fields)) {
newDesc = getParamFields(param.fields, newDesc);
}
}
});
return newDesc;
}

function getParamFields(fields: ScriptApiEntry[], newDesc: string) {
fields.forEach((field) => {
newDesc += ` ${sanitizeForComment(JSON.stringify(field))}`;
});
return newDesc;
}

function getExampleComments(examples: ScriptApiExample[], newDesc: string) {
examples.forEach((example) => {
const desc = example.desc || example.description;
if (desc) {
newDesc += `\n * @example ${sanitizeForComment(desc)}`;
}
});
return newDesc;
}

// Main Functions

// Function to generate TypeScript definitions for ScriptApiTable
Expand All @@ -320,58 +405,82 @@ function generateTableDefinition(
details: ScriptDetails,
start = false,
): string {
let tableDeclaration = `export namespace ${getName(entry.name)} {\n`;
const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK;
let tableDeclaration = `export namespace ${name} {\n`;
if (start) {
tableDeclaration = details.isLua
? `declare module '${getName(entry.name)}.${getName(entry.name)}' {\n`
: `declare namespace ${getName(entry.name)} {\n`;
? `declare module '${name}.${name}' {\n`
: `declare namespace ${name} {\n`;
}

return `${tableDeclaration}${generateTypeScriptDefinitions(entry.members, details)}\n}`;
if (entry.members && Array.isArray(entry.members)) {
return `${tableDeclaration}${generateTypeScriptDefinitions(entry.members, details)}}`;
} else {
return `${tableDeclaration}}`;
}
}
// Function to generate TypeScript definitions for ScriptApiFunction
function generateFunctionDefinition(entry: ScriptApiFunction): string {
const comment = getComments(entry);
let definition = `${comment}export function ${getName(entry.name)}(`;
if (entry.parameters) {
entry.parameters.forEach((param, index) => {
const name = getName(param.name);
definition += `${name}${param.optional ? '?' : ''}: ${getType(param.type, 'param')}`;
if (index < entry.parameters!.length - 1) {
definition += ', ';
}
});
function generateFunctionDefinition(
entry: ScriptApiFunction,
isParam: boolean,
): string {
const parameters = entry.parameters
? entry.parameters.map(getParameterDefinition).join(', ')
: '';
const returnType = getReturnType(entry.return || entry.returns);
if (isParam) {
return `(${parameters}) => ${returnType}`;
} else {
const comment = getComments(entry);
const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK;
return `${comment}export function ${name}(${parameters}): ${returnType};\n`;
}
}
function getParameterDefinition(param: ScriptApiParameter): string {
const name = param.name ? getName(param.name) : DEFAULT_NAME_IF_BLANK;
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 &&
Array.isArray(param.fields)
) {
// Try to get the exact parameters of a table
type = `{ ${param.fields.map(getParameterDefinition).join('; ')} }`;
}
return `${name}${optional}: ${type}`;
}
function getReturnType(
returnObj: ScriptApiEntry | ScriptApiEntry[] | undefined,
): string {
if (!returnObj) {
return 'void';
}
// People don't use `return` and `returns` consistently, so check for both
const returnObj = entry.return || entry.returns;
if (returnObj) {
definition += `): `;
// Handle a special situation where the func has multiple return values
if (Array.isArray(returnObj)) {
if (returnObj.length > 1) {
definition += `LuaMultiReturn<[`;
returnObj.forEach((obj, index) => {
definition += `${getType(obj.type, 'return')}`;
if (index < returnObj.length - 1) {
definition += ', ';
}
});
definition += `]>`;
return `LuaMultiReturn<[${returnObj.map((ret) => getType(ret.type, 'return')).join(', ')}]>`;
} else {
definition += `${returnObj ? returnObj.map((ret) => getType(ret.type, 'return')).join(' | ') : DEFAULT_RETURN_TYPE}`;
return `${returnObj.map((ret) => getType(ret.type, 'return')).join(', ')}`;
}
} else if (returnObj.type) {
return getType(returnObj.type, 'return');
} else {
definition += `): void`;
return 'void'; // Fallback in case we can't parse it at all
}
return `${definition};\n`;
}

// Function to generate TypeScript definitions for ScriptApiEntry
function generateEntryDefinition(entry: ScriptApiEntry): string {
const name = getName(entry.name);
const name = entry.name ? getName(entry.name) : DEFAULT_NAME_IF_BLANK;
const varType = isAllUppercase(name) ? 'const' : 'let';
const type = getType(entry.type, 'return');
const comment = getComments(entry);
Expand All @@ -389,7 +498,7 @@ function generateTypeScriptDefinitions(

api.forEach((entry) => {
// Handle nested properties
if (entry.name.includes('.')) {
if (entry.name && entry.name.includes('.')) {
const namePieces = entry.name.split('.');
const entryNamespace = namePieces[0];
const entryName = namePieces[1];
Expand All @@ -407,7 +516,7 @@ function generateTypeScriptDefinitions(
definitions === '',
);
} else if (isApiFunc(entry)) {
definitions += generateFunctionDefinition(entry);
definitions += generateFunctionDefinition(entry, false);
} else {
definitions += generateEntryDefinition(entry);
}
Expand All @@ -425,7 +534,7 @@ function generateTypeScriptDefinitions(
if (isApiTable(entry)) {
definitions += generateTableDefinition(entry, details);
} else if (isApiFunc(entry)) {
definitions += generateFunctionDefinition(entry);
definitions += generateFunctionDefinition(entry, false);
} else {
definitions += generateEntryDefinition(entry);
}
Expand Down
14 changes: 11 additions & 3 deletions tests/game.project
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
[project]
title = Test
dependencies#0 = https://github.com/britzl/boom/archive/refs/heads/main.zip
# 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
# Value from fuzz.script_api as a zip
dependencies#7 = data:application/zip;base64,UEsDBBQACAAIADtMPlgAAAAAAAAAAJ8EAAAPACAAZnV6ei5zY3JpcHRfYXBpVVQNAAcCM7llAzO5ZQIzuWV1eAsAAQT1AQAABBQAAACllNFqgzAUhu/7FMeOeeFovdhdYawg29hg3UX7Aqk5tgGNkhxLJ334JWq7KNoVlitDzjnfn/w/zkCyDBcwfS2rypO7lyOh1F4upxMA+i7sES23Kdo9Rx2bfVMjcglJrqBthA92YOtYiYIgynldn2G2RaUX5hNgdiYlpiEKmgq7WkpSyphEA7arhdnxqIH2CIXKD4Ijd1GxQc3PLQVTBkEXZAcbO0gHq0kZ9e5BC94YYg8ElMMWwV4A+Rw+S012z+DAUsGhGXVRo5BKJYek2AHPPBoRFCgxKqhBjz4AHllWpNhhOu9obWKg65r+jOmk61IY7FD6ihF60lzP4++yKOk2z95Qou3UwCS07S7Ps6Pq8Jgact/sqoNheNXDk/Rt3E77pd4PPN9XYZWy9H+mOnLo8UHTkJqnbqJ+bb+vqmH1/qDhYQBBCGH4d6CGLOoQ5ldCvmvd4qNW3ZKv1nMT3aExolHWj1jkr9ab5WrTj9WdiVX9D/oBUEsHCN+ExiB1AQAAnwQAAFBLAQIUAxQACAAIADtMPljfhMYgdQEAAJ8EAAAPACAAAAAAAAAAAACkgQAAAABmdXp6LnNjcmlwdF9hcGlVVA0ABwIzuWUDM7llAjO5ZXV4CwABBPUBAAAEFAAAAFBLBQYAAAAAAQABAF0AAADSAQAAAAA=
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
dependencies#10 = https://github.com/defold/extension-webview/archive/refs/tags/1.4.7.zip
dependencies#11 = https://github.com/indiesoftby/defold-yametrica/archive/refs/tags/1.0.0.zip
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
Loading

0 comments on commit 4521ad9

Please sign in to comment.