feat(coding-agent/lsp): added SwiftLint linter client with JSON reporter and bash.virtualTerminal setting

- Added SwiftLint linter client with JSON reporter support for Swift file linting.
- Changed SwiftLint configuration to use 'lint' command with JSON reporter for structured output.
- Changed bash.virtualTerminal default setting from 'on' to 'off' for standard non-interactive execution.
- Implemented SwiftLintClient with lint() method that executes swiftlint with JSON output and converts violations to LSP diagnostics.
- Added bash.virtualTerminal settings option with 'on' (PTY-backed interactive) and 'off' (standard non-interactive) values.
This commit is contained in:
can1357
2026-02-12 04:47:24 +01:00
parent 3d29303296
commit af20324d62
7 changed files with 136 additions and 3 deletions
+3 -1
View File
@@ -1,9 +1,9 @@
# Changelog
## [Unreleased]
### Added
- Added SwiftLint linter client with JSON reporter support for Swift file linting
- Added `--no-pty` flag to disable PTY-based interactive bash execution
- Added `PI_NO_PTY` environment variable to disable PTY-based interactive bash execution
- Added `bash.virtualTerminal` setting to control PTY-backed interactive execution for bash commands
@@ -12,6 +12,8 @@
### Changed
- Changed `bash.virtualTerminal` default from `on` to `off` for standard non-interactive bash execution
- Changed SwiftLint configuration to use `lint` command with JSON reporter instead of `analyze` for improved diagnostic parsing
- Changed diff line format from space-separated (`+123 content`) to pipe-delimited (`+123|content`) for improved parsing reliability
- Changed bash tool to use interactive PTY execution by default when UI is available, falling back to standard execution when disabled
@@ -633,7 +633,7 @@ export const SETTINGS_SCHEMA = {
"bash.virtualTerminal": {
type: "enum",
values: ["on", "off"] as const,
default: "on",
default: "off",
ui: {
tab: "bash",
label: "Virtual terminal",
@@ -10,6 +10,7 @@ import { LspLinterClient } from "./lsp-linter-client";
export { BiomeClient } from "./biome-client";
export { LspLinterClient } from "./lsp-linter-client";
export { SwiftLintClient } from "./swiftlint-client";
// Cache of linter clients by server name + cwd
const clientCache = new Map<string, LinterClient>();
@@ -0,0 +1,120 @@
/**
* SwiftLint CLI-based linter client.
* Parses SwiftLint's JSON reporter output into LSP Diagnostic format.
*/
import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
/** Shape of a single violation from `swiftlint lint --reporter json`. */
interface SwiftLintViolation {
character: number;
file: string;
line: number;
reason: string;
rule_id: string;
severity: "Error" | "Warning";
type: string;
}
function parseSeverity(severity: string): DiagnosticSeverity {
switch (severity) {
case "Error":
return 1;
case "Warning":
return 2;
default:
return 2;
}
}
async function runSwiftLint(
args: string[],
cwd: string,
resolvedCommand?: string,
): Promise<{ stdout: string; stderr: string; success: boolean }> {
const command = resolvedCommand ?? "swiftlint";
try {
const proc = Bun.spawn([command, ...args], {
cwd,
stdout: "pipe",
stderr: "pipe",
windowsHide: true,
});
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
await proc.exited;
// swiftlint exits non-zero when violations found — that's not a failure
return { stdout, stderr, success: stdout.length > 0 };
} catch (err) {
return { stdout: "", stderr: String(err), success: false };
}
}
/**
* SwiftLint CLI-based linter client.
* Runs `swiftlint lint --reporter json` and converts violations to LSP diagnostics.
*/
export class SwiftLintClient implements LinterClient {
/** Factory method for creating SwiftLintClient instances */
static create(config: ServerConfig, cwd: string): LinterClient {
return new SwiftLintClient(config, cwd);
}
constructor(
private readonly config: ServerConfig,
private readonly cwd: string,
) {}
async format(_filePath: string, content: string): Promise<string> {
// SwiftLint doesn't support formatting
return content;
}
async lint(filePath: string): Promise<Diagnostic[]> {
const result = await runSwiftLint(
["lint", "--quiet", "--reporter", "json", filePath],
this.cwd,
this.config.resolvedCommand,
);
if (!result.success) {
return [];
}
return this.#parseJsonOutput(result.stdout);
}
#parseJsonOutput(jsonOutput: string): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
try {
const violations: SwiftLintViolation[] = JSON.parse(jsonOutput);
for (const v of violations) {
// SwiftLint lines/characters are 1-based; LSP is 0-based
const line = Math.max(0, v.line - 1);
const character = Math.max(0, v.character - 1);
diagnostics.push({
range: {
start: { line, character },
end: { line, character },
},
severity: parseSeverity(v.severity),
message: v.reason,
source: "swiftlint",
code: v.rule_id,
});
}
} catch {
// JSON parse failed, return empty
}
return diagnostics;
}
dispose(): void {
// Nothing to dispose for CLI client
}
}
+5
View File
@@ -6,6 +6,7 @@ import { YAML } from "bun";
import { globSync } from "glob";
import { getConfigDirPaths } from "../config";
import { BiomeClient } from "./clients/biome-client";
import { SwiftLintClient } from "./clients/swiftlint-client";
import DEFAULTS from "./defaults.json" with { type: "json" };
import type { ServerConfig } from "./types";
@@ -137,6 +138,10 @@ function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<str
updated.biome = { ...updated.biome, createClient: BiomeClient.create };
}
if (updated.swiftlint) {
updated.swiftlint = { ...updated.swiftlint, createClient: SwiftLintClient.create };
}
if (updated.omnisharp?.args) {
const args = updated.omnisharp.args.map(arg => (arg === PID_TOKEN ? String(process.pid) : arg));
updated.omnisharp = { ...updated.omnisharp, args };
+1 -1
View File
@@ -456,7 +456,7 @@
},
"swiftlint": {
"command": "swiftlint",
"args": ["analyze", "--quiet"],
"args": ["lint", "--quiet", "--reporter", "json"],
"fileTypes": [".swift"],
"rootMarkers": [".swiftlint.yml", ".swiftlint.yaml", "Package.swift", "*.xcodeproj"],
"isLinter": true
@@ -139,6 +139,11 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
{ value: "20", label: "20 messages" },
{ value: "30", label: "30 messages" },
],
// Virtual terminal
"bash.virtualTerminal": [
{ value: "on", label: "On", description: "PTY-backed interactive execution" },
{ value: "off", label: "Off", description: "Standard non-interactive execution" },
],
// Provider options
"providers.webSearch": [
{ value: "auto", label: "Auto", description: "Priority: Exa > Perplexity > Anthropic" },