/**
 * TypeScript Compiler Scanner
 *
 * Uses the TypeScript compiler API to detect type safety issues:
 * - `any` type usage
 * - @ts-ignore and @ts-expect-error comments
 * - Missing return types on exported functions
 * - Unsafe type assertions (as unknown as T)
 * - Type coverage metrics
 *
 * @module scanners/typescript
 */
import ts from "typescript";
import { access } from "fs/promises";
import { join, relative } from "path";
import { logger } from "../logger.js";
function buildErrorDetails(phase, message, suggestions = []) {
    return {
        message,
        phase,
        suggestions,
    };
}
/**
 * Run TypeScript analysis and return findings
 */
export async function runTypeScriptAnalysis(projectPath) {
    const startTime = Date.now();
    const findings = [];
    // Check if tsconfig.json exists
    const tsconfigPath = join(projectPath, "tsconfig.json");
    try {
        await access(tsconfigPath);
    }
    catch {
        logger.debug("scanners.typescript_no_config", { projectPath });
        return {
            scanner: "tsc",
            findings: [],
            duration: Date.now() - startTime,
            success: false,
            error: "No tsconfig.json found",
            errorDetails: buildErrorDetails("init", "No tsconfig.json found in project root", [
                "Create a tsconfig.json: npx tsc --init",
                "Or specify a custom path in scanner options",
                "If this is not a TypeScript project, disable the tsc scanner",
            ]),
        };
    }
    try {
        // Parse tsconfig
        const configFile = ts.readConfigFile(tsconfigPath, (path) => {
            try {
                // Synchronous read for TypeScript API compatibility
                const fs = require("fs");
                return fs.readFileSync(path, "utf-8");
            }
            catch {
                return undefined;
            }
        });
        if (configFile.error) {
            const errorText = ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
            return {
                scanner: "tsc",
                findings: [],
                duration: Date.now() - startTime,
                success: false,
                error: `Failed to parse tsconfig.json: ${errorText}`,
                errorDetails: buildErrorDetails("parse", errorText, [
                    "Check tsconfig.json for JSON syntax errors",
                    "Validate extends path if using a base config",
                    "Run: npx tsc --showConfig to diagnose",
                ]),
            };
        }
        const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, projectPath);
        // Create program
        const program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
        const checker = program.getTypeChecker();
        // Get all TypeScript/TSX files
        const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("node_modules"));
        let filesScanned = 0;
        for (const sourceFile of sourceFiles) {
            filesScanned++;
            const relativePath = relative(projectPath, sourceFile.fileName);
            // Find issues in this file
            const fileFindings = analyzeSourceFile(sourceFile, checker, relativePath);
            findings.push(...fileFindings);
        }
        // Also check for compiler diagnostics
        const diagnostics = ts.getPreEmitDiagnostics(program);
        for (const diagnostic of diagnostics) {
            if (diagnostic.file && diagnostic.start !== undefined) {
                const { line } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
                const relativePath = relative(projectPath, diagnostic.file.fileName);
                // Skip node_modules
                if (relativePath.includes("node_modules"))
                    continue;
                const severity = diagnostic.category === ts.DiagnosticCategory.Error
                    ? "high"
                    : diagnostic.category === ts.DiagnosticCategory.Warning
                        ? "medium"
                        : "low";
                findings.push({
                    scanner: "tsc",
                    ruleId: `tsc:TS${diagnostic.code}`,
                    file: relativePath,
                    line: line + 1,
                    message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
                    severity,
                    confidence: 100,
                    evidence: getLineContent(diagnostic.file, line),
                    metadata: {
                        code: diagnostic.code,
                        category: ts.DiagnosticCategory[diagnostic.category],
                    },
                });
            }
        }
        logger.info("scanners.typescript_complete", {
            projectPath,
            filesScanned,
            findingsCount: findings.length,
            duration: Date.now() - startTime,
        });
        return {
            scanner: "tsc",
            findings,
            duration: Date.now() - startTime,
            success: true,
            filesScanned,
            version: ts.version,
        };
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        const errorStack = error instanceof Error ? error.stack : undefined;
        logger.error("scanners.typescript_failed", { error: errorMessage, stack: errorStack });
        const suggestions = [];
        if (errorMessage.includes("Cannot find module")) {
            suggestions.push("Run: npm install to install dependencies");
        }
        if (errorMessage.includes("memory")) {
            suggestions.push("Try increasing Node.js heap: NODE_OPTIONS=--max-old-space-size=4096");
        }
        if (suggestions.length === 0) {
            suggestions.push("Check that TypeScript is installed: npm install typescript");
            suggestions.push("Validate tsconfig.json is correct");
        }
        return {
            scanner: "tsc",
            findings: [],
            duration: Date.now() - startTime,
            success: false,
            error: errorMessage,
            errorDetails: buildErrorDetails("scan", errorMessage, suggestions),
        };
    }
}
/**
 * Analyze a source file for type safety issues
 */
function analyzeSourceFile(sourceFile, checker, relativePath) {
    const findings = [];
    function visit(node) {
        // Check for `any` type annotations
        if (ts.isTypeReferenceNode(node)) {
            const typeName = node.typeName.getText(sourceFile);
            if (typeName === "any") {
                const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
                findings.push({
                    scanner: "tsc",
                    ruleId: "tsc:explicit-any",
                    file: relativePath,
                    line: line + 1,
                    message: "Explicit 'any' type annotation reduces type safety",
                    severity: "medium",
                    confidence: 100,
                    evidence: getLineContent(sourceFile, line),
                    cweIds: ["CWE-704"], // Incorrect Type Conversion
                });
            }
        }
        // Check for any in type assertions
        if (ts.isAsExpression(node)) {
            const typeText = node.type.getText(sourceFile);
            if (typeText === "any" || typeText.includes("as any") || typeText.includes("as unknown as")) {
                const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
                findings.push({
                    scanner: "tsc",
                    ruleId: "tsc:unsafe-assertion",
                    file: relativePath,
                    line: line + 1,
                    message: "Unsafe type assertion bypasses type checking",
                    severity: "medium",
                    confidence: 100,
                    evidence: getLineContent(sourceFile, line),
                    cweIds: ["CWE-704"],
                });
            }
        }
        // Check for @ts-ignore and @ts-expect-error comments
        const leadingComments = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
        if (leadingComments) {
            for (const comment of leadingComments) {
                const commentText = sourceFile.text.slice(comment.pos, comment.end);
                if (commentText.includes("@ts-ignore") || commentText.includes("@ts-nocheck")) {
                    const { line } = sourceFile.getLineAndCharacterOfPosition(comment.pos);
                    findings.push({
                        scanner: "tsc",
                        ruleId: "tsc:ts-ignore",
                        file: relativePath,
                        line: line + 1,
                        message: "@ts-ignore suppresses type errors - consider fixing the underlying issue",
                        severity: "low",
                        confidence: 100,
                        evidence: commentText.trim(),
                    });
                }
            }
        }
        // Check for exported functions without return types
        if (ts.isFunctionDeclaration(node) &&
            node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&
            !node.type) {
            const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
            const funcName = node.name?.getText(sourceFile) || "anonymous";
            findings.push({
                scanner: "tsc",
                ruleId: "tsc:missing-return-type",
                file: relativePath,
                line: line + 1,
                message: `Exported function '${funcName}' should have an explicit return type`,
                severity: "low",
                confidence: 100,
                evidence: getLineContent(sourceFile, line),
            });
        }
        // Check for exported arrow functions without return types
        if (ts.isVariableStatement(node) &&
            node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
            for (const decl of node.declarationList.declarations) {
                if (decl.initializer &&
                    ts.isArrowFunction(decl.initializer) &&
                    !decl.type &&
                    !decl.initializer.type) {
                    const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
                    const varName = decl.name.getText(sourceFile);
                    findings.push({
                        scanner: "tsc",
                        ruleId: "tsc:missing-return-type",
                        file: relativePath,
                        line: line + 1,
                        message: `Exported function '${varName}' should have an explicit return type`,
                        severity: "low",
                        confidence: 100,
                        evidence: getLineContent(sourceFile, line),
                    });
                }
            }
        }
        ts.forEachChild(node, visit);
    }
    visit(sourceFile);
    return findings;
}
/**
 * Get the content of a specific line
 */
function getLineContent(sourceFile, lineNumber) {
    const lines = sourceFile.text.split("\n");
    return lines[lineNumber]?.trim() || "";
}
/**
 * Calculate type coverage for a project
 */
export async function calculateTypeCoverage(projectPath) {
    const tsconfigPath = join(projectPath, "tsconfig.json");
    try {
        await access(tsconfigPath);
    }
    catch {
        return null;
    }
    try {
        const configFile = ts.readConfigFile(tsconfigPath, (path) => {
            const fs = require("fs");
            return fs.readFileSync(path, "utf-8");
        });
        if (configFile.error)
            return null;
        const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, projectPath);
        const program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
        const checker = program.getTypeChecker();
        let totalSymbols = 0;
        let explicitlyTyped = 0;
        let anyTypes = 0;
        let unknownTypes = 0;
        for (const sourceFile of program.getSourceFiles()) {
            if (sourceFile.isDeclarationFile || sourceFile.fileName.includes("node_modules")) {
                continue;
            }
            function visit(node) {
                if (ts.isVariableDeclaration(node) || ts.isParameter(node)) {
                    totalSymbols++;
                    if (node.type) {
                        explicitlyTyped++;
                        const typeText = node.type.getText(sourceFile);
                        if (typeText === "any")
                            anyTypes++;
                        if (typeText === "unknown")
                            unknownTypes++;
                    }
                }
                ts.forEachChild(node, visit);
            }
            visit(sourceFile);
        }
        return {
            totalSymbols,
            explicitlyTyped,
            anyTypes,
            unknownTypes,
            coveragePercent: totalSymbols > 0 ? (explicitlyTyped / totalSymbols) * 100 : 100,
        };
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=typescript.js.map