pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
145 lines (144 loc) • 5.19 kB
JavaScript
import * as path from "node:path";
import { safeSpawnAsync } from "../../safe-spawn.js";
import { PRIORITY } from "../priorities.js";
import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
const swiftlint = createAvailabilityChecker("swiftlint", ".exe");
// SwiftLint rules whose rule class declares a `corrector` — i.e. rules that
// `swiftlint --fix` rewrites deterministically. The JSON reporter does not
// surface a per-violation `is_corrected` flag, so we keep a curated allowlist.
// Source: https://realm.github.io/SwiftLint/rule-directory.html (each rule
// page indicates whether a corrector exists). Conservative — when in doubt,
// leave a rule off this list.
const SWIFTLINT_FIXABLE_RULES = new Set([
"closing_brace",
"colon",
"comma",
"comma_inheritance",
"control_statement",
"duplicate_imports",
"empty_collection_literal",
"empty_enum_arguments",
"empty_parameters",
"empty_parentheses_with_trailing_closure",
"empty_string",
"explicit_init",
"file_header",
"joined_default_parameter",
"legacy_constant",
"legacy_constructor",
"legacy_hashing",
"legacy_nsgeometry_functions",
"mark",
"modifier_order",
"no_extension_access_modifier",
"no_space_in_method_call",
"number_separator",
"opening_brace",
"operator_usage_whitespace",
"prefer_zero_over_explicit_init",
"private_over_fileprivate",
"protocol_property_accessors_order",
"redundant_discardable_let",
"redundant_nil_coalescing",
"redundant_objc_attribute",
"redundant_optional_initialization",
"redundant_string_enum_value",
"redundant_type_annotation",
"redundant_void_return",
"return_arrow_whitespace",
"self_in_property_initialization",
"shorthand_operator",
"sorted_imports",
"statement_position",
"trailing_comma",
"trailing_newline",
"trailing_semicolon",
"trailing_whitespace",
"unneeded_break_in_switch",
"unneeded_parentheses_in_closure_argument",
"unused_capture_list",
"vertical_parameter_alignment",
"vertical_whitespace",
"vertical_whitespace_between_cases",
"vertical_whitespace_closing_braces",
"vertical_whitespace_opening_braces",
"void_function_in_ternary",
"void_return",
"yoda_condition",
]);
function parseSwiftLintOutput(raw, filePath) {
const diagnostics = [];
if (!raw.trim())
return diagnostics;
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed))
return diagnostics;
for (const item of parsed) {
if (!item.reason)
continue;
const severityMap = {
error: "error",
warning: "warning",
info: "info",
};
const severity = severityMap[item.severity?.toLowerCase() ?? ""] ?? "warning";
const ruleId = item.rule_id ?? "swiftlint";
const fixable = SWIFTLINT_FIXABLE_RULES.has(ruleId);
diagnostics.push({
id: `swiftlint-${item.line}-${ruleId}`,
message: `[${ruleId}] ${item.reason}`,
filePath,
line: item.line ?? 1,
column: item.character ?? 1,
severity,
semantic: severity === "error" ? "blocking" : "warning",
tool: "swiftlint",
rule: ruleId,
defectClass: "style",
fixable,
fixSuggestion: fixable
? "Run `swiftlint --fix` to apply the deterministic auto-correction for this rule."
: undefined,
});
}
}
catch {
return diagnostics;
}
return diagnostics;
}
const swiftlintRunner = {
id: "swiftlint",
appliesTo: ["swift"],
priority: PRIORITY.GENERAL_ANALYSIS,
enabledByDefault: true,
skipTestFiles: false,
async run(ctx) {
const cwd = ctx.cwd || process.cwd();
let cmd = null;
if (await (swiftlint.isAvailableAsync(cwd))) {
cmd = swiftlint.getCommand(cwd);
}
else {
cmd = await resolveToolCommandWithInstallFallback(cwd, "swiftlint");
}
if (!cmd)
return { status: "skipped", diagnostics: [], semantic: "none" };
const result = await safeSpawnAsync(cmd, ["--reporter", "json", path.resolve(cwd, ctx.filePath)], { cwd, timeout: 15000 });
// SwiftLint exits non-zero on violations — stdout still has the JSON
const raw = result.stdout || "";
const diagnostics = parseSwiftLintOutput(raw, ctx.filePath);
if (diagnostics.length === 0) {
return { status: "succeeded", diagnostics: [], semantic: "none" };
}
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
return {
status: hasBlocking ? "failed" : "succeeded",
diagnostics,
semantic: hasBlocking ? "blocking" : "warning",
};
},
};
export default swiftlintRunner;
export { parseSwiftLintOutput, SWIFTLINT_FIXABLE_RULES };