← Files Plugin EvalARCHIVED FILE

src/evaluators/typescript.js

8.14 KB · Sep 30, 2026 · 23:18 UTC

↓ Download file

import { createArtifact, createCheck, createMetric } from "../core/schema.js";
import { readText, relativePath } from "../lib/files.js";

function countMatches(text, expression) {
  return (text.match(expression) || []).length;
}

function countDecisionPoints(text) {
  return (
    countMatches(text, /\bif\b/g) +
    countMatches(text, /\bfor\b/g) +
    countMatches(text, /\bwhile\b/g) +
    countMatches(text, /\bcase\b/g) +
    countMatches(text, /\bcatch\b/g) +
    countMatches(text, /&&/g) +
    countMatches(text, /\|\|/g) +
    countMatches(text, /\?/g)
  );
}

function detectFunctions(lines) {
  const functions = [];
  const startMatchers = [
    /\b(?:async\s+)?function\s+([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*\{/,
    /\b(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/,
    /^\s*(?:public|private|protected|static|readonly|\s)*(?!if\b|for\b|while\b|switch\b|catch\b)([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*\{/,
  ];

  for (let index = 0; index < lines.length; index += 1) {
    const line = lines[index];
    const match = startMatchers.map((matcher) => matcher.exec(line)).find(Boolean);
    if (!match) {
      continue;
    }

    let braceBalance = countMatches(line, /\{/g) - countMatches(line, /\}/g);
    let endIndex = index;
    while (braceBalance > 0 && endIndex + 1 < lines.length) {
      endIndex += 1;
      braceBalance += countMatches(lines[endIndex], /\{/g) - countMatches(lines[endIndex], /\}/g);
    }

    const slice = lines.slice(index, endIndex + 1);
    const source = slice.join("\n");
    const params = match[2]
      .split(",")
      .map((item) => item.trim())
      .filter(Boolean).length;

    let braceDepth = 0;
    let maxDepth = 0;
    for (const sliceLine of slice) {
      braceDepth += countMatches(sliceLine, /\{/g);
      maxDepth = Math.max(maxDepth, braceDepth);
      braceDepth -= countMatches(sliceLine, /\}/g);
    }

    functions.push({
      name: match[1],
      startLine: index + 1,
      endLine: endIndex + 1,
      length: slice.length,
      params,
      complexity: 1 + countDecisionPoints(source),
      nesting: Math.max(0, maxDepth - 1),
    });
  }

  return functions;
}

export async function analyzeTypeScriptFiles(filePaths, rootPath) {
  const checks = [];
  const metrics = [];
  const artifacts = [];

  if (filePaths.length === 0) {
    return { checks, metrics, artifacts };
  }

  const perFile = [];
  let totalCommentLines = 0;
  let totalCodeLines = 0;
  let totalLongLines = 0;
  let totalImports = 0;
  let totalClasses = 0;
  let maxComplexity = 0;
  let maxFunctionLength = 0;
  let maxNesting = 0;
  let functionCount = 0;
  let totalFunctionLength = 0;
  let testFileCount = 0;

  for (const filePath of filePaths) {
    const source = await readText(filePath);
    const lines = source.replace(/\r\n/g, "\n").split("\n");
    const functions = detectFunctions(lines);
    const commentLines = lines.filter((line) => line.trim().startsWith("//") || line.trim().startsWith("*") || line.trim().startsWith("/*")).length;
    const longLines = lines.filter((line) => line.length > 120).length;
    const imports = lines.filter((line) => line.trim().startsWith("import ")).length;
    const classes = countMatches(source, /\bclass\s+[A-Za-z0-9_$]+/g);
    const complexity = 1 + countDecisionPoints(source);
    const fileMaxComplexity = functions.reduce((max, fn) => Math.max(max, fn.complexity), complexity);
    const fileMaxFunctionLength = functions.reduce((max, fn) => Math.max(max, fn.length), 0);
    const fileMaxNesting = functions.reduce((max, fn) => Math.max(max, fn.nesting), 0);

    totalCommentLines += commentLines;
    totalCodeLines += lines.filter((line) => line.trim()).length;
    totalLongLines += longLines;
    totalImports += imports;
    totalClasses += classes;
    maxComplexity = Math.max(maxComplexity, fileMaxComplexity);
    maxFunctionLength = Math.max(maxFunctionLength, fileMaxFunctionLength);
    maxNesting = Math.max(maxNesting, fileMaxNesting);
    functionCount += functions.length;
    totalFunctionLength += functions.reduce((sum, fn) => sum + fn.length, 0);
    if (/(^|\/)(tests?|__tests__)\/|(\.test|\.spec)\./.test(filePath)) {
      testFileCount += 1;
    }

    perFile.push({
      path: relativePath(rootPath, filePath),
      functionCount: functions.length,
      complexity: fileMaxComplexity,
      maxFunctionLength: fileMaxFunctionLength,
      maxNesting: fileMaxNesting,
      longLines,
    });
  }

  const averageFunctionLength = functionCount > 0 ? Number((totalFunctionLength / functionCount).toFixed(2)) : 0;
  const commentRatio = totalCodeLines > 0 ? Number((totalCommentLines / totalCodeLines).toFixed(3)) : 0;

  metrics.push(
    createMetric({
      id: "ts_file_count",
      category: "code-quality",
      value: filePaths.length,
      unit: "files",
      band: filePaths.length > 0 ? "good" : "info",
    }),
    createMetric({
      id: "ts_function_count",
      category: "code-quality",
      value: functionCount,
      unit: "functions",
      band: functionCount > 0 ? "good" : "info",
    }),
    createMetric({
      id: "ts_max_cyclomatic_complexity",
      category: "complexity",
      value: maxComplexity,
      unit: "score",
      band: maxComplexity >= 18 ? "heavy" : maxComplexity >= 10 ? "moderate" : "good",
    }),
    createMetric({
      id: "ts_average_function_length",
      category: "readability",
      value: averageFunctionLength,
      unit: "lines",
      band: averageFunctionLength >= 50 ? "heavy" : averageFunctionLength >= 30 ? "moderate" : "good",
    }),
    createMetric({
      id: "ts_max_nesting_depth",
      category: "complexity",
      value: maxNesting,
      unit: "levels",
      band: maxNesting >= 5 ? "heavy" : maxNesting >= 3 ? "moderate" : "good",
    }),
    createMetric({
      id: "ts_comment_ratio",
      category: "readability",
      value: commentRatio,
      unit: "ratio",
      band: commentRatio < 0.03 ? "moderate" : "good",
    }),
    createMetric({
      id: "ts_test_file_count",
      category: "best-practice",
      value: testFileCount,
      unit: "files",
      band: testFileCount > 0 ? "good" : "moderate",
    }),
  );

  if (maxComplexity >= 18) {
    checks.push(
      createCheck({
        id: "ts-complexity-high",
        category: "complexity",
        severity: "warning",
        status: "warn",
        message: "At least one TypeScript function has high cyclomatic complexity.",
        evidence: [`Max complexity: ${maxComplexity}`],
        remediation: ["Split complex functions into smaller branches with clearer responsibilities."],
      }),
    );
  }

  if (maxFunctionLength >= 80) {
    checks.push(
      createCheck({
        id: "ts-function-length-high",
        category: "readability",
        severity: "warning",
        status: "warn",
        message: "At least one TypeScript function is long enough to hurt readability.",
        evidence: [`Max function length: ${maxFunctionLength} lines`],
        remediation: ["Break large functions into smaller helpers with single-purpose logic."],
      }),
    );
  }

  if (totalLongLines > 0) {
    checks.push(
      createCheck({
        id: "ts-long-lines",
        category: "readability",
        severity: "warning",
        status: "warn",
        message: "Some TypeScript lines exceed 120 characters.",
        evidence: [`Long lines: ${totalLongLines}`],
        remediation: ["Wrap long expressions and object literals to keep scanability high."],
      }),
    );
  }

  if (filePaths.length > 0 && testFileCount === 0) {
    checks.push(
      createCheck({
        id: "ts-tests-missing",
        category: "best-practice",
        severity: "warning",
        status: "warn",
        message: "TypeScript source files were found without matching test files.",
        evidence: [`Source files: ${filePaths.length}`],
        remediation: ["Add `.test.ts` or `.spec.ts` coverage for the main TypeScript logic."],
      }),
    );
  }

  artifacts.push(
    createArtifact({
      id: "ts-per-file-analysis",
      type: "analysis",
      label: "TypeScript per-file analysis",
      description: "Heuristic TypeScript quality and complexity breakdown by file.",
      data: {
        files: perFile,
      },
    }),
  );

  return { checks, metrics, artifacts };
}

SHA-256: 7554a0b564f46023b93b5ec9a7bc2d30c3f1488e420ab75dee26080d23af4bd3