315 lines
12 KiB
JavaScript
315 lines
12 KiB
JavaScript
import fs from 'fs/promises';
|
|
import { log } from "./logger.js";
|
|
import path from "path";
|
|
import Parser from 'tree-sitter';
|
|
import Cpp from 'tree-sitter-cpp';
|
|
const parser = new Parser();
|
|
parser.setLanguage(Cpp);
|
|
// 1. Parse a C++ file and extract all defines (commented and uncommented)
|
|
function parseDefines(fileContent) {
|
|
const defines = new Map();
|
|
const tree = parser.parse(fileContent);
|
|
function traverse(node) {
|
|
if (node.type === 'preproc_def') {
|
|
const nameNode = node.childForFieldName('name');
|
|
const valueNode = node.childForFieldName('value');
|
|
if (nameNode) {
|
|
defines.set(nameNode.text, {
|
|
value: valueNode?.text.trim() ?? null,
|
|
isComment: false
|
|
});
|
|
}
|
|
}
|
|
else if (node.type === 'comment' && node.text.includes('#define')) {
|
|
const match = node.text.match(/#define\s+(\S+)\s*(.*)/);
|
|
if (match) {
|
|
defines.set(match[1], {
|
|
value: match[2].trim() || null,
|
|
isComment: true
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
for (const child of node.children) {
|
|
traverse(child);
|
|
}
|
|
}
|
|
}
|
|
traverse(tree.rootNode);
|
|
return defines;
|
|
}
|
|
// 2. Build a map of all possible features from features.h (Feature -> Header)
|
|
export async function buildFeatureCatalog(projectRoot) {
|
|
const catalog = new Map();
|
|
const featuresPath = path.join(projectRoot, 'src', 'features.h');
|
|
log.silly(`Building feature catalog from: ${featuresPath}`);
|
|
const featuresContent = await fs.readFile(featuresPath, 'utf-8');
|
|
const tree = parser.parse(featuresContent);
|
|
function findIfdefs(node) {
|
|
if (node.type === 'preproc_ifdef') {
|
|
const featureName = node.childForFieldName('name')?.text;
|
|
const includePath = node.descendantsOfType('preproc_include')[0]?.childForFieldName('path')?.text.replace(/["<>]/g, '');
|
|
if (featureName && includePath) {
|
|
catalog.set(featureName, includePath);
|
|
}
|
|
}
|
|
for (const child of node.children) {
|
|
findIfdefs(child);
|
|
}
|
|
}
|
|
findIfdefs(tree.rootNode);
|
|
log.silly(`Found ${catalog.size} potential features in catalog.`);
|
|
return catalog;
|
|
}
|
|
// 3. Create a map of ClassName -> FeatureFlag for all components
|
|
export async function buildClassNameToFeatureMap(featureCatalog, includeDirs) {
|
|
const map = new Map();
|
|
// --- AST DUMP LOGIC ---
|
|
const astDir = path.join(process.cwd(), 'ast');
|
|
await fs.mkdir(astDir, { recursive: true });
|
|
function serializeNode(node) {
|
|
const children = node.children.map(serializeNode);
|
|
const result = { type: node.type };
|
|
if (children.length > 0) {
|
|
result.children = children;
|
|
}
|
|
else {
|
|
result.text = node.text;
|
|
}
|
|
return result;
|
|
}
|
|
// --- END AST DUMP LOGIC ---
|
|
for (const [featureFlag, headerFile] of featureCatalog.entries()) {
|
|
let absolutePath = null;
|
|
for (const dir of includeDirs) {
|
|
const testPath = path.join(dir, headerFile);
|
|
try {
|
|
await fs.stat(testPath);
|
|
absolutePath = testPath;
|
|
break;
|
|
}
|
|
catch (e) { /* continue */ }
|
|
}
|
|
if (absolutePath) {
|
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
const tree = parser.parse(content);
|
|
const className = findClassName(tree.rootNode);
|
|
if (className) {
|
|
map.set(className, featureFlag);
|
|
// --- AST DUMP FOR THIS COMPONENT ---
|
|
const astJson = JSON.stringify(serializeNode(tree.rootNode), null, 2);
|
|
const componentName = path.basename(headerFile, '.h');
|
|
await fs.writeFile(path.join(astDir, `${componentName}.json`), astJson);
|
|
log.info(`[DEBUG] AST for ${componentName} written to ${astDir}`);
|
|
// --- END AST DUMP ---
|
|
}
|
|
}
|
|
}
|
|
log.silly(`Mapped ${map.size} class names to feature flags by parsing headers.`);
|
|
return map;
|
|
}
|
|
// 4. For a given component header, find its dependencies
|
|
export async function analyzeComponentDependencies(headerPath, includeDirs, classNameToFeatureMap) {
|
|
const dependencies = new Set();
|
|
let absolutePath = null;
|
|
for (const dir of includeDirs) {
|
|
const testPath = path.join(dir, headerPath);
|
|
try {
|
|
await fs.stat(testPath);
|
|
absolutePath = testPath;
|
|
break;
|
|
}
|
|
catch (e) { /* continue */ }
|
|
}
|
|
if (!absolutePath) {
|
|
log.warn(`Could not find header file: ${headerPath}`);
|
|
return [];
|
|
}
|
|
log.silly(`Analyzing dependencies for: ${absolutePath}`);
|
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
const tree = parser.parse(content);
|
|
// Find dependencies from constructor
|
|
const className = findClassName(tree.rootNode);
|
|
if (!className)
|
|
return [];
|
|
const constructorDeclarations = tree.rootNode.descendantsOfType('constructor_or_destructor_declaration');
|
|
for (const declaration of constructorDeclarations) {
|
|
const functionName = declaration.childForFieldName('name')?.text;
|
|
if (functionName !== className)
|
|
continue; // It's a destructor, skip it
|
|
const declarators = declaration.descendantsOfType('function_declarator');
|
|
for (const declarator of declarators) {
|
|
const params = declarator.descendantsOfType('parameter_declaration');
|
|
for (const param of params) {
|
|
const typeNode = param.descendantsOfType('type_identifier')[0];
|
|
const typeName = typeNode?.text;
|
|
if (typeName && classNameToFeatureMap.has(typeName)) {
|
|
dependencies.add(classNameToFeatureMap.get(typeName));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Array.from(dependencies);
|
|
}
|
|
function findClassName(rootNode) {
|
|
const classSpecifier = rootNode.descendantsOfType('class_specifier')[0];
|
|
return classSpecifier?.childForFieldName('name')?.text ?? null;
|
|
}
|
|
// Main orchestration function
|
|
export async function generateFeatureTree(projectRoot) {
|
|
// Load and parse config.h
|
|
const configPath = path.join(projectRoot, 'src', 'config.h');
|
|
log.silly(`Loading config from: ${configPath}`);
|
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
const allDefines = parseDefines(configContent);
|
|
// Filter for active features
|
|
const enabledFeatures = [];
|
|
for (const [name, { isComment }] of allDefines.entries()) {
|
|
if (name.startsWith('ENABLE_') && !isComment) {
|
|
enabledFeatures.push(name);
|
|
}
|
|
}
|
|
log.info(`Found ${enabledFeatures.length} enabled features.`);
|
|
// Build the catalog of all possible features
|
|
const featureCatalog = await buildFeatureCatalog(projectRoot);
|
|
const includeDirs = [
|
|
path.join(projectRoot, 'src'),
|
|
path.join(projectRoot, 'lib/polymech-base/src')
|
|
];
|
|
const classNameToFeatureMap = await buildClassNameToFeatureMap(featureCatalog, includeDirs);
|
|
const finalTree = {};
|
|
for (const feature of enabledFeatures) {
|
|
const headerFile = featureCatalog.get(feature);
|
|
if (!headerFile) {
|
|
log.warn(`Enabled feature "${feature}" not found in features.h catalog.`);
|
|
continue;
|
|
}
|
|
const dependencies = await analyzeComponentDependencies(headerFile, includeDirs, classNameToFeatureMap);
|
|
const settings = {};
|
|
const baseName = feature.replace('ENABLE_', '');
|
|
for (const [define, { value, isComment }] of allDefines.entries()) {
|
|
if (!isComment && define.startsWith(baseName + '_') && value) {
|
|
settings[define] = value;
|
|
}
|
|
}
|
|
finalTree[feature] = {
|
|
headerFile,
|
|
dependencies,
|
|
settings
|
|
};
|
|
}
|
|
return finalTree;
|
|
}
|
|
async function* getFiles(dir) {
|
|
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
for (const dirent of dirents) {
|
|
const res = path.resolve(dir, dirent.name);
|
|
if (dirent.isDirectory()) {
|
|
yield* getFiles(res);
|
|
}
|
|
else if (res.endsWith('.h')) {
|
|
yield res;
|
|
}
|
|
}
|
|
}
|
|
export async function findComponents(includeDirs) {
|
|
const componentMap = new Map();
|
|
const parser = new Parser();
|
|
parser.setLanguage(Cpp);
|
|
const childByType = (node, type) => {
|
|
for (const child of node.children) {
|
|
if (child.grammarType === type) {
|
|
return child;
|
|
}
|
|
}
|
|
};
|
|
const childrenByType = (node, type) => {
|
|
const children = [];
|
|
for (const child of node.children) {
|
|
if (child.grammarType === type) {
|
|
children.push(child);
|
|
}
|
|
}
|
|
};
|
|
for (const dir of includeDirs) {
|
|
for await (const f of getFiles(dir)) {
|
|
try {
|
|
const content = await fs.readFile(f, 'utf-8');
|
|
if (!content)
|
|
continue;
|
|
const tree = parser.parse(content);
|
|
const classNodes = tree.rootNode.descendantsOfType('class_specifier');
|
|
const classNode = classNodes[0];
|
|
if (!classNode)
|
|
continue;
|
|
const nameNode = childByType(classNode, 'type_identifier');
|
|
if (!nameNode)
|
|
continue;
|
|
// componentMap.set(nameNode.text, f);
|
|
for (const classNode of classNodes) {
|
|
const baseClauseNode = childByType(classNode, 'base_class_clause');
|
|
if (!baseClauseNode)
|
|
continue;
|
|
const nameNode = childByType(baseClauseNode, 'identifier');
|
|
if (!nameNode)
|
|
continue;
|
|
if (nameNode.text === 'Component') {
|
|
const nameNode = childByType(baseClauseNode, 'identifier');
|
|
if (!nameNode)
|
|
continue;
|
|
componentMap.set(nameNode.text, f);
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.warn(`Skipping file: ${f}`, error instanceof Error ? error.message : error);
|
|
}
|
|
}
|
|
}
|
|
log.info(`Discovered ${componentMap.size} components inheriting from 'Component'.`);
|
|
return componentMap;
|
|
}
|
|
async function getDependencies(classNode, className, componentMap) {
|
|
const dependencies = [];
|
|
const constructorNode = classNode.descendantsOfType('declaration').find(decl => decl.childForFieldName('declarator')?.childForFieldName('declarator')?.text === className);
|
|
if (constructorNode) {
|
|
const params = constructorNode.descendantsOfType('parameter_declaration');
|
|
for (const param of params) {
|
|
const typeNode = param.childForFieldName('type');
|
|
const typeName = typeNode?.text.replace('*', '').trim();
|
|
if (typeName && componentMap.has(typeName)) {
|
|
const headerFile = componentMap.get(typeName);
|
|
const depTree = await getComponentTree(typeName, componentMap);
|
|
if (depTree)
|
|
dependencies.push(depTree);
|
|
}
|
|
}
|
|
}
|
|
return Array.from(new Map(dependencies.map(item => [item.name, item])).values());
|
|
}
|
|
export async function getComponentTree(targetComponentName, allComponents) {
|
|
const headerFile = allComponents.get(targetComponentName);
|
|
if (!headerFile) {
|
|
log.warn(`Component "${targetComponentName}" not found in component map.`);
|
|
return null;
|
|
}
|
|
const parser = new Parser();
|
|
parser.setLanguage(Cpp);
|
|
const content = await fs.readFile(headerFile, 'utf-8');
|
|
const tree = parser.parse(content);
|
|
const classNode = tree.rootNode.descendantsOfType('class_specifier')
|
|
.find(node => node.childForFieldName('name')?.text === targetComponentName);
|
|
if (!classNode) {
|
|
log.warn(`Could not find class specifier for "${targetComponentName}" in ${headerFile}`);
|
|
return null;
|
|
}
|
|
const baseComponent = classNode.childForFieldName('base')?.text.replace('public', '').trim() ?? null;
|
|
const dependencies = await getDependencies(classNode, targetComponentName, allComponents);
|
|
return {
|
|
name: targetComponentName,
|
|
headerFile: path.relative(path.resolve(headerFile, '..', '..', '..'), headerFile).replace(/\\/g, '/'),
|
|
baseComponent,
|
|
dependencies
|
|
};
|
|
}
|
|
//# sourceMappingURL=di.js.map
|