pi-lens 2.2.9 → 3.0.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 +198 -0
- package/README.md +709 -519
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/file-time.test.ts +276 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/format-service.test.ts +339 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/__tests__/formatters.test.ts +401 -0
- package/clients/amain-types.js +164 -0
- package/clients/amain-types.ts +165 -0
- package/clients/architect-client.js +56 -12
- package/clients/architect-client.ts +81 -16
- package/clients/ast-grep-client.js +2 -2
- package/clients/ast-grep-client.ts +14 -39
- package/clients/ast-grep-parser.ts +1 -1
- package/clients/ast-grep-rule-manager.js +8 -0
- package/clients/ast-grep-rule-manager.ts +10 -1
- package/clients/ast-grep-types.js +9 -0
- package/clients/ast-grep-types.ts +106 -0
- package/clients/auto-loop.js +10 -0
- package/clients/auto-loop.ts +14 -1
- package/clients/biome-client.js +81 -19
- package/clients/biome-client.ts +103 -22
- package/clients/bus/bus.js +191 -0
- package/clients/bus/bus.ts +251 -0
- package/clients/bus/events.js +214 -0
- package/clients/bus/events.ts +279 -0
- package/clients/bus/index.js +8 -0
- package/clients/bus/index.ts +9 -0
- package/clients/bus/integration.js +158 -0
- package/clients/bus/integration.ts +214 -0
- package/clients/complexity-client.js +13 -7
- package/clients/complexity-client.ts +13 -7
- package/clients/config-validator.js +465 -0
- package/clients/config-validator.ts +558 -0
- package/clients/dependency-checker.js +4 -10
- package/clients/dependency-checker.ts +4 -10
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
- package/clients/dispatch/bus-dispatcher.js +177 -0
- package/clients/dispatch/bus-dispatcher.ts +251 -0
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.edge.test.ts +100 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.format.test.ts +58 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.inline.test.ts +93 -0
- package/clients/dispatch/dispatcher.js +19 -53
- package/clients/dispatch/dispatcher.ts +20 -67
- package/clients/dispatch/plan.js +9 -4
- package/clients/dispatch/plan.ts +9 -4
- package/clients/dispatch/runners/architect.js +21 -7
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/architect.test.ts +162 -0
- package/clients/dispatch/runners/architect.ts +22 -7
- package/clients/dispatch/runners/ast-grep-napi.js +462 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
- package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
- package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
- package/clients/dispatch/runners/ast-grep.js +62 -19
- package/clients/dispatch/runners/ast-grep.ts +70 -18
- package/clients/dispatch/runners/biome.js +29 -53
- package/clients/dispatch/runners/biome.ts +29 -63
- package/clients/dispatch/runners/config-validation.js +67 -0
- package/clients/dispatch/runners/config-validation.ts +82 -0
- package/clients/dispatch/runners/go-vet.js +4 -28
- package/clients/dispatch/runners/go-vet.ts +4 -32
- package/clients/dispatch/runners/index.js +30 -10
- package/clients/dispatch/runners/index.ts +30 -10
- package/clients/dispatch/runners/oxlint.js +141 -0
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/oxlint.test.ts +303 -0
- package/clients/dispatch/runners/oxlint.ts +175 -0
- package/clients/dispatch/runners/pyright.js +40 -70
- package/clients/dispatch/runners/pyright.test.js +16 -2
- package/clients/dispatch/runners/pyright.test.ts +14 -2
- package/clients/dispatch/runners/pyright.ts +48 -91
- package/clients/dispatch/runners/python-slop.js +97 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/python-slop.test.ts +298 -0
- package/clients/dispatch/runners/python-slop.ts +124 -0
- package/clients/dispatch/runners/ruff.js +18 -71
- package/clients/dispatch/runners/ruff.ts +19 -79
- package/clients/dispatch/runners/rust-clippy.js +28 -32
- package/clients/dispatch/runners/rust-clippy.ts +29 -31
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
- package/clients/dispatch/runners/shellcheck.js +147 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/shellcheck.test.ts +129 -0
- package/clients/dispatch/runners/shellcheck.ts +188 -0
- package/clients/dispatch/runners/similarity.js +230 -0
- package/clients/dispatch/runners/similarity.ts +339 -0
- package/clients/dispatch/runners/spellcheck.js +106 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/runners/spellcheck.test.ts +214 -0
- package/clients/dispatch/runners/spellcheck.ts +136 -0
- package/clients/dispatch/runners/tree-sitter.js +107 -0
- package/clients/dispatch/runners/tree-sitter.ts +135 -0
- package/clients/dispatch/runners/ts-lsp.js +104 -33
- package/clients/dispatch/runners/ts-lsp.ts +120 -38
- package/clients/dispatch/runners/ts-slop.js +113 -0
- package/clients/dispatch/runners/ts-slop.test.js +180 -0
- package/clients/dispatch/runners/ts-slop.test.ts +230 -0
- package/clients/dispatch/runners/ts-slop.ts +142 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
- package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
- package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
- package/clients/dispatch/runners/utils.js +2 -4
- package/clients/dispatch/runners/utils.ts +2 -4
- package/clients/dispatch/types.ts +1 -1
- package/clients/dispatch/utils/format-utils.js +49 -0
- package/clients/dispatch/utils/format-utils.ts +60 -0
- package/clients/dogfood.test.js +201 -0
- package/clients/dogfood.test.ts +269 -0
- package/clients/file-time.js +152 -0
- package/clients/file-time.ts +208 -0
- package/clients/file-utils.js +40 -0
- package/clients/file-utils.ts +44 -0
- package/clients/fix-scanners.js +10 -20
- package/clients/fix-scanners.ts +10 -22
- package/clients/format-service.js +172 -0
- package/clients/format-service.ts +254 -0
- package/clients/formatters.js +435 -0
- package/clients/formatters.ts +508 -0
- package/clients/go-client.js +5 -14
- package/clients/go-client.ts +5 -13
- package/clients/installer/index.js +356 -0
- package/clients/installer/index.ts +426 -0
- package/clients/jscpd-client.js +11 -9
- package/clients/jscpd-client.ts +12 -8
- package/clients/knip-client.js +3 -7
- package/clients/knip-client.ts +3 -6
- package/clients/lsp/__tests__/client.test.js +325 -0
- package/clients/lsp/__tests__/client.test.ts +434 -0
- package/clients/lsp/__tests__/config.test.js +166 -0
- package/clients/lsp/__tests__/config.test.ts +209 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/integration.test.ts +160 -0
- package/clients/lsp/__tests__/launch.test.js +260 -0
- package/clients/lsp/__tests__/launch.test.ts +329 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/server.test.ts +332 -0
- package/clients/lsp/__tests__/service.test.js +417 -0
- package/clients/lsp/__tests__/service.test.ts +499 -0
- package/clients/lsp/client.js +235 -0
- package/clients/lsp/client.ts +328 -0
- package/clients/lsp/config.js +115 -0
- package/clients/lsp/config.ts +149 -0
- package/clients/lsp/index.js +222 -0
- package/clients/lsp/index.ts +280 -0
- package/clients/lsp/installer/index.js +391 -0
- package/clients/lsp/interactive-install.js +210 -0
- package/clients/lsp/interactive-install.ts +251 -0
- package/clients/lsp/language.js +170 -0
- package/clients/lsp/language.ts +216 -0
- package/clients/lsp/launch.js +174 -0
- package/clients/lsp/launch.ts +240 -0
- package/clients/lsp/lsp/launch.js +116 -0
- package/clients/lsp/lsp/server.js +532 -0
- package/clients/lsp/lsp-index.js +10 -0
- package/clients/lsp/lsp-index.ts +11 -0
- package/clients/lsp/path-utils.js +48 -0
- package/clients/lsp/path-utils.ts +52 -0
- package/clients/lsp/server.js +615 -0
- package/clients/lsp/server.ts +800 -0
- package/clients/lsp/test-py-spawn/requirements.txt +1 -0
- package/clients/lsp/test-py-spawn/test.py +3 -0
- package/clients/lsp/test-py-svc/requirements.txt +1 -0
- package/clients/lsp/test-py-svc/test.py +3 -0
- package/clients/lsp/test-python-project/requirements.txt +1 -0
- package/clients/lsp/test-python-project/test.py +5 -0
- package/clients/metrics-history.js +2 -2
- package/clients/metrics-history.ts +2 -2
- package/clients/production-readiness.js +522 -0
- package/clients/production-readiness.ts +556 -0
- package/clients/project-index.js +255 -0
- package/clients/project-index.ts +383 -0
- package/clients/project-metadata.js +531 -0
- package/clients/project-metadata.ts +624 -0
- package/clients/ruff-client.js +56 -16
- package/clients/ruff-client.ts +72 -15
- package/clients/runner-tracker.js +152 -0
- package/clients/runner-tracker.ts +213 -0
- package/clients/rust-client.js +4 -11
- package/clients/rust-client.ts +5 -11
- package/clients/safe-spawn.js +96 -0
- package/clients/safe-spawn.ts +128 -0
- package/clients/scan-architectural-debt.js +3 -6
- package/clients/scan-architectural-debt.ts +3 -6
- package/clients/scan-utils.js +5 -20
- package/clients/scan-utils.ts +5 -29
- package/clients/secrets-scanner.js +3 -17
- package/clients/secrets-scanner.ts +4 -20
- package/clients/services/__tests__/effect-integration.test.js +86 -0
- package/clients/services/__tests__/effect-integration.test.ts +111 -0
- package/clients/services/effect-integration.js +194 -0
- package/clients/services/effect-integration.ts +268 -0
- package/clients/services/index.js +7 -0
- package/clients/services/index.ts +8 -0
- package/clients/services/runner-service.js +105 -0
- package/clients/services/runner-service.ts +179 -0
- package/clients/sg-runner.js +87 -13
- package/clients/sg-runner.ts +97 -13
- package/clients/state-matrix.js +160 -0
- package/clients/state-matrix.ts +202 -0
- package/clients/subprocess-client.js +10 -9
- package/clients/subprocess-client.ts +10 -8
- package/clients/test-runner-client.js +3 -7
- package/clients/test-runner-client.ts +3 -6
- package/clients/tool-availability.js +4 -10
- package/clients/tool-availability.ts +4 -9
- package/clients/tree-sitter-client.js +564 -0
- package/clients/tree-sitter-client.ts +797 -0
- package/clients/tree-sitter-query-loader.js +355 -0
- package/clients/tree-sitter-query-loader.ts +425 -0
- package/clients/type-coverage-client.js +3 -7
- package/clients/type-coverage-client.ts +3 -6
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.codefix.test.ts +186 -0
- package/clients/typescript-client.js +43 -0
- package/clients/typescript-client.ts +98 -0
- package/commands/booboo.js +799 -219
- package/commands/booboo.ts +1004 -225
- package/commands/clients/ast-grep-client.js +250 -0
- package/commands/clients/ast-grep-parser.js +86 -0
- package/commands/clients/ast-grep-rule-manager.js +91 -0
- package/commands/clients/ast-grep-types.js +9 -0
- package/commands/clients/biome-client.js +380 -0
- package/commands/clients/complexity-client.js +667 -0
- package/commands/clients/file-kinds.js +177 -0
- package/commands/clients/file-utils.js +40 -0
- package/commands/clients/jscpd-client.js +169 -0
- package/commands/clients/knip-client.js +211 -0
- package/commands/clients/ruff-client.js +297 -0
- package/commands/clients/safe-spawn.js +88 -0
- package/commands/clients/scan-utils.js +83 -0
- package/commands/clients/sg-runner.js +190 -0
- package/commands/clients/types.js +11 -0
- package/commands/clients/typescript-client.js +505 -0
- package/commands/fix-from-booboo.js +398 -0
- package/commands/fix-from-booboo.ts +485 -0
- package/commands/fix-simplified.js +618 -0
- package/commands/fix-simplified.ts +768 -0
- package/commands/rate.js +10 -14
- package/commands/rate.ts +9 -16
- package/default-architect.yaml +59 -15
- package/index.ts +342 -429
- package/package.json +16 -3
- package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
- package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
- package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
- package/rules/python-slop-rules/.sgconfig.yml +4 -0
- package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
- package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
- package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
- package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
- package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
- package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
- package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
- package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
- package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
- package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
- package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
- package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
- package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
- package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
- package/rules/ts-slop-rules/.sgconfig.yml +4 -0
- package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
- package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
- package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
- package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
- package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
- package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
- package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
- package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
- package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
- package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
- package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
- package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
- package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
- package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
- package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
- package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
- package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
- package/skills/ast-grep/SKILL.md +182 -0
- package/clients/dispatch/runners/secrets.js +0 -109
- package/commands/fix.js +0 -244
- package/commands/fix.ts +0 -373
- package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
|
4
|
+
import type { DispatchContext } from "../types.js";
|
|
5
|
+
|
|
6
|
+
function createMockContext(
|
|
7
|
+
filePath: string,
|
|
8
|
+
kind: "jsts" | "python" | "go" | "rust" = "jsts",
|
|
9
|
+
cwd?: string,
|
|
10
|
+
): DispatchContext {
|
|
11
|
+
return {
|
|
12
|
+
filePath,
|
|
13
|
+
cwd: cwd || process.cwd(),
|
|
14
|
+
kind,
|
|
15
|
+
autofix: false,
|
|
16
|
+
deltaMode: false,
|
|
17
|
+
baselines: { get: () => undefined, set: () => {}, clear: () => {} } as any,
|
|
18
|
+
pi: { getFlag: () => false } as any,
|
|
19
|
+
hasTool: async () => false,
|
|
20
|
+
log: () => {},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("architect runner", () => {
|
|
25
|
+
const testDir = path.join(process.env.TEMP || "/tmp", `architect_test_${Date.now()}`);
|
|
26
|
+
const configPath = path.join(testDir, ".pi-lens", "architect.yaml");
|
|
27
|
+
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
// Create test config
|
|
30
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
configPath,
|
|
33
|
+
`version: "1.0"
|
|
34
|
+
rules:
|
|
35
|
+
- pattern: "**/*.ts"
|
|
36
|
+
max_lines: 50
|
|
37
|
+
must_not:
|
|
38
|
+
- pattern: 'hardcoded_secret_12345'
|
|
39
|
+
message: "No hardcoded secrets"
|
|
40
|
+
fix: "Use process.env.SECRET"
|
|
41
|
+
- pattern: 'console\.log'
|
|
42
|
+
message: "No console.log in production"
|
|
43
|
+
`,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(testDir)) {
|
|
50
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore cleanup errors
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should load default config when no user config exists", async () => {
|
|
58
|
+
const module = await import("./architect.js");
|
|
59
|
+
const runner = module.default;
|
|
60
|
+
|
|
61
|
+
// Use a unique temp dir with no user config (will fall back to default)
|
|
62
|
+
const noUserConfigDir = path.join(process.env.TEMP || "/tmp", `no_arch_user_config_${Date.now()}`);
|
|
63
|
+
fs.mkdirSync(noUserConfigDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
// Create a very large file that should trigger default max_lines rule
|
|
66
|
+
const tmpFile = path.join(noUserConfigDir, `large_${Date.now()}.ts`);
|
|
67
|
+
fs.writeFileSync(tmpFile, Array(5000).fill("// line").join("\n"));
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await runner.run(
|
|
71
|
+
createMockContext(tmpFile, "jsts", noUserConfigDir),
|
|
72
|
+
);
|
|
73
|
+
// Should use default config and find violations
|
|
74
|
+
expect(result.status).toBe("succeeded");
|
|
75
|
+
// Should have size violation from default config
|
|
76
|
+
expect(result.diagnostics.some((d) => d.message.includes("line limit"))).toBe(true);
|
|
77
|
+
} finally {
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
|
|
80
|
+
if (fs.existsSync(noUserConfigDir)) fs.rmdirSync(noUserConfigDir);
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should detect file size violations", async () => {
|
|
86
|
+
const module = await import("./architect.js");
|
|
87
|
+
const runner = module.default;
|
|
88
|
+
|
|
89
|
+
const tmpFile = path.join(testDir, `large_file_${Date.now()}.ts`);
|
|
90
|
+
// Create file with 100 lines (exceeds 50 line limit)
|
|
91
|
+
fs.writeFileSync(tmpFile, Array(100).fill("// line").join("\n"));
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
|
|
95
|
+
expect(result.status).toBe("succeeded");
|
|
96
|
+
expect(result.diagnostics.length).toBeGreaterThan(0);
|
|
97
|
+
expect(result.diagnostics.some((d) => d.message.includes("50 line limit"))).toBe(
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
100
|
+
} finally {
|
|
101
|
+
try {
|
|
102
|
+
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should detect pattern violations", async () => {
|
|
108
|
+
const module = await import("./architect.js");
|
|
109
|
+
const runner = module.default;
|
|
110
|
+
|
|
111
|
+
const tmpFile = path.join(testDir, `bad_patterns_${Date.now()}.ts`);
|
|
112
|
+
fs.writeFileSync(
|
|
113
|
+
tmpFile,
|
|
114
|
+
`const x = hardcoded_secret_12345;
|
|
115
|
+
console.log(x);
|
|
116
|
+
`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
|
|
121
|
+
expect(result.status).toBe("succeeded");
|
|
122
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
|
|
123
|
+
expect(
|
|
124
|
+
result.diagnostics.some((d) => d.message.includes("hardcoded")),
|
|
125
|
+
).toBe(true);
|
|
126
|
+
expect(
|
|
127
|
+
result.diagnostics.some((d) => d.message.includes("console.log")),
|
|
128
|
+
).toBe(true);
|
|
129
|
+
} finally {
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should return no diagnostics for clean files", async () => {
|
|
137
|
+
const module = await import("./architect.js");
|
|
138
|
+
const runner = module.default;
|
|
139
|
+
|
|
140
|
+
const tmpFile = path.join(testDir, `clean_${Date.now()}.ts`);
|
|
141
|
+
// Small file (20 lines) with no violations
|
|
142
|
+
fs.writeFileSync(tmpFile, Array(20).fill("// clean code").join("\n"));
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
|
|
146
|
+
expect(result.status).toBe("succeeded");
|
|
147
|
+
expect(result.diagnostics.length).toBe(0);
|
|
148
|
+
} finally {
|
|
149
|
+
try {
|
|
150
|
+
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should skip test files", async () => {
|
|
156
|
+
const module = await import("./architect.js");
|
|
157
|
+
const runner = module.default;
|
|
158
|
+
|
|
159
|
+
// The runner should have skipTestFiles: true
|
|
160
|
+
expect(runner.skipTestFiles).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -23,6 +23,7 @@ const architectRunner: RunnerDefinition = {
|
|
|
23
23
|
appliesTo: ["jsts", "python", "go", "rust", "cxx", "shell", "cmake"],
|
|
24
24
|
priority: 40,
|
|
25
25
|
enabledByDefault: true,
|
|
26
|
+
skipTestFiles: true, // Skip test files - rules can be noisy there
|
|
26
27
|
|
|
27
28
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
28
29
|
const relPath = ctx.filePath.replace(ctx.cwd, "").replace(/\\/g, "/");
|
|
@@ -44,15 +45,29 @@ const architectRunner: RunnerDefinition = {
|
|
|
44
45
|
// Check for violations
|
|
45
46
|
const violations = architectClient.checkFile(relPath, content);
|
|
46
47
|
for (const v of violations) {
|
|
48
|
+
// Build message with inline fix guidance
|
|
49
|
+
let message = v.message;
|
|
50
|
+
let fixSuggestion: string | undefined = v.fix;
|
|
51
|
+
|
|
52
|
+
if (v.fix) {
|
|
53
|
+
const fixPreview = v.fix.length > 60 ? `${v.fix.substring(0, 60)}...` : v.fix;
|
|
54
|
+
message += `\n💡 Suggested fix: ${fixPreview}`;
|
|
55
|
+
} else if (v.note) {
|
|
56
|
+
const notePreview = v.note.length > 80 ? `${v.note.substring(0, 80)}...` : v.note;
|
|
57
|
+
message += `\n📝 ${notePreview}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
diagnostics.push({
|
|
48
61
|
id: `architect-${v.line || 0}-${v.pattern}`,
|
|
49
|
-
message
|
|
62
|
+
message,
|
|
50
63
|
filePath: ctx.filePath,
|
|
51
64
|
line: v.line,
|
|
52
|
-
severity: "
|
|
53
|
-
semantic: "
|
|
65
|
+
severity: "warning",
|
|
66
|
+
semantic: "warning",
|
|
54
67
|
tool: "architect",
|
|
55
68
|
rule: v.pattern,
|
|
69
|
+
fixable: !!v.fix,
|
|
70
|
+
fixSuggestion,
|
|
56
71
|
});
|
|
57
72
|
}
|
|
58
73
|
|
|
@@ -64,8 +79,8 @@ const architectRunner: RunnerDefinition = {
|
|
|
64
79
|
id: `architect-size-${lineCount}`,
|
|
65
80
|
message: sizeViolation.message,
|
|
66
81
|
filePath: ctx.filePath,
|
|
67
|
-
severity: "
|
|
68
|
-
semantic: "
|
|
82
|
+
severity: "warning",
|
|
83
|
+
semantic: "warning",
|
|
69
84
|
tool: "architect",
|
|
70
85
|
rule: "file-size-limit",
|
|
71
86
|
fixSuggestion: "Split into smaller modules",
|
|
@@ -77,9 +92,9 @@ const architectRunner: RunnerDefinition = {
|
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
return {
|
|
80
|
-
status: "
|
|
95
|
+
status: "succeeded", // Warnings don't fail the run
|
|
81
96
|
diagnostics,
|
|
82
|
-
semantic: "
|
|
97
|
+
semantic: "warning",
|
|
83
98
|
};
|
|
84
99
|
},
|
|
85
100
|
};
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ast-grep NAPI runner for dispatch system
|
|
3
|
+
*
|
|
4
|
+
* Uses @ast-grep/napi for programmatic parsing instead of CLI.
|
|
5
|
+
* Handles TypeScript/JavaScript/CSS/HTML files with YAML rule support.
|
|
6
|
+
*
|
|
7
|
+
* Replaces CLI-based runners for faster performance (100x speedup).
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
// Lazy load the napi package
|
|
14
|
+
let sg;
|
|
15
|
+
async function loadSg() {
|
|
16
|
+
if (sg)
|
|
17
|
+
return sg;
|
|
18
|
+
try {
|
|
19
|
+
sg = await import("@ast-grep/napi");
|
|
20
|
+
return sg;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Supported extensions for NAPI
|
|
27
|
+
const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
|
|
28
|
+
function canHandle(filePath) {
|
|
29
|
+
return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
function getLang(filePath, sgModule) {
|
|
32
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
33
|
+
switch (ext) {
|
|
34
|
+
case ".ts": return sgModule.Lang.TypeScript;
|
|
35
|
+
case ".tsx": return sgModule.Lang.Tsx;
|
|
36
|
+
case ".js":
|
|
37
|
+
case ".jsx": return sgModule.Lang.JavaScript;
|
|
38
|
+
case ".css": return sgModule.Lang.Css;
|
|
39
|
+
case ".html":
|
|
40
|
+
case ".htm": return sgModule.Lang.Html;
|
|
41
|
+
default: return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function loadYamlRules(ruleDir) {
|
|
45
|
+
const rules = [];
|
|
46
|
+
if (!fs.existsSync(ruleDir))
|
|
47
|
+
return rules;
|
|
48
|
+
const files = fs.readdirSync(ruleDir).filter(f => f.endsWith(".yml"));
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(path.join(ruleDir, file), "utf-8");
|
|
52
|
+
// Split by --- to handle multiple YAML documents in one file
|
|
53
|
+
const documents = content.split(/^---$/m).filter(d => d.trim());
|
|
54
|
+
for (const doc of documents) {
|
|
55
|
+
const rule = parseSimpleYaml(doc.trim());
|
|
56
|
+
if (rule && rule.id) {
|
|
57
|
+
rules.push(rule);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Skip invalid files
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return rules;
|
|
66
|
+
}
|
|
67
|
+
function parseSimpleYaml(content) {
|
|
68
|
+
const lines = content.split("\n");
|
|
69
|
+
const rule = { id: "", metadata: {} };
|
|
70
|
+
let currentSection = "root";
|
|
71
|
+
let sectionStack = [];
|
|
72
|
+
let multilineBuffer = [];
|
|
73
|
+
let multilineKey = "";
|
|
74
|
+
function getCurrentObj() {
|
|
75
|
+
if (sectionStack.length === 0)
|
|
76
|
+
return rule;
|
|
77
|
+
return sectionStack[sectionStack.length - 1].obj;
|
|
78
|
+
}
|
|
79
|
+
function getIndent(line) {
|
|
80
|
+
let count = 0;
|
|
81
|
+
for (const char of line) {
|
|
82
|
+
if (char === " ")
|
|
83
|
+
count++;
|
|
84
|
+
else if (char === "\t")
|
|
85
|
+
count += 2;
|
|
86
|
+
else
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
return count;
|
|
90
|
+
}
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const line = lines[i];
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
95
|
+
continue;
|
|
96
|
+
if (trimmed === "---")
|
|
97
|
+
continue;
|
|
98
|
+
const indent = getIndent(line);
|
|
99
|
+
// Pop stack if indent decreased
|
|
100
|
+
while (sectionStack.length > 0 && indent <= sectionStack[sectionStack.length - 1].indent) {
|
|
101
|
+
sectionStack.pop();
|
|
102
|
+
}
|
|
103
|
+
// Check for multiline continuation
|
|
104
|
+
if (line.startsWith(" ") && !trimmed.includes(":") && multilineKey) {
|
|
105
|
+
multilineBuffer.push(trimmed);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Flush multiline buffer
|
|
109
|
+
if (multilineKey && multilineBuffer.length > 0) {
|
|
110
|
+
const value = multilineBuffer.join("\n");
|
|
111
|
+
const current = getCurrentObj();
|
|
112
|
+
if (multilineKey === "pattern" && current) {
|
|
113
|
+
current.pattern = value;
|
|
114
|
+
}
|
|
115
|
+
multilineKey = "";
|
|
116
|
+
multilineBuffer = [];
|
|
117
|
+
}
|
|
118
|
+
const colonIndex = trimmed.indexOf(":");
|
|
119
|
+
const key = colonIndex > 0 ? trimmed.substring(0, colonIndex).trim() : trimmed;
|
|
120
|
+
const value = colonIndex > 0 ? trimmed.substring(colonIndex + 1).trim() : "";
|
|
121
|
+
if (key === "id") {
|
|
122
|
+
rule.id = value.replace(/^["']|["']$/g, "");
|
|
123
|
+
}
|
|
124
|
+
else if (key === "language") {
|
|
125
|
+
rule.language = value;
|
|
126
|
+
}
|
|
127
|
+
else if (key === "severity") {
|
|
128
|
+
rule.severity = value;
|
|
129
|
+
}
|
|
130
|
+
else if (key === "message") {
|
|
131
|
+
if (value === "|") {
|
|
132
|
+
multilineKey = "message";
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
rule.message = value.replace(/^["']|["']$/g, "");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (key === "metadata") {
|
|
139
|
+
currentSection = "metadata";
|
|
140
|
+
const newObj = {};
|
|
141
|
+
rule.metadata = newObj;
|
|
142
|
+
sectionStack.push({ name: "metadata", indent, obj: newObj });
|
|
143
|
+
}
|
|
144
|
+
else if (key === "rule") {
|
|
145
|
+
currentSection = "rule";
|
|
146
|
+
const newObj = {};
|
|
147
|
+
rule.rule = newObj;
|
|
148
|
+
sectionStack.push({ name: "rule", indent, obj: newObj });
|
|
149
|
+
}
|
|
150
|
+
else if (sectionStack.length > 0) {
|
|
151
|
+
const current = getCurrentObj();
|
|
152
|
+
const currentSectionName = sectionStack[sectionStack.length - 1]?.name;
|
|
153
|
+
if (key === "weight" && currentSectionName === "metadata") {
|
|
154
|
+
if (!rule.metadata)
|
|
155
|
+
rule.metadata = {};
|
|
156
|
+
rule.metadata.weight = parseInt(value, 10) || 3;
|
|
157
|
+
}
|
|
158
|
+
else if (key === "category" && currentSectionName === "metadata") {
|
|
159
|
+
if (!rule.metadata)
|
|
160
|
+
rule.metadata = {};
|
|
161
|
+
rule.metadata.category = value.replace(/^["']|["']$/g, "");
|
|
162
|
+
}
|
|
163
|
+
else if (key === "pattern") {
|
|
164
|
+
if (value === "|") {
|
|
165
|
+
multilineKey = "pattern";
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Strip all surrounding quotes (handle nested quotes from YAML)
|
|
169
|
+
let stripped = value;
|
|
170
|
+
while (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length > 1) {
|
|
171
|
+
stripped = stripped.slice(1, -1);
|
|
172
|
+
}
|
|
173
|
+
while (stripped.startsWith("'") && stripped.endsWith("'") && stripped.length > 1) {
|
|
174
|
+
stripped = stripped.slice(1, -1);
|
|
175
|
+
}
|
|
176
|
+
current.pattern = stripped;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (key === "kind") {
|
|
180
|
+
current.kind = value;
|
|
181
|
+
}
|
|
182
|
+
else if (key === "regex") {
|
|
183
|
+
// Strip all surrounding quotes
|
|
184
|
+
let stripped = value;
|
|
185
|
+
while (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length > 1) {
|
|
186
|
+
stripped = stripped.slice(1, -1);
|
|
187
|
+
}
|
|
188
|
+
while (stripped.startsWith("'") && stripped.endsWith("'") && stripped.length > 1) {
|
|
189
|
+
stripped = stripped.slice(1, -1);
|
|
190
|
+
}
|
|
191
|
+
current.regex = stripped;
|
|
192
|
+
}
|
|
193
|
+
else if (key === "has" || key === "not") {
|
|
194
|
+
const newObj = {};
|
|
195
|
+
current[key] = newObj;
|
|
196
|
+
sectionStack.push({ name: key, indent, obj: newObj });
|
|
197
|
+
}
|
|
198
|
+
else if (key === "any" || key === "all") {
|
|
199
|
+
if (!current[key])
|
|
200
|
+
current[key] = [];
|
|
201
|
+
// Check if next lines with more indent are list items
|
|
202
|
+
let j = i + 1;
|
|
203
|
+
while (j < lines.length) {
|
|
204
|
+
const nextLine = lines[j];
|
|
205
|
+
const nextTrimmed = nextLine.trim();
|
|
206
|
+
if (!nextTrimmed || nextTrimmed.startsWith("#")) {
|
|
207
|
+
j++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const nextIndent = getIndent(nextLine);
|
|
211
|
+
if (nextIndent <= indent)
|
|
212
|
+
break;
|
|
213
|
+
if (nextTrimmed.startsWith("- ")) {
|
|
214
|
+
// New list item
|
|
215
|
+
const itemObj = {};
|
|
216
|
+
current[key].push(itemObj);
|
|
217
|
+
sectionStack.push({ name: key, indent: nextIndent, obj: itemObj });
|
|
218
|
+
// Parse the item content after "- "
|
|
219
|
+
const itemContent = nextTrimmed.substring(2);
|
|
220
|
+
if (itemContent.includes(":")) {
|
|
221
|
+
const [itemKey, itemVal] = itemContent.split(":", 2);
|
|
222
|
+
if (itemKey.trim() === "pattern") {
|
|
223
|
+
itemObj.pattern = itemVal.trim().replace(/^["']|["']$/g, "");
|
|
224
|
+
}
|
|
225
|
+
else if (itemKey.trim() === "kind") {
|
|
226
|
+
itemObj.kind = itemVal.trim();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (itemContent) {
|
|
230
|
+
// Assume it's a pattern
|
|
231
|
+
itemObj.pattern = itemContent.replace(/^["']|["']$/g, "");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
j++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Flush remaining multiline buffer
|
|
240
|
+
if (multilineKey && multilineBuffer.length > 0) {
|
|
241
|
+
const value = multilineBuffer.join("\n");
|
|
242
|
+
const current = getCurrentObj();
|
|
243
|
+
if (multilineKey === "pattern" && current) {
|
|
244
|
+
current.pattern = value;
|
|
245
|
+
}
|
|
246
|
+
else if (multilineKey === "message") {
|
|
247
|
+
rule.message = value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return rule.id ? rule : null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if a rule uses structured conditions (has/any/all/not/regex)
|
|
254
|
+
*/
|
|
255
|
+
function isStructuredRule(rule) {
|
|
256
|
+
if (!rule.rule)
|
|
257
|
+
return false;
|
|
258
|
+
return !!(rule.rule.has || rule.rule.any || rule.rule.all || rule.rule.not || rule.rule.regex);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Execute a structured rule using manual AST traversal
|
|
262
|
+
*/
|
|
263
|
+
function executeStructuredRule(rootNode, condition, matches = []) {
|
|
264
|
+
// Start with finding nodes by kind or pattern
|
|
265
|
+
let candidates = [];
|
|
266
|
+
if (condition.pattern) {
|
|
267
|
+
// Use pattern matching via findAll
|
|
268
|
+
try {
|
|
269
|
+
candidates = rootNode.findAll(condition.pattern);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return matches;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else if (condition.kind) {
|
|
276
|
+
// Manual traversal for kind matching
|
|
277
|
+
function findByKind(node, kind) {
|
|
278
|
+
const results = [];
|
|
279
|
+
if (node.kind() === kind) {
|
|
280
|
+
results.push(node);
|
|
281
|
+
}
|
|
282
|
+
for (const child of node.children()) {
|
|
283
|
+
results.push(...findByKind(child, kind));
|
|
284
|
+
}
|
|
285
|
+
return results;
|
|
286
|
+
}
|
|
287
|
+
candidates = findByKind(rootNode, condition.kind);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// No kind or pattern, search all nodes
|
|
291
|
+
function getAllNodes(node) {
|
|
292
|
+
const results = [node];
|
|
293
|
+
for (const child of node.children()) {
|
|
294
|
+
results.push(...getAllNodes(child));
|
|
295
|
+
}
|
|
296
|
+
return results;
|
|
297
|
+
}
|
|
298
|
+
candidates = getAllNodes(rootNode);
|
|
299
|
+
}
|
|
300
|
+
// Filter candidates by conditions
|
|
301
|
+
for (const candidate of candidates) {
|
|
302
|
+
let matchesCondition = true;
|
|
303
|
+
// Check 'has' condition
|
|
304
|
+
if (condition.has && matchesCondition) {
|
|
305
|
+
const subMatches = executeStructuredRule(candidate, condition.has, []);
|
|
306
|
+
if (subMatches.length === 0)
|
|
307
|
+
matchesCondition = false;
|
|
308
|
+
}
|
|
309
|
+
// Check 'not' condition
|
|
310
|
+
if (condition.not && matchesCondition) {
|
|
311
|
+
const subMatches = executeStructuredRule(candidate, condition.not, []);
|
|
312
|
+
if (subMatches.length > 0)
|
|
313
|
+
matchesCondition = false;
|
|
314
|
+
}
|
|
315
|
+
// Check 'any' condition (at least one must match)
|
|
316
|
+
if (condition.any && matchesCondition) {
|
|
317
|
+
let anyMatches = false;
|
|
318
|
+
for (const subCondition of condition.any) {
|
|
319
|
+
const subMatches = executeStructuredRule(candidate, subCondition, []);
|
|
320
|
+
if (subMatches.length > 0) {
|
|
321
|
+
anyMatches = true;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!anyMatches)
|
|
326
|
+
matchesCondition = false;
|
|
327
|
+
}
|
|
328
|
+
// Check 'all' condition (all must match)
|
|
329
|
+
if (condition.all && matchesCondition) {
|
|
330
|
+
for (const subCondition of condition.all) {
|
|
331
|
+
const subMatches = executeStructuredRule(candidate, subCondition, []);
|
|
332
|
+
if (subMatches.length === 0) {
|
|
333
|
+
matchesCondition = false;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Check 'regex' condition
|
|
339
|
+
if (condition.regex && matchesCondition) {
|
|
340
|
+
const text = candidate.text();
|
|
341
|
+
const regex = new RegExp(condition.regex);
|
|
342
|
+
if (!regex.test(text))
|
|
343
|
+
matchesCondition = false;
|
|
344
|
+
}
|
|
345
|
+
if (matchesCondition) {
|
|
346
|
+
matches.push(candidate);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return matches;
|
|
350
|
+
}
|
|
351
|
+
const astGrepNapiRunner = {
|
|
352
|
+
id: "ast-grep-napi",
|
|
353
|
+
appliesTo: ["jsts"], // TypeScript/JavaScript only
|
|
354
|
+
priority: 15, // Run early (after type checkers, before other linters)
|
|
355
|
+
enabledByDefault: true,
|
|
356
|
+
skipTestFiles: true,
|
|
357
|
+
async run(ctx) {
|
|
358
|
+
const startTime = Date.now();
|
|
359
|
+
if (!canHandle(ctx.filePath)) {
|
|
360
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
361
|
+
}
|
|
362
|
+
const sgModule = await loadSg();
|
|
363
|
+
if (!sgModule) {
|
|
364
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
365
|
+
}
|
|
366
|
+
if (!fs.existsSync(ctx.filePath)) {
|
|
367
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
368
|
+
}
|
|
369
|
+
const lang = getLang(ctx.filePath, sgModule);
|
|
370
|
+
if (!lang) {
|
|
371
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
372
|
+
}
|
|
373
|
+
const content = fs.readFileSync(ctx.filePath, "utf-8");
|
|
374
|
+
let root;
|
|
375
|
+
try {
|
|
376
|
+
root = sgModule.parse(lang, content);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
380
|
+
}
|
|
381
|
+
const diagnostics = [];
|
|
382
|
+
const rootNode = root.root();
|
|
383
|
+
// Load rules from ts-slop-rules only (complementary to ast-grep CLI)
|
|
384
|
+
const ruleDirs = [
|
|
385
|
+
path.join(process.cwd(), "rules/ts-slop-rules/rules"),
|
|
386
|
+
];
|
|
387
|
+
for (const ruleDir of ruleDirs) {
|
|
388
|
+
const rules = loadYamlRules(ruleDir);
|
|
389
|
+
for (const rule of rules) {
|
|
390
|
+
// Skip rules for different languages (case-insensitive)
|
|
391
|
+
const lang = rule.language?.toLowerCase();
|
|
392
|
+
if (lang && lang !== "typescript" && lang !== "javascript") {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
let matches = [];
|
|
397
|
+
if (isStructuredRule(rule) && rule.rule) {
|
|
398
|
+
// Use structured rule execution
|
|
399
|
+
matches = executeStructuredRule(rootNode, rule.rule, []);
|
|
400
|
+
}
|
|
401
|
+
else if (rule.rule?.pattern || rule.rule?.kind) {
|
|
402
|
+
// Use simple pattern matching
|
|
403
|
+
const pattern = rule.rule.pattern || rule.rule.kind;
|
|
404
|
+
if (pattern) {
|
|
405
|
+
try {
|
|
406
|
+
matches = rootNode.findAll(pattern);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// Pattern failed, try manual traversal for kind
|
|
410
|
+
if (rule.rule.kind) {
|
|
411
|
+
function findByKind(node, kind) {
|
|
412
|
+
const results = [];
|
|
413
|
+
if (node.kind() === kind)
|
|
414
|
+
results.push(node);
|
|
415
|
+
for (const child of node.children()) {
|
|
416
|
+
results.push(...findByKind(child, kind));
|
|
417
|
+
}
|
|
418
|
+
return results;
|
|
419
|
+
}
|
|
420
|
+
matches = findByKind(rootNode, rule.rule.kind);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
for (const match of matches) {
|
|
426
|
+
const range = match.range();
|
|
427
|
+
const weight = rule.metadata?.weight || 3;
|
|
428
|
+
const severity = weight >= 4 ? "error" : "warning";
|
|
429
|
+
diagnostics.push({
|
|
430
|
+
id: `ast-grep-napi-${range.start.line}-${rule.id}`,
|
|
431
|
+
message: `[${rule.metadata?.category || "slop"}] ${rule.message || rule.id}`,
|
|
432
|
+
filePath: ctx.filePath,
|
|
433
|
+
line: range.start.line + 1,
|
|
434
|
+
column: range.start.column + 1,
|
|
435
|
+
severity,
|
|
436
|
+
semantic: severity === "error" ? "blocking" : "warning",
|
|
437
|
+
tool: "ast-grep-napi",
|
|
438
|
+
rule: rule.id,
|
|
439
|
+
fixable: false,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Rule failed, skip
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const elapsed = Date.now() - startTime;
|
|
449
|
+
if (diagnostics.length > 0 || elapsed > 50) {
|
|
450
|
+
console.error(`[ast-grep-napi] ${ctx.filePath}: ${elapsed}ms, ${diagnostics.length} issues`);
|
|
451
|
+
}
|
|
452
|
+
if (diagnostics.length === 0) {
|
|
453
|
+
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
status: "failed",
|
|
457
|
+
diagnostics,
|
|
458
|
+
semantic: "warning",
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
export default astGrepNapiRunner;
|