pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
2,037 lines • 60.8 kB
JavaScript
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),
});
}