UNPKG

@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
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 };