HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/.codex/.tmp/plugins/plugins/plugin-eval/src/evaluators/python.js
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, /\belif\b/g) +
    countMatches(text, /\bfor\b/g) +
    countMatches(text, /\bwhile\b/g) +
    countMatches(text, /\bexcept\b/g) +
    countMatches(text, /\band\b/g) +
    countMatches(text, /\bor\b/g)
  );
}

function detectFunctions(lines) {
  const functions = [];
  for (let index = 0; index < lines.length; index += 1) {
    const line = lines[index];
    const match = /^(\s*)(?:async\s+def|def)\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*:/.exec(line);
    if (!match) {
      continue;
    }

    const indent = match[1].length;
    let endIndex = index;
    for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
      const cursorLine = lines[cursor];
      const trimmed = cursorLine.trim();
      if (!trimmed) {
        continue;
      }
      const cursorIndent = cursorLine.length - cursorLine.trimStart().length;
      if (cursorIndent <= indent && !trimmed.startsWith("#")) {
        break;
      }
      endIndex = cursor;
    }

    const slice = lines.slice(index, endIndex + 1);
    const source = slice.join("\n");
    const maxIndent = slice.reduce((max, sliceLine) => {
      if (!sliceLine.trim()) {
        return max;
      }
      const currentIndent = sliceLine.length - sliceLine.trimStart().length;
      return Math.max(max, currentIndent);
    }, indent);
    const params = match[3]
      .split(",")
      .map((item) => item.trim())
      .filter(Boolean).length;

    functions.push({
      name: match[2],
      startLine: index + 1,
      endLine: endIndex + 1,
      length: slice.length,
      params,
      complexity: 1 + countDecisionPoints(source),
      nesting: Math.max(0, Math.round((maxIndent - indent) / 4)),
    });
  }
  return functions;
}

export async function analyzePythonFiles(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("#")).length;
    const longLines = lines.filter((line) => line.length > 120).length;
    const imports = lines.filter((line) => /^(\s*)(import|from)\b/.test(line)).length;
    const classes = countMatches(source, /^\s*class\s+[A-Za-z0-9_]+/gm);
    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_[^/]+\.py$/.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: "py_file_count",
      category: "code-quality",
      value: filePaths.length,
      unit: "files",
      band: filePaths.length > 0 ? "good" : "info",
    }),
    createMetric({
      id: "py_function_count",
      category: "code-quality",
      value: functionCount,
      unit: "functions",
      band: functionCount > 0 ? "good" : "info",
    }),
    createMetric({
      id: "py_max_cyclomatic_complexity",
      category: "complexity",
      value: maxComplexity,
      unit: "score",
      band: maxComplexity >= 18 ? "heavy" : maxComplexity >= 10 ? "moderate" : "good",
    }),
    createMetric({
      id: "py_average_function_length",
      category: "readability",
      value: averageFunctionLength,
      unit: "lines",
      band: averageFunctionLength >= 50 ? "heavy" : averageFunctionLength >= 30 ? "moderate" : "good",
    }),
    createMetric({
      id: "py_max_nesting_depth",
      category: "complexity",
      value: maxNesting,
      unit: "levels",
      band: maxNesting >= 5 ? "heavy" : maxNesting >= 3 ? "moderate" : "good",
    }),
    createMetric({
      id: "py_comment_ratio",
      category: "readability",
      value: commentRatio,
      unit: "ratio",
      band: commentRatio < 0.03 ? "moderate" : "good",
    }),
    createMetric({
      id: "py_test_file_count",
      category: "best-practice",
      value: testFileCount,
      unit: "files",
      band: testFileCount > 0 ? "good" : "moderate",
    }),
  );

  if (maxComplexity >= 18) {
    checks.push(
      createCheck({
        id: "py-complexity-high",
        category: "complexity",
        severity: "warning",
        status: "warn",
        message: "At least one Python function has high cyclomatic complexity.",
        evidence: [`Max complexity: ${maxComplexity}`],
        remediation: ["Split complex functions into smaller helpers or guard clauses."],
      }),
    );
  }

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

  if (totalLongLines > 0) {
    checks.push(
      createCheck({
        id: "py-long-lines",
        category: "readability",
        severity: "warning",
        status: "warn",
        message: "Some Python lines exceed 120 characters.",
        evidence: [`Long lines: ${totalLongLines}`],
        remediation: ["Wrap long expressions and keep docstrings or SQL fragments easier to scan."],
      }),
    );
  }

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

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

  return { checks, metrics, artifacts };
}