@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
479 lines (422 loc) • 13.5 kB
JavaScript
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
/**
* Enhanced AST Engine with full @babel/traverse implementation
* Supports React/TypeScript/Next.js specific transformations
*/
class EnhancedASTEngine {
constructor() {
this.cache = new Map();
this.transformationStats = {
missingKeys: 0,
propTypes: 0,
imports: 0,
hooks: 0
};
}
/**
* Parse source code into AST with React/TypeScript support
*/
parseCode(code, filename = 'unknown.tsx') {
const cacheKey = `${filename}:${code.length}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const ast = parse(code, {
sourceType: 'module',
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
plugins: [
'jsx',
'typescript',
'decorators-legacy',
'dynamicImport',
'nullishCoalescingOperator',
'optionalChaining',
'exportDefaultFrom',
'exportNamespaceFrom',
'asyncGenerators',
'functionBind',
'objectRestSpread',
'classProperties'
]
});
this.cache.set(cacheKey, ast);
return ast;
} catch (error) {
console.warn(`[AST] Parse failed for ${filename}:`, error.message);
return null;
}
}
/**
* Transform missing keys in JSX elements (Layer 3: Components)
* Adds key props to JSX elements in map/forEach operations
*/
transformMissingKeys(ast, context = {}) {
const transformations = [];
let keyCounter = 0;
traverse(ast, {
JSXElement(path) {
// Check if element is in a map/forEach callback
const isInMapCallback = this._isInMapCallback(path);
const hasKeyProp = this._hasKeyProperty(path.node);
if (isInMapCallback && !hasKeyProp) {
const keyValue = this._generateKeyValue(context, keyCounter++);
transformations.push({
type: 'missing-key',
location: path.node.loc,
action: () => {
const keyAttr = t.jsxAttribute(
t.jsxIdentifier('key'),
t.jsxExpressionContainer(keyValue)
);
path.node.openingElement.attributes.push(keyAttr);
},
description: `Added key prop to JSX element in map operation`
});
}
}
});
this.transformationStats.missingKeys += transformations.length;
return transformations;
}
/**
* Transform PropTypes to TypeScript interfaces (Layer 3: Components)
*/
transformPropTypes(ast, context = {}) {
const transformations = [];
const propTypesImports = [];
const componentProps = new Map();
// First pass: collect PropTypes definitions
traverse(ast, {
AssignmentExpression(path) {
if (this._isPropTypesAssignment(path.node)) {
const componentName = this._extractComponentName(path);
const propTypesObject = path.node.right;
if (t.isObjectExpression(propTypesObject)) {
const tsInterface = this._convertPropTypesToInterface(
componentName,
propTypesObject,
context
);
componentProps.set(componentName, tsInterface);
transformations.push({
type: 'proptypes-to-ts',
location: path.node.loc,
action: () => path.remove(),
tsInterface,
description: `Converted PropTypes to TypeScript interface for ${componentName}`
});
}
}
},
ImportDeclaration(path) {
if (path.node.source.value === 'prop-types') {
propTypesImports.push(path);
transformations.push({
type: 'remove-proptypes-import',
location: path.node.loc,
action: () => path.remove(),
description: 'Removed PropTypes import'
});
}
}
});
// Second pass: update component function signatures
componentProps.forEach((tsInterface, componentName) => {
traverse(ast, {
FunctionDeclaration(path) {
if (path.node.id && path.node.id.name === componentName) {
this._addTypeScriptProps(path, tsInterface);
}
},
VariableDeclarator(path) {
if (t.isIdentifier(path.node.id, { name: componentName })) {
if (t.isArrowFunctionExpression(path.node.init) ||
t.isFunctionExpression(path.node.init)) {
this._addTypeScriptProps(path, tsInterface);
}
}
}
});
});
this.transformationStats.propTypes += transformations.length;
return transformations;
}
/**
* Optimize imports and remove unused ones (Layer 3: Components)
*/
transformImports(ast, context = {}) {
const transformations = [];
const usedImports = new Set();
const allImports = new Map();
// First pass: collect all imports
traverse(ast, {
ImportDeclaration(path) {
const source = path.node.source.value;
allImports.set(source, {
path,
specifiers: path.node.specifiers,
used: new Set()
});
}
});
// Second pass: track usage
traverse(ast, {
Identifier(path) {
// Skip import declarations themselves
if (path.isImportSpecifier() || path.isImportDefaultSpecifier()) {
return;
}
const name = path.node.name;
allImports.forEach((importInfo, source) => {
importInfo.specifiers.forEach(spec => {
if ((t.isImportSpecifier(spec) && spec.imported.name === name) ||
(t.isImportDefaultSpecifier(spec) && spec.local.name === name) ||
(t.isImportNamespaceSpecifier(spec) && spec.local.name === name)) {
importInfo.used.add(name);
usedImports.add(source);
}
});
});
}
});
// Generate transformations for unused imports
allImports.forEach((importInfo, source) => {
const unusedSpecifiers = importInfo.specifiers.filter(spec => {
const name = spec.local.name;
return !importInfo.used.has(name);
});
if (unusedSpecifiers.length === importInfo.specifiers.length) {
// Remove entire import
transformations.push({
type: 'remove-unused-import',
location: importInfo.path.node.loc,
action: () => importInfo.path.remove(),
description: `Removed unused import from '${source}'`
});
} else if (unusedSpecifiers.length > 0) {
// Remove specific specifiers
transformations.push({
type: 'optimize-import',
location: importInfo.path.node.loc,
action: () => {
importInfo.path.node.specifiers = importInfo.specifiers.filter(spec =>
!unusedSpecifiers.includes(spec)
);
},
description: `Removed ${unusedSpecifiers.length} unused import specifiers from '${source}'`
});
}
});
this.transformationStats.imports += transformations.length;
return transformations;
}
/**
* Optimize React hooks patterns (Layer 3: Components)
*/
transformHooks(ast, context = {}) {
const transformations = [];
traverse(ast, {
VariableDeclarator(path) {
// useState optimization
if (this._isUseStateCall(path.node.init)) {
const stateVarName = this._extractStateVariableName(path);
const optimization = this._analyzeStateUsage(path, stateVarName);
if (optimization) {
transformations.push({
type: 'optimize-usestate',
location: path.node.loc,
action: optimization.action,
description: optimization.description
});
}
}
// useEffect optimization
if (this._isUseEffectCall(path.node.init)) {
const effectOptimization = this._analyzeEffectDependencies(path);
if (effectOptimization) {
transformations.push({
type: 'optimize-useeffect',
location: path.node.loc,
action: effectOptimization.action,
description: effectOptimization.description
});
}
}
}
});
this.transformationStats.hooks += transformations.length;
return transformations;
}
/**
* Apply transformations to AST and generate code
*/
applyTransformations(ast, transformations) {
const results = {
success: 0,
failed: 0,
errors: []
};
transformations.forEach((transformation, index) => {
try {
transformation.action();
results.success++;
} catch (error) {
results.failed++;
results.errors.push({
transformation: transformation.type,
error: error.message,
index
});
}
});
return results;
}
/**
* Generate code from AST
*/
generateCode(ast, options = {}) {
try {
const result = generate(ast, {
compact: false,
comments: true,
retainLines: true,
...options
});
return {
code: result.code,
map: result.map
};
} catch (error) {
console.error('[AST] Code generation failed:', error.message);
return null;
}
}
/**
* Get transformation statistics
*/
getStats() {
return { ...this.transformationStats };
}
/**
* Reset cache and stats
*/
reset() {
this.cache.clear();
this.transformationStats = {
missingKeys: 0,
propTypes: 0,
imports: 0,
hooks: 0
};
}
// Helper methods
_isInMapCallback(path) {
let parent = path.parent;
while (parent) {
if (t.isCallExpression(parent) &&
t.isMemberExpression(parent.callee) &&
t.isIdentifier(parent.callee.property) &&
['map', 'forEach', 'filter'].includes(parent.callee.property.name)) {
return true;
}
parent = parent.parent;
}
return false;
}
_hasKeyProperty(jsxElement) {
return jsxElement.openingElement.attributes.some(attr =>
t.isJSXAttribute(attr) &&
t.isJSXIdentifier(attr.name, { name: 'key' })
);
}
_generateKeyValue(context, counter) {
// Try to use context variables like index, item.id, etc.
if (context.indexVariable) {
return t.identifier(context.indexVariable);
}
if (context.itemVariable && context.idProperty) {
return t.memberExpression(
t.identifier(context.itemVariable),
t.identifier(context.idProperty)
);
}
// Fallback to generated key
return t.stringLiteral(`key-${counter}`);
}
_isPropTypesAssignment(node) {
return t.isAssignmentExpression(node) &&
t.isMemberExpression(node.left) &&
t.isIdentifier(node.left.property, { name: 'propTypes' });
}
_extractComponentName(path) {
if (t.isMemberExpression(path.node.left) &&
t.isIdentifier(path.node.left.object)) {
return path.node.left.object.name;
}
return 'Component';
}
_convertPropTypesToInterface(componentName, propTypesObject, context) {
const properties = [];
propTypesObject.properties.forEach(prop => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
const propName = prop.key.name;
const tsType = this._convertPropTypeToTSType(prop.value);
properties.push(`${propName}: ${tsType}`);
}
});
return `interface ${componentName}Props {\n ${properties.join(';\n ')};\n}`;
}
_convertPropTypeToTSType(propTypeNode) {
// Basic PropTypes to TypeScript mapping
const propTypeMap = {
'PropTypes.string': 'string',
'PropTypes.number': 'number',
'PropTypes.bool': 'boolean',
'PropTypes.array': 'any[]',
'PropTypes.object': 'object',
'PropTypes.func': 'Function',
'PropTypes.node': 'React.ReactNode',
'PropTypes.element': 'React.ReactElement'
};
if (t.isMemberExpression(propTypeNode)) {
const propType = generate(propTypeNode).code;
return propTypeMap[propType] || 'any';
}
return 'any';
}
_addTypeScriptProps(path, tsInterface) {
// Add TypeScript interface to function parameters
// This is a simplified implementation
console.log(`Adding TypeScript props: ${tsInterface}`);
}
_isUseStateCall(node) {
return t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: 'useState' });
}
_isUseEffectCall(node) {
return t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: 'useEffect' });
}
_extractStateVariableName(path) {
if (t.isArrayPattern(path.node.id) && path.node.id.elements.length >= 1) {
return path.node.id.elements[0].name;
}
return null;
}
_analyzeStateUsage(path, stateVarName) {
// Analyze if state is used optimally
// Return optimization if needed
return null;
}
_analyzeEffectDependencies(path) {
// Analyze useEffect dependencies
// Return optimization if needed
return null;
}
}
module.exports = { EnhancedASTEngine };