spine-runtimes/spine-cpp/extract-spine-cpp-types.js

654 lines
20 KiB
JavaScript
Executable File

#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const scriptDir = path.dirname(path.resolve(__filename));
const spineIncludeDir = path.join(scriptDir, 'spine-cpp', 'include');
function showHelp() {
console.log(`
extract-spine-cpp-types.js - Extract type information from spine-cpp header files
USAGE:
extract-spine-cpp-types.js [<header-file>]
extract-spine-cpp-types.js --help
DESCRIPTION:
Extracts classes, structs, enums, and their public members from C++ headers.
Without arguments, processes all headers in spine-cpp/include.
In all-files mode, performs two passes:
1. Extract all type definitions
2. Add inherited methods from supertypes (including template instantiations)
OUTPUT FORMAT:
Single file mode: Array of type definitions
All files mode: Object with relative file paths as keys
TYPE DEFINITION STRUCTURE:
{
"name": "ClassName", // Type name
"kind": "class" | "struct" | "enum", // Type kind
"loc": { // Source location
"line": 45,
"col": 15
},
"superTypes": ["BaseClass", "Interface"], // Base classes (optional)
"members": [...], // Public members (classes/structs)
"values": [...], // Enum constants (enums only)
"isTemplate": true, // Present if type is a template
"templateParams": ["T", "U"], // Template parameter names (templates only)
"isAbstract": true // Present if class has pure virtual methods
}
MEMBER STRUCTURE:
Fields:
{
"kind": "field",
"name": "fieldName",
"type": "int"
}
Methods:
{
"kind": "method",
"name": "methodName",
"returnType": "void",
"parameters": [
{"name": "param1", "type": "int"},
{"name": "param2", "type": "const String &"}
],
"isStatic": false,
"isVirtual": true,
"isPure": false,
"fromSupertype": "BaseClass" // Present if inherited (all-files mode only)
}
Constructors:
{
"kind": "constructor",
"name": "ClassName",
"parameters": [...]
}
ENUM VALUES:
{
"name": "ENUM_VALUE",
"value": "1 << 0" // Present only if explicitly initialized
}
INHERITANCE (all-files mode only):
- Methods inherited from non-template supertypes are included
- Methods inherited from template supertypes have parameters substituted
- SpineObject methods are excluded (considered noise)
- Methods are marked with "fromSupertype" field
- Template supertypes appear as "TemplateName<Type1, Type2>"
NOTES:
- Only public members are extracted
- Forward declarations are excluded
- Template instantiations (e.g., Vector<int>) are excluded
- Friend declarations are excluded
- Implicit/compiler-generated methods are excluded
- Destructors and operator methods are excluded (not needed for C wrapper generation)
EXAMPLES:
# Extract types from single file
extract-spine-cpp-types.js spine-cpp/include/spine/Animation.h > animation.json
# Extract all types with inheritance resolution
extract-spine-cpp-types.js > all-spine-types.json
# Query specific type
extract-spine-cpp-types.js > types.json
jq '."spine/Bone.h"[] | select(.name == "Bone")' types.json
`);
}
function extractLocalTypes(headerFile, typeMap = null) {
const absHeaderPath = path.resolve(headerFile);
const sourceContent = fs.readFileSync(absHeaderPath, 'utf8');
const sourceLines = sourceContent.split('\n');
// Get AST from clang
const cmd = `clang++ -Xclang -ast-dump=json -fsyntax-only -std=c++11 -I "${spineIncludeDir}" "${absHeaderPath}" 2>/dev/null`;
const maxBuffer = headerFile.includes('Debug.h') ? 500 : 200; // MB
let astJson;
try {
const output = execSync(cmd, { encoding: 'utf8', maxBuffer: maxBuffer * 1024 * 1024 });
astJson = JSON.parse(output);
} catch (error) {
throw new Error(error.code === 'ENOBUFS'
? `AST output too large (>${maxBuffer}MB)`
: error.message);
}
const types = [];
function extractEnumValueFromSource(enumConstNode) {
if (!enumConstNode.loc) return undefined;
const line = sourceLines[enumConstNode.loc.line - 1];
if (!line) return undefined;
// Find enum name and check for '='
const nameMatch = line.match(new RegExp(`\\b${enumConstNode.name}\\b`));
if (!nameMatch) return undefined;
const afterName = line.substring(nameMatch.index + enumConstNode.name.length);
const equalIndex = afterName.indexOf('=');
if (equalIndex === -1) return null; // No explicit value
// Extract value expression
let valueText = afterName.substring(equalIndex + 1);
// Handle multi-line values
let currentLine = enumConstNode.loc.line;
while (currentLine < sourceLines.length && !valueText.match(/[,}]/)) {
valueText += '\n' + sourceLines[currentLine++];
}
// Extract up to comma or brace
const endMatch = valueText.match(/^(.*?)([,}])/s);
if (endMatch) valueText = endMatch[1];
// Clean up
return valueText
.replace(/\/\/.*$/gm, '') // Remove single-line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
function extractReturnType(methodNode) {
const fullType = methodNode.type?.qualType || '';
const match = fullType.match(/^(.+?)\s*\(/);
return match ? match[1].trim() : 'void';
}
function extractParameters(methodNode) {
return (methodNode.inner || [])
.filter(n => n.kind === 'ParmVarDecl')
.map(n => ({
name: n.name || '',
type: n.type?.qualType || ''
}));
}
function isInTargetFile(node) {
if (!node.loc) return false;
const targetPath = path.resolve(headerFile);
const loc = node.loc;
// Check direct file location
if (loc.file) return path.resolve(loc.file) === targetPath;
// Check macro locations
for (const locType of ['spellingLoc', 'expansionLoc']) {
if (loc[locType]) {
if (loc[locType].includedFrom) return false;
if (loc[locType].file) return path.resolve(loc[locType].file) === targetPath;
}
}
// If included from another file, reject
if (loc.includedFrom) return false;
// No location info - assume it's in the main file
return true;
}
function extractTypeInfo(node) {
const info = {
name: node.name || '',
kind: node.kind === 'EnumDecl' ? 'enum' : (node.tagUsed || ''),
loc: {
line: node.loc?.line || 0,
col: node.loc?.col || 0
}
};
// Extract base classes
if (node.bases?.length > 0) {
info.superTypes = node.bases.map(b => b.type?.qualType || '').filter(Boolean);
}
// For enums, extract the values
if (node.kind === 'EnumDecl') {
info.values = (node.inner || [])
.filter(n => n.kind === 'EnumConstantDecl')
.map(n => {
const enumValue = { name: n.name || '' };
const sourceValue = extractEnumValueFromSource(n);
if (sourceValue === null) {
// Implicit value - no value property
} else if (sourceValue) {
enumValue.value = sourceValue;
} else if (n.inner?.length > 0) {
enumValue.value = "<<extraction failed>>";
}
return enumValue;
});
return info;
}
// For classes/structs, extract public members
info.members = [];
let currentAccess = node.tagUsed === 'struct' ? 'public' : 'private';
let hasPureVirtual = false;
for (const inner of node.inner || []) {
if (inner.kind === 'AccessSpecDecl') {
currentAccess = inner.access || 'private';
continue;
}
if (inner.kind === 'FriendDecl' || currentAccess !== 'public') continue;
const member = extractMember(inner, node);
if (member) {
info.members.push(member);
// Check if this is a pure virtual method
if (member.kind === 'method' && member.isPure) {
hasPureVirtual = true;
}
}
}
// Mark as abstract if it has pure virtual methods
if (hasPureVirtual) {
info.isAbstract = true;
}
return info;
}
function extractMember(inner, parent) {
if (inner.isImplicit) return null;
switch (inner.kind) {
case 'FieldDecl':
return {
kind: 'field',
name: inner.name || '',
type: inner.type?.qualType || ''
};
case 'CXXMethodDecl':
if (!inner.name) return null;
// Skip operators - not needed for C wrapper generation
if (inner.name.startsWith('operator')) return null;
return {
kind: 'method',
name: inner.name,
returnType: extractReturnType(inner),
parameters: extractParameters(inner),
isStatic: inner.storageClass === 'static',
isVirtual: inner.virtual || false,
isPure: inner.pure || false
};
case 'CXXConstructorDecl':
return {
kind: 'constructor',
name: inner.name || parent.name || '',
parameters: extractParameters(inner)
};
case 'CXXDestructorDecl':
// Skip destructors - not needed for C wrapper generation
return null;
default:
return null;
}
}
function processNode(node, inSpineNamespace = false) {
if (!node || typeof node !== 'object') return;
// Handle spine namespace
if (node.kind === 'NamespaceDecl' && node.name === 'spine') {
(node.inner || []).forEach(n => processNode(n, true));
return;
}
// Recurse to find spine namespace
if (!inSpineNamespace) {
(node.inner || []).forEach(n => processNode(n, false));
return;
}
// Process type declarations
const typeKinds = ['CXXRecordDecl', 'ClassTemplateDecl', 'EnumDecl', 'TypedefDecl', 'TypeAliasDecl'];
if (!typeKinds.includes(node.kind)) return;
// Skip if not in target file or invalid
if (!isInTargetFile(node) ||
node.isImplicit ||
!node.name ||
node.name.startsWith('_') ||
node.name.includes('<')) return;
// Skip forward declarations
if (node.previousDecl && (!node.inner || node.inner.length === 0)) return;
// Extract type info
if (node.kind === 'ClassTemplateDecl') {
const classNode = (node.inner || []).find(n => n.kind === 'CXXRecordDecl');
if (classNode) {
const typeInfo = extractTypeInfo(classNode);
typeInfo.isTemplate = true;
// Extract template parameters
const templateParams = [];
for (const inner of node.inner || []) {
if (inner.kind === 'TemplateTypeParmDecl' && inner.name) {
templateParams.push(inner.name);
}
}
if (templateParams.length > 0) {
typeInfo.templateParams = templateParams;
}
types.push(typeInfo);
}
} else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) {
types.push(extractTypeInfo(node));
} else if (node.kind === 'EnumDecl') {
types.push(extractTypeInfo(node));
} else if (node.kind === 'TypedefDecl' || node.kind === 'TypeAliasDecl') {
types.push(extractTypeInfo(node));
}
}
processNode(astJson);
// Filter out forward declarations and sort by line number
const filteredTypes = types
.filter(t => !(t.members && t.members.length === 0))
.sort((a, b) => a.loc.line - b.loc.line);
// Add inherited methods if we have a type map
if (typeMap) {
for (const type of filteredTypes) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
}
}
}
return filteredTypes;
}
function addInheritedMethods(type, typeMap) {
const inheritedMethods = [];
const ownMethodSignatures = new Set();
// Build a set of method signatures from this type
for (const member of type.members) {
if (member.kind === 'method') {
const sig = getMethodSignature(member);
ownMethodSignatures.add(sig);
}
}
// Process each supertype
for (const superTypeName of type.superTypes) {
// Clean up the supertype name (remove namespaces, etc)
const cleanName = superTypeName.replace(/^.*::/, '');
// Skip SpineObject inheritance - it's just noise
if (cleanName === 'SpineObject') continue;
// Check if this is a template supertype
const templateMatch = cleanName.match(/^([^<]+)<(.+)>$/);
if (templateMatch) {
const templateClassName = templateMatch[1];
const templateArgs = templateMatch[2];
const templateType = typeMap.get(templateClassName);
if (templateType && templateType.isTemplate && templateType.members) {
// Process template inheritance
addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
);
} else if (templateType && !templateType.isTemplate) {
// Template might not be marked as isTemplate, try anyway
addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
);
}
} else {
// Non-template supertype
const superType = typeMap.get(cleanName);
if (!superType || !superType.members) continue;
// Add non-overridden methods from supertype
for (const member of superType.members) {
if (member.kind === 'method') {
const sig = getMethodSignature(member);
if (!ownMethodSignatures.has(sig)) {
const inheritedMember = { ...member, fromSupertype: cleanName };
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig); // Prevent duplicates from multiple inheritance
}
}
}
}
}
// Add inherited methods to the type
type.members.push(...inheritedMethods);
}
function addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
) {
// Parse template arguments (handle multiple args)
const argsList = [];
let depth = 0;
let currentArg = '';
for (const char of templateArgs) {
if (char === '<') depth++;
else if (char === '>') depth--;
if (char === ',' && depth === 0) {
argsList.push(currentArg.trim());
currentArg = '';
} else {
currentArg += char;
}
}
if (currentArg.trim()) {
argsList.push(currentArg.trim());
}
// Build a mapping of template params to actual types
const paramMap = new Map();
// Use the actual template parameters if we have them
if (templateType.templateParams && templateType.templateParams.length === argsList.length) {
templateType.templateParams.forEach((param, i) => {
paramMap.set(param, argsList[i]);
});
} else {
// Fallback: if we don't have template param info, skip substitution
console.error(`Warning: Template ${templateClassName} missing parameter info, skipping substitution`);
return;
}
// Process each member of the template
for (const member of templateType.members) {
if (member.kind === 'method') {
// Skip template constructors - they have weird names like "Pose<P>"
if (member.name.includes('<')) continue;
// Clone the member and substitute template parameters
const inheritedMember = JSON.parse(JSON.stringify(member));
inheritedMember.fromSupertype = `${templateClassName}<${templateArgs}>`;
// Replace template parameters in return type
if (inheritedMember.returnType) {
inheritedMember.returnType = substituteTemplateParams(
inheritedMember.returnType, paramMap
);
}
// Replace template parameters in parameters
if (inheritedMember.parameters) {
for (const param of inheritedMember.parameters) {
param.type = substituteTemplateParams(param.type, paramMap);
}
}
// Check if this method is overridden
const sig = getMethodSignature(inheritedMember);
if (!ownMethodSignatures.has(sig)) {
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig);
}
}
}
}
function substituteTemplateParams(typeStr, paramMap) {
let result = typeStr;
// Replace template parameters in order of length (longest first)
// to avoid replacing substrings (e.g., V before V1)
const sortedParams = Array.from(paramMap.keys()).sort((a, b) => b.length - a.length);
for (const param of sortedParams) {
const regex = new RegExp(`\\b${param}\\b`, 'g');
result = result.replace(regex, paramMap.get(param));
}
return result;
}
function getMethodSignature(method) {
// Create a signature that identifies method overrides
// For virtual methods, just use the name
// For non-virtual methods, include parameter types
let sig = method.name;
if (method.parameters && method.parameters.length > 0) {
sig += '(' + method.parameters.map(p => p.type).join(',') + ')';
} else {
sig += '()';
}
// Add const qualifier if present
if (method.returnType && method.returnType.includes('const')) {
sig += ' const';
}
return sig;
}
function findAllHeaderFiles() {
const headers = [];
function walkDir(dir) {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
walkDir(fullPath);
} else if (file.endsWith('.h') && file !== 'spine.h') {
headers.push(fullPath);
}
});
}
walkDir(spineIncludeDir);
return headers.sort();
}
// Main execution
const arg = process.argv[2];
if (arg === '--help' || arg === '-h') {
showHelp();
process.exit(0);
}
if (arg) {
// Single file mode - no inheritance support
if (!fs.existsSync(arg)) {
console.error(`Error: File not found: ${arg}`);
process.exit(1);
}
try {
const types = extractLocalTypes(arg);
if (types.length === 0) {
console.error('No types found in the specified file');
process.exit(1);
}
console.log(JSON.stringify(types, null, 2));
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
} else {
// Process all files
const allHeaders = findAllHeaderFiles();
const allTypes = {};
let processed = 0, errors = 0;
console.error(`Processing ${allHeaders.length} header files...`);
// First pass: extract all types without inheritance
const typeMap = new Map();
for (const headerFile of allHeaders) {
const relPath = path.relative(spineIncludeDir, headerFile);
process.stderr.write(`\r\x1b[K Pass 1 - Processing ${++processed}/${allHeaders.length}: ${relPath}...`);
try {
const types = extractLocalTypes(headerFile);
if (types.length > 0) {
allTypes[relPath] = types;
// Build type map
for (const type of types) {
typeMap.set(type.name, type);
}
}
} catch (error) {
errors++;
console.error(`\n ERROR processing ${relPath}: ${error.message}`);
}
}
// Second pass: add inherited methods
console.error(`\n Pass 2 - Adding inherited methods...`);
processed = 0;
for (const headerFile of allHeaders) {
const relPath = path.relative(spineIncludeDir, headerFile);
if (!allTypes[relPath]) continue;
process.stderr.write(`\r\x1b[K Pass 2 - Processing ${++processed}/${Object.keys(allTypes).length}: ${relPath}...`);
for (const type of allTypes[relPath]) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
}
}
}
console.error(`\n Completed: ${Object.keys(allTypes).length} files processed, ${errors} errors`);
console.log(JSON.stringify(allTypes, null, 2));
}