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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user