pi-lens 2.1.0 → 2.2.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/CHANGELOG.md +27 -0
- package/README.md +70 -1
- package/clients/ast-grep-client.js +12 -12
- package/clients/ast-grep-client.ts +21 -11
- package/clients/dispatch/dispatcher.js +2 -2
- package/clients/dispatch/dispatcher.ts +2 -2
- package/clients/dispatch/runners/index.js +3 -1
- package/clients/dispatch/runners/index.ts +3 -1
- package/clients/dispatch/runners/pyright.js +68 -0
- package/clients/dispatch/runners/pyright.test.js +84 -0
- package/clients/dispatch/runners/pyright.test.ts +109 -0
- package/clients/dispatch/runners/pyright.ts +102 -0
- package/clients/dispatch/runners/secrets.js +109 -0
- package/clients/secrets-scanner.js +113 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/secrets-scanner.test.ts +113 -0
- package/clients/secrets-scanner.ts +134 -0
- package/clients/sg-runner.js +15 -2
- package/clients/sg-runner.ts +25 -2
- package/commands/fix.js +48 -50
- package/commands/fix.ts +71 -61
- package/commands/rate.js +285 -0
- package/commands/rate.test.js +119 -0
- package/commands/rate.test.ts +131 -0
- package/commands/rate.ts +348 -0
- package/commands/refactor.js +33 -9
- package/commands/refactor.ts +44 -11
- package/default-architect.yaml +7 -0
- package/index.ts +58 -10
- package/package.json +1 -1
- package/rules/ast-grep-rules/rules/no-default-export.yml +19 -0
- package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +9 -6
- package/rules/ast-grep-rules/rules/no-process-env.yml +12 -12
- package/rules/ast-grep-rules/rules/no-relative-imports.yml +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.1.1] - 2026-03-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Content-level secret scanning**: Catches secrets in ANY file type on write/edit (`.env`, `.yaml`, `.json`, not just TypeScript). Blocks before save with patterns for `sk-*`, `ghp_*`, `AKIA*`, private keys, hardcoded passwords.
|
|
9
|
+
- **Project rules integration**: Scans for `.claude/rules/`, `.agents/rules/`, `CLAUDE.md`, `AGENTS.md` at session start and surfaces in system prompt.
|
|
10
|
+
- **Grep-ability rules**: New ast-grep rules for `no-default-export` and `no-relative-cross-package-import` to improve agent searchability.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Inline feedback stripped to blocking only**: Warnings no longer shown inline (noise). Only blocking violations and test failures interrupt the agent.
|
|
14
|
+
- **booboo-fix output compacted**: Summary in terminal, full plan in `.pi-lens/reports/fix-plan.tsv`.
|
|
15
|
+
- **booboo-refactor output compacted**: Top 5 worst offenders in terminal, full ranked list in `.pi-lens/reports/refactor-ranked.tsv`.
|
|
16
|
+
- **`ast_grep_search` new params**: Added `selector` (extract specific AST node) and `context` (show surrounding lines).
|
|
17
|
+
- **`ast_grep_replace` mode indicator**: Shows `[DRY-RUN]` or `[APPLIED]` prefix.
|
|
18
|
+
- **no-hardcoded-secrets**: Fixed to only flag actual hardcoded strings (not `process.env` assignments).
|
|
19
|
+
- **no-process-env**: Now only flags secret-related env vars (not PORT, NODE_ENV, etc.).
|
|
20
|
+
- **Removed Factory AI article reference** from architect.yaml.
|
|
21
|
+
|
|
5
22
|
## [2.0.40] - 2026-03-27
|
|
6
23
|
|
|
7
24
|
### Changed
|
|
@@ -281,6 +298,16 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
281
298
|
### Changed
|
|
282
299
|
- **Improved ast-grep tool descriptions**: Better pattern guidance to prevent overly broad searches.
|
|
283
300
|
|
|
301
|
+
## [2.2.0] - 2026-03-29
|
|
302
|
+
|
|
303
|
+
### Added
|
|
304
|
+
- **`/lens-rate` command**: Visual code quality scoring across 6 dimensions (Type Safety, Complexity, Security, Architecture, Dead Code, Tests). Shows grade A-F and colored progress bars.
|
|
305
|
+
- **Pyright runner**: Real Python type-checking via pyright. Catches type errors like `result: str = add(1, 2)` that ruff misses. Runs alongside ruff (pyright for types, ruff for linting).
|
|
306
|
+
- **Vitest config**: Increased test timeout to 15s for CLI spawn tests. Fixes flaky test failures when npx downloads packages.
|
|
307
|
+
|
|
308
|
+
### Fixed
|
|
309
|
+
- **Test flakiness**: Availability tests (biome, knip, jscpd) no longer timeout when npx is downloading packages.
|
|
310
|
+
|
|
284
311
|
## [1.3.0] - 2026-03-23
|
|
285
312
|
|
|
286
313
|
### Changed
|
package/README.md
CHANGED
|
@@ -16,6 +16,74 @@ pi install git:github.com/apmantza/pi-lens
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## What's New (v2.2)
|
|
20
|
+
|
|
21
|
+
### `/lens-rate` — Code Quality Scoring
|
|
22
|
+
|
|
23
|
+
Visual scoring breakdown across 6 dimensions with grade A-F:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌─────────────────────────────────────────────────────────┐
|
|
27
|
+
│ 📊 CODE QUALITY SCORE: 85/100 (B) │
|
|
28
|
+
├─────────────────────────────────────────────────────────┤
|
|
29
|
+
│ 🔷 Type Safety 🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜ 85 │
|
|
30
|
+
│ 🧩 Complexity 🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜ 82 │
|
|
31
|
+
│ 🔒 Security 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100 │
|
|
32
|
+
│ 🏗️ Architecture 🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜ 80 │
|
|
33
|
+
│ 🗑️ Dead Code 🟩🟩🟩🟩🟩🟩🟩🟩🟩⬜ 90 │
|
|
34
|
+
│ ✅ Tests 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100 │
|
|
35
|
+
└─────────────────────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Scores are calculated from real scan data: type-coverage, complexity metrics, secret detection, architect rules, knip, and test results.
|
|
39
|
+
|
|
40
|
+
### Python Type-Checking (Pyright)
|
|
41
|
+
|
|
42
|
+
Python files now get **real type-checking** via pyright, not just linting:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# ruff won't catch this, but pyright will:
|
|
46
|
+
def add(x: int, y: int) -> int:
|
|
47
|
+
return x + y
|
|
48
|
+
|
|
49
|
+
result: str = add(1, 2) # ❌ pyright: Type "int" not assignable to "str"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Pyright runs alongside ruff — pyright catches type errors, ruff catches style issues.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What's New (v2.1)
|
|
57
|
+
|
|
58
|
+
### Content-Level Secret Scanning
|
|
59
|
+
|
|
60
|
+
Secrets are now blocked **before** they're saved, on **all file types**:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
🔴 STOP — 1 potential secret(s) in src/config.ts:
|
|
64
|
+
L12: Possible Stripe or OpenAI API key (sk-*)
|
|
65
|
+
→ Remove before continuing. Use env vars instead.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Works on `.env`, `.yaml`, `.json`, `.md` — not just TypeScript. Catches `sk-*`, `ghp_*`, `AKIA*`, private keys, hardcoded passwords.
|
|
69
|
+
|
|
70
|
+
### Compact Output
|
|
71
|
+
|
|
72
|
+
Inline feedback stripped to **blocking only** — no more warning noise:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
🔴 STOP — 2 issue(s) must fixed:
|
|
76
|
+
L23: var total = sum(items); — use 'let' or 'const'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Warnings are tracked and surfaced via `/lens-booboo`. booboo-fix and booboo-refactor output compacted to summaries with TSV files for full details.
|
|
80
|
+
|
|
81
|
+
### Project Rules Integration
|
|
82
|
+
|
|
83
|
+
Scans for `.claude/rules/`, `.agents/rules/`, `CLAUDE.md`, `AGENTS.md` at session start. Project-specific rules are surfaced in the system prompt — the agent knows to read them when relevant. Works alongside pi-lens architect rules.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
19
87
|
## What's New (v2.0)
|
|
20
88
|
|
|
21
89
|
### Declarative Dispatch System
|
|
@@ -184,7 +252,7 @@ These files provide **general project guidance** (coding conventions, workflow r
|
|
|
184
252
|
|
|
185
253
|
| Tool | Description |
|
|
186
254
|
|---|---|
|
|
187
|
-
| **`ast_grep_search`** | Search code patterns using AST-aware matching. Supports meta-variables: `$VAR` (single node), `$$$` (multiple). Example: `console.log($MSG)` |
|
|
255
|
+
| **`ast_grep_search`** | Search code patterns using AST-aware matching. Supports meta-variables: `$VAR` (single node), `$$$` (multiple). Optional: `selector` (extract specific AST node), `context` (show surrounding lines). Example: `console.log($MSG)` |
|
|
188
256
|
| **`ast_grep_replace`** | Replace code patterns with AST-aware rewriting. Dry-run by default, use `apply=true` to apply changes. Example: `pattern='console.log($MSG)' rewrite='logger.info($MSG)'` |
|
|
189
257
|
|
|
190
258
|
Supported languages: c, cpp, csharp, css, dart, elixir, go, haskell, html, java, javascript, json, kotlin, lua, php, python, ruby, rust, scala, sql, swift, tsx, typescript, yaml
|
|
@@ -327,6 +395,7 @@ Each rule includes a `message` and `note` that are shown in diagnostics, so the
|
|
|
327
395
|
| `type-coverage` | `npm i -D type-coverage` | TypeScript `any` coverage percentage |
|
|
328
396
|
| `madge` | `npm i -D madge` | Circular dependency detection |
|
|
329
397
|
| `ruff` | `pip install ruff` | Python lint + format + autofix |
|
|
398
|
+
| `pyright` | `npx pyright` (auto-installed) | Python type-checking |
|
|
330
399
|
|
|
331
400
|
---
|
|
332
401
|
|
|
@@ -45,16 +45,16 @@ export class AstGrepClient {
|
|
|
45
45
|
/**
|
|
46
46
|
* Search for AST patterns in files
|
|
47
47
|
*/
|
|
48
|
-
async search(pattern, lang, paths) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
async search(pattern, lang, paths, options) {
|
|
49
|
+
const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
|
|
50
|
+
if (options?.selector) {
|
|
51
|
+
args.push("--selector", options.selector);
|
|
52
|
+
}
|
|
53
|
+
if (options?.context !== undefined) {
|
|
54
|
+
args.push("--context", String(options.context));
|
|
55
|
+
}
|
|
56
|
+
args.push(...paths);
|
|
57
|
+
return this.runner.exec(args);
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* Search and replace AST patterns
|
|
@@ -165,8 +165,8 @@ message: found
|
|
|
165
165
|
}
|
|
166
166
|
return exports;
|
|
167
167
|
}
|
|
168
|
-
formatMatches(matches, isDryRun = false) {
|
|
169
|
-
return this.runner.formatMatches(matches, isDryRun);
|
|
168
|
+
formatMatches(matches, isDryRun = false, showModeIndicator = false) {
|
|
169
|
+
return this.runner.formatMatches(matches, isDryRun, 50, showModeIndicator);
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
172
|
* Scan a file against all rules
|
|
@@ -92,16 +92,17 @@ export class AstGrepClient {
|
|
|
92
92
|
pattern: string,
|
|
93
93
|
lang: string,
|
|
94
94
|
paths: string[],
|
|
95
|
+
options?: { selector?: string; context?: number },
|
|
95
96
|
): Promise<{ matches: AstGrepMatch[]; error?: string }> {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
|
|
98
|
+
if (options?.selector) {
|
|
99
|
+
args.push("--selector", options.selector);
|
|
100
|
+
}
|
|
101
|
+
if (options?.context !== undefined) {
|
|
102
|
+
args.push("--context", String(options.context));
|
|
103
|
+
}
|
|
104
|
+
args.push(...paths);
|
|
105
|
+
return this.runner.exec(args);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
/**
|
|
@@ -257,8 +258,17 @@ message: found
|
|
|
257
258
|
return exports;
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
formatMatches(
|
|
261
|
-
|
|
261
|
+
formatMatches(
|
|
262
|
+
matches: AstGrepMatch[],
|
|
263
|
+
isDryRun = false,
|
|
264
|
+
showModeIndicator = false,
|
|
265
|
+
): string {
|
|
266
|
+
return this.runner.formatMatches(
|
|
267
|
+
matches as SgMatch[],
|
|
268
|
+
isDryRun,
|
|
269
|
+
50,
|
|
270
|
+
showModeIndicator,
|
|
271
|
+
);
|
|
262
272
|
}
|
|
263
273
|
|
|
264
274
|
/**
|
|
@@ -191,9 +191,9 @@ export async function dispatchForFile(ctx, groups) {
|
|
|
191
191
|
const blockers = allDiagnostics.filter((d) => d.semantic === "blocking");
|
|
192
192
|
const warnings = allDiagnostics.filter((d) => d.semantic === "warning" || d.semantic === "none");
|
|
193
193
|
const fixedItems = allDiagnostics.filter((d) => d.semantic === "fixed");
|
|
194
|
-
// Format output
|
|
194
|
+
// Format output — only blocking issues shown inline
|
|
195
|
+
// Warnings tracked but not shown (noise) — surfaced via /lens-booboo
|
|
195
196
|
let output = formatDiagnostics(blockers, "blocking");
|
|
196
|
-
output += formatDiagnostics(warnings, "warning");
|
|
197
197
|
output += formatDiagnostics(fixedItems, "fixed");
|
|
198
198
|
return {
|
|
199
199
|
diagnostics: allDiagnostics,
|
|
@@ -266,9 +266,9 @@ export async function dispatchForFile(
|
|
|
266
266
|
);
|
|
267
267
|
const fixedItems = allDiagnostics.filter((d) => d.semantic === "fixed");
|
|
268
268
|
|
|
269
|
-
// Format output
|
|
269
|
+
// Format output — only blocking issues shown inline
|
|
270
|
+
// Warnings tracked but not shown (noise) — surfaced via /lens-booboo
|
|
270
271
|
let output = formatDiagnostics(blockers, "blocking");
|
|
271
|
-
output += formatDiagnostics(warnings, "warning");
|
|
272
272
|
output += formatDiagnostics(fixedItems, "fixed");
|
|
273
273
|
|
|
274
274
|
return {
|
|
@@ -7,12 +7,14 @@ import architectRunner from "./architect.js";
|
|
|
7
7
|
import astGrepRunner from "./ast-grep.js";
|
|
8
8
|
import biomeRunner from "./biome.js";
|
|
9
9
|
import goVetRunner from "./go-vet.js";
|
|
10
|
+
import pyrightRunner from "./pyright.js";
|
|
10
11
|
import ruffRunner from "./ruff.js";
|
|
11
12
|
import rustClippyRunner from "./rust-clippy.js";
|
|
12
13
|
import tsLspRunner from "./ts-lsp.js";
|
|
13
14
|
import typeSafetyRunner from "./type-safety.js";
|
|
14
15
|
// Register all runners (ordered by priority)
|
|
15
|
-
registerRunner(tsLspRunner);
|
|
16
|
+
registerRunner(tsLspRunner); // TypeScript type-checking
|
|
17
|
+
registerRunner(pyrightRunner); // Python type-checking
|
|
16
18
|
registerRunner(biomeRunner);
|
|
17
19
|
registerRunner(ruffRunner);
|
|
18
20
|
registerRunner(typeSafetyRunner);
|
|
@@ -8,13 +8,15 @@ import architectRunner from "./architect.js";
|
|
|
8
8
|
import astGrepRunner from "./ast-grep.js";
|
|
9
9
|
import biomeRunner from "./biome.js";
|
|
10
10
|
import goVetRunner from "./go-vet.js";
|
|
11
|
+
import pyrightRunner from "./pyright.js";
|
|
11
12
|
import ruffRunner from "./ruff.js";
|
|
12
13
|
import rustClippyRunner from "./rust-clippy.js";
|
|
13
14
|
import tsLspRunner from "./ts-lsp.js";
|
|
14
15
|
import typeSafetyRunner from "./type-safety.js";
|
|
15
16
|
|
|
16
17
|
// Register all runners (ordered by priority)
|
|
17
|
-
registerRunner(tsLspRunner);
|
|
18
|
+
registerRunner(tsLspRunner); // TypeScript type-checking
|
|
19
|
+
registerRunner(pyrightRunner); // Python type-checking
|
|
18
20
|
registerRunner(biomeRunner);
|
|
19
21
|
registerRunner(ruffRunner);
|
|
20
22
|
registerRunner(typeSafetyRunner);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pyright runner for dispatch system
|
|
3
|
+
*
|
|
4
|
+
* Provides real Python type-checking (not just linting).
|
|
5
|
+
* Catches type errors like: result: str = add(1, 2) # Type "int" not assignable to "str"
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
const pyrightRunner = {
|
|
9
|
+
id: "pyright",
|
|
10
|
+
appliesTo: ["python"],
|
|
11
|
+
priority: 5, // Higher priority than ruff (10) - type errors are more important
|
|
12
|
+
enabledByDefault: true,
|
|
13
|
+
async run(ctx) {
|
|
14
|
+
// Run pyright with JSON output
|
|
15
|
+
const result = spawnSync("npx", ["pyright", "--outputjson", ctx.filePath], {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
timeout: 60000, // Pyright can be slower on first run
|
|
18
|
+
shell: true,
|
|
19
|
+
});
|
|
20
|
+
// Pyright returns non-zero when errors found, that's OK
|
|
21
|
+
if (result.error) {
|
|
22
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
23
|
+
}
|
|
24
|
+
const output = (result.stdout || "").trim();
|
|
25
|
+
if (!output) {
|
|
26
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const data = JSON.parse(output);
|
|
30
|
+
const diagnostics = parsePyrightOutput(data, ctx.filePath);
|
|
31
|
+
if (diagnostics.length === 0) {
|
|
32
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
33
|
+
}
|
|
34
|
+
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
35
|
+
return {
|
|
36
|
+
status: hasErrors ? "failed" : "succeeded",
|
|
37
|
+
diagnostics,
|
|
38
|
+
semantic: "warning",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// JSON parse failed, skip
|
|
43
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function parsePyrightOutput(data, filePath) {
|
|
48
|
+
if (!data.generalDiagnostics)
|
|
49
|
+
return [];
|
|
50
|
+
return data.generalDiagnostics
|
|
51
|
+
.filter((d) => {
|
|
52
|
+
// Only include errors and warnings, skip informational
|
|
53
|
+
return d.severity === "error" || d.severity === "warning";
|
|
54
|
+
})
|
|
55
|
+
.map((d) => ({
|
|
56
|
+
id: `pyright-${d.range.start.line}-${d.rule}`,
|
|
57
|
+
message: d.message.split("\n")[0], // First line only (pyright has multi-line messages)
|
|
58
|
+
filePath,
|
|
59
|
+
line: d.range.start.line + 1, // Pyright is 0-indexed, we're 1-indexed
|
|
60
|
+
column: d.range.start.character + 1,
|
|
61
|
+
severity: d.severity === "error" ? "error" : "warning",
|
|
62
|
+
semantic: d.severity === "error" ? "blocking" : "warning",
|
|
63
|
+
tool: "pyright",
|
|
64
|
+
rule: d.rule,
|
|
65
|
+
fixable: false, // Pyright can't auto-fix, only suggest
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
export default pyrightRunner;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
function createMockContext(filePath) {
|
|
6
|
+
return {
|
|
7
|
+
filePath,
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
kind: "python",
|
|
10
|
+
autofix: false,
|
|
11
|
+
deltaMode: false,
|
|
12
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
13
|
+
pi: {},
|
|
14
|
+
hasTool: async () => false,
|
|
15
|
+
log: () => { },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe("pyright runner", () => {
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
it("should have correct runner definition", async () => {
|
|
21
|
+
const pyrightModule = await import("./pyright.js");
|
|
22
|
+
const runner = pyrightModule.default;
|
|
23
|
+
expect(runner.id).toBe("pyright");
|
|
24
|
+
expect(runner.appliesTo).toEqual(["python"]);
|
|
25
|
+
expect(runner.priority).toBe(5); // Higher priority than ruff
|
|
26
|
+
expect(runner.enabledByDefault).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it("should detect pyright availability", () => {
|
|
29
|
+
const { spawnSync } = require("node:child_process");
|
|
30
|
+
const result = spawnSync("npx", ["pyright", "--version"], {
|
|
31
|
+
encoding: "utf-8",
|
|
32
|
+
timeout: 10000,
|
|
33
|
+
shell: true,
|
|
34
|
+
});
|
|
35
|
+
expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
|
|
36
|
+
});
|
|
37
|
+
it("should type-check Python files and find errors", async () => {
|
|
38
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_${Date.now()}.py`);
|
|
39
|
+
fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
|
|
40
|
+
return x + y
|
|
41
|
+
|
|
42
|
+
result: str = add(1, 2)
|
|
43
|
+
|
|
44
|
+
def greet(name: str) -> str:
|
|
45
|
+
return "Hello " + name
|
|
46
|
+
|
|
47
|
+
greet(123)
|
|
48
|
+
`);
|
|
49
|
+
try {
|
|
50
|
+
const pyrightModule = await import("./pyright.js");
|
|
51
|
+
const runner = pyrightModule.default;
|
|
52
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
53
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
|
|
54
|
+
expect(result.diagnostics.some((d) => d.tool === "pyright")).toBe(true);
|
|
55
|
+
expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
fs.unlinkSync(tmpFile);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
it("should pass valid Python files", async () => {
|
|
62
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_ok_${Date.now()}.py`);
|
|
63
|
+
fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
|
|
64
|
+
return x + y
|
|
65
|
+
|
|
66
|
+
result: str = str(add(1, 2))
|
|
67
|
+
|
|
68
|
+
def greet(name: str) -> str:
|
|
69
|
+
return "Hello " + name
|
|
70
|
+
|
|
71
|
+
greet("world")
|
|
72
|
+
`);
|
|
73
|
+
try {
|
|
74
|
+
const pyrightModule = await import("./pyright.js");
|
|
75
|
+
const runner = pyrightModule.default;
|
|
76
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
77
|
+
expect(result.status).toBe("succeeded");
|
|
78
|
+
expect(result.diagnostics.length).toBe(0);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
fs.unlinkSync(tmpFile);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import type { DispatchContext } from "../types.js";
|
|
6
|
+
|
|
7
|
+
function createMockContext(filePath: string): DispatchContext {
|
|
8
|
+
return {
|
|
9
|
+
filePath,
|
|
10
|
+
cwd: process.cwd(),
|
|
11
|
+
kind: "python" as any,
|
|
12
|
+
autofix: false,
|
|
13
|
+
deltaMode: false,
|
|
14
|
+
baselines: { get: () => [], add: () => {}, save: () => {} } as any,
|
|
15
|
+
pi: {} as any,
|
|
16
|
+
hasTool: async () => false,
|
|
17
|
+
log: () => {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("pyright runner", () => {
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
it("should have correct runner definition", async () => {
|
|
25
|
+
const pyrightModule = await import("./pyright.js");
|
|
26
|
+
const runner = pyrightModule.default;
|
|
27
|
+
|
|
28
|
+
expect(runner.id).toBe("pyright");
|
|
29
|
+
expect(runner.appliesTo).toEqual(["python"]);
|
|
30
|
+
expect(runner.priority).toBe(5); // Higher priority than ruff
|
|
31
|
+
expect(runner.enabledByDefault).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should detect pyright availability", () => {
|
|
35
|
+
const { spawnSync } =
|
|
36
|
+
require("node:child_process") as typeof import("node:child_process");
|
|
37
|
+
const result = spawnSync("npx", ["pyright", "--version"], {
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
timeout: 10000,
|
|
40
|
+
shell: true,
|
|
41
|
+
});
|
|
42
|
+
expect(
|
|
43
|
+
result.error || result.status !== 0 ? "not available" : "available",
|
|
44
|
+
).toBe("available");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should type-check Python files and find errors", async () => {
|
|
48
|
+
const tmpFile = path.join(
|
|
49
|
+
process.env.TEMP || "/tmp",
|
|
50
|
+
`pyright_test_${Date.now()}.py`,
|
|
51
|
+
);
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
tmpFile,
|
|
54
|
+
`def add(x: int, y: int) -> int:
|
|
55
|
+
return x + y
|
|
56
|
+
|
|
57
|
+
result: str = add(1, 2)
|
|
58
|
+
|
|
59
|
+
def greet(name: str) -> str:
|
|
60
|
+
return "Hello " + name
|
|
61
|
+
|
|
62
|
+
greet(123)
|
|
63
|
+
`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const pyrightModule = await import("./pyright.js");
|
|
68
|
+
const runner = pyrightModule.default;
|
|
69
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
70
|
+
|
|
71
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
|
|
72
|
+
expect(result.diagnostics.some((d) => d.tool === "pyright")).toBe(true);
|
|
73
|
+
expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
|
|
74
|
+
} finally {
|
|
75
|
+
fs.unlinkSync(tmpFile);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should pass valid Python files", async () => {
|
|
80
|
+
const tmpFile = path.join(
|
|
81
|
+
process.env.TEMP || "/tmp",
|
|
82
|
+
`pyright_test_ok_${Date.now()}.py`,
|
|
83
|
+
);
|
|
84
|
+
fs.writeFileSync(
|
|
85
|
+
tmpFile,
|
|
86
|
+
`def add(x: int, y: int) -> int:
|
|
87
|
+
return x + y
|
|
88
|
+
|
|
89
|
+
result: str = str(add(1, 2))
|
|
90
|
+
|
|
91
|
+
def greet(name: str) -> str:
|
|
92
|
+
return "Hello " + name
|
|
93
|
+
|
|
94
|
+
greet("world")
|
|
95
|
+
`,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const pyrightModule = await import("./pyright.js");
|
|
100
|
+
const runner = pyrightModule.default;
|
|
101
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
102
|
+
|
|
103
|
+
expect(result.status).toBe("succeeded");
|
|
104
|
+
expect(result.diagnostics.length).toBe(0);
|
|
105
|
+
} finally {
|
|
106
|
+
fs.unlinkSync(tmpFile);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pyright runner for dispatch system
|
|
3
|
+
*
|
|
4
|
+
* Provides real Python type-checking (not just linting).
|
|
5
|
+
* Catches type errors like: result: str = add(1, 2) # Type "int" not assignable to "str"
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import type {
|
|
10
|
+
Diagnostic,
|
|
11
|
+
DispatchContext,
|
|
12
|
+
RunnerDefinition,
|
|
13
|
+
RunnerResult,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
|
|
16
|
+
const pyrightRunner: RunnerDefinition = {
|
|
17
|
+
id: "pyright",
|
|
18
|
+
appliesTo: ["python"],
|
|
19
|
+
priority: 5, // Higher priority than ruff (10) - type errors are more important
|
|
20
|
+
enabledByDefault: true,
|
|
21
|
+
|
|
22
|
+
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
23
|
+
// Run pyright with JSON output
|
|
24
|
+
const result = spawnSync("npx", ["pyright", "--outputjson", ctx.filePath], {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
timeout: 60000, // Pyright can be slower on first run
|
|
27
|
+
shell: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Pyright returns non-zero when errors found, that's OK
|
|
31
|
+
if (result.error) {
|
|
32
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const output = (result.stdout || "").trim();
|
|
36
|
+
if (!output) {
|
|
37
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(output);
|
|
42
|
+
const diagnostics = parsePyrightOutput(data, ctx.filePath);
|
|
43
|
+
|
|
44
|
+
if (diagnostics.length === 0) {
|
|
45
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
status: hasErrors ? "failed" : "succeeded",
|
|
52
|
+
diagnostics,
|
|
53
|
+
semantic: "warning",
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
// JSON parse failed, skip
|
|
57
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
interface PyrightDiagnostic {
|
|
63
|
+
file: string;
|
|
64
|
+
severity: "error" | "warning" | "information";
|
|
65
|
+
message: string;
|
|
66
|
+
range: {
|
|
67
|
+
start: { line: number; character: number };
|
|
68
|
+
end: { line: number; character: number };
|
|
69
|
+
};
|
|
70
|
+
rule: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface PyrightResult {
|
|
74
|
+
generalDiagnostics: PyrightDiagnostic[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parsePyrightOutput(
|
|
78
|
+
data: PyrightResult,
|
|
79
|
+
filePath: string,
|
|
80
|
+
): Diagnostic[] {
|
|
81
|
+
if (!data.generalDiagnostics) return [];
|
|
82
|
+
|
|
83
|
+
return data.generalDiagnostics
|
|
84
|
+
.filter((d) => {
|
|
85
|
+
// Only include errors and warnings, skip informational
|
|
86
|
+
return d.severity === "error" || d.severity === "warning";
|
|
87
|
+
})
|
|
88
|
+
.map((d) => ({
|
|
89
|
+
id: `pyright-${d.range.start.line}-${d.rule}`,
|
|
90
|
+
message: d.message.split("\n")[0], // First line only (pyright has multi-line messages)
|
|
91
|
+
filePath,
|
|
92
|
+
line: d.range.start.line + 1, // Pyright is 0-indexed, we're 1-indexed
|
|
93
|
+
column: d.range.start.character + 1,
|
|
94
|
+
severity: d.severity === "error" ? "error" : "warning",
|
|
95
|
+
semantic: d.severity === "error" ? "blocking" : "warning",
|
|
96
|
+
tool: "pyright",
|
|
97
|
+
rule: d.rule,
|
|
98
|
+
fixable: false, // Pyright can't auto-fix, only suggest
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default pyrightRunner;
|