pi-lens 3.7.1 → 3.8.2
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 +217 -0
- package/README.md +706 -619
- package/clients/architect-client.ts +7 -2
- package/clients/ast-grep-client.ts +7 -1
- package/clients/dispatch/plan.ts +10 -4
- package/clients/dispatch/runners/architect.ts +20 -7
- package/clients/dispatch/runners/ast-grep-napi.ts +5 -2
- package/clients/dispatch/runners/ast-grep.ts +29 -18
- package/clients/dispatch/runners/biome.ts +4 -4
- package/clients/dispatch/runners/python-slop.ts +17 -7
- package/clients/dispatch/runners/ruff.ts +4 -4
- package/clients/dispatch/runners/tree-sitter.ts +30 -19
- package/clients/dispatch/runners/ts-slop.ts +17 -7
- package/clients/dispatch/runners/utils/runner-helpers.ts +76 -8
- package/clients/dispatch/utils/format-utils.ts +2 -1
- package/clients/fix-scanners.ts +8 -8
- package/clients/installer/index.ts +19 -1
- package/clients/lsp/index.ts +0 -40
- package/clients/lsp/launch.ts +5 -2
- package/clients/package-root.ts +44 -0
- package/clients/pipeline.ts +179 -8
- package/clients/scan-utils.ts +20 -32
- package/clients/sg-runner.ts +7 -5
- package/clients/source-filter.ts +222 -0
- package/clients/startup-scan.ts +142 -0
- package/clients/todo-scanner.ts +44 -55
- package/clients/tree-sitter-cache.ts +315 -0
- package/clients/tree-sitter-client.ts +208 -52
- package/clients/tree-sitter-fixer.ts +217 -0
- package/clients/tree-sitter-navigator.ts +329 -0
- package/clients/tree-sitter-query-loader.ts +55 -32
- package/commands/booboo.ts +47 -35
- package/default-architect.yaml +76 -87
- package/docs/ARCHITECTURE.md +74 -0
- package/docs/AST_GREP_RULES.md +266 -0
- package/docs/COMPLEXITY_METRICS.md +120 -0
- package/docs/EXCLUSIONS.md +83 -0
- package/docs/LSP_CONFIG.md +240 -0
- package/docs/TREE_SITTER_RULES.md +340 -0
- package/docs/WRITING_NEW_AST_GREP_RULES.md +200 -0
- package/index.ts +209 -86
- package/package.json +13 -4
- package/rules/ast-grep-rules/rules/array-callback-return-js.yml +33 -0
- package/rules/ast-grep-rules/rules/array-callback-return.yml +1 -1
- package/rules/ast-grep-rules/rules/constructor-super-js.yml +22 -0
- package/rules/ast-grep-rules/rules/empty-catch-js.yml +45 -0
- package/rules/ast-grep-rules/rules/empty-catch.yml +1 -1
- package/rules/ast-grep-rules/rules/getter-return-js.yml +59 -0
- package/rules/ast-grep-rules/rules/getter-return.yml +1 -1
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +12 -0
- package/rules/ast-grep-rules/rules/jsx-boolean-short-circuit.yml +1 -1
- package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +14 -0
- package/rules/ast-grep-rules/rules/missed-concurrency-js.yml +25 -0
- package/rules/ast-grep-rules/rules/nested-ternary-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-alert-js.yml +6 -0
- package/rules/ast-grep-rules/rules/no-architecture-violation.yml +21 -18
- package/rules/ast-grep-rules/rules/no-array-constructor-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-async-promise-executor-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-async-promise-executor.yml +1 -1
- package/rules/ast-grep-rules/rules/no-await-in-loop-js.yml +30 -0
- package/rules/ast-grep-rules/rules/no-await-in-promise-all-js.yml +20 -0
- package/rules/ast-grep-rules/rules/no-await-in-promise-all.yml +1 -1
- package/rules/ast-grep-rules/rules/no-bare-except.yml +1 -1
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +16 -0
- package/rules/ast-grep-rules/rules/no-compare-neg-zero-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-compare-neg-zero.yml +1 -1
- package/rules/ast-grep-rules/rules/no-comparison-to-none.yml +1 -1
- package/rules/ast-grep-rules/rules/no-cond-assign-js.yml +36 -0
- package/rules/ast-grep-rules/rules/no-cond-assign.yml +1 -1
- package/rules/ast-grep-rules/rules/no-constant-condition-js.yml +25 -0
- package/rules/ast-grep-rules/rules/no-constant-condition.yml +1 -1
- package/rules/ast-grep-rules/rules/no-constructor-return-js.yml +28 -0
- package/rules/ast-grep-rules/rules/no-constructor-return.yml +1 -1
- package/rules/ast-grep-rules/rules/no-discarded-error-js.yml +25 -0
- package/rules/ast-grep-rules/rules/no-discarded-error.yml +25 -0
- package/rules/ast-grep-rules/rules/no-dupe-args-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-dupe-keys-js.yml +73 -0
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +25 -0
- package/rules/ast-grep-rules/rules/no-hardcoded-secrets-js.yml +17 -0
- package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-inner-html-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-insecure-randomness-js.yml +20 -0
- package/rules/ast-grep-rules/rules/no-insecure-randomness.yml +1 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +11 -0
- package/rules/ast-grep-rules/rules/no-nan-comparison-js.yml +22 -0
- package/rules/ast-grep-rules/rules/no-nan-comparison.yml +22 -0
- package/rules/ast-grep-rules/rules/no-new-symbol-js.yml +8 -0
- package/rules/ast-grep-rules/rules/no-new-wrappers-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-open-redirect-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-prototype-builtins-js.yml +15 -0
- package/rules/ast-grep-rules/rules/no-prototype-builtins.yml +1 -1
- package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +13 -0
- package/rules/ast-grep-rules/rules/no-sql-in-code.yml +1 -1
- package/rules/ast-grep-rules/rules/no-throw-string-js.yml +12 -0
- package/rules/ast-grep-rules/rules/no-throw-string.yml +1 -1
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +10 -0
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +10 -0
- package/rules/ast-grep-rules/rules/toctou-js.yml +112 -0
- package/rules/ast-grep-rules/rules/toctou.yml +1 -1
- package/rules/ast-grep-rules/rules/unchecked-sync-fs-js.yml +44 -0
- package/rules/ast-grep-rules/rules/unchecked-sync-fs.yml +44 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-js.yml +31 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +48 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-ruby.yml +47 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call.yml +31 -0
- package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +15 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +47 -0
- package/rules/tree-sitter-queries/go/go-defer-in-loop.yml +47 -0
- package/rules/tree-sitter-queries/go/go-hardcoded-secrets.yml +54 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +1 -1
- package/rules/tree-sitter-queries/python/python-debugger.yml +46 -0
- package/rules/tree-sitter-queries/python/python-empty-except.yml +48 -0
- package/rules/tree-sitter-queries/python/python-hardcoded-secrets.yml +44 -0
- package/rules/tree-sitter-queries/python/python-mutable-class-attr.yml +57 -0
- package/rules/tree-sitter-queries/python/python-print-statement.yml +53 -0
- package/rules/tree-sitter-queries/python/python-raise-string.yml +38 -0
- package/rules/tree-sitter-queries/python/python-unsafe-regex.yml +58 -0
- package/rules/tree-sitter-queries/ruby/ruby-debugger.yml +44 -0
- package/rules/tree-sitter-queries/ruby/ruby-empty-rescue.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +43 -0
- package/rules/tree-sitter-queries/ruby/ruby-hardcoded-secrets.yml +40 -0
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +48 -0
- package/rules/tree-sitter-queries/ruby/ruby-puts-statement.yml +52 -0
- package/rules/tree-sitter-queries/ruby/ruby-rescue-exception.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-unsafe-regex.yml +49 -0
- package/rules/tree-sitter-queries/rust/rust-clone-in-loop.yml +49 -0
- package/rules/tree-sitter-queries/rust/rust-unwrap.yml +45 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +3 -3
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +13 -27
- package/rules/tree-sitter-queries/typescript/injections.scm +40 -0
- package/rules/tree-sitter-queries/typescript/no-console-in-tests.yml +52 -0
- package/rules/tree-sitter-queries/typescript/sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +71 -0
- package/rules/tree-sitter-queries/typescript/variable-shadowing.yml +51 -0
- package/scripts/download-grammars.ts +78 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { minimatch } from "minimatch";
|
|
15
|
+
import { resolvePackagePath } from "./package-root.js";
|
|
15
16
|
|
|
16
17
|
// --- Types ---
|
|
17
18
|
|
|
@@ -91,6 +92,8 @@ export class ArchitectClient {
|
|
|
91
92
|
// Try multiple possible locations for the default config
|
|
92
93
|
const possibleDefaultPaths = [
|
|
93
94
|
path.join(projectRoot, "default-architect.yaml"),
|
|
95
|
+
path.join(projectRoot, ".pi-lens", "default-architect.yaml"),
|
|
96
|
+
resolvePackagePath(import.meta.url, "default-architect.yaml"),
|
|
94
97
|
path.join(projectRoot, "..", "default-architect.yaml"),
|
|
95
98
|
path.join(process.cwd(), "default-architect.yaml"),
|
|
96
99
|
];
|
|
@@ -170,7 +173,7 @@ export class ArchitectClient {
|
|
|
170
173
|
|
|
171
174
|
for (const check of rule.must_not) {
|
|
172
175
|
// We use 'g' to find all occurrences and correctly report line numbers
|
|
173
|
-
const regex = new RegExp(check.pattern, "
|
|
176
|
+
const regex = new RegExp(check.pattern, "gim");
|
|
174
177
|
let match: RegExpExecArray | null;
|
|
175
178
|
|
|
176
179
|
// biome-ignore lint/suspicious/noAssignInExpressions: RegExp.exec iteration
|
|
@@ -286,7 +289,9 @@ export class ArchitectClient {
|
|
|
286
289
|
) {
|
|
287
290
|
// Extract everything after "pattern:" and unquote
|
|
288
291
|
const raw = trimmed.replace(/^-?\s*pattern:\s*/, "").trim();
|
|
289
|
-
|
|
292
|
+
let unquoted = raw.replace(/^["']|["']$/g, "");
|
|
293
|
+
// Single-quoted YAML: '' is an escaped single-quote
|
|
294
|
+
if (raw.startsWith("'")) unquoted = unquoted.split("''").join("'");
|
|
290
295
|
if (unquoted) {
|
|
291
296
|
violation = { pattern: unquoted, message: "" };
|
|
292
297
|
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
RuleDescription,
|
|
20
20
|
SgMatch,
|
|
21
21
|
} from "./ast-grep-types.js";
|
|
22
|
+
import { resolvePackagePath } from "./package-root.js";
|
|
22
23
|
import { SgRunner } from "./sg-runner.js";
|
|
23
24
|
|
|
24
25
|
const _getExtensionDir = () => {
|
|
@@ -38,7 +39,12 @@ export class AstGrepClient {
|
|
|
38
39
|
private runner: SgRunner;
|
|
39
40
|
|
|
40
41
|
constructor(ruleDir?: string, verbose = false) {
|
|
41
|
-
|
|
42
|
+
const projectRuleDir = path.join(process.cwd(), "rules");
|
|
43
|
+
this.ruleDir =
|
|
44
|
+
ruleDir ||
|
|
45
|
+
(fs.existsSync(projectRuleDir)
|
|
46
|
+
? projectRuleDir
|
|
47
|
+
: resolvePackagePath(import.meta.url, "rules"));
|
|
42
48
|
this.log = verbose
|
|
43
49
|
? (msg: string) => console.error(`[ast-grep] ${msg}`)
|
|
44
50
|
: () => {};
|
package/clients/dispatch/plan.ts
CHANGED
|
@@ -43,9 +43,11 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
|
|
|
43
43
|
{ mode: "fallback", runnerIds: ["similarity"], filterKinds: ["jsts"] },
|
|
44
44
|
// ESLint: only fires when project has eslint config (skips Biome/OxLint projects)
|
|
45
45
|
{ mode: "fallback", runnerIds: ["eslint"], filterKinds: ["jsts"] },
|
|
46
|
+
// Architectural rules: warning-only, fast (pure regex). Needed per-write so the
|
|
47
|
+
// all-clear signal can report "N warnings -> /lens-booboo" accurately.
|
|
48
|
+
{ mode: "fallback", runnerIds: ["architect"], filterKinds: ["jsts"] },
|
|
46
49
|
// Note: ast-grep CLI kept for ast_grep_search/ast_grep_replace tools only
|
|
47
50
|
// Note: biome, oxlint handled by direct auto-fix calls in index.ts (not in dispatch)
|
|
48
|
-
// Architectural rules (guidance only, not blocking) - runs via /lens-booboo only
|
|
49
51
|
],
|
|
50
52
|
},
|
|
51
53
|
|
|
@@ -60,8 +62,8 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
|
|
|
60
62
|
{ mode: "all", runnerIds: ["pyright"], filterKinds: ["python"] },
|
|
61
63
|
// LSP type checking (unified) - when --lens-lsp enabled
|
|
62
64
|
{ mode: "all", runnerIds: ["lsp"], filterKinds: ["python"] },
|
|
65
|
+
{ mode: "fallback", runnerIds: ["architect"], filterKinds: ["python"] },
|
|
63
66
|
// Note: ruff handled by direct auto-fix calls in index.ts (not in dispatch)
|
|
64
|
-
// Architectural rules (guidance only, not blocking) - runs via /lens-booboo only
|
|
65
67
|
],
|
|
66
68
|
},
|
|
67
69
|
|
|
@@ -77,7 +79,8 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
|
|
|
77
79
|
{ mode: "fallback", runnerIds: ["go-vet"], filterKinds: ["go"] },
|
|
78
80
|
// golangci-lint: only fires when project has .golangci.yml config
|
|
79
81
|
{ mode: "fallback", runnerIds: ["golangci-lint"], filterKinds: ["go"] },
|
|
80
|
-
//
|
|
82
|
+
// Structural analysis
|
|
83
|
+
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["go"] },
|
|
81
84
|
],
|
|
82
85
|
},
|
|
83
86
|
|
|
@@ -91,7 +94,8 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
|
|
|
91
94
|
{ mode: "all", runnerIds: ["lsp"], filterKinds: ["rust"] },
|
|
92
95
|
// Cargo clippy for additional checks
|
|
93
96
|
{ mode: "fallback", runnerIds: ["rust-clippy"], filterKinds: ["rust"] },
|
|
94
|
-
//
|
|
97
|
+
// Structural analysis
|
|
98
|
+
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["rust"] },
|
|
95
99
|
],
|
|
96
100
|
},
|
|
97
101
|
|
|
@@ -102,6 +106,8 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
|
|
|
102
106
|
name: "Ruby Linting",
|
|
103
107
|
groups: [
|
|
104
108
|
{ mode: "fallback", runnerIds: ["rubocop"], filterKinds: ["ruby"] },
|
|
109
|
+
// Structural analysis
|
|
110
|
+
{ mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["ruby"] },
|
|
105
111
|
],
|
|
106
112
|
},
|
|
107
113
|
|
|
@@ -18,6 +18,18 @@ import type {
|
|
|
18
18
|
} from "../types.js";
|
|
19
19
|
import { readFileContent } from "./utils.js";
|
|
20
20
|
|
|
21
|
+
// Module-level singleton — loadConfig once per cwd, not on every file write
|
|
22
|
+
let _client: ArchitectClient | null = null;
|
|
23
|
+
let _loadedCwd: string | null = null;
|
|
24
|
+
|
|
25
|
+
function getClient(cwd: string): ArchitectClient {
|
|
26
|
+
if (_client && _loadedCwd === cwd) return _client;
|
|
27
|
+
_client = new ArchitectClient();
|
|
28
|
+
_client.loadConfig(cwd);
|
|
29
|
+
_loadedCwd = cwd;
|
|
30
|
+
return _client;
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
const architectRunner: RunnerDefinition = {
|
|
22
34
|
id: "architect",
|
|
23
35
|
appliesTo: ["jsts", "python", "go", "rust", "cxx", "shell", "cmake"],
|
|
@@ -33,8 +45,7 @@ const architectRunner: RunnerDefinition = {
|
|
|
33
45
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
34
46
|
}
|
|
35
47
|
|
|
36
|
-
const architectClient =
|
|
37
|
-
architectClient.loadConfig(ctx.cwd);
|
|
48
|
+
const architectClient = getClient(ctx.cwd);
|
|
38
49
|
|
|
39
50
|
if (!architectClient.hasConfig()) {
|
|
40
51
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
@@ -47,16 +58,18 @@ const architectRunner: RunnerDefinition = {
|
|
|
47
58
|
for (const v of violations) {
|
|
48
59
|
// Build message with inline fix guidance
|
|
49
60
|
let message = v.message;
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
const fixSuggestion: string | undefined = v.fix;
|
|
62
|
+
|
|
52
63
|
if (v.fix) {
|
|
53
|
-
const fixPreview =
|
|
64
|
+
const fixPreview =
|
|
65
|
+
v.fix.length > 60 ? `${v.fix.substring(0, 60)}...` : v.fix;
|
|
54
66
|
message += `\n💡 Suggested fix: ${fixPreview}`;
|
|
55
67
|
} else if (v.note) {
|
|
56
|
-
const notePreview =
|
|
68
|
+
const notePreview =
|
|
69
|
+
v.note.length > 80 ? `${v.note.substring(0, 80)}...` : v.note;
|
|
57
70
|
message += `\n📝 ${notePreview}`;
|
|
58
71
|
}
|
|
59
|
-
|
|
72
|
+
|
|
60
73
|
diagnostics.push({
|
|
61
74
|
id: `architect-${v.line || 0}-${v.pattern}`,
|
|
62
75
|
message,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
+
import { resolvePackagePath } from "../../package-root.js";
|
|
12
13
|
import type {
|
|
13
14
|
Diagnostic,
|
|
14
15
|
DispatchContext,
|
|
@@ -356,8 +357,10 @@ const astGrepNapiRunner: RunnerDefinition = {
|
|
|
356
357
|
const diagnostics: Diagnostic[] = [];
|
|
357
358
|
|
|
358
359
|
const ruleDirs = [
|
|
359
|
-
path.join(process.cwd(), "rules
|
|
360
|
-
path.join(process.cwd(), "rules
|
|
360
|
+
path.join(process.cwd(), "rules", "ast-grep-rules", "rules"),
|
|
361
|
+
path.join(process.cwd(), "rules", "ast-grep-rules"),
|
|
362
|
+
resolvePackagePath(import.meta.url, "rules", "ast-grep-rules", "rules"),
|
|
363
|
+
resolvePackagePath(import.meta.url, "rules", "ast-grep-rules"),
|
|
361
364
|
];
|
|
362
365
|
|
|
363
366
|
for (const ruleDir of ruleDirs) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
+
import { resolvePackagePath } from "../../package-root.js";
|
|
12
13
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
13
14
|
import type {
|
|
14
15
|
Diagnostic,
|
|
@@ -16,6 +17,7 @@ import type {
|
|
|
16
17
|
RunnerDefinition,
|
|
17
18
|
RunnerResult,
|
|
18
19
|
} from "../types.js";
|
|
20
|
+
import { getSgCommand, isSgAvailable } from "./utils/runner-helpers.js";
|
|
19
21
|
|
|
20
22
|
// Simple YAML fix: field extractor
|
|
21
23
|
function extractFixFromRule(
|
|
@@ -49,12 +51,8 @@ const astGrepRunner: RunnerDefinition = {
|
|
|
49
51
|
skipTestFiles: true, // Many rules are noisy in tests
|
|
50
52
|
|
|
51
53
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
52
|
-
// Check if ast-grep is available (
|
|
53
|
-
|
|
54
|
-
timeout: 5000,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (check.error || check.status !== 0) {
|
|
54
|
+
// Check if ast-grep is available (local bin preferred over npx)
|
|
55
|
+
if (!isSgAvailable()) {
|
|
58
56
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
59
57
|
}
|
|
60
58
|
|
|
@@ -64,10 +62,18 @@ const astGrepRunner: RunnerDefinition = {
|
|
|
64
62
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
const args = [
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const { cmd: sgCmd, args: sgPre } = getSgCommand();
|
|
66
|
+
const args = [
|
|
67
|
+
...sgPre,
|
|
68
|
+
"sg",
|
|
69
|
+
"scan",
|
|
70
|
+
"--config",
|
|
71
|
+
configPath,
|
|
72
|
+
"--json",
|
|
73
|
+
ctx.filePath,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const result = await safeSpawnAsync(sgCmd, args, {
|
|
71
77
|
timeout: 30000,
|
|
72
78
|
});
|
|
73
79
|
|
|
@@ -94,15 +100,20 @@ const astGrepRunner: RunnerDefinition = {
|
|
|
94
100
|
|
|
95
101
|
function findAstGrepConfig(cwd: string): string | undefined {
|
|
96
102
|
const candidates = [
|
|
97
|
-
"rules
|
|
98
|
-
".sgconfig.yml",
|
|
99
|
-
"sgconfig.yml",
|
|
103
|
+
path.join(cwd, "rules", "ast-grep-rules", ".sgconfig.yml"),
|
|
104
|
+
path.join(cwd, ".sgconfig.yml"),
|
|
105
|
+
path.join(cwd, "sgconfig.yml"),
|
|
106
|
+
resolvePackagePath(
|
|
107
|
+
import.meta.url,
|
|
108
|
+
"rules",
|
|
109
|
+
"ast-grep-rules",
|
|
110
|
+
".sgconfig.yml",
|
|
111
|
+
),
|
|
100
112
|
];
|
|
101
113
|
|
|
102
114
|
for (const candidate of candidates) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return fullPath;
|
|
115
|
+
if (fs.existsSync(candidate)) {
|
|
116
|
+
return candidate;
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
|
|
@@ -119,8 +130,8 @@ function parseAstGrepOutput(
|
|
|
119
130
|
// Try to parse as JSON
|
|
120
131
|
// Determine rule directory for fix: extraction
|
|
121
132
|
const ruleDir = _configPath
|
|
122
|
-
? path.dirname(_configPath)
|
|
123
|
-
:
|
|
133
|
+
? path.join(path.dirname(_configPath), "rules")
|
|
134
|
+
: resolvePackagePath(import.meta.url, "rules", "ast-grep-rules", "rules");
|
|
124
135
|
|
|
125
136
|
try {
|
|
126
137
|
const parsed = JSON.parse(raw);
|
|
@@ -37,10 +37,10 @@ const biomeRunner: RunnerDefinition = {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
40
|
+
// No --write here: dispatch runners report issues for agent understanding,
|
|
41
|
+
// not silent correction. Auto-format (biome --write) already runs in the
|
|
42
|
+
// format phase before dispatch, handling all safe style transforms.
|
|
43
|
+
// Silently rewriting here would leave the agent's context window stale.
|
|
44
44
|
const args = useNpx
|
|
45
45
|
? ["biome", "check", ctx.filePath]
|
|
46
46
|
: ["check", ctx.filePath];
|
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
14
|
import { safeSpawn } from "../../safe-spawn.js";
|
|
15
|
-
import {
|
|
16
|
-
createConfigFinder,
|
|
17
|
-
isSgAvailable,
|
|
18
|
-
} from "./utils/runner-helpers.js";
|
|
19
15
|
import type {
|
|
20
16
|
Diagnostic,
|
|
21
17
|
DispatchContext,
|
|
22
18
|
RunnerDefinition,
|
|
23
19
|
RunnerResult,
|
|
24
20
|
} from "../types.js";
|
|
21
|
+
import {
|
|
22
|
+
createConfigFinder,
|
|
23
|
+
getSgCommand,
|
|
24
|
+
isSgAvailable,
|
|
25
|
+
} from "./utils/runner-helpers.js";
|
|
25
26
|
|
|
26
27
|
const findSlopConfig = createConfigFinder("python-slop-rules");
|
|
27
28
|
|
|
@@ -45,9 +46,18 @@ const pythonSlopRunner: RunnerDefinition = {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
// Run ast-grep scan
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const { cmd: sgCmd, args: sgPre } = getSgCommand();
|
|
50
|
+
const args = [
|
|
51
|
+
...sgPre,
|
|
52
|
+
"sg",
|
|
53
|
+
"scan",
|
|
54
|
+
"--config",
|
|
55
|
+
configPath,
|
|
56
|
+
"--json",
|
|
57
|
+
ctx.filePath,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const result = safeSpawn(sgCmd, args, {
|
|
51
61
|
timeout: 30000,
|
|
52
62
|
});
|
|
53
63
|
|
|
@@ -35,10 +35,10 @@ const ruffRunner: RunnerDefinition = {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
38
|
+
// No --fix here: dispatch runners report issues for agent understanding,
|
|
39
|
+
// not silent correction. Auto-fix (ruff --fix) already runs in the
|
|
40
|
+
// format phase before dispatch, handling all safe style transforms.
|
|
41
|
+
// Silently rewriting here would leave the agent's context window stale.
|
|
42
42
|
const args = ["check", ctx.filePath];
|
|
43
43
|
|
|
44
44
|
const result = await safeSpawnAsync(ruff.getCommand()!, args, {
|
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
// Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
|
|
27
27
|
// WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
|
|
28
28
|
let _sharedClient: TreeSitterClient | null = null;
|
|
29
|
+
|
|
29
30
|
function getSharedClient(): TreeSitterClient {
|
|
30
31
|
if (!_sharedClient) {
|
|
31
32
|
_sharedClient = new TreeSitterClient();
|
|
@@ -117,7 +118,7 @@ function matchesSecretPattern(varName: string): boolean {
|
|
|
117
118
|
|
|
118
119
|
const treeSitterRunner: RunnerDefinition = {
|
|
119
120
|
id: "tree-sitter",
|
|
120
|
-
appliesTo: ["jsts", "python"],
|
|
121
|
+
appliesTo: ["jsts", "python", "go", "rust", "ruby"],
|
|
121
122
|
priority: 14, // Between oxlint (12) and ast-grep-napi (15)
|
|
122
123
|
enabledByDefault: true,
|
|
123
124
|
skipTestFiles: false, // Run on test files too (structural issues matter there)
|
|
@@ -136,21 +137,23 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
136
137
|
|
|
137
138
|
// Determine language from file extension
|
|
138
139
|
const filePath = ctx.filePath;
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
140
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
141
|
+
const EXT_TO_LANG: Record<string, string> = {
|
|
142
|
+
".ts": "typescript",
|
|
143
|
+
".mts": "typescript",
|
|
144
|
+
".cts": "typescript",
|
|
145
|
+
".tsx": "tsx",
|
|
146
|
+
".js": "javascript",
|
|
147
|
+
".mjs": "javascript",
|
|
148
|
+
".cjs": "javascript",
|
|
149
|
+
".jsx": "javascript",
|
|
150
|
+
".py": "python",
|
|
151
|
+
".go": "go",
|
|
152
|
+
".rs": "rust",
|
|
153
|
+
".rb": "ruby",
|
|
154
|
+
};
|
|
155
|
+
const languageId = EXT_TO_LANG[ext];
|
|
156
|
+
if (!languageId) {
|
|
154
157
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
155
158
|
}
|
|
156
159
|
|
|
@@ -197,7 +200,7 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
197
200
|
languageQueries = allQueries.filter(
|
|
198
201
|
(q) =>
|
|
199
202
|
q.language === languageId ||
|
|
200
|
-
(
|
|
203
|
+
(languageId === "javascript" && q.language === "typescript"),
|
|
201
204
|
);
|
|
202
205
|
|
|
203
206
|
// Save to cache
|
|
@@ -245,8 +248,9 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
245
248
|
continue; // Skip this match - filter didn't pass
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
//
|
|
249
|
-
|
|
251
|
+
// check_secret_pattern post-filter is handled in tree-sitter-client.ts
|
|
252
|
+
// Legacy: hardcoded-secrets id check (kept for backward compat)
|
|
253
|
+
if (query.id === "hardcoded-secrets" && !query.post_filter) {
|
|
250
254
|
// Extract variable name from captures
|
|
251
255
|
const varName = match.captures?.VARNAME || "";
|
|
252
256
|
if (!varName || !matchesSecretPattern(varName)) {
|
|
@@ -276,6 +280,13 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
276
280
|
semantic,
|
|
277
281
|
tool: "tree-sitter",
|
|
278
282
|
rule: query.id,
|
|
283
|
+
// Surface fix intent to agent — tree-sitter never auto-applies;
|
|
284
|
+
// linters (biome/ruff/eslint) own the autofix phase.
|
|
285
|
+
fixable: query.has_fix,
|
|
286
|
+
fixSuggestion:
|
|
287
|
+
query.has_fix && query.fix_action
|
|
288
|
+
? `${query.fix_action} this statement`
|
|
289
|
+
: undefined,
|
|
279
290
|
});
|
|
280
291
|
}
|
|
281
292
|
} catch (err) {
|
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
14
|
import { safeSpawn } from "../../safe-spawn.js";
|
|
15
|
-
import {
|
|
16
|
-
createConfigFinder,
|
|
17
|
-
isSgAvailable,
|
|
18
|
-
} from "./utils/runner-helpers.js";
|
|
19
15
|
import type {
|
|
20
16
|
Diagnostic,
|
|
21
17
|
DispatchContext,
|
|
22
18
|
RunnerDefinition,
|
|
23
19
|
RunnerResult,
|
|
24
20
|
} from "../types.js";
|
|
21
|
+
import {
|
|
22
|
+
createConfigFinder,
|
|
23
|
+
getSgCommand,
|
|
24
|
+
isSgAvailable,
|
|
25
|
+
} from "./utils/runner-helpers.js";
|
|
25
26
|
|
|
26
27
|
const findSlopConfig = createConfigFinder("ts-slop-rules");
|
|
27
28
|
|
|
@@ -47,9 +48,18 @@ const tsSlopRunner: RunnerDefinition = {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// Run ast-grep scan
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const { cmd: sgCmd, args: sgPre } = getSgCommand();
|
|
52
|
+
const args = [
|
|
53
|
+
...sgPre,
|
|
54
|
+
"sg",
|
|
55
|
+
"scan",
|
|
56
|
+
"--config",
|
|
57
|
+
configPath,
|
|
58
|
+
"--json",
|
|
59
|
+
ctx.filePath,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const result = safeSpawn(sgCmd, args, {
|
|
53
63
|
timeout: 30000,
|
|
54
64
|
});
|
|
55
65
|
|
|
@@ -41,9 +41,7 @@ export function createVenvFinder(
|
|
|
41
41
|
for (const venvPath of venvPaths) {
|
|
42
42
|
const fullPath = path.join(cwd, venvPath);
|
|
43
43
|
if (fs.existsSync(fullPath)) {
|
|
44
|
-
return quoteWindows && windowsExt
|
|
45
|
-
? `"${fullPath}"`
|
|
46
|
-
: fullPath;
|
|
44
|
+
return quoteWindows && windowsExt ? `"${fullPath}"` : fullPath;
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
|
|
@@ -142,21 +140,91 @@ export function createConfigFinder(
|
|
|
142
140
|
|
|
143
141
|
// Shared sg availability cache across all slop runners
|
|
144
142
|
let sgAvailable: boolean | null = null;
|
|
143
|
+
let sgCmd: string | null = null;
|
|
144
|
+
let sgCmdArgs: string[] = [];
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* Check if ast-grep CLI is available
|
|
147
|
+
* Check if ast-grep CLI (sg) is available.
|
|
148
|
+
* Prefers local node_modules/.bin/sg, then global sg, then npx --no sg (cache-only).
|
|
148
149
|
*/
|
|
149
150
|
export function isSgAvailable(): boolean {
|
|
150
151
|
if (sgAvailable !== null) return sgAvailable;
|
|
151
152
|
|
|
152
|
-
|
|
153
|
+
// 1. Local node_modules/.bin/sg
|
|
154
|
+
const isWin = process.platform === "win32";
|
|
155
|
+
const localSg = path.join(
|
|
156
|
+
process.cwd(),
|
|
157
|
+
"node_modules",
|
|
158
|
+
".bin",
|
|
159
|
+
isWin ? "sg.cmd" : "sg",
|
|
160
|
+
);
|
|
161
|
+
if (fs.existsSync(localSg)) {
|
|
162
|
+
const check = safeSpawn(localSg, ["--version"], { timeout: 5000 });
|
|
163
|
+
if (!check.error && check.status === 0) {
|
|
164
|
+
sgCmd = localSg;
|
|
165
|
+
sgCmdArgs = [];
|
|
166
|
+
sgAvailable = true;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Global sg
|
|
172
|
+
const globalCheck = safeSpawn("sg", ["--version"], { timeout: 5000 });
|
|
173
|
+
if (!globalCheck.error && globalCheck.status === 0) {
|
|
174
|
+
sgCmd = "sg";
|
|
175
|
+
sgCmdArgs = [];
|
|
176
|
+
sgAvailable = true;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. npx --no (cache-only, no silent download)
|
|
181
|
+
const npxCheck = safeSpawn("npx", ["--no", "sg", "--version"], {
|
|
153
182
|
timeout: 5000,
|
|
154
183
|
});
|
|
155
|
-
|
|
156
|
-
sgAvailable
|
|
184
|
+
sgAvailable = !npxCheck.error && npxCheck.status === 0;
|
|
185
|
+
if (sgAvailable) {
|
|
186
|
+
sgCmd = "npx";
|
|
187
|
+
sgCmdArgs = ["--no"];
|
|
188
|
+
}
|
|
157
189
|
return sgAvailable;
|
|
158
190
|
}
|
|
159
191
|
|
|
192
|
+
export function getSgCommand(): { cmd: string; args: string[] } {
|
|
193
|
+
return { cmd: sgCmd ?? "npx", args: sgCmdArgs.length ? sgCmdArgs : ["--no"] };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// LOCAL-FIRST BINARY RESOLUTION
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Find a tool binary preferring local node_modules/.bin over global PATH.
|
|
202
|
+
* Only falls back to npx as a last resort (avoids silent network downloads).
|
|
203
|
+
*
|
|
204
|
+
* Returns: { cmd, args } where args may include ["npx", toolName] preamble.
|
|
205
|
+
*/
|
|
206
|
+
export function resolveLocalFirst(
|
|
207
|
+
toolName: string,
|
|
208
|
+
cwd: string,
|
|
209
|
+
windowsExt = ".cmd",
|
|
210
|
+
): { cmd: string; args: string[] } {
|
|
211
|
+
const isWin = process.platform === "win32";
|
|
212
|
+
const binName = isWin ? `${toolName}${windowsExt}` : toolName;
|
|
213
|
+
|
|
214
|
+
// 1. Local node_modules/.bin (project-installed)
|
|
215
|
+
const local = path.join(cwd, "node_modules", ".bin", binName);
|
|
216
|
+
if (fs.existsSync(local)) return { cmd: local, args: [] };
|
|
217
|
+
|
|
218
|
+
// 2. Global PATH (already installed system-wide)
|
|
219
|
+
const globalCheck = safeSpawn(toolName, ["--version"], { timeout: 3000 });
|
|
220
|
+
if (!globalCheck.error && globalCheck.status === 0) {
|
|
221
|
+
return { cmd: toolName, args: [] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 3. npx fallback — only for already-cached packages (no silent download)
|
|
225
|
+
return { cmd: "npx", args: ["--no", toolName] };
|
|
226
|
+
}
|
|
227
|
+
|
|
160
228
|
// =============================================================================
|
|
161
229
|
// PRE-BUILT CHECKERS FOR COMMON TOOLS
|
|
162
230
|
// =============================================================================
|
|
@@ -164,4 +232,4 @@ export function isSgAvailable(): boolean {
|
|
|
164
232
|
export const pyright = createAvailabilityChecker("pyright", ".exe");
|
|
165
233
|
export const ruff = createAvailabilityChecker("ruff", ".exe");
|
|
166
234
|
export const biome = createAvailabilityChecker("biome");
|
|
167
|
-
export const sg = { isAvailable: isSgAvailable, getCommand:
|
|
235
|
+
export const sg = { isAvailable: isSgAvailable, getCommand: getSgCommand };
|
|
@@ -19,7 +19,8 @@ export const EMOJI: Record<string, string> = {
|
|
|
19
19
|
export function formatDiagnostic(d: Diagnostic): string {
|
|
20
20
|
const line = d.line ? `L${d.line}: ` : "";
|
|
21
21
|
const indented = d.message.split("\n").join("\n ");
|
|
22
|
-
|
|
22
|
+
const fix = d.fixSuggestion ? `\n 💡 Fix: ${d.fixSuggestion}` : "";
|
|
23
|
+
return ` ${line}${indented}${fix}`;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
package/clients/fix-scanners.ts
CHANGED
|
@@ -14,6 +14,10 @@ import * as nodeFs from "node:fs";
|
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import type { BiomeClient } from "./biome-client.js";
|
|
16
16
|
import type { ComplexityClient } from "./complexity-client.js";
|
|
17
|
+
import {
|
|
18
|
+
getSgCommand,
|
|
19
|
+
isSgAvailable,
|
|
20
|
+
} from "./dispatch/runners/utils/runner-helpers.js";
|
|
17
21
|
import { EXCLUDED_DIRS } from "./file-utils.js";
|
|
18
22
|
import type { JscpdClient } from "./jscpd-client.js";
|
|
19
23
|
import type { KnipClient } from "./knip-client.js";
|
|
@@ -119,17 +123,13 @@ export function scanAstGrep(
|
|
|
119
123
|
isTsProject: boolean,
|
|
120
124
|
configPath: string,
|
|
121
125
|
): AstIssue[] {
|
|
122
|
-
|
|
123
|
-
nodeFs.existsSync(path.join(targetPath, "node_modules", ".bin", "sg")) ||
|
|
124
|
-
safeSpawn("npx", ["sg", "--version"], {
|
|
125
|
-
timeout: 5000,
|
|
126
|
-
}).status === 0;
|
|
127
|
-
|
|
128
|
-
if (!hasSg) return [];
|
|
126
|
+
if (!isSgAvailable()) return [];
|
|
129
127
|
|
|
128
|
+
const { cmd: sgCmd, args: sgPre } = getSgCommand();
|
|
130
129
|
const result = safeSpawn(
|
|
131
|
-
|
|
130
|
+
sgCmd,
|
|
132
131
|
[
|
|
132
|
+
...sgPre,
|
|
133
133
|
"sg",
|
|
134
134
|
"scan",
|
|
135
135
|
"--config",
|