Advertisement

← vibesdk

worker/services/code-fixer/fixers/ts2724.ts

15.8 KB Download file
/**
 * TS2724: Incorrect named import fixer
 * Handles cases where a named import doesn't exist but TypeScript suggests alternatives
 * Example: "'@/components/ui/sonner' has no exported member named 'toast'. Did you mean 'Toaster'?"
 */

import { CodeIssue } from '../../sandbox/sandboxTypes';
import { FixerContext, FixResult, FixedIssue, UnfixableIssue, FileObject } from '../types';
import { generateCode } from '../utils/ast';
import { getFileAST, findImportAtLocation, getFileExports } from '../utils/imports';
import { createObjectLogger } from '../../../logger';
import * as t from '@babel/types';
import { resolvePathAlias, findModuleFile } from '../utils/paths';
import {
    createUnfixableIssue,
    createExternalModuleError,
    handleFixerError,
    createFixerLogMessages,
    createSourceFileParseError,
    createMissingImportError
} from '../utils/helpers';
import { isExternalModule } from '../utils/modules';

const logger = createObjectLogger({ name: 'TS2724Fixer' }, 'TS2724Fixer');

/**
 * Fix TS2724 "Incorrect named import" errors
 * Replaces incorrect named imports with the suggested correct ones from TypeScript
 */
export function fixIncorrectNamedImport(
    context: FixerContext,
    issues: CodeIssue[]
): FixResult {
    const logs = createFixerLogMessages('TS2724Fixer', issues.length);
    logger.info(logs.start);
    
    const fixedIssues: FixedIssue[] = [];
    const unfixableIssues: UnfixableIssue[] = [];
    const modifiedFilesMap = new Map<string, FileObject>();
    const newFiles: FileObject[] = [];
    
    // Group issues by file to handle multiple corrections in the same import statement
    const issuesByFile = new Map<string, CodeIssue[]>();
    for (const issue of issues) {
        const fileIssues = issuesByFile.get(issue.filePath) || [];
        fileIssues.push(issue);
        issuesByFile.set(issue.filePath, fileIssues);
    }
    
    // Process each file's issues together
    for (const [filePath, fileIssues] of issuesByFile) {
        try {
            // Get source file AST once
            const sourceAST = getFileAST(
                filePath,
                context.files
            );
            
            if (!sourceAST) {
                logger.error(`Failed to parse source file: ${filePath}`);
                for (const issue of fileIssues) {
                    unfixableIssues.push(createSourceFileParseError(issue));
                }
                continue;
            }
            
            // Group replacements by module specifier and line
            const replacementsByImport = new Map<string, Map<string, string>>();
            const processedIssues: CodeIssue[] = [];
            
            for (const issue of fileIssues) {
                logger.info(logs.processing(issue));
                
                // Parse the error message to extract module, incorrect import, and suggested import
                const parseResult = parseTS2724ErrorMessage(issue.message);
                if (!parseResult) {
                    logger.warn(`Could not parse TS2724 error message: ${issue.message}`);
                    unfixableIssues.push(createUnfixableIssue(issue, 'Could not parse error message to extract import names'));
                    continue;
                }

                const { moduleSpecifier, incorrectImport, suggestedImport } = parseResult;
                
                // Check for external modules - we can't fix imports from external modules
                if (isExternalModule(moduleSpecifier)) {
                    logger.info(`Skipping external module: ${moduleSpecifier}`);
                    unfixableIssues.push(createExternalModuleError(issue, moduleSpecifier));
                    continue;
                }
                
                // Find the import statement at the error location
                const importInfo = findImportAtLocation(sourceAST, issue.line);
                if (!importInfo) {
                    logger.warn(`No import found at line ${issue.line} in ${filePath}`);
                    unfixableIssues.push(createMissingImportError(issue));
                    continue;
                }
                
                // Verify the import matches our expected module and incorrect import name
                if (importInfo.moduleSpecifier !== moduleSpecifier) {
                    logger.warn(`Module specifier mismatch. Expected: ${moduleSpecifier}, Found: ${importInfo.moduleSpecifier}`);
                    unfixableIssues.push(createUnfixableIssue(issue, 'Module specifier does not match error message'));
                    continue;
                }
                
                if (!importInfo.namedImports.includes(incorrectImport)) {
                    logger.warn(`Incorrect import '${incorrectImport}' not found in named imports: ${importInfo.namedImports.join(', ')}`);
                    unfixableIssues.push(createUnfixableIssue(issue, `Named import '${incorrectImport}' not found in import statement`));
                    continue;
                }
                
                // Verify the suggested export actually exists in the target module
                // Resolve the module path
                const resolvedPath = resolvePathAlias(moduleSpecifier);
                const targetFile = findModuleFile(
                    resolvedPath,
                    filePath,
                    context.files
                );
                
                
                if (targetFile) {
                    // Get exports from the target file
                    const targetAST = getFileAST(
                        targetFile,
                        context.files
                    );
                    
                    if (targetAST) {
                        const exports = getFileExports(targetAST);
                        
                        // Check if the suggested export exists
                        if (exports.namedExports.includes(suggestedImport) || 
                            exports.defaultExport === suggestedImport) {
                            // The suggested export exists, accumulate the replacement
                            const key = `${moduleSpecifier}:${issue.line}`;
                            if (!replacementsByImport.has(key)) {
                                replacementsByImport.set(key, new Map());
                            }
                            replacementsByImport.get(key)!.set(incorrectImport, suggestedImport);
                            processedIssues.push(issue);
                        } else {
                            logger.warn(`Suggested export '${suggestedImport}' not found in ${targetFile}`);
                            unfixableIssues.push(createUnfixableIssue(issue, `Suggested export '${suggestedImport}' not found in target module`));
                        }
                    } else {
                        logger.warn(`Could not parse target file: ${targetFile}`);
                        unfixableIssues.push(createUnfixableIssue(issue, 'Could not parse target module file'));
                    }
                } else {
                    logger.warn(`Could not find target module file for: ${moduleSpecifier}`);
                    unfixableIssues.push(createUnfixableIssue(issue, 'Target module file not found'));
                }
            }
            
            // Apply all replacements at once
            if (replacementsByImport.size > 0) {
                const fixedAST = applyMultipleNamedImportReplacements(sourceAST, replacementsByImport);
                if (fixedAST) {
                    // Generate the fixed code
                    const { code: fixedCode } = generateCode(fixedAST);
                    
                    // Store the result
                    modifiedFilesMap.set(filePath, {
                        filePath,
                        fileContents: fixedCode
                    });
                    
                    // Record all fixed issues
                    for (const issue of processedIssues) {
                        const parseResult = parseTS2724ErrorMessage(issue.message);
                        if (parseResult) {
                            fixedIssues.push({
                                issueCode: issue.ruleId || 'TS2724',
                                filePath: issue.filePath,
                                line: issue.line,
                                column: issue.column,
                                originalMessage: issue.message,
                                fixApplied: `Replaced incorrect named import '${parseResult.incorrectImport}' with '${parseResult.suggestedImport}' in module '${parseResult.moduleSpecifier}'`,
                                fixType: 'import_fix'
                            });
                            logger.info(`Successfully fixed TS2724 issue: replaced '${parseResult.incorrectImport}' with '${parseResult.suggestedImport}' in ${issue.filePath}`);
                        }
                    }
                }
            }
            
        } catch (error) {
            logger.error(`Error fixing TS2724 issues in ${filePath}:`, error);
            for (const issue of fileIssues) {
                unfixableIssues.push(handleFixerError(issue, error as Error, 'TS2724Fixer'));
            }
        }
    }
    
    logger.info(logs.completed(fixedIssues.length, unfixableIssues.length, modifiedFilesMap.size, newFiles.length));
    
    return {
        fixedIssues,
        unfixableIssues,
        modifiedFiles: Array.from(modifiedFilesMap.values()),
        newFiles
    };
}

/**
 * Parse TS2724 error message to extract module specifier, incorrect import, and suggested import
 * 
 * Examples:
 * - "'@/components/ui/sonner' has no exported member named 'toast'. Did you mean 'Toaster'?"
 * - "Module './utils' has no exported member 'utilFunction'. Did you mean 'utilityFunction'?"
 * - "'react' has no exported member named 'useCallback'. Did you mean 'useCallBack'?"
 */
export function parseTS2724ErrorMessage(errorMessage: string): {
    moduleSpecifier: string;
    incorrectImport: string;
    suggestedImport: string;
} | null {
    // Pattern 1: Standard format with single quotes around module
    // "'@/components/ui/sonner' has no exported member named 'toast'. Did you mean 'Toaster'?"
    const pattern1 = /^'([^']+)'\s+has no exported member named\s+'([^']+)'\.?\s+Did you mean\s+'([^']+)'\??\s*$/i;
    const match1 = errorMessage.match(pattern1);
    
    if (match1) {
        return {
            moduleSpecifier: match1[1],
            incorrectImport: match1[2],
            suggestedImport: match1[3]
        };
    }
    
    // Pattern 2: Module format
    // "Module './utils' has no exported member 'utilFunction'. Did you mean 'utilityFunction'?"
    const pattern2 = /^Module\s+'([^']+)'\s+has no exported member\s+'([^']+)'\.?\s+Did you mean\s+'([^']+)'\??\s*$/i;
    const match2 = errorMessage.match(pattern2);
    
    if (match2) {
        return {
            moduleSpecifier: match2[1],
            incorrectImport: match2[2],
            suggestedImport: match2[3]
        };
    }
    
    // Pattern 3: Alternative format with "named" keyword
    // "'react' has no exported member named 'useCallback'. Did you mean 'useCallBack'?"
    const pattern3 = /^'([^']+)'\s+has no exported member\s+named\s+'([^']+)'\.?\s+Did you mean\s+'([^']+)'\??\s*$/i;
    const match3 = errorMessage.match(pattern3);
    
    if (match3) {
        return {
            moduleSpecifier: match3[1],
            incorrectImport: match3[2],
            suggestedImport: match3[3]
        };
    }
    
    // Pattern 4: Handle double quotes instead of single quotes
    // '"@/components/ui/sonner" has no exported member named "toast". Did you mean "Toaster"?'
    const pattern4 = /^"([^"]+)"\s+has no exported member named\s+"([^"]+)"\.?\s+Did you mean\s+"([^"]+)"\??\s*$/i;
    const match4 = errorMessage.match(pattern4);
    
    if (match4) {
        return {
            moduleSpecifier: match4[1],
            incorrectImport: match4[2],
            suggestedImport: match4[3]
        };
    }
    
    return null;
}

/**
 * Replace a named import in the AST with a different named import
 */
export function replaceNamedImport(
    ast: t.File,
    moduleSpecifier: string,
    oldImportName: string,
    newImportName: string
): t.File | null {
    let importReplaced = false;
    
    // Create a copy of the AST to avoid mutating the original
    const newAST = t.cloneNode(ast, true, true);
    
    // Traverse the AST to find and replace the import
    t.traverseFast(newAST, (node) => {
        if (t.isImportDeclaration(node) && node.source.value === moduleSpecifier) {
            // Find the specific named import to replace
            node.specifiers = node.specifiers.map(specifier => {
                if (t.isImportSpecifier(specifier)) {
                    // Handle both regular named imports and aliased imports
                    const importedName = t.isIdentifier(specifier.imported) 
                        ? specifier.imported.name 
                        : specifier.imported.value;
                    
                    if (importedName === oldImportName) {
                        // Replace with the new import name
                        const newSpecifier = t.importSpecifier(
                            specifier.local, // Keep the same local name
                            t.identifier(newImportName) // Use the new imported name
                        );
                        importReplaced = true;
                        return newSpecifier;
                    }
                }
                return specifier;
            });
        }
    });
    
    return importReplaced ? newAST : null;
}

/**
 * Apply multiple named import replacements to handle multiple corrections in the same statement
 */
export function applyMultipleNamedImportReplacements(
    ast: t.File,
    replacementsByImport: Map<string, Map<string, string>>
): t.File | null {
    let anyReplaced = false;
    
    // Create a copy of the AST to avoid mutating the original
    const newAST = t.cloneNode(ast, true, true);
    
    // Traverse the AST to find and replace imports
    t.traverseFast(newAST, (node) => {
        if (t.isImportDeclaration(node)) {
            const moduleSpecifier = node.source.value;
            const line = node.loc?.start.line || 1; // Default to 1 if no location
            const key = `${moduleSpecifier}:${line}`;
            
            const replacements = replacementsByImport.get(key);
            if (replacements) {
                // Apply all replacements for this import
                node.specifiers = node.specifiers.map(specifier => {
                    if (t.isImportSpecifier(specifier)) {
                        const importedName = t.isIdentifier(specifier.imported) 
                            ? specifier.imported.name 
                            : specifier.imported.value;
                        
                        const newName = replacements.get(importedName);
                        if (newName) {
                            // Replace with the new import name
                            const newSpecifier = t.importSpecifier(
                                specifier.local, // Keep the same local name
                                t.identifier(newName) // Use the new imported name
                            );
                            anyReplaced = true;
                            return newSpecifier;
                        }
                    }
                    return specifier;
                });
            }
        }
    });
    
    return anyReplaced ? newAST : null;
}