UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

704 lines (608 loc) 21.1 kB
const { parse } = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generate = require('@babel/generator').default; const t = require('@babel/types'); /** * Type-Aware Transforms Module * Handles TypeScript-specific transformations and type inference */ class TypeAwareTransforms { constructor() { this.typeDefinitions = new Map(); this.componentProps = new Map(); this.exportedTypes = new Set(); this.importedTypes = new Map(); } /** * Infer and add missing TypeScript types */ inferMissingTypes(ast, filePath, context = {}) { const transformations = []; const unTypedVariables = []; const unTypedFunctions = []; traverse(ast, { VariableDeclarator(path) { if (this._needsTypeAnnotation(path)) { const inferredType = this._inferVariableType(path); if (inferredType) { unTypedVariables.push({ path, name: path.node.id.name, inferredType, confidence: inferredType.confidence }); } } }, FunctionDeclaration(path) { if (this._functionNeedsTypes(path)) { const typeInfo = this._inferFunctionTypes(path); if (typeInfo) { unTypedFunctions.push({ path, name: path.node.id?.name || 'anonymous', ...typeInfo }); } } }, ArrowFunctionExpression(path) { if (this._functionNeedsTypes(path)) { const typeInfo = this._inferFunctionTypes(path); if (typeInfo && path.parent.type === 'VariableDeclarator') { unTypedFunctions.push({ path, name: path.parent.id?.name || 'anonymous', ...typeInfo }); } } } }); // Add type annotations for variables unTypedVariables.forEach(({ path, name, inferredType, confidence }) => { if (confidence > 0.7) { // Only add types we're confident about transformations.push({ type: 'add-variable-type', location: path.node.loc, action: () => { path.node.id.typeAnnotation = t.tsTypeAnnotation( this._createTSType(inferredType.type) ); }, description: `Added ${inferredType.type} type annotation to ${name}`, confidence }); } }); // Add type annotations for functions unTypedFunctions.forEach(({ path, name, paramTypes, returnType, confidence }) => { if (confidence > 0.7) { transformations.push({ type: 'add-function-types', location: path.node.loc, action: () => { // Add parameter types if (paramTypes && path.node.params) { path.node.params.forEach((param, index) => { if (paramTypes[index] && !param.typeAnnotation) { param.typeAnnotation = t.tsTypeAnnotation( this._createTSType(paramTypes[index]) ); } }); } // Add return type if (returnType && !path.node.returnType) { path.node.returnType = t.tsTypeAnnotation( this._createTSType(returnType) ); } }, description: `Added type annotations to function ${name}`, confidence }); } }); return transformations; } /** * Generate TypeScript interfaces from object patterns */ generateInterfacesFromUsage(ast, filePath, context = {}) { const transformations = []; const objectPatterns = new Map(); const functionProps = new Map(); // Collect object usage patterns traverse(ast, { ObjectExpression(path) { const pattern = this._analyzeObjectPattern(path); if (pattern.isComplex) { const key = this._generatePatternKey(pattern); if (!objectPatterns.has(key)) { objectPatterns.set(key, pattern); } } }, FunctionDeclaration(path) { if (this._isReactComponent(path)) { const propsUsage = this._analyzePropsUsage(path); if (propsUsage.properties.length > 0) { functionProps.set(path.node.id?.name || 'Component', propsUsage); } } }, ArrowFunctionExpression(path) { if (this._isReactComponent(path)) { const propsUsage = this._analyzePropsUsage(path); if (propsUsage.properties.length > 0 && path.parent.type === 'VariableDeclarator') { functionProps.set(path.parent.id?.name || 'Component', propsUsage); } } } }); // Generate interfaces for React component props functionProps.forEach((propsUsage, componentName) => { const interfaceName = `${componentName}Props`; const interfaceCode = this._generatePropsInterface(interfaceName, propsUsage); transformations.push({ type: 'generate-props-interface', location: { line: 1, column: 0 }, action: () => { const interfaceAST = parse(interfaceCode, { sourceType: 'module', plugins: ['typescript'] }); // Insert at the top of the file (after imports) const lastImportIndex = this._findLastImportIndex(ast); ast.body.splice(lastImportIndex + 1, 0, ...interfaceAST.body); }, description: `Generated ${interfaceName} interface`, interfaceCode }); }); // Generate interfaces for complex object patterns objectPatterns.forEach((pattern, key) => { const interfaceName = this._generateInterfaceName(pattern); const interfaceCode = this._generateObjectInterface(interfaceName, pattern); transformations.push({ type: 'generate-object-interface', location: { line: 1, column: 0 }, action: () => { const interfaceAST = parse(interfaceCode, { sourceType: 'module', plugins: ['typescript'] }); const lastImportIndex = this._findLastImportIndex(ast); ast.body.splice(lastImportIndex + 1, 0, ...interfaceAST.body); }, description: `Generated ${interfaceName} interface`, interfaceCode }); }); return transformations; } /** * Optimize TypeScript imports and type-only imports */ optimizeTypeImports(ast, filePath, context = {}) { const transformations = []; const typeOnlyImports = []; const valueImports = []; const typeUsage = new Map(); // First pass: categorize imports traverse(ast, { ImportDeclaration(path) { const source = path.node.source.value; path.node.specifiers.forEach(spec => { if (t.isImportSpecifier(spec)) { const importName = spec.imported.name; const localName = spec.local.name; if (this._isTypeOnlyUsed(ast, localName)) { typeOnlyImports.push({ source, importName, localName, path }); } else { valueImports.push({ source, importName, localName, path }); } } }); } }); // Group imports by source const importGroups = new Map(); [...typeOnlyImports, ...valueImports].forEach(imp => { if (!importGroups.has(imp.source)) { importGroups.set(imp.source, { types: [], values: [] }); } if (typeOnlyImports.includes(imp)) { importGroups.get(imp.source).types.push(imp); } else { importGroups.get(imp.source).values.push(imp); } }); // Create optimized import statements importGroups.forEach((group, source) => { if (group.types.length > 0 && group.values.length > 0) { // Need both type and value imports transformations.push({ type: 'optimize-mixed-imports', location: { line: 1, column: 0 }, action: () => { // Remove old imports group.types.concat(group.values).forEach(imp => { if (imp.path) imp.path.remove(); }); // Add optimized imports const typeImport = this._createTypeOnlyImport(source, group.types); const valueImport = this._createValueImport(source, group.values); ast.body.unshift(valueImport, typeImport); }, description: `Optimized imports from ${source} (separated types and values)` }); } else if (group.types.length > 0) { // Only type imports needed transformations.push({ type: 'convert-to-type-import', location: { line: 1, column: 0 }, action: () => { group.types.forEach(imp => { if (imp.path && !imp.path.node.importKind) { imp.path.node.importKind = 'type'; } }); }, description: `Converted imports from ${source} to type-only imports` }); } }); return transformations; } /** * Add strict TypeScript compiler checks */ enforceStrictTypes(ast, filePath, context = {}) { const transformations = []; const issues = []; traverse(ast, { TSAnyKeyword(path) { issues.push({ type: 'any-type-usage', location: path.node.loc, message: 'Avoid using "any" type' }); }, Identifier(path) { // Check for implicit any if (this._hasImplicitAny(path)) { issues.push({ type: 'implicit-any', location: path.node.loc, message: `Variable ${path.node.name} has implicit any type` }); } }, CallExpression(path) { // Check for unsafe type assertions if (t.isTSAsExpression(path.parent) || t.isTSTypeAssertion(path.parent)) { const confidence = this._analyzeTypeAssertionSafety(path); if (confidence < 0.5) { issues.push({ type: 'unsafe-type-assertion', location: path.node.loc, message: 'Potentially unsafe type assertion' }); } } } }); // Generate transformations for fixable issues issues.forEach(issue => { if (issue.type === 'implicit-any' && issue.location) { transformations.push({ type: 'fix-implicit-any', location: issue.location, action: () => { // Add explicit type annotation // This would need more context-specific implementation }, description: issue.message, severity: 'warning' }); } }); return transformations; } // Helper methods _needsTypeAnnotation(path) { // Check if variable already has type annotation if (path.node.id.typeAnnotation) return false; // Check if it's a simple literal that doesn't need annotation if (t.isLiteral(path.node.init)) return false; return true; } _inferVariableType(path) { const init = path.node.init; if (t.isStringLiteral(init)) { return { type: 'string', confidence: 0.9 }; } if (t.isNumericLiteral(init)) { return { type: 'number', confidence: 0.9 }; } if (t.isBooleanLiteral(init)) { return { type: 'boolean', confidence: 0.9 }; } if (t.isArrayExpression(init)) { const elementType = this._inferArrayElementType(init); return { type: `${elementType}[]`, confidence: 0.8 }; } if (t.isObjectExpression(init)) { return { type: 'object', confidence: 0.6 }; } if (t.isCallExpression(init)) { return this._inferCallExpressionType(init); } return null; } _functionNeedsTypes(path) { // Check if function already has parameter or return types const hasParamTypes = path.node.params.some(param => param.typeAnnotation); const hasReturnType = path.node.returnType; return !hasParamTypes || !hasReturnType; } _inferFunctionTypes(path) { const paramTypes = []; const returnType = this._inferReturnType(path); // Analyze parameter usage path.node.params.forEach((param, index) => { if (t.isIdentifier(param)) { const usage = this._analyzeParameterUsage(path, param.name); paramTypes[index] = usage.inferredType; } }); return { paramTypes, returnType: returnType.type, confidence: Math.min(returnType.confidence, 0.8) }; } _createTSType(typeString) { switch (typeString) { case 'string': return t.tsStringKeyword(); case 'number': return t.tsNumberKeyword(); case 'boolean': return t.tsBooleanKeyword(); case 'object': return t.tsObjectKeyword(); default: if (typeString.endsWith('[]')) { const elementType = typeString.slice(0, -2); return t.tsArrayType(this._createTSType(elementType)); } return t.tsTypeReference(t.identifier(typeString)); } } _analyzeObjectPattern(path) { const properties = []; let isComplex = false; if (t.isObjectExpression(path.node)) { path.node.properties.forEach(prop => { if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { const propType = this._inferValueType(prop.value); properties.push({ name: prop.key.name, type: propType.type, optional: false }); if (propType.type !== 'string' && propType.type !== 'number') { isComplex = true; } } }); } return { properties, isComplex }; } _isReactComponent(path) { // Check if function returns JSX let returnsJSX = false; traverse(path.node, { ReturnStatement(returnPath) { if (t.isJSXElement(returnPath.node.argument) || t.isJSXFragment(returnPath.node.argument)) { returnsJSX = true; } } }); return returnsJSX; } _analyzePropsUsage(path) { const properties = []; const propsParam = path.node.params[0]; if (t.isIdentifier(propsParam)) { const propsName = propsParam.name; traverse(path.node, { MemberExpression(memberPath) { if (t.isIdentifier(memberPath.node.object, { name: propsName }) && t.isIdentifier(memberPath.node.property)) { const propName = memberPath.node.property.name; const usage = this._analyzePropUsage(memberPath); properties.push({ name: propName, type: usage.inferredType || 'any', optional: usage.isOptional }); } } }); } return { properties }; } _generatePropsInterface(interfaceName, propsUsage) { const properties = propsUsage.properties.map(prop => ` ${prop.name}${prop.optional ? '?' : ''}: ${prop.type};` ).join('\n'); return `interface ${interfaceName} {\n${properties}\n}`; } _generateObjectInterface(interfaceName, pattern) { const properties = pattern.properties.map(prop => ` ${prop.name}${prop.optional ? '?' : ''}: ${prop.type};` ).join('\n'); return `interface ${interfaceName} {\n${properties}\n}`; } _findLastImportIndex(ast) { let lastIndex = -1; ast.body.forEach((node, index) => { if (t.isImportDeclaration(node)) { lastIndex = index; } }); return lastIndex; } _isTypeOnlyUsed(ast, localName) { let isTypeOnly = true; traverse(ast, { Identifier(path) { if (path.node.name === localName && !this._isInTypeContext(path)) { isTypeOnly = false; } } }); return isTypeOnly; } _isInTypeContext(path) { // Check if identifier is used in type annotation context return t.isTSTypeReference(path.parent) || t.isTSTypeAnnotation(path.parent) || t.isTSAsExpression(path.parent); } _createTypeOnlyImport(source, typeImports) { const specifiers = typeImports.map(imp => t.importSpecifier(t.identifier(imp.localName), t.identifier(imp.importName)) ); const importDecl = t.importDeclaration(specifiers, t.stringLiteral(source)); importDecl.importKind = 'type'; return importDecl; } _createValueImport(source, valueImports) { const specifiers = valueImports.map(imp => t.importSpecifier(t.identifier(imp.localName), t.identifier(imp.importName)) ); return t.importDeclaration(specifiers, t.stringLiteral(source)); } _hasImplicitAny(path) { // Simple check for implicit any - would need more sophisticated analysis return t.isIdentifier(path.node) && !path.node.typeAnnotation && path.isBindingIdentifier(); } _analyzeTypeAssertionSafety(path) { // Analyze type assertion safety - return confidence score return 0.8; // Placeholder } _inferArrayElementType(arrayExpr) { if (arrayExpr.elements.length === 0) return 'unknown'; const firstElement = arrayExpr.elements[0]; if (t.isStringLiteral(firstElement)) return 'string'; if (t.isNumericLiteral(firstElement)) return 'number'; if (t.isBooleanLiteral(firstElement)) return 'boolean'; return 'any'; } _inferCallExpressionType(callExpr) { // Analyze function call to infer return type if (t.isIdentifier(callExpr.callee)) { const funcName = callExpr.callee.name; // Common patterns if (funcName === 'useState') return { type: 'any', confidence: 0.6 }; if (funcName === 'useEffect') return { type: 'void', confidence: 0.9 }; if (funcName === 'fetch') return { type: 'Promise<Response>', confidence: 0.8 }; } return { type: 'unknown', confidence: 0.3 }; } _inferReturnType(path) { let returnType = 'void'; let confidence = 0.8; traverse(path.node, { ReturnStatement(returnPath) { if (returnPath.node.argument) { if (t.isJSXElement(returnPath.node.argument)) { returnType = 'JSX.Element'; confidence = 0.9; } else if (t.isStringLiteral(returnPath.node.argument)) { returnType = 'string'; confidence = 0.9; } else if (t.isNumericLiteral(returnPath.node.argument)) { returnType = 'number'; confidence = 0.9; } } } }); return { type: returnType, confidence }; } _analyzeParameterUsage(path, paramName) { let inferredType = 'any'; let isOptional = false; traverse(path.node, { MemberExpression(memberPath) { if (t.isIdentifier(memberPath.node.object, { name: paramName })) { // Parameter is accessed as object inferredType = 'object'; } }, CallExpression(callPath) { if (t.isIdentifier(callPath.node.callee, { name: paramName })) { // Parameter is called as function inferredType = 'Function'; } } }); return { inferredType, isOptional }; } _inferValueType(valueNode) { if (t.isStringLiteral(valueNode)) return { type: 'string' }; if (t.isNumericLiteral(valueNode)) return { type: 'number' }; if (t.isBooleanLiteral(valueNode)) return { type: 'boolean' }; if (t.isArrayExpression(valueNode)) return { type: 'any[]' }; if (t.isObjectExpression(valueNode)) return { type: 'object' }; return { type: 'any' }; } _generatePatternKey(pattern) { return pattern.properties.map(p => `${p.name}:${p.type}`).join(','); } _generateInterfaceName(pattern) { // Generate meaningful interface name based on pattern const firstProp = pattern.properties[0]; return firstProp ? `${firstProp.name.charAt(0).toUpperCase()}${firstProp.name.slice(1)}Data` : 'DataInterface'; } _analyzePropUsage(memberPath) { // Analyze how a prop is used to infer its type let inferredType = 'any'; let isOptional = false; // Check if used with optional chaining if (memberPath.node.optional) { isOptional = true; } // Check parent context for type hints const parent = memberPath.parent; if (t.isConditionalExpression(parent) || t.isLogicalExpression(parent)) { isOptional = true; } return { inferredType, isOptional }; } getAnalysisData() { return { typeDefinitions: Object.fromEntries(this.typeDefinitions), componentProps: Object.fromEntries(this.componentProps), exportedTypes: Array.from(this.exportedTypes), importedTypes: Object.fromEntries(this.importedTypes) }; } reset() { this.typeDefinitions.clear(); this.componentProps.clear(); this.exportedTypes.clear(); this.importedTypes.clear(); } } module.exports = { TypeAwareTransforms };