UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

2,037 lines • 60.8 kB
import * as fs from "node:fs";
import * as path from "node:path";
import { logLatency } from "./latency-logger.js";
import { findNearestContaining, walkUpDirs } from "./path-utils.js";
import { loadProjectSnapshot } from "./project-snapshot.js";
const FORMATTER_POLICY_BY_EXTENSION = new Map([
    [
        ".js",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".jsx",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".mjs",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".cjs",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".ts",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".tsx",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".mts",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".cts",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".py",
        {
            formatterNames: ["black", "ruff"],
            defaultFormatter: "ruff",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".pyi",
        {
            formatterNames: ["black", "ruff"],
            defaultFormatter: "ruff",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".json",
        {
            formatterNames: ["biome", "prettier"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: false,
            gate: "mixed",
        },
    ],
    [
        ".jsonc",
        {
            formatterNames: ["biome", "prettier"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: false,
            gate: "mixed",
        },
    ],
    [
        ".css",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".scss",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".sass",
        {
            formatterNames: ["biome", "prettier", "oxfmt"],
            defaultFormatter: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".less",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".html",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".htm",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".yaml",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".yml",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".md",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            // Prettier's markdown defaults reflow lines, normalize emphasis (* -> _),
            // and restyle lists. Opt-in via project prettier config; do not run by default.
            defaultWhenUnconfigured: false,
            gate: "smart-default",
        },
    ],
    [
        ".mdx",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: false,
            gate: "smart-default",
        },
    ],
    [
        ".graphql",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".gql",
        {
            formatterNames: ["prettier"],
            defaultFormatter: "prettier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        // ktfmt is offered alongside ktlint but only wins when the project opts in
        // (hasExplicitFormatterConfig("ktfmt")); otherwise ktlint stays the
        // smart-default. #129
        ".kt",
        {
            formatterNames: ["ktfmt", "ktlint"],
            defaultFormatter: "ktlint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".kts",
        {
            formatterNames: ["ktfmt", "ktlint"],
            defaultFormatter: "ktlint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".swift",
        {
            formatterNames: ["swiftformat"],
            defaultFormatter: "swiftformat",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".fs",
        {
            formatterNames: ["fantomas"],
            defaultFormatter: "fantomas",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".fsi",
        {
            formatterNames: ["fantomas"],
            defaultFormatter: "fantomas",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".fsx",
        {
            formatterNames: ["fantomas"],
            defaultFormatter: "fantomas",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".nix",
        {
            formatterNames: ["nixfmt"],
            defaultFormatter: "nixfmt",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".ex",
        {
            formatterNames: ["mix"],
            defaultFormatter: "mix",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".exs",
        {
            formatterNames: ["mix"],
            defaultFormatter: "mix",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".eex",
        {
            formatterNames: ["mix"],
            defaultFormatter: "mix",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".heex",
        {
            formatterNames: ["mix"],
            defaultFormatter: "mix",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".leex",
        {
            formatterNames: ["mix"],
            defaultFormatter: "mix",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".gleam",
        {
            formatterNames: ["gleam"],
            defaultFormatter: "gleam",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".c",
        {
            formatterNames: ["clang-format"],
            defaultFormatter: "clang-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".cc",
        {
            formatterNames: ["clang-format"],
            defaultFormatter: "clang-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".cpp",
        {
            formatterNames: ["clang-format"],
            defaultFormatter: "clang-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".cxx",
        {
            formatterNames: ["clang-format"],
            defaultFormatter: "clang-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".h",
        {
            formatterNames: ["clang-format"],
            defaultFormatter: "clang-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".hpp",
        {
            formatterNames: ["clang-format"],
            defaultFormatter: "clang-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".ino",
        {
            formatterNames: ["clang-format"],
            defaultFormatter: "clang-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".php",
        {
            formatterNames: ["php-cs-fixer"],
            defaultFormatter: "php-cs-fixer",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".cs",
        {
            formatterNames: ["csharpier"],
            defaultFormatter: "csharpier",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".lua",
        {
            formatterNames: ["stylua"],
            defaultFormatter: "stylua",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".hs",
        {
            formatterNames: ["ormolu"],
            defaultFormatter: "ormolu",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".lhs",
        {
            formatterNames: ["ormolu"],
            defaultFormatter: "ormolu",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".ml",
        {
            formatterNames: ["ocamlformat"],
            defaultFormatter: "ocamlformat",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".mli",
        {
            formatterNames: ["ocamlformat"],
            defaultFormatter: "ocamlformat",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".go",
        {
            formatterNames: ["gofmt"],
            defaultFormatter: "gofmt",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".rs",
        {
            formatterNames: ["rustfmt"],
            defaultFormatter: "rustfmt",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".sh",
        {
            formatterNames: ["shfmt"],
            defaultFormatter: "shfmt",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".bash",
        {
            formatterNames: ["shfmt"],
            defaultFormatter: "shfmt",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".fish",
        {
            formatterNames: ["fish-indent"],
            defaultFormatter: "fish-indent",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".toml",
        {
            formatterNames: ["taplo"],
            defaultFormatter: "taplo",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".tf",
        {
            formatterNames: ["terraform"],
            defaultFormatter: "terraform",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".tfvars",
        {
            formatterNames: ["terraform"],
            defaultFormatter: "terraform",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".dart",
        {
            formatterNames: ["dart"],
            defaultFormatter: "dart",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".zig",
        {
            formatterNames: ["zig"],
            defaultFormatter: "zig",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".zon",
        {
            formatterNames: ["zig"],
            defaultFormatter: "zig",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".java",
        {
            formatterNames: ["google-java-format"],
            defaultFormatter: "google-java-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".clj",
        {
            formatterNames: ["cljfmt"],
            defaultFormatter: "cljfmt",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".cljc",
        {
            formatterNames: ["cljfmt"],
            defaultFormatter: "cljfmt",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".cljs",
        {
            formatterNames: ["cljfmt"],
            defaultFormatter: "cljfmt",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".cmake",
        {
            formatterNames: ["cmake-format"],
            defaultFormatter: "cmake-format",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        },
    ],
    [
        ".ps1",
        {
            formatterNames: ["psscriptanalyzer-format"],
            defaultFormatter: "psscriptanalyzer-format",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".psm1",
        {
            formatterNames: ["psscriptanalyzer-format"],
            defaultFormatter: "psscriptanalyzer-format",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
    [
        ".psd1",
        {
            formatterNames: ["psscriptanalyzer-format"],
            defaultFormatter: "psscriptanalyzer-format",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        },
    ],
]);
// oxfmt supports these extensions — registered as a candidate formatter for each.
// Using a post-processing pass avoids repeating the same modification across
// many map entries (and keeps SonarCloud's duplication gate happy).
const OXFMT_SUPPORTED_EXTENSIONS = new Set([
    ".js", ".jsx", ".mjs", ".cjs",
    ".ts", ".tsx", ".mts", ".cts",
    ".vue",
    ".css", ".scss", ".less",
    ".html", ".htm",
    ".json", ".jsonc",
    ".yaml", ".yml",
    ".md", ".mdx",
    ".graphql", ".gql",
    ".toml",
]);
// Add .vue entry (no prior formatter policy existed for this extension)
FORMATTER_POLICY_BY_EXTENSION.set(".vue", {
    formatterNames: ["prettier"],
    defaultFormatter: "prettier",
    defaultWhenUnconfigured: false,
    gate: "config-first",
});
for (const [ext, policy] of FORMATTER_POLICY_BY_EXTENSION) {
    if (OXFMT_SUPPORTED_EXTENSIONS.has(ext) && !policy.formatterNames.includes("oxfmt")) {
        policy.formatterNames.push("oxfmt");
    }
}
const AUTO_INSTALLABLE_DEFAULT_FORMATTERS = new Map([
    ["biome", "biome"],
    ["ruff", "ruff"],
    ["prettier", "prettier"],
    ["shfmt", "shfmt"],
    ["taplo", "taplo"],
    ["ktlint", "ktlint"],
]);
export function getFormatterPolicyForExtension(ext) {
    return FORMATTER_POLICY_BY_EXTENSION.get(ext.toLowerCase());
}
export function getFormatterPolicyForFile(filePath) {
    return getFormatterPolicyForExtension(path.extname(filePath));
}
export function getSmartDefaultFormatterName(filePath) {
    const policy = getFormatterPolicyForFile(filePath);
    if (!policy?.defaultWhenUnconfigured)
        return undefined;
    return policy.defaultFormatter;
}
export function getAutoInstallToolIdForFormatter(formatterName) {
    return AUTO_INSTALLABLE_DEFAULT_FORMATTERS.get(formatterName);
}
export function getToolExecutionPolicy(toolId) {
    return TOOL_EXECUTION_POLICY.get(toolId);
}
export function shouldAutoInstallTool(toolId) {
    return getToolExecutionPolicy(toolId)?.autoInstall ?? false;
}
export function getAutofixCapability(toolId) {
    return AUTOFIX_CAPABILITIES.get(toolId);
}
/** Tool ids declared as safe pipeline-autofix capable (for consistency guards). */
export function listSafePipelineAutofixTools() {
    return [...AUTOFIX_CAPABILITIES.entries()]
        .filter(([, cap]) => cap.safePipelineAutofix)
        .map(([id]) => id);
}
export function canToolAutoFix(toolId) {
    return getAutofixCapability(toolId)?.toolSupportsFix ?? false;
}
export function isSafePipelineAutofixTool(toolId) {
    return getAutofixCapability(toolId)?.safePipelineAutofix ?? false;
}
export function getToolCommandSpec(toolId) {
    return TOOL_COMMAND_SPECS.get(toolId);
}
const AUTOFIX_CAPABILITIES = new Map([
    [
        "biome",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "eslint",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "ruff",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "stylelint",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "sqlfluff",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "rubocop",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "ktlint",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "ktfmt",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "rust-clippy",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "dart-analyze",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "golangci-lint",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "detekt",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "markdownlint",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
    [
        "oxlint",
        { toolSupportsFix: true, safePipelineAutofix: true, fixKind: "pipeline" },
    ],
]);
const TOOL_EXECUTION_POLICY = new Map([
    ["biome", { gate: "smart-default", autoInstall: true }],
    ["ruff", { gate: "smart-default", autoInstall: true }],
    ["oxlint", { gate: "smart-default", autoInstall: true }],
    ["stylelint", { gate: "smart-default", autoInstall: true }],
    ["sqlfluff", { gate: "smart-default", autoInstall: true }],
    ["rubocop", { gate: "smart-default", autoInstall: true }],
    ["yamllint", { gate: "smart-default", autoInstall: true }],
    ["actionlint", { gate: "smart-default", autoInstall: true }],
    ["markdownlint", { gate: "smart-default", autoInstall: true }],
    ["mypy", { gate: "config-first", autoInstall: true }],
    ["taplo", { gate: "smart-default", autoInstall: true }],
    ["hadolint", { gate: "smart-default", autoInstall: true }],
    ["htmlhint", { gate: "smart-default", autoInstall: true }],
    ["ktlint", { gate: "smart-default", autoInstall: true }],
    // ktfmt is opt-in (a project's explicit formatting choice), so it only runs
    // when its config marker is present — config-first, but auto-installable via
    // the maven-JAR strategy once elected (#129).
    ["ktfmt", { gate: "config-first", autoInstall: true }],
    ["golangci-lint", { gate: "config-first", autoInstall: true }],
    // SpotBugs is opt-in (lens-spotbugs flag) + config-first (needs a Java build
    // descriptor + compiled .class files), auto-installed via the archive
    // strategy when elected (#133).
    ["spotbugs", { gate: "config-first", autoInstall: true }],
    // Opengrep is opt-in (lens-opengrep flag or a discovered rule file) and
    // config-first, but auto-installable via the GitHub single-binary strategy
    // once elected — no login/token required, unlike Semgrep (#111).
    ["opengrep", { gate: "config-first", autoInstall: true }],
    ["phpstan", { gate: "config-first", autoInstall: false }],
    ["eslint", { gate: "config-first", autoInstall: false }],
    ["prettier", { gate: "smart-default", autoInstall: true }],
    ["vale", { gate: "config-first", autoInstall: false }],
    ["swiftlint", { gate: "smart-default", autoInstall: true }],
]);
const TOOL_COMMAND_SPECS = new Map([
    [
        "eslint",
        {
            command: "eslint",
            windowsExt: ".cmd",
            versionArgs: ["--version"],
            managedToolId: "eslint",
        },
    ],
    [
        "stylelint",
        {
            command: "stylelint",
            windowsExt: ".cmd",
            versionArgs: ["--version"],
            managedToolId: "stylelint",
        },
    ],
    [
        "sqlfluff",
        {
            command: "sqlfluff",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "sqlfluff",
        },
    ],
    [
        "oxlint",
        {
            command: "oxlint",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "oxlint",
        },
    ],
    [
        "ruff",
        {
            command: "ruff",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "ruff",
        },
    ],
    [
        "biome",
        {
            command: "biome",
            windowsExt: ".cmd",
            versionArgs: ["--version"],
            managedToolId: "biome",
        },
    ],
    [
        "rubocop",
        {
            command: "rubocop",
            versionArgs: ["--version"],
            managedToolId: "rubocop",
        },
    ],
    [
        "yamllint",
        {
            command: "yamllint",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "yamllint",
        },
    ],
    [
        "actionlint",
        {
            command: "actionlint",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "actionlint",
        },
    ],
    [
        "markdownlint",
        {
            command: "markdownlint-cli2",
            windowsExt: ".cmd",
            versionArgs: ["--version"],
            managedToolId: "markdownlint",
        },
    ],
    [
        "vale",
        {
            command: "vale",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "vale",
        },
    ],
    [
        "swiftlint",
        {
            command: "swiftlint",
            versionArgs: ["--version"],
            managedToolId: "swiftlint",
        },
    ],
    [
        "mypy",
        {
            command: "mypy",
            versionArgs: ["--version"],
            managedToolId: "mypy",
        },
    ],
    [
        "phpstan",
        {
            command: "phpstan",
            windowsExt: ".bat",
            versionArgs: ["--version"],
            managedToolId: "phpstan",
        },
    ],
    [
        "taplo",
        {
            command: "taplo",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "taplo",
        },
    ],
    [
        "hadolint",
        {
            command: "hadolint",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "hadolint",
        },
    ],
    [
        "htmlhint",
        {
            command: "htmlhint",
            versionArgs: ["--version"],
            managedToolId: "htmlhint",
        },
    ],
    [
        "ktlint",
        {
            command: "ktlint",
            windowsExt: ".exe",
            versionArgs: ["--version"],
            managedToolId: "ktlint",
        },
    ],
    [
        "prettier",
        {
            command: "prettier",
            windowsExt: ".cmd",
            versionArgs: ["--version"],
            managedToolId: "prettier",
        },
    ],
]);
const STYLELINT_CONFIGS = [
    ".stylelintrc",
    ".stylelintrc.json",
    ".stylelintrc.jsonc",
    ".stylelintrc.yaml",
    ".stylelintrc.yml",
    ".stylelintrc.js",
    ".stylelintrc.cjs",
    "stylelint.config.js",
    "stylelint.config.cjs",
    "stylelint.config.mjs",
];
const SQLFLUFF_CONFIGS = [
    ".sqlfluff",
    "pyproject.toml",
    "setup.cfg",
    "tox.ini",
];
const RUBOCOP_CONFIGS = [".rubocop.yml", ".rubocop.yaml"];
const MYPY_CONFIGS = ["mypy.ini", ".mypy.ini", "setup.cfg", "pyproject.toml"];
const YAMLLINT_CONFIGS = [
    ".yamllint",
    ".yamllint.yml",
    ".yamllint.yaml",
    "pyproject.toml",
    "setup.cfg",
    "tox.ini",
];
const MARKDOWNLINT_CONFIGS = [
    ".markdownlint.json",
    ".markdownlint.jsonc",
    ".markdownlint.yaml",
    ".markdownlint.yml",
    ".markdownlintrc",
];
const PRETTIER_CONFIGS = [
    ".prettierrc",
    ".prettierrc.json",
    ".prettierrc.yml",
    ".prettierrc.yaml",
    ".prettierrc.js",
    ".prettierrc.cjs",
    ".prettierrc.mjs",
    "prettier.config.js",
    "prettier.config.cjs",
    "prettier.config.mjs",
    "prettier.config.ts",
];
const RUFF_PROJECT_CONFIGS = ["ruff.toml", ".ruff.toml"];
const GOLANGCI_CONFIGS = [
    ".golangci.yml",
    ".golangci.yaml",
    ".golangci.toml",
    ".golangci.json",
];
const PHPSTAN_CONFIGS = [
    "phpstan.neon",
    "phpstan.neon.dist",
    "phpstan.dist.neon",
];
const VITE_CONFIGS = [
    "vite.config.ts",
    "vite.config.mts",
    "vite.config.cts",
    "vite.config.js",
    "vite.config.mjs",
    "vite.config.cjs",
];
export function getLinterPolicyForFile(filePath, context = {}) {
    const ext = path.extname(filePath).toLowerCase();
    if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
        const policy = getJstsLintPolicy({
            hasEslintConfig: context.hasEslintConfig,
            hasOxlintConfig: context.hasOxlintConfig,
            hasBiomeConfig: context.hasBiomeConfig,
        });
        return {
            runnerNames: ["eslint", "oxlint", "biome-check-json"],
            preferredRunners: policy.preferredRunners,
            defaultRunner: policy.preferredRunners[0],
            defaultWhenUnconfigured: !policy.hasEslintConfig && !policy.hasOxlintConfig,
            gate: policy.hasEslintConfig ? "config-first" : "smart-default",
        };
    }
    if ([".py", ".pyi"].includes(ext)) {
        const preferredRunners = ["ruff-lint"];
        if (context.hasMypyConfig)
            preferredRunners.push("mypy");
        return {
            runnerNames: ["ruff-lint", "mypy"],
            preferredRunners,
            defaultRunner: "ruff-lint",
            defaultWhenUnconfigured: true,
            gate: context.hasMypyConfig ? "mixed" : "smart-default",
        };
    }
    if ([".css", ".scss", ".sass", ".less"].includes(ext)) {
        return {
            runnerNames: ["stylelint"],
            preferredRunners: ["stylelint"],
            defaultRunner: "stylelint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if (ext === ".sql") {
        return {
            runnerNames: ["sqlfluff"],
            preferredRunners: ["sqlfluff"],
            defaultRunner: "sqlfluff",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".rb", ".rake", ".gemspec", ".ru"].includes(ext)) {
        return {
            runnerNames: ["rubocop"],
            preferredRunners: ["rubocop"],
            defaultRunner: "rubocop",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".yaml", ".yml"].includes(ext)) {
        return {
            runnerNames: ["yamllint"],
            preferredRunners: ["yamllint"],
            defaultRunner: "yamllint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".md", ".mdx"].includes(ext)) {
        return {
            runnerNames: ["markdownlint"],
            preferredRunners: ["markdownlint"],
            defaultRunner: "markdownlint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".html", ".htm"].includes(ext)) {
        return {
            runnerNames: ["htmlhint"],
            preferredRunners: ["htmlhint"],
            defaultRunner: "htmlhint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if (path.basename(filePath).toLowerCase() === "dockerfile") {
        return {
            runnerNames: ["hadolint"],
            preferredRunners: ["hadolint"],
            defaultRunner: "hadolint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".kt", ".kts"].includes(ext)) {
        // When the project opts into ktfmt, ktfmt (a pure formatter wired as a safe
        // autofix) owns Kotlin formatting; ktlint's lint steps aside so its style
        // suggestions don't conflict with ktfmt's output. detekt's *semantic* lint
        // still runs when configured. #129
        const preferredRunners = [];
        if (!context.hasKtfmtConfig)
            preferredRunners.push("ktlint");
        if (context.hasDetektConfig)
            preferredRunners.push("detekt");
        return {
            runnerNames: ["ktlint", "detekt"],
            preferredRunners,
            defaultRunner: preferredRunners[0],
            defaultWhenUnconfigured: true,
            gate: context.hasKtfmtConfig
                ? "config-first"
                : context.hasDetektConfig
                    ? "mixed"
                    : "smart-default",
        };
    }
    if (ext === ".toml") {
        return {
            runnerNames: ["taplo"],
            preferredRunners: ["taplo"],
            defaultRunner: "taplo",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if (ext === ".go") {
        return {
            runnerNames: ["golangci-lint"],
            preferredRunners: context.hasGolangciConfig ? ["golangci-lint"] : [],
            defaultRunner: "golangci-lint",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        };
    }
    if (ext === ".php") {
        return {
            runnerNames: ["phpstan"],
            preferredRunners: context.hasPhpstanConfig ? ["phpstan"] : [],
            defaultRunner: "phpstan",
            defaultWhenUnconfigured: false,
            gate: "config-first",
        };
    }
    if (ext === ".rs") {
        return {
            runnerNames: ["rust-clippy"],
            preferredRunners: ["rust-clippy"],
            defaultRunner: "rust-clippy",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".sh", ".bash"].includes(ext)) {
        return {
            runnerNames: ["shellcheck"],
            preferredRunners: ["shellcheck"],
            defaultRunner: "shellcheck",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if (ext === ".fish") {
        return {
            runnerNames: ["fish-indent"],
            preferredRunners: ["fish-indent"],
            defaultRunner: "fish-indent",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".tf", ".tfvars"].includes(ext)) {
        return {
            runnerNames: ["tflint"],
            preferredRunners: ["tflint"],
            defaultRunner: "tflint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".ex", ".exs", ".eex", ".heex", ".leex"].includes(ext)) {
        return {
            runnerNames: ["credo"],
            preferredRunners: ["credo"],
            defaultRunner: "credo",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".ino"].includes(ext)) {
        return {
            runnerNames: ["cpp-check"],
            preferredRunners: ["cpp-check"],
            defaultRunner: "cpp-check",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if (ext === ".dart") {
        return {
            runnerNames: ["dart-analyze"],
            preferredRunners: ["dart-analyze"],
            defaultRunner: "dart-analyze",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if (ext === ".gleam") {
        return {
            runnerNames: ["gleam-check"],
            preferredRunners: ["gleam-check"],
            defaultRunner: "gleam-check",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if ([".ps1", ".psm1", ".psd1"].includes(ext)) {
        return {
            runnerNames: ["psscriptanalyzer"],
            preferredRunners: ["psscriptanalyzer"],
            defaultRunner: "psscriptanalyzer",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    if (ext === ".prisma") {
        return {
            runnerNames: ["prisma-validate"],
            preferredRunners: ["prisma-validate"],
            defaultRunner: "prisma-validate",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
        };
    }
    return undefined;
}
/**
 * Returns the cached `ProjectConventions` for `cwd` if a project snapshot
 * exists, otherwise `undefined`. Reads from the snapshot file rather than
 * re-running the detector so the hot path (per-file dispatch) stays cheap.
 *
 * The snapshot is refreshed by `saveRuntimeProjectSnapshot` during the
 * normal runtime lifecycle, so consumers see a value updated at session-
 * start / project-seq bumps rather than on every read.
 */
export function getCachedProjectConventions(cwd) {
    const snapshot = loadProjectSnapshot(cwd);
    return snapshot?.conventions;
}
export function getLinterPolicyForCwd(filePath, cwd) {
    const context = {
        hasEslintConfig: hasEslintConfig(cwd),
        hasOxlintConfig: hasOxlintConfig(cwd),
        hasBiomeConfig: hasBiomeConfig(cwd),
        hasStylelintConfig: hasStylelintConfig(cwd),
        hasSqlfluffConfig: hasSqlfluffConfig(cwd),
        hasRubocopConfig: hasRubocopConfig(cwd),
        hasYamllintConfig: hasYamllintConfig(cwd),
        hasMarkdownlintConfig: hasMarkdownlintConfig(cwd),
        hasGolangciConfig: hasGolangciConfig(cwd),
        hasPhpstanConfig: hasPhpstanConfig(cwd),
        hasMypyConfig: hasMypyConfig(cwd),
        hasDetektConfig: hasDetektConfig(cwd),
        hasKtfmtConfig: hasKtfmtConfig(cwd),
    };
    const policy = getLinterPolicyForFile(filePath, context);
    if (policy) {
        const conventions = getCachedProjectConventions(cwd);
        if (conventions && conventions.frameworks.length > 0) {
            policy.frameworkHints = conventions.frameworks.map((f) => f.id);
        }
    }
    logLatency({
        type: "phase",
        phase: "linter_selected",
        filePath,
        durationMs: 0,
        metadata: {
            runner: policy?.defaultRunner ?? null,
            gate: policy?.gate ?? null,
            cwd,
            context,
            frameworkHints: policy?.frameworkHints ?? null,
        },
    });
    return policy;
}
export function getAutofixPolicyForFile(filePath, context = {}) {
    const ext = path.extname(filePath).toLowerCase();
    if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
        // Mirror the JS/TS lint policy's precedence: eslint (config-first) →
        // oxlint (config-first) → biome (smart-default). oxlint is config-gated so
        // it never conflicts with the biome default.
        if (context.hasEslintConfig) {
            return {
                toolNames: ["eslint", "oxlint", "biome"],
                preferredTools: ["eslint"],
                defaultTool: "eslint",
                defaultWhenUnconfigured: false,
                gate: "config-first",
                safe: true,
            };
        }
        if (context.hasOxlintConfig) {
            return {
                toolNames: ["eslint", "oxlint", "biome"],
                preferredTools: ["oxlint"],
                defaultTool: "oxlint",
                defaultWhenUnconfigured: false,
                // Mirror the JS lint policy, which gates on eslint only (oxlint vs
                // biome are both "smart-default" there).
                gate: "smart-default",
                safe: true,
            };
        }
        return {
            toolNames: ["eslint", "oxlint", "biome"],
            preferredTools: ["biome"],
            defaultTool: "biome",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if ([".json", ".jsonc"].includes(ext)) {
        if (!context.hasBiomeConfig) {
            return undefined;
        }
        return {
            toolNames: ["biome"],
            preferredTools: ["biome"],
            defaultTool: "biome",
            defaultWhenUnconfigured: false,
            gate: "config-first",
            safe: true,
        };
    }
    if ([".py", ".pyi"].includes(ext)) {
        return {
            toolNames: ["ruff"],
            preferredTools: ["ruff"],
            defaultTool: "ruff",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if ([".css", ".scss", ".sass", ".less"].includes(ext)) {
        return {
            toolNames: ["stylelint"],
            preferredTools: ["stylelint"],
            defaultTool: "stylelint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if (ext === ".sql") {
        return {
            toolNames: ["sqlfluff"],
            preferredTools: ["sqlfluff"],
            defaultTool: "sqlfluff",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if ([".rb", ".rake", ".gemspec", ".ru"].includes(ext)) {
        return {
            toolNames: ["rubocop"],
            preferredTools: ["rubocop"],
            defaultTool: "rubocop",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if ([".kt", ".kts"].includes(ext)) {
        // ktfmt is config-first and the project's explicit formatting choice, so it
        // wins over both detekt and the ktlint smart-default when opted in (#129).
        if (context.hasKtfmtConfig) {
            return {
                toolNames: ["ktfmt", "detekt", "ktlint"],
                preferredTools: ["ktfmt"],
                defaultTool: "ktfmt",
                defaultWhenUnconfigured: false,
                gate: "config-first",
                safe: true,
            };
        }
        // detekt --auto-correct is config-first; with a detekt config present it
        // wins over the ktlint smart-default (and is the autofix path on Windows,
        // where ktlint's install is currently broken, #218).
        if (context.hasDetektConfig) {
            return {
                toolNames: ["detekt", "ktlint"],
                preferredTools: ["detekt"],
                defaultTool: "detekt",
                defaultWhenUnconfigured: false,
                gate: "config-first",
                safe: true,
            };
        }
        return {
            toolNames: ["ktlint", "detekt"],
            preferredTools: ["ktlint"],
            defaultTool: "ktlint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if (ext === ".go") {
        // golangci-lint run --fix is config-first: only apply when the project
        // opted in with a .golangci.* config.
        return {
            toolNames: ["golangci-lint"],
            preferredTools: context.hasGolangciConfig ? ["golangci-lint"] : [],
            defaultTool: "golangci-lint",
            defaultWhenUnconfigured: false,
            gate: "config-first",
            safe: true,
        };
    }
    if ([".md", ".mdx"].includes(ext)) {
        // markdownlint --fix is a smart-default (deterministic MD### fixes run with
        // built-in rules; config optional) — matches the markdownlint lint policy.
        return {
            toolNames: ["markdownlint"],
            preferredTools: ["markdownlint"],
            defaultTool: "markdownlint",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if (ext === ".rs") {
        return {
            toolNames: ["rust-clippy"],
            preferredTools: ["rust-clippy"],
            defaultTool: "rust-clippy",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    if (ext === ".dart") {
        return {
            toolNames: ["dart-analyze"],
            preferredTools: ["dart-analyze"],
            defaultTool: "dart-analyze",
            defaultWhenUnconfigured: true,
            gate: "smart-default",
            safe: true,
        };
    }
    return undefined;
}
export function getPreferredAutofixTools(filePath, context) {
    return getAutofixPolicyForFile(filePath, context)?.preferredTools ?? [];
}
const ESLINT_CONFIGS = [
    ".eslintrc",
    ".eslintrc.js",
    ".eslintrc.cjs",
    ".eslintrc.json",
    ".eslintrc.yaml",
    ".eslintrc.yml",
    "eslint.config.js",
    "eslint.config.mjs",
    "eslint.config.cjs",
    "eslint.config.ts",
];
function* walkUpDirsUntilPackageJson(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        yield dir;
        if (fs.existsSync(path.join(dir, "package.json")))
            return;
    }
}
function findNearestPackageJsonPath(cwd) {
    let dir = cwd;
    const root = path.parse(dir).root;
    while (true) {
        const pkgPath = path.join(dir, "package.json");
        if (fs.existsSync(pkgPath))
            return pkgPath;
        if (dir === root)
            break;
        const parent = path.dirname(dir);
        if (parent === dir)
            break;
        dir = parent;
    }
    return undefined;
}
export function hasNearestPackageJsonDependency(cwd, dependencyName) {
    const pkgPath = findNearestPackageJsonPath(cwd);
    if (!pkgPath)
        return false;
    try {
        const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
        return Boolean(pkg.dependencies?.[dependencyName] ??
            pkg.devDependencies?.[dependencyName]);
    }
    catch { }
    return false;
}
export function hasNearestPackageJsonField(cwd, fieldName) {
    const pkgPath = findNearestPackageJsonPath(cwd);
    if (!pkgPath)
        return false;
    try {
        const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
        return pkg[fieldName] !== undefined;
    }
    catch { }
    return false;
}
export function hasEslintConfig(cwd) {
    for (const dir of walkUpDirsUntilPackageJson(cwd)) {
        for (const cfg of ESLINT_CONFIGS) {
            if (fs.existsSync(path.join(dir, cfg)))
                return true;
        }
        const pkgPath = path.join(dir, "package.json");
        if (fs.existsSync(pkgPath)) {
            try {
                if (JSON.parse(fs.readFileSync(pkgPath, "utf-8")).eslintConfig)
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasBiomeConfig(cwd) {
    return getBiomeConfigPath(cwd) !== undefined;
}
export function getBiomeConfigPath(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        const jsoncPath = path.join(dir, "biome.jsonc");
        if (fs.existsSync(jsoncPath))
            return jsoncPath;
        const jsonPath = path.join(dir, "biome.json");
        if (fs.existsSync(jsonPath))
            return jsonPath;
    }
    return undefined;
}
export function hasOxfmtConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        if (fs.existsSync(path.join(dir, "oxfmt.toml")))
            return true;
        if (fs.existsSync(path.join(dir, ".oxfmtrc.json")))
            return true;
        if (hasVitePlusConfig(dir))
            return true;
        const pkgPath = path.join(dir, "package.json");
        if (fs.existsSync(pkgPath)) {
            try {
                const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
                const deps = {
                    ...pkg.dependencies,
                    ...pkg.devDependencies,
                };
                // Published package is `oxfmt`; the scoped name does not exist on npm.
                if (deps["oxfmt"] || deps["@oxc-project/oxfmt"])
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasStylelintConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        if (STYLELINT_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg)))) {
            return true;
        }
        const pkgPath = path.join(dir, "package.json");
        if (fs.existsSync(pkgPath)) {
            try {
                const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
                if (pkg.stylelint)
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasSqlfluffConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        for (const cfg of SQLFLUFF_CONFIGS) {
            const cfgPath = path.join(dir, cfg);
            if (!fs.existsSync(cfgPath))
                continue;
            if (cfg === "pyproject.toml") {
                try {
                    const content = fs.readFileSync(cfgPath, "utf-8");
                    if (content.includes("[tool.sqlfluff]"))
                        return true;
                }
                catch { }
                continue;
            }
            if (cfg === "setup.cfg" || cfg === "tox.ini") {
                try {
                    const content = fs.readFileSync(cfgPath, "utf-8");
                    if (content.includes("[sqlfluff]"))
                        return true;
                }
                catch { }
                continue;
            }
            return true;
        }
    }
    for (const dir of walkUpDirs(cwd)) {
        for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
            const depPath = path.join(dir, depFile);
            if (!fs.existsSync(depPath))
                continue;
            try {
                const content = fs.readFileSync(depPath, "utf-8").toLowerCase();
                if (content.includes("sqlfluff"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasRubocopConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        for (const cfg of RUBOCOP_CONFIGS) {
            if (fs.existsSync(path.join(dir, cfg)))
                return true;
        }
        const gemfile = path.join(dir, "Gemfile");
        if (fs.existsSync(gemfile)) {
            try {
                const content = fs.readFileSync(gemfile, "utf-8");
                if (content.includes("rubocop"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasMypyConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        for (const cfg of MYPY_CONFIGS) {
            const cfgPath = path.join(dir, cfg);
            if (!fs.existsSync(cfgPath))
                continue;
            if (cfg === "setup.cfg") {
                try {
                    if (fs.readFileSync(cfgPath, "utf-8").includes("[mypy]"))
                        return true;
                }
                catch { }
                continue;
            }
            if (cfg === "pyproject.toml") {
                try {
                    if (fs.readFileSync(cfgPath, "utf-8").includes("[tool.mypy]"))
                        return true;
                }
                catch { }
                continue;
            }
            return true;
        }
    }
    return false;
}
export function hasYamllintConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        for (const cfg of YAMLLINT_CONFIGS) {
            const cfgPath = path.join(dir, cfg);
            if (!fs.existsSync(cfgPath))
                continue;
            if (cfg === "pyproject.toml") {
                try {
                    const content = fs.readFileSync(cfgPath, "utf-8");
                    if (content.includes("[tool.yamllint]"))
                        return true;
                }
                catch { }
                continue;
            }
            if (cfg === "setup.cfg" || cfg === "tox.ini") {
                try {
                    const content = fs.readFileSync(cfgPath, "utf-8");
                    if (content.includes("[yamllint]"))
                        return true;
                }
                catch { }
                continue;
            }
            return true;
        }
    }
    for (const dir of walkUpDirs(cwd)) {
        for (const depFile of ["requirements.txt", "Pipfile", "pyproject.toml"]) {
            const depPath = path.join(dir, depFile);
            if (!fs.existsSync(depPath))
                continue;
            try {
                const content = fs.readFileSync(depPath, "utf-8").toLowerCase();
                if (content.includes("yamllint"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasMarkdownlintConfig(cwd) {
    return findNearestContaining(cwd, MARKDOWNLINT_CONFIGS) !== undefined;
}
export function hasPrettierConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        if (PRETTIER_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))))
            return true;
        const pkgPath = path.join(dir, "package.json");
        if (fs.existsSync(pkgPath)) {
            try {
                const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
                if (Object.hasOwn(pkg, "prettier"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasBlackConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        const pyproject = path.join(dir, "pyproject.toml");
        if (fs.existsSync(pyproject)) {
            try {
                if (fs.readFileSync(pyproject, "utf-8").includes("[tool.black]"))
                    return true;
            }
            catch { }
        }
    }
    for (const dir of walkUpDirs(cwd)) {
        for (const depFile of ["requirements.txt", "Pipfile"]) {
            const depPath = path.join(dir, depFile);
            if (!fs.existsSync(depPath))
                continue;
            try {
                if (fs.readFileSync(depPath, "utf-8").toLowerCase().includes("black"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasRuffConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        for (const cfg of RUFF_PROJECT_CONFIGS) {
            if (fs.existsSync(path.join(dir, cfg)))
                return true;
        }
        const pyproject = path.join(dir, "pyproject.toml");
        if (fs.existsSync(pyproject)) {
            try {
                if (fs.readFileSync(pyproject, "utf-8").includes("[tool.ruff]"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasGolangciConfig(cwd) {
    return findNearestContaining(cwd, GOLANGCI_CONFIGS) !== undefined;
}
export function hasClangFormatConfig(cwd) {
    return (findNearestContaining(cwd, [".clang-format", "_clang-format"]) !== undefined);
}
export function hasPhpCsFixerConfig(cwd) {
    return (findNearestContaining(cwd, [
        ".php-cs-fixer.php",
        ".php-cs-fixer.dist.php",
    ]) !== undefined);
}
export function hasStyluaConfig(cwd) {
    return (findNearestContaining(cwd, ["stylua.toml", ".stylua.toml"]) !== undefined);
}
export function hasOcamlformatConfig(cwd) {
    return findNearestContaining(cwd, [".ocamlformat"]) !== undefined;
}
export function hasGoogleJavaFormatConfig(cwd) {
    // google-java-format has no standard config file — gate on .editorconfig
    // with indent_size defined (common Java project signal) or explicit opt-in marker.
    return (findNearestContaining(cwd, [".google-java-format", ".editorconfig"]) !==
        undefined);
}
export function hasCljfmtConfig(cwd) {
    return (findNearestContaining(cwd, [".cljfmt.edn", "cljfmt.edn", ".cljfmt"]) !==
        undefined);
}
export function hasCmakeFormatConfig(cwd) {
    return (findNearestContaining(cwd, [
        ".cmake-format",
        ".cmake-format.yaml",
        ".cmake-format.yml",
        ".cmake-format.json",
        ".cmake-format.py",
        "cmake-format.yaml",
        "cmake-format.yml",
    ]) !== undefined);
}
export function hasPhpstanConfig(cwd) {
    return findNearestContaining(cwd, PHPSTAN_CONFIGS) !== undefined;
}
const DETEKT_CONFIGS = [
    "detekt.yml",
    ".detekt.yml",
    path.join("config", "detekt", "detekt.yml"),
    path.join("detekt", "detekt.yml"),
];
export function hasDetektConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        if (DETEKT_CONFIGS.some((cfg) => fs.existsSync(path.join(dir, cfg))))
            return true;
    }
    return false;
}
// ktfmt has no native config file format; these are pi-lens opt-in markers plus
// the gradle-plugin signal, so a project that uses ktfmt elects it as its Kotlin
// formatter instead of the ktlint smart-default (#129).
const KTFMT_CONFIG_FILES = [".ktfmt", ".ktfmt.kts"];
const KTFMT_GRADLE_FILES = [
    "build.gradle.kts",
    "build.gradle",
    "settings.gradle.kts",
    "settings.gradle",
];
export function hasKtfmtConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        if (KTFMT_CONFIG_FILES.some((cfg) => fs.existsSync(path.join(dir, cfg))))
            return true;
        for (const gradle of KTFMT_GRADLE_FILES) {
            const p = path.join(dir, gradle);
            if (fs.existsSync(p)) {
                try {
                    // Match the gradle plugin id / artifact, not a stray substring.
                    if (/ktfmt|com\.facebook\.ktfmt/i.test(fs.readFileSync(p, "utf-8")))
                        return true;
                }
                catch { }
            }
        }
    }
    return false;
}
// SpotBugs (#133) analyzes compiled bytecode, so it needs BOTH a Java build
// descriptor AND a compiled-classes dir. These two helpers gate the runner.
const JAVA_BUILD_DESCRIPTORS = [
    "pom.xml",
    "build.gradle",
    "build.gradle.kts",
    "settings.gradle",
    "settings.gradle.kts",
];
// Common compiled-output dirs: Maven (target/classes), Gradle (build/classes),
// IntelliJ (out/production), Eclipse (bin/main).
const COMPILED_CLASSES_DIRS = [
    path.join("target", "classes"),
    path.join("build", "classes"),
    path.join("out", "production"),
    path.join("bin", "main"),
];
export function hasJavaBuildDescriptor(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        if (JAVA_BUILD_DESCRIPTORS.some((d) => fs.existsSync(path.join(dir, d))))
            return true;
    }
    return false;
}
/** First existing compiled-classes dir at/above cwd, or undefined. */
export function findCompiledClassesDir(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        for (const rel of COMPILED_CLASSES_DIRS) {
            const candidate = path.join(dir, rel);
            if (fs.existsSync(candidate))
                return candidate;
        }
    }
    return undefined;
}
export function hasStandardrbConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        const gemfile = path.join(dir, "Gemfile");
        if (fs.existsSync(gemfile)) {
            try {
                if (fs.readFileSync(gemfile, "utf-8").includes("standard"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function getRubocopCommand(cwd) {
    const gemfile = path.join(cwd, "Gemfile");
    if (fs.existsSync(gemfile)) {
        try {
            const content = fs.readFileSync(gemfile, "utf-8");
            if (content.includes("rubocop")) {
                return { cmd: "bundle", args: ["exec", "rubocop"] };
            }
        }
        catch { }
    }
    return { cmd: "rubocop", args: [] };
}
export function hasVitePlusConfig(cwd) {
    for (const dir of walkUpDirs(cwd)) {
        if (fs.existsSync(path.join(dir, "vite-plus.json")))
            return true;
        const pkgPath = path.join(dir, "package.json");
        if (fs.existsSync(pkgPath)) {
            try {
                const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
                const deps = { ...pkg.dependencies, ...pkg.devDependencies };
                if (deps["vite-plus"] || deps["@voidzero-dev/vite-plus-core"]) {
                    return true;
                }
            }
            catch { }
        }
        for (const cfg of VITE_CONFIGS) {
            const cfgPath = path.join(dir, cfg);
            if (!fs.existsSync(cfgPath))
                continue;
            try {
                const content = fs.readFileSync(cfgPath, "utf-8");
                if (content.includes("vite-plus"))
                    return true;
            }
            catch { }
        }
    }
    return false;
}
export function hasOxlintConfig(cwd) {
    for (const dir of walkUpDirsUntilPackageJson(cwd)) {
        if (fs.existsSync(path.join(dir, ".oxlintrc.json")) ||
            fs.existsSync(path.join(dir, "oxlint.json")))
            return true;
    }
    return hasVitePlusConfig(cwd);
}
export function getPreferredJstsLintRunners(context) {
    if (context.hasEslintConfig)
        return ["eslint"];
    if (context.hasOxlintConfig)
        return ["oxlint"];
    if (context.hasBiomeConfig)
        return ["biome-check-json"];
    return ["oxlint", "biome-check-json"];
}
export function getJstsLintPolicy(context) {
    const hasEslint = !!context.hasEslintConfig;
    const hasOxlint = !!context.hasOxlintConfig;
    const hasBiome = !!context.hasBiomeConfig;
    return {
        hasEslintConfig: hasEslint,
        hasOxlintConfig: hasOxlint,
        hasBiomeConfig: hasBiome,
        preferredRunners: getPreferredJstsLintRunners({
            hasEslintConfig: hasEslint,
            hasOxlintConfig: hasOxlint,
            hasBiomeConfig: hasBiome,
        }),
        hasExplicitNonBiomeLinter: hasEslint || hasOxlint,
    };
}
export function getJstsLintPolicyForCwd(cwd) {
    return getJstsLintPolicy({
        hasEslintConfig: hasEslintConfig(cwd),
        hasOxlintConfig: hasOxlintConfig(cwd),
        hasBiomeConfig: hasBiomeConfig(cwd),
    });
}