pi-lens 1.1.0
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.
- package/README.md +152 -0
- package/clients/ast-grep-client.ts +254 -0
- package/clients/biome-client.ts +362 -0
- package/clients/dependency-checker.ts +368 -0
- package/clients/jscpd-client.ts +144 -0
- package/clients/knip-client.ts +244 -0
- package/clients/ruff-client.ts +295 -0
- package/clients/todo-scanner.ts +161 -0
- package/clients/type-coverage-client.ts +141 -0
- package/clients/types.ts +59 -0
- package/clients/typescript-client.ts +542 -0
- package/index.ts +471 -0
- package/package.json +50 -0
- package/rules/.sgconfig.yml +4 -0
- package/rules/ast-grep-rules/.sgconfig.yml +4 -0
- package/rules/ast-grep-rules/rules/array-callback-return.yml +33 -0
- package/rules/ast-grep-rules/rules/constructor-super.yml +22 -0
- package/rules/ast-grep-rules/rules/empty-catch.yml +16 -0
- package/rules/ast-grep-rules/rules/getter-return.yml +59 -0
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +12 -0
- package/rules/ast-grep-rules/rules/in-correct-optional-input-type.yml +63 -0
- package/rules/ast-grep-rules/rules/missing-component-decorator.yml +30 -0
- package/rules/ast-grep-rules/rules/nested-ternary.yml +10 -0
- package/rules/ast-grep-rules/rules/no-alert.yml +6 -0
- package/rules/ast-grep-rules/rules/no-any-type.yml +11 -0
- package/rules/ast-grep-rules/rules/no-array-constructor.yml +9 -0
- package/rules/ast-grep-rules/rules/no-as-any.yml +8 -0
- package/rules/ast-grep-rules/rules/no-async-promise-executor.yml +15 -0
- package/rules/ast-grep-rules/rules/no-await-in-loop.yml +29 -0
- package/rules/ast-grep-rules/rules/no-await-in-promise-all.yml +19 -0
- package/rules/ast-grep-rules/rules/no-bare-except.yml +13 -0
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +16 -0
- package/rules/ast-grep-rules/rules/no-compare-neg-zero.yml +13 -0
- package/rules/ast-grep-rules/rules/no-comparison-to-none.yml +12 -0
- package/rules/ast-grep-rules/rules/no-cond-assign.yml +36 -0
- package/rules/ast-grep-rules/rules/no-console-log.yml +9 -0
- package/rules/ast-grep-rules/rules/no-constant-condition.yml +25 -0
- package/rules/ast-grep-rules/rules/no-constructor-return.yml +28 -0
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +13 -0
- package/rules/ast-grep-rules/rules/no-debugger.yml +10 -0
- package/rules/ast-grep-rules/rules/no-delete-operator.yml +9 -0
- package/rules/ast-grep-rules/rules/no-dupe-args.yml +15 -0
- package/rules/ast-grep-rules/rules/no-dupe-class-members.yml +76 -0
- package/rules/ast-grep-rules/rules/no-dupe-keys.yml +73 -0
- package/rules/ast-grep-rules/rules/no-eval.yml +12 -0
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast.yml +25 -0
- package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +13 -0
- package/rules/ast-grep-rules/rules/no-implied-eval.yml +15 -0
- package/rules/ast-grep-rules/rules/no-inline-styles.yml +18 -0
- package/rules/ast-grep-rules/rules/no-inner-html.yml +13 -0
- package/rules/ast-grep-rules/rules/no-insecure-randomness.yml +20 -0
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +11 -0
- package/rules/ast-grep-rules/rules/no-lonely-if.yml +13 -0
- package/rules/ast-grep-rules/rules/no-mutable-default.yml +11 -0
- package/rules/ast-grep-rules/rules/no-nested-links.yml +28 -0
- package/rules/ast-grep-rules/rules/no-new-symbol.yml +8 -0
- package/rules/ast-grep-rules/rules/no-new-wrappers.yml +13 -0
- package/rules/ast-grep-rules/rules/no-non-null-assertion.yml +14 -0
- package/rules/ast-grep-rules/rules/no-open-redirect.yml +15 -0
- package/rules/ast-grep-rules/rules/no-prototype-builtins-native.yml +17 -0
- package/rules/ast-grep-rules/rules/no-prototype-builtins.yml +15 -0
- package/rules/ast-grep-rules/rules/no-return-await.yml +15 -0
- package/rules/ast-grep-rules/rules/no-shadow.yml +20 -0
- package/rules/ast-grep-rules/rules/no-sql-in-code.yml +13 -0
- package/rules/ast-grep-rules/rules/no-star-imports.yml +11 -0
- package/rules/ast-grep-rules/rules/no-string-ref.yml +15 -0
- package/rules/ast-grep-rules/rules/no-throw-string.yml +12 -0
- package/rules/ast-grep-rules/rules/no-unnecessary-state-initializer.yml +17 -0
- package/rules/ast-grep-rules/rules/no-useless-concat.yml +17 -0
- package/rules/ast-grep-rules/rules/no-var.yml +10 -0
- package/rules/ast-grep-rules/rules/prefer-const.yml +16 -0
- package/rules/ast-grep-rules/rules/prefer-nullish-coalescing.yml +23 -0
- package/rules/ast-grep-rules/rules/prefer-optional-chain.yml +14 -0
- package/rules/ast-grep-rules/rules/prefer-template.yml +20 -0
- package/rules/ast-grep-rules/rules/require-await.yml +14 -0
- package/rules/ast-grep-rules/rules/strict-equality.yml +10 -0
- package/rules/ast-grep-rules/rules/strict-inequality.yml +10 -0
- package/rules/ast-grep-rules/rules/switch-needs-default.yml +12 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# pi-lens
|
|
2
|
+
|
|
3
|
+
Real-time code quality feedback for [pi](https://github.com/mariozechner/pi-coding-agent). Every write and edit is automatically analysed — diagnostics are injected directly into the tool result so the agent sees them without any extra steps.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
### On every write / edit
|
|
10
|
+
|
|
11
|
+
| Tool | What it checks |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **TypeScript LSP** | Type errors and warnings, using the project's `tsconfig.json` (walks up from the file to find it; falls back to `ES2020 + DOM` defaults) |
|
|
14
|
+
| **ast-grep** | 60+ structural rules: `no-var`, `no-eval`, `no-debugger`, `no-as-any`, `prefer-template`, `no-throw-string`, `no-hardcoded-secrets`, `no-return-await`, nested ternaries, strict equality, and more |
|
|
15
|
+
| **Biome** | Lint + format for JS/TS/JSX/TSX/CSS/JSON. Auto-fixes on every write by default |
|
|
16
|
+
| **Ruff** | Lint + format for Python. Auto-fixes on every write by default |
|
|
17
|
+
|
|
18
|
+
### Pre-write hints
|
|
19
|
+
|
|
20
|
+
Before every write or edit to an existing file, the agent sees a summary of what's already broken:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
⚠ Pre-write: file already has 5 TypeScript error(s) — fix before adding more
|
|
24
|
+
⚠ Pre-write: file already has 9 Biome issue(s)
|
|
25
|
+
⚠ Pre-write: file already has 8 structural violations
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Prevents piling new errors on top of existing ones.
|
|
29
|
+
|
|
30
|
+
### Session start summary (injected into first tool result)
|
|
31
|
+
|
|
32
|
+
On every new session, the following scans run against the whole project and are delivered once into the first tool result:
|
|
33
|
+
|
|
34
|
+
| Tool | What it reports |
|
|
35
|
+
|---|---|
|
|
36
|
+
| **TODO scanner** | All TODO / FIXME / HACK / BUG / DEPRECATED annotations, sorted by severity |
|
|
37
|
+
| **Knip** | Unused exports, types, and unlisted dependencies |
|
|
38
|
+
| **jscpd** | Duplicate code blocks — file, line, size, percentage of codebase |
|
|
39
|
+
| **type-coverage** | Percentage of identifiers properly typed; lists exact locations of `any` |
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
[Session Start]
|
|
45
|
+
[TODOs] 3 annotation(s) found (2 FIXME, 1 TODO):
|
|
46
|
+
🔴 src/auth.ts:42 — FIXME: token refresh not implemented
|
|
47
|
+
🟠 src/parser.ts:17 — HACK: bypassing validation
|
|
48
|
+
📝 src/api.ts:88 — TODO: add rate limiting
|
|
49
|
+
|
|
50
|
+
[Knip] 2 issue(s) — 2 unused export(s):
|
|
51
|
+
Unused exports:
|
|
52
|
+
- legacyFormat (utils.ts)
|
|
53
|
+
- oldParser (parser.ts)
|
|
54
|
+
|
|
55
|
+
[jscpd] 2 duplicate block(s) — 1.2% of codebase (47/3920 lines):
|
|
56
|
+
16 lines — openrouter.ts:183 ↔ openrouter.ts:135
|
|
57
|
+
11 lines — cline-auth.ts:51 ↔ kilo-auth.ts:9
|
|
58
|
+
|
|
59
|
+
[type-coverage] ⚠ 94.3% typed (3870/4107 identifiers):
|
|
60
|
+
auth.ts:138:44 — undefined as any
|
|
61
|
+
config.ts:52:12 — err
|
|
62
|
+
... and 12 more
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### On-demand commands
|
|
66
|
+
|
|
67
|
+
| Command | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `/find-todos [path]` | Scan for TODO/FIXME/HACK annotations |
|
|
70
|
+
| `/dead-code` | Find unused exports/files/dependencies (requires knip) |
|
|
71
|
+
| `/check-deps` | Circular dependency scan (requires madge) |
|
|
72
|
+
| `/format [file\|--all]` | Apply Biome formatting |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Core (required for JS/TS feedback)
|
|
80
|
+
npm install -D @biomejs/biome @ast-grep/cli
|
|
81
|
+
|
|
82
|
+
# Dead code + duplicate detection + type coverage (highly recommended)
|
|
83
|
+
npm install -D knip jscpd type-coverage
|
|
84
|
+
|
|
85
|
+
# Circular dependency detection
|
|
86
|
+
npm install -D madge
|
|
87
|
+
|
|
88
|
+
# Python support
|
|
89
|
+
pip install ruff
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Flags
|
|
95
|
+
|
|
96
|
+
| Flag | Default | Description |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `--autofix-biome` | **`true`** | Auto-fix Biome lint/format issues on every write |
|
|
99
|
+
| `--autofix-ruff` | **`true`** | Auto-fix Ruff issues on every write |
|
|
100
|
+
| `--no-biome` | `false` | Disable Biome |
|
|
101
|
+
| `--no-ast-grep` | `false` | Disable ast-grep |
|
|
102
|
+
| `--no-ruff` | `false` | Disable Ruff |
|
|
103
|
+
| `--no-lsp` | `false` | Disable TypeScript LSP |
|
|
104
|
+
| `--lens-verbose` | `false` | Enable verbose logging |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## ast-grep rules
|
|
109
|
+
|
|
110
|
+
Rules live in `rules/ast-grep-rules/rules/`. All rules are YAML files you can edit or extend.
|
|
111
|
+
|
|
112
|
+
**Security**
|
|
113
|
+
`no-eval`, `no-implied-eval`, `no-hardcoded-secrets`, `no-insecure-randomness`, `no-open-redirect`, `no-sql-in-code`, `no-inner-html`, `no-dangerously-set-inner-html`, `no-javascript-url`
|
|
114
|
+
|
|
115
|
+
**TypeScript**
|
|
116
|
+
`no-any-type`, `no-as-any`, `no-non-null-assertion`
|
|
117
|
+
|
|
118
|
+
**Style**
|
|
119
|
+
`no-var`, `prefer-const`, `prefer-template`, `no-useless-concat`, `prefer-nullish-coalescing`, `prefer-optional-chain`, `nested-ternary`, `no-lonely-if`
|
|
120
|
+
|
|
121
|
+
**Correctness**
|
|
122
|
+
`no-debugger`, `no-throw-string`, `no-return-await`, `no-await-in-loop`, `no-await-in-promise-all`, `require-await`, `empty-catch`, `strict-equality`, `strict-inequality`
|
|
123
|
+
|
|
124
|
+
**Patterns**
|
|
125
|
+
`no-console-log`, `no-alert`, `no-delete-operator`, `no-shadow`, `no-star-imports`, `switch-needs-default`
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## External dependencies summary
|
|
130
|
+
|
|
131
|
+
| Package | Install | Purpose |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `@biomejs/biome` | `npm i -D @biomejs/biome` | JS/TS/CSS/JSON lint + format + autofix |
|
|
134
|
+
| `@ast-grep/cli` | `npm i -D @ast-grep/cli` | 60+ structural pattern rules |
|
|
135
|
+
| `knip` | `npm i -D knip` | Unused exports, types, unlisted deps |
|
|
136
|
+
| `jscpd` | `npm i -D jscpd` | Copy-paste / duplicate code detection |
|
|
137
|
+
| `type-coverage` | `npm i -D type-coverage` | TypeScript `any` coverage percentage |
|
|
138
|
+
| `madge` | `npm i -D madge` | Circular dependency detection |
|
|
139
|
+
| `ruff` | `pip install ruff` | Python lint + format + autofix |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## TypeScript LSP — tsconfig detection
|
|
144
|
+
|
|
145
|
+
The LSP walks up from the edited file's directory until it finds a `tsconfig.json`. If found, it uses that project's exact `compilerOptions` (paths, strict settings, lib, etc.). If not found, it falls back to sensible defaults:
|
|
146
|
+
|
|
147
|
+
- `target: ES2020`
|
|
148
|
+
- `lib: ["es2020", "dom", "dom.iterable"]`
|
|
149
|
+
- `moduleResolution: bundler`
|
|
150
|
+
- `strict: true`
|
|
151
|
+
|
|
152
|
+
The compiler options are refreshed automatically when you switch between projects within a session.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AstGrep Client for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* Structural code analysis using ast-grep CLI.
|
|
5
|
+
* Scans files against YAML rule definitions.
|
|
6
|
+
*
|
|
7
|
+
* Requires: npm install -D @ast-grep/cli
|
|
8
|
+
* Rules: ./rules/ directory
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
|
|
15
|
+
// --- Types ---
|
|
16
|
+
|
|
17
|
+
export interface AstGrepDiagnostic {
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
endLine: number;
|
|
21
|
+
endColumn: number;
|
|
22
|
+
severity: "error" | "warning" | "info" | "hint";
|
|
23
|
+
message: string;
|
|
24
|
+
rule: string;
|
|
25
|
+
file: string;
|
|
26
|
+
fix?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// New ast-grep JSON format
|
|
30
|
+
interface AstGrepJsonDiagnostic {
|
|
31
|
+
ruleId: string;
|
|
32
|
+
severity: string;
|
|
33
|
+
message: string;
|
|
34
|
+
note?: string;
|
|
35
|
+
labels: Array<{
|
|
36
|
+
text: string;
|
|
37
|
+
range: {
|
|
38
|
+
start: { line: number; column: number };
|
|
39
|
+
end: { line: number; column: number };
|
|
40
|
+
};
|
|
41
|
+
file?: string;
|
|
42
|
+
style: string;
|
|
43
|
+
}>;
|
|
44
|
+
// Legacy format support
|
|
45
|
+
Message?: { text: string };
|
|
46
|
+
Severity?: string;
|
|
47
|
+
spans?: Array<{
|
|
48
|
+
context: string;
|
|
49
|
+
range: {
|
|
50
|
+
start: { line: number; column: number };
|
|
51
|
+
end: { line: number; column: number };
|
|
52
|
+
};
|
|
53
|
+
file: string;
|
|
54
|
+
}>;
|
|
55
|
+
name?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Client ---
|
|
59
|
+
|
|
60
|
+
export class AstGrepClient {
|
|
61
|
+
private available: boolean | null = null;
|
|
62
|
+
private ruleDir: string;
|
|
63
|
+
private log: (msg: string) => void;
|
|
64
|
+
|
|
65
|
+
constructor(ruleDir?: string, verbose = false) {
|
|
66
|
+
this.ruleDir = ruleDir || path.join(typeof __dirname !== "undefined" ? __dirname : ".", "..", "rules");
|
|
67
|
+
this.log = verbose
|
|
68
|
+
? (msg: string) => console.log(`[ast-grep] ${msg}`)
|
|
69
|
+
: () => {};
|
|
70
|
+
try {
|
|
71
|
+
const nodeFs2 = require("node:fs") as typeof import("node:fs");
|
|
72
|
+
nodeFs2.appendFileSync("C:/Users/R3LiC/Desktop/pi-lens-debug.log",
|
|
73
|
+
`[${new Date().toISOString()}] AstGrepClient constructed, __dirname=${typeof __dirname !== "undefined" ? __dirname : "undefined"}, ruleDir=${this.ruleDir}\n`);
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if ast-grep CLI is available
|
|
79
|
+
*/
|
|
80
|
+
isAvailable(): boolean {
|
|
81
|
+
if (this.available !== null) return this.available;
|
|
82
|
+
|
|
83
|
+
const result = spawnSync("npx", ["sg", "--version"], {
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
timeout: 10000,
|
|
86
|
+
shell: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.available = !result.error && result.status === 0;
|
|
90
|
+
if (this.available) {
|
|
91
|
+
this.log("ast-grep available");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return this.available;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Scan a file against all rules
|
|
99
|
+
*/
|
|
100
|
+
scanFile(filePath: string): AstGrepDiagnostic[] {
|
|
101
|
+
if (!this.isAvailable()) return [];
|
|
102
|
+
|
|
103
|
+
const absolutePath = path.resolve(filePath);
|
|
104
|
+
if (!fs.existsSync(absolutePath)) return [];
|
|
105
|
+
|
|
106
|
+
const configPath = path.join(this.ruleDir, ".sgconfig.yml");
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const result = spawnSync("npx", [
|
|
110
|
+
"sg",
|
|
111
|
+
"scan",
|
|
112
|
+
"--config", configPath,
|
|
113
|
+
"--json",
|
|
114
|
+
absolutePath,
|
|
115
|
+
], {
|
|
116
|
+
encoding: "utf-8",
|
|
117
|
+
timeout: 15000,
|
|
118
|
+
shell: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ast-grep exits 1 when it finds issues
|
|
122
|
+
const output = result.stdout || result.stderr || "";
|
|
123
|
+
if (!output.trim()) return [];
|
|
124
|
+
|
|
125
|
+
return this.parseOutput(output, absolutePath);
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
this.log(`Scan error: ${err.message}`);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format diagnostics for LLM consumption
|
|
134
|
+
*/
|
|
135
|
+
formatDiagnostics(diags: AstGrepDiagnostic[]): string {
|
|
136
|
+
if (diags.length === 0) return "";
|
|
137
|
+
|
|
138
|
+
const errors = diags.filter(d => d.severity === "error");
|
|
139
|
+
const warnings = diags.filter(d => d.severity === "warning");
|
|
140
|
+
|
|
141
|
+
let output = `[ast-grep] ${diags.length} structural issue(s)`;
|
|
142
|
+
if (errors.length) output += ` — ${errors.length} error(s)`;
|
|
143
|
+
if (warnings.length) output += ` — ${warnings.length} warning(s)`;
|
|
144
|
+
output += ":\n";
|
|
145
|
+
|
|
146
|
+
for (const d of diags.slice(0, 15)) {
|
|
147
|
+
const loc = d.line === d.endLine
|
|
148
|
+
? `L${d.line}`
|
|
149
|
+
: `L${d.line}-${d.endLine}`;
|
|
150
|
+
const fix = d.fix ? " [fixable]" : "";
|
|
151
|
+
output += ` [${d.rule}] ${loc} ${d.message}${fix}\n`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (diags.length > 15) {
|
|
155
|
+
output += ` ... and ${diags.length - 15} more\n`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Internal ---
|
|
162
|
+
|
|
163
|
+
private parseOutput(output: string, filterFile: string): AstGrepDiagnostic[] {
|
|
164
|
+
const diagnostics: AstGrepDiagnostic[] = [];
|
|
165
|
+
const resolvedFilterFile = path.resolve(filterFile);
|
|
166
|
+
|
|
167
|
+
// Try parsing as JSON array first (new format)
|
|
168
|
+
try {
|
|
169
|
+
const items: AstGrepJsonDiagnostic[] = JSON.parse(output);
|
|
170
|
+
if (Array.isArray(items)) {
|
|
171
|
+
for (const item of items) {
|
|
172
|
+
const diag = this.parseDiagnostic(item, resolvedFilterFile);
|
|
173
|
+
if (diag) diagnostics.push(diag);
|
|
174
|
+
}
|
|
175
|
+
return diagnostics;
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// Not a JSON array, try ndjson format (legacy)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Parse ndjson (one JSON object per line) - legacy format
|
|
182
|
+
const lines = output.split("\n").filter(l => l.trim());
|
|
183
|
+
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
try {
|
|
186
|
+
const item: AstGrepJsonDiagnostic = JSON.parse(line);
|
|
187
|
+
const diag = this.parseDiagnostic(item, resolvedFilterFile);
|
|
188
|
+
if (diag) diagnostics.push(diag);
|
|
189
|
+
} catch {
|
|
190
|
+
// Skip unparseable lines
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return diagnostics;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private parseDiagnostic(item: AstGrepJsonDiagnostic, filterFile: string): AstGrepDiagnostic | null {
|
|
198
|
+
// New format uses labels array
|
|
199
|
+
if (item.labels && item.labels.length > 0) {
|
|
200
|
+
const label = item.labels.find(l => l.style === "primary") || item.labels[0];
|
|
201
|
+
const filePath = path.resolve(label.file || filterFile);
|
|
202
|
+
|
|
203
|
+
// Filter to our file
|
|
204
|
+
if (filePath !== filterFile) return null;
|
|
205
|
+
|
|
206
|
+
const start = label.range?.start || { line: 0, column: 0 };
|
|
207
|
+
const end = label.range?.end || start;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
line: start.line + 1, // ast-grep is 0-indexed, we want 1-indexed
|
|
211
|
+
column: start.column,
|
|
212
|
+
endLine: end.line + 1,
|
|
213
|
+
endColumn: end.column,
|
|
214
|
+
severity: this.mapSeverity(item.severity),
|
|
215
|
+
message: item.message || "Unknown issue",
|
|
216
|
+
rule: item.ruleId || "unknown",
|
|
217
|
+
file: filePath,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Legacy format uses spans array
|
|
222
|
+
if (item.spans && item.spans.length > 0) {
|
|
223
|
+
const span = item.spans[0];
|
|
224
|
+
const filePath = path.resolve(span.file || filterFile);
|
|
225
|
+
|
|
226
|
+
// Filter to our file
|
|
227
|
+
if (filePath !== filterFile) return null;
|
|
228
|
+
|
|
229
|
+
const start = span.range?.start || { line: 0, column: 0 };
|
|
230
|
+
const end = span.range?.end || start;
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
line: start.line + 1,
|
|
234
|
+
column: start.column,
|
|
235
|
+
endLine: end.line + 1,
|
|
236
|
+
endColumn: end.column,
|
|
237
|
+
severity: this.mapSeverity(item.severity || item.Severity || "warning"),
|
|
238
|
+
message: item.Message?.text || item.message || "Unknown issue",
|
|
239
|
+
rule: item.name || item.ruleId || "unknown",
|
|
240
|
+
file: filePath,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private mapSeverity(severity: string): AstGrepDiagnostic["severity"] {
|
|
248
|
+
const lower = severity.toLowerCase();
|
|
249
|
+
if (lower === "error") return "error";
|
|
250
|
+
if (lower === "warning") return "warning";
|
|
251
|
+
if (lower === "info") return "info";
|
|
252
|
+
return "hint";
|
|
253
|
+
}
|
|
254
|
+
}
|