pi-lens 3.6.7 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * golangci-lint runner for dispatch system
3
+ *
4
+ * Runs golangci-lint when a .golangci.yml config is present.
5
+ * golangci-lint is the standard meta-linter for Go projects — it runs
6
+ * staticcheck, errcheck, gosimple, and many others in one pass.
7
+ *
8
+ * Gate: skips when no .golangci.yml/.golangci.yaml config is found (project
9
+ * relies on go-vet only). This avoids noisy default-rule runs on projects
10
+ * that haven't opted in.
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import { safeSpawnAsync } from "../../safe-spawn.js";
16
+ import { stripAnsi } from "../../sanitize.js";
17
+ import type {
18
+ Diagnostic,
19
+ DispatchContext,
20
+ RunnerDefinition,
21
+ RunnerResult,
22
+ } from "../types.js";
23
+
24
+ const GOLANGCI_CONFIGS = [
25
+ ".golangci.yml",
26
+ ".golangci.yaml",
27
+ ".golangci.toml",
28
+ ".golangci.json",
29
+ ];
30
+
31
+ function hasGolangciConfig(cwd: string): boolean {
32
+ for (const cfg of GOLANGCI_CONFIGS) {
33
+ if (fs.existsSync(path.join(cwd, cfg))) return true;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ interface GolangciIssue {
39
+ FromLinter: string;
40
+ Text: string;
41
+ Severity?: string;
42
+ Pos: {
43
+ Filename: string;
44
+ Line: number;
45
+ Column: number;
46
+ };
47
+ }
48
+
49
+ interface GolangciOutput {
50
+ Issues: GolangciIssue[] | null;
51
+ }
52
+
53
+ function parseGolangciJson(raw: string, filePath: string): Diagnostic[] {
54
+ try {
55
+ const output: GolangciOutput = JSON.parse(raw);
56
+ if (!output.Issues) return [];
57
+
58
+ const absFile = path.resolve(filePath);
59
+
60
+ return output.Issues.filter(
61
+ (issue) => path.resolve(issue.Pos.Filename) === absFile,
62
+ ).map((issue) => {
63
+ const severity = issue.Severity === "error" ? "error" : "warning";
64
+ return {
65
+ id: `golangci:${issue.FromLinter}:${issue.Pos.Line}`,
66
+ message: `${issue.FromLinter}: ${issue.Text}`,
67
+ filePath,
68
+ line: issue.Pos.Line,
69
+ column: issue.Pos.Column,
70
+ severity,
71
+ semantic: severity === "error" ? "blocking" : "warning",
72
+ tool: "golangci-lint",
73
+ rule: issue.FromLinter,
74
+ } satisfies Diagnostic;
75
+ });
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ const golangciRunner: RunnerDefinition = {
82
+ id: "golangci-lint",
83
+ appliesTo: ["go"],
84
+ priority: 20,
85
+ enabledByDefault: true,
86
+
87
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
88
+ const cwd = ctx.cwd || process.cwd();
89
+
90
+ // Only run if project has opted in via config file
91
+ if (!hasGolangciConfig(cwd)) {
92
+ return { status: "skipped", diagnostics: [], semantic: "none" };
93
+ }
94
+
95
+ // Check availability
96
+ const versionCheck = await safeSpawnAsync("golangci-lint", ["version"], {
97
+ timeout: 10000,
98
+ cwd,
99
+ });
100
+ if (versionCheck.error || versionCheck.status !== 0) {
101
+ return { status: "skipped", diagnostics: [], semantic: "none" };
102
+ }
103
+
104
+ // Run on the specific file. golangci-lint accepts file paths directly.
105
+ const result = await safeSpawnAsync(
106
+ "golangci-lint",
107
+ ["run", "--out-format=json", ctx.filePath],
108
+ { timeout: 60000, cwd },
109
+ );
110
+
111
+ const raw = stripAnsi(result.stdout + result.stderr);
112
+
113
+ if (result.status === 0) {
114
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
115
+ }
116
+
117
+ const diagnostics = parseGolangciJson(result.stdout, ctx.filePath);
118
+
119
+ if (diagnostics.length === 0) {
120
+ // Non-zero exit but no parseable issues — likely a config/tool error
121
+ return { status: "skipped", diagnostics: [], semantic: "none" };
122
+ }
123
+
124
+ const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
125
+ return {
126
+ status: "failed",
127
+ diagnostics,
128
+ semantic: hasErrors ? "blocking" : "warning",
129
+ };
130
+ },
131
+ };
132
+
133
+ export default golangciRunner;
@@ -7,11 +7,14 @@ import architectRunner from "./architect.js";
7
7
  import astGrepNapiRunner from "./ast-grep-napi.js";
8
8
  import biomeRunner from "./biome.js";
9
9
  import configValidationRunner from "./config-validation.js";
10
+ import eslintRunner from "./eslint.js";
10
11
  import goVetRunner from "./go-vet.js";
12
+ import golangciRunner from "./golangci-lint.js";
11
13
  import lspRunner from "./lsp.js";
12
14
  import oxlintRunner from "./oxlint.js";
13
15
  import pyrightRunner from "./pyright.js";
14
16
  import pythonSlopRunner from "./python-slop.js";
17
+ import rubocopRunner from "./rubocop.js";
15
18
  import ruffRunner from "./ruff.js";
16
19
  import rustClippyRunner from "./rust-clippy.js";
17
20
  import shellcheckRunner from "./shellcheck.js";
@@ -44,6 +47,9 @@ registerRunner(shellcheckRunner); // Shell script linting (priority 20)
44
47
  // CLI ast-grep kept for ast_grep_search/ast_grep_replace tools only
45
48
  registerRunner(similarityRunner); // Semantic reuse detection (priority 35)
46
49
  registerRunner(architectRunner); // Architectural rules (priority 40)
50
+ registerRunner(eslintRunner); // ESLint (priority 12, jsts, config-gated)
51
+ registerRunner(golangciRunner); // golangci-lint (priority 20, go, config-gated)
52
+ registerRunner(rubocopRunner); // RuboCop lint (priority 10, ruby)
47
53
  registerRunner(spellcheckRunner); // Spellcheck for markdown/docs (priority 30)
48
54
  registerRunner(goVetRunner); // Go analysis (priority 50)
49
55
  registerRunner(rustClippyRunner); // Rust analysis (priority 50)
@@ -117,7 +117,11 @@ const lspRunner: RunnerDefinition = {
117
117
  return {
118
118
  status: hasErrors ? "failed" : "succeeded",
119
119
  diagnostics,
120
- semantic: hasErrors ? "blocking" : "warning",
120
+ semantic: hasErrors
121
+ ? "blocking"
122
+ : diagnostics.length > 0
123
+ ? "warning"
124
+ : "none",
121
125
  };
122
126
  },
123
127
  };
@@ -82,7 +82,11 @@ const pyrightRunner: RunnerDefinition = {
82
82
  return {
83
83
  status: hasErrors ? "failed" : "succeeded",
84
84
  diagnostics,
85
- semantic: hasErrors ? "blocking" : "warning",
85
+ semantic: hasErrors
86
+ ? "blocking"
87
+ : diagnostics.length > 0
88
+ ? "warning"
89
+ : "none",
86
90
  };
87
91
  } catch {
88
92
  // JSON parse error
@@ -0,0 +1,141 @@
1
+ /**
2
+ * RuboCop runner for dispatch system
3
+ *
4
+ * Runs rubocop in lint-only mode (no auto-correct) on Ruby files.
5
+ * Auto-correct is handled by the formatter pipeline — this runner
6
+ * only reports remaining offenses after formatting.
7
+ *
8
+ * Supports bundle exec (preferred in Bundler projects).
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { safeSpawnAsync } from "../../safe-spawn.js";
14
+ import type {
15
+ Diagnostic,
16
+ DispatchContext,
17
+ RunnerDefinition,
18
+ RunnerResult,
19
+ } from "../types.js";
20
+
21
+ function findRubocop(cwd: string): { cmd: string; args: string[] } {
22
+ // Prefer bundle exec if Gemfile exists
23
+ const gemfile = path.join(cwd, "Gemfile");
24
+ if (fs.existsSync(gemfile)) {
25
+ try {
26
+ const content = fs.readFileSync(gemfile, "utf-8");
27
+ if (content.includes("rubocop")) {
28
+ return { cmd: "bundle", args: ["exec", "rubocop"] };
29
+ }
30
+ } catch {}
31
+ }
32
+ return { cmd: "rubocop", args: [] };
33
+ }
34
+
35
+ interface RubocopOffense {
36
+ severity: string;
37
+ message: string;
38
+ cop_name: string;
39
+ correctable: boolean;
40
+ location: {
41
+ line: number;
42
+ column: number;
43
+ };
44
+ }
45
+
46
+ interface RubocopFile {
47
+ path: string;
48
+ offenses: RubocopOffense[];
49
+ }
50
+
51
+ interface RubocopOutput {
52
+ files: RubocopFile[];
53
+ }
54
+
55
+ const SEVERITY_MAP: Record<string, "error" | "warning" | "info"> = {
56
+ fatal: "error",
57
+ error: "error",
58
+ warning: "warning",
59
+ convention: "warning",
60
+ refactor: "info",
61
+ };
62
+
63
+ function parseRubocopJson(raw: string, filePath: string): Diagnostic[] {
64
+ try {
65
+ const output: RubocopOutput = JSON.parse(raw);
66
+ const diagnostics: Diagnostic[] = [];
67
+
68
+ for (const file of output.files) {
69
+ for (const offense of file.offenses) {
70
+ const severity = SEVERITY_MAP[offense.severity] ?? "warning";
71
+ diagnostics.push({
72
+ id: `rubocop:${offense.cop_name}:${offense.location.line}`,
73
+ message: `${offense.cop_name}: ${offense.message}`,
74
+ filePath,
75
+ line: offense.location.line,
76
+ column: offense.location.column,
77
+ severity,
78
+ semantic: severity === "error" ? "blocking" : "warning",
79
+ tool: "rubocop",
80
+ rule: offense.cop_name,
81
+ fixable: offense.correctable,
82
+ });
83
+ }
84
+ }
85
+
86
+ return diagnostics;
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+
92
+ const rubocopRunner: RunnerDefinition = {
93
+ id: "rubocop",
94
+ appliesTo: ["ruby"],
95
+ priority: 10,
96
+ enabledByDefault: true,
97
+
98
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
99
+ const cwd = ctx.cwd || process.cwd();
100
+ const { cmd, args } = findRubocop(cwd);
101
+
102
+ // Check availability
103
+ const versionCheck = await safeSpawnAsync(cmd, [...args, "--version"], {
104
+ timeout: 10000,
105
+ cwd,
106
+ });
107
+ if (versionCheck.error || versionCheck.status !== 0) {
108
+ return { status: "skipped", diagnostics: [], semantic: "none" };
109
+ }
110
+
111
+ // Lint only — no auto-correct (formatter handles that)
112
+ const result = await safeSpawnAsync(
113
+ cmd,
114
+ [...args, "--format", "json", "--no-color", ctx.filePath],
115
+ { timeout: 30000, cwd },
116
+ );
117
+
118
+ // rubocop exits 0 = no offenses, 1 = offenses found, 2 = fatal error
119
+ if (result.status === 2) {
120
+ return { status: "skipped", diagnostics: [], semantic: "none" };
121
+ }
122
+
123
+ if (result.status === 0) {
124
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
125
+ }
126
+
127
+ const diagnostics = parseRubocopJson(result.stdout, ctx.filePath);
128
+ if (diagnostics.length === 0) {
129
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
130
+ }
131
+
132
+ const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
133
+ return {
134
+ status: "failed",
135
+ diagnostics,
136
+ semantic: hasErrors ? "blocking" : "warning",
137
+ };
138
+ },
139
+ };
140
+
141
+ export default rubocopRunner;
@@ -51,7 +51,11 @@ const typeSafetyRunner: RunnerDefinition = {
51
51
  return {
52
52
  status: hasErrors ? "failed" : "succeeded",
53
53
  diagnostics,
54
- semantic: hasErrors ? "blocking" : "warning",
54
+ semantic: hasErrors
55
+ ? "blocking"
56
+ : diagnostics.length > 0
57
+ ? "warning"
58
+ : "none",
55
59
  };
56
60
  },
57
61
  };
@@ -20,7 +20,8 @@ export type FileKind =
20
20
  | "json" // JSON (.json)
21
21
  | "markdown" // Markdown (.md, .mdx)
22
22
  | "css" // CSS (.css, .scss, .less)
23
- | "yaml"; // YAML (.yaml, .yml)
23
+ | "yaml" // YAML (.yaml, .yml)
24
+ | "ruby"; // Ruby (.rb, .rake, .gemspec, .ru)
24
25
 
25
26
  // --- Extension Maps ---
26
27
 
@@ -50,6 +51,7 @@ const KIND_EXTENSIONS: Record<FileKind, readonly string[]> = {
50
51
  markdown: [".md", ".mdx"],
51
52
  css: [".css", ".scss", ".sass", ".less"],
52
53
  yaml: [".yaml", ".yml"],
54
+ ruby: [".rb", ".rake", ".gemspec", ".ru"],
53
55
  };
54
56
 
55
57
  // Reverse map: extension → file kind (for fast lookup)
@@ -161,6 +163,7 @@ export function getFileKindLabel(kind: FileKind): string {
161
163
  markdown: "Markdown",
162
164
  css: "CSS",
163
165
  yaml: "YAML",
166
+ ruby: "Ruby",
164
167
  };
165
168
  return labels[kind] ?? kind;
166
169
  }
@@ -211,6 +214,7 @@ export function getLanguageId(kind: FileKind): string {
211
214
  markdown: "markdown",
212
215
  css: "css",
213
216
  yaml: "yaml",
217
+ ruby: "ruby",
214
218
  };
215
219
  return languageIds[kind] ?? "plaintext";
216
220
  }