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