pi-lens 3.6.2 → 3.6.4
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 +10 -2
- package/package.json +4 -4
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/file-time.test.ts +0 -276
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/format-service.test.ts +0 -339
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/__tests__/formatters.test.ts +0 -401
- package/clients/agent-behavior-client.js +0 -110
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/agent-behavior-client.test.ts +0 -116
- package/clients/amain-types.js +0 -164
- package/clients/architect-client.js +0 -291
- package/clients/ast-grep-client.js +0 -253
- package/clients/ast-grep-parser.js +0 -84
- package/clients/ast-grep-rule-manager.js +0 -89
- package/clients/ast-grep-types.js +0 -9
- package/clients/auto-loop.js +0 -131
- package/clients/biome-client.js +0 -420
- package/clients/biome-client.test.js +0 -144
- package/clients/biome-client.test.ts +0 -163
- package/clients/cache/rule-cache.js +0 -72
- package/clients/cache-manager.js +0 -245
- package/clients/cache-manager.test.js +0 -197
- package/clients/cache-manager.test.ts +0 -299
- package/clients/complexity-client.js +0 -675
- package/clients/complexity-client.test.js +0 -234
- package/clients/complexity-client.test.ts +0 -255
- package/clients/config-validator.js +0 -465
- package/clients/dependency-checker.js +0 -325
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dependency-checker.test.ts +0 -71
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
- package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
- package/clients/dispatch/debug.log +0 -1
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.edge.test.ts +0 -100
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.format.test.ts +0 -58
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.inline.test.ts +0 -93
- package/clients/dispatch/dispatcher.js +0 -381
- package/clients/dispatch/dispatcher.test.js +0 -116
- package/clients/dispatch/dispatcher.test.ts +0 -149
- package/clients/dispatch/integration.js +0 -108
- package/clients/dispatch/plan.js +0 -183
- package/clients/dispatch/runners/architect.js +0 -83
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/architect.test.ts +0 -162
- package/clients/dispatch/runners/ast-grep-napi.js +0 -405
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
- package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
- package/clients/dispatch/runners/ast-grep.js +0 -157
- package/clients/dispatch/runners/biome.js +0 -55
- package/clients/dispatch/runners/config-validation.js +0 -67
- package/clients/dispatch/runners/go-vet.js +0 -48
- package/clients/dispatch/runners/index.js +0 -47
- package/clients/dispatch/runners/lsp.js +0 -102
- package/clients/dispatch/runners/oxlint.js +0 -67
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/oxlint.test.ts +0 -303
- package/clients/dispatch/runners/pyright.js +0 -100
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/pyright.test.ts +0 -121
- package/clients/dispatch/runners/python-slop.js +0 -97
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/python-slop.test.ts +0 -298
- package/clients/dispatch/runners/ruff.js +0 -48
- package/clients/dispatch/runners/rust-clippy.js +0 -102
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
- package/clients/dispatch/runners/shellcheck.js +0 -147
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/shellcheck.test.ts +0 -129
- package/clients/dispatch/runners/similarity.js +0 -230
- package/clients/dispatch/runners/spellcheck.js +0 -106
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/spellcheck.test.ts +0 -214
- package/clients/dispatch/runners/tree-sitter.js +0 -246
- package/clients/dispatch/runners/ts-lsp.js +0 -125
- package/clients/dispatch/runners/ts-slop.js +0 -113
- package/clients/dispatch/runners/type-safety.js +0 -142
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
- package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
- package/clients/dispatch/runners/utils.js +0 -51
- package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
- package/clients/dispatch/types.js +0 -16
- package/clients/dispatch/utils/format-utils.js +0 -44
- package/clients/dogfood.test.js +0 -201
- package/clients/dogfood.test.ts +0 -269
- package/clients/file-kinds.js +0 -177
- package/clients/file-kinds.test.js +0 -169
- package/clients/file-kinds.test.ts +0 -210
- package/clients/file-time.js +0 -152
- package/clients/file-utils.js +0 -40
- package/clients/fix-scanners.js +0 -204
- package/clients/format-service.js +0 -184
- package/clients/formatters.js +0 -488
- package/clients/go-client.js +0 -203
- package/clients/go-client.test.js +0 -127
- package/clients/go-client.test.ts +0 -143
- package/clients/installer/index.js +0 -403
- package/clients/interviewer-templates.js +0 -75
- package/clients/interviewer.js +0 -173
- package/clients/jscpd-client.js +0 -196
- package/clients/jscpd-client.test.js +0 -127
- package/clients/jscpd-client.test.ts +0 -145
- package/clients/knip-client.js +0 -239
- package/clients/knip-client.test.js +0 -112
- package/clients/knip-client.test.ts +0 -128
- package/clients/latency-logger.js +0 -40
- package/clients/lsp/__tests__/client.test.js +0 -310
- package/clients/lsp/__tests__/client.test.ts +0 -412
- package/clients/lsp/__tests__/config.test.js +0 -167
- package/clients/lsp/__tests__/config.test.ts +0 -217
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/integration.test.ts +0 -160
- package/clients/lsp/__tests__/launch.test.js +0 -313
- package/clients/lsp/__tests__/launch.test.ts +0 -394
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/server.test.ts +0 -332
- package/clients/lsp/__tests__/service.test.js +0 -438
- package/clients/lsp/__tests__/service.test.ts +0 -530
- package/clients/lsp/client.js +0 -350
- package/clients/lsp/config.js +0 -112
- package/clients/lsp/index.js +0 -318
- package/clients/lsp/installer/index.js +0 -391
- package/clients/lsp/interactive-install.js +0 -221
- package/clients/lsp/language.js +0 -170
- package/clients/lsp/launch.js +0 -329
- package/clients/lsp/lsp/launch.js +0 -116
- package/clients/lsp/lsp/server.js +0 -532
- package/clients/lsp/lsp-index.js +0 -10
- package/clients/lsp/path-utils.js +0 -5
- package/clients/lsp/server.js +0 -725
- package/clients/lsp/test-py-spawn/requirements.txt +0 -1
- package/clients/lsp/test-py-spawn/test.py +0 -3
- package/clients/lsp/test-py-svc/requirements.txt +0 -1
- package/clients/lsp/test-py-svc/test.py +0 -3
- package/clients/lsp/test-python-project/requirements.txt +0 -1
- package/clients/lsp/test-python-project/test.py +0 -5
- package/clients/metrics-client.js +0 -107
- package/clients/metrics-client.test.js +0 -128
- package/clients/metrics-client.test.ts +0 -163
- package/clients/metrics-history.js +0 -367
- package/clients/path-utils.js +0 -142
- package/clients/pipeline.js +0 -272
- package/clients/production-readiness.js +0 -522
- package/clients/project-index.js +0 -255
- package/clients/project-metadata.js +0 -531
- package/clients/ruff-client.js +0 -325
- package/clients/ruff-client.test.js +0 -132
- package/clients/ruff-client.test.ts +0 -153
- package/clients/rules-scanner.js +0 -97
- package/clients/runner-tracker.js +0 -152
- package/clients/rust-client.js +0 -205
- package/clients/rust-client.test.js +0 -108
- package/clients/rust-client.test.ts +0 -130
- package/clients/safe-spawn-async.js +0 -163
- package/clients/safe-spawn.js +0 -241
- package/clients/sanitize.js +0 -291
- package/clients/sanitize.test.js +0 -177
- package/clients/sanitize.test.ts +0 -223
- package/clients/scan-architectural-debt.js +0 -167
- package/clients/scan-utils.js +0 -83
- package/clients/secrets-scanner.js +0 -119
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/secrets-scanner.test.ts +0 -113
- package/clients/sg-runner.js +0 -292
- package/clients/state-matrix.js +0 -160
- package/clients/subprocess-client.js +0 -65
- package/clients/symbol-types.js +0 -5
- package/clients/test-runner-client.js +0 -523
- package/clients/test-runner-client.test.js +0 -192
- package/clients/test-runner-client.test.ts +0 -253
- package/clients/test-utils.js +0 -27
- package/clients/test-utils.ts +0 -36
- package/clients/todo-scanner.js +0 -200
- package/clients/todo-scanner.test.js +0 -301
- package/clients/todo-scanner.test.ts +0 -352
- package/clients/tool-availability.js +0 -207
- package/clients/tree-sitter-client.js +0 -601
- package/clients/tree-sitter-query-loader.js +0 -355
- package/clients/tree-sitter-symbol-extractor.js +0 -289
- package/clients/ts-service.js +0 -129
- package/clients/type-coverage-client.js +0 -127
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/type-coverage-client.test.ts +0 -125
- package/clients/type-safety-client.js +0 -138
- package/clients/types.js +0 -11
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.codefix.test.ts +0 -186
- package/clients/typescript-client.js +0 -509
- package/clients/typescript-client.test.js +0 -105
- package/clients/typescript-client.test.ts +0 -126
- package/commands/booboo.js +0 -1007
- package/commands/fix-from-booboo.js +0 -398
- package/commands/fix-simplified.js +0 -618
- package/commands/rate.js +0 -281
- package/commands/rate.test.js +0 -119
- package/commands/rate.test.ts +0 -131
- package/commands/refactor.js +0 -130
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Architect Client for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* Loads path-based architectural rules from .pi-lens/architect.yaml
|
|
5
|
-
* and checks file paths against them.
|
|
6
|
-
*
|
|
7
|
-
* Provides:
|
|
8
|
-
* - Pre-write hints: what rules apply before the agent edits
|
|
9
|
-
* - Post-write validation: check for violations after edits
|
|
10
|
-
*/
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
import { minimatch } from "minimatch";
|
|
14
|
-
// --- Client ---
|
|
15
|
-
export class ArchitectClient {
|
|
16
|
-
constructor(verbose = false) {
|
|
17
|
-
this.config = null;
|
|
18
|
-
this.isUserConfig = false;
|
|
19
|
-
this.log = verbose
|
|
20
|
-
? (msg) => console.error(`[architect] ${msg}`)
|
|
21
|
-
: () => { };
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Load architect config from project root.
|
|
25
|
-
* Falls back to built-in default if no user config exists.
|
|
26
|
-
*/
|
|
27
|
-
loadConfig(projectRoot) {
|
|
28
|
-
// Try user config locations first
|
|
29
|
-
const userCandidates = [
|
|
30
|
-
path.join(projectRoot, ".pi-lens", "architect.yaml"),
|
|
31
|
-
path.join(projectRoot, "architect.yaml"),
|
|
32
|
-
path.join(projectRoot, ".pi-lens", "architect.yml"),
|
|
33
|
-
];
|
|
34
|
-
for (const configPath of userCandidates) {
|
|
35
|
-
try {
|
|
36
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
37
|
-
this.config = this.parseYaml(content);
|
|
38
|
-
this.configPath = configPath;
|
|
39
|
-
this.isUserConfig = true;
|
|
40
|
-
this.log(`Loaded user architect config from ${configPath}`);
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
this.log(`Could not read ${configPath}: ${error}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Fall back to built-in default
|
|
48
|
-
try {
|
|
49
|
-
// Try multiple possible locations for the default config
|
|
50
|
-
const possibleDefaultPaths = [
|
|
51
|
-
path.join(projectRoot, "default-architect.yaml"),
|
|
52
|
-
path.join(projectRoot, "..", "default-architect.yaml"),
|
|
53
|
-
path.join(process.cwd(), "default-architect.yaml"),
|
|
54
|
-
];
|
|
55
|
-
// Handle both CommonJS and ESM environments
|
|
56
|
-
if (typeof __dirname !== "undefined") {
|
|
57
|
-
possibleDefaultPaths.push(path.join(__dirname, "..", "default-architect.yaml"));
|
|
58
|
-
possibleDefaultPaths.push(path.join(__dirname, "..", "..", "default-architect.yaml"));
|
|
59
|
-
}
|
|
60
|
-
for (const defaultPath of possibleDefaultPaths) {
|
|
61
|
-
try {
|
|
62
|
-
const content = fs.readFileSync(defaultPath, "utf-8");
|
|
63
|
-
this.config = this.parseYaml(content);
|
|
64
|
-
this.configPath = defaultPath;
|
|
65
|
-
this.isUserConfig = false;
|
|
66
|
-
this.log("Using default architect rules (create .pi-lens/architect.yaml to customize)");
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// Try next path
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
this.log("No architect config available");
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
this.log("No architect config available");
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Check if the loaded config is user-defined (not default)
|
|
83
|
-
*/
|
|
84
|
-
isUserDefined() {
|
|
85
|
-
return this.isUserConfig;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Check if config is loaded
|
|
89
|
-
*/
|
|
90
|
-
hasConfig() {
|
|
91
|
-
return this.config !== null;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Get rules that apply to a file path
|
|
95
|
-
*/
|
|
96
|
-
getRulesForFile(filePath) {
|
|
97
|
-
if (!this.config)
|
|
98
|
-
return [];
|
|
99
|
-
const matched = [];
|
|
100
|
-
for (const rule of this.config.rules) {
|
|
101
|
-
if (minimatch(filePath, rule.pattern, { matchBase: true })) {
|
|
102
|
-
matched.push(rule);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return matched;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Check code content against rules for a file path
|
|
109
|
-
* Returns violations found
|
|
110
|
-
*/
|
|
111
|
-
checkFile(filePath, content) {
|
|
112
|
-
const rules = this.getRulesForFile(filePath);
|
|
113
|
-
const violations = [];
|
|
114
|
-
for (const rule of rules) {
|
|
115
|
-
if (!rule.must_not)
|
|
116
|
-
continue;
|
|
117
|
-
for (const check of rule.must_not) {
|
|
118
|
-
// We use 'g' to find all occurrences and correctly report line numbers
|
|
119
|
-
const regex = new RegExp(check.pattern, "gi");
|
|
120
|
-
let match;
|
|
121
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: RegExp.exec iteration
|
|
122
|
-
while ((match = regex.exec(content)) !== null) {
|
|
123
|
-
// Convert index to line number
|
|
124
|
-
const lineNum = content.slice(0, match.index).split("\n").length;
|
|
125
|
-
violations.push({
|
|
126
|
-
pattern: rule.pattern,
|
|
127
|
-
message: check.message,
|
|
128
|
-
line: lineNum,
|
|
129
|
-
fix: check.fix,
|
|
130
|
-
note: check.note,
|
|
131
|
-
});
|
|
132
|
-
// Prevent infinite loop on empty matches
|
|
133
|
-
if (match.index === regex.lastIndex) {
|
|
134
|
-
regex.lastIndex++;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return violations;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Check file size against max_lines rule
|
|
143
|
-
* Returns violation if file exceeds the limit
|
|
144
|
-
*/
|
|
145
|
-
checkFileSize(filePath, lineCount) {
|
|
146
|
-
const rules = this.getRulesForFile(filePath);
|
|
147
|
-
for (const rule of rules) {
|
|
148
|
-
if (rule.max_lines && lineCount > rule.max_lines) {
|
|
149
|
-
return {
|
|
150
|
-
pattern: rule.pattern,
|
|
151
|
-
message: `File is ${lineCount} lines — exceeds ${rule.max_lines} line limit. Split into smaller modules.`,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Get pre-write hints for a file path
|
|
159
|
-
* Returns rules that will apply to the file being written
|
|
160
|
-
*/
|
|
161
|
-
getHints(filePath) {
|
|
162
|
-
const rules = this.getRulesForFile(filePath);
|
|
163
|
-
const hints = [];
|
|
164
|
-
for (const rule of rules) {
|
|
165
|
-
if (rule.must_not) {
|
|
166
|
-
for (const check of rule.must_not) {
|
|
167
|
-
hints.push(check.message);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (rule.must) {
|
|
171
|
-
for (const req of rule.must) {
|
|
172
|
-
hints.push(`Must: ${req}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return hints;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Simple YAML parser for architect.yaml format
|
|
180
|
-
* Handles the specific structure we need
|
|
181
|
-
*/
|
|
182
|
-
parseYaml(content) {
|
|
183
|
-
const config = { rules: [] };
|
|
184
|
-
// Split into top-level rule blocks (4-space indent "- pattern:")
|
|
185
|
-
const ruleBlocks = content.split(/(?=^ {2}- pattern:)/m);
|
|
186
|
-
for (const block of ruleBlocks) {
|
|
187
|
-
const lines = block.split("\n");
|
|
188
|
-
let rule = null;
|
|
189
|
-
let section = null;
|
|
190
|
-
let violation = null;
|
|
191
|
-
for (const line of lines) {
|
|
192
|
-
const trimmed = line.trim();
|
|
193
|
-
if (trimmed.startsWith("#") || !trimmed)
|
|
194
|
-
continue;
|
|
195
|
-
// Version (top-level)
|
|
196
|
-
if (trimmed.startsWith("version:") && !rule) {
|
|
197
|
-
config.version = trimmed.split(":")[1]?.trim().replace(/['"]/g, "");
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
// Rule pattern
|
|
201
|
-
const ruleMatch = trimmed.match(/^-?\s*pattern:\s*["'](.+?)["']/);
|
|
202
|
-
if (ruleMatch && trimmed.startsWith("-") && !section) {
|
|
203
|
-
rule = { pattern: ruleMatch[1], must_not: [], must: [] };
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
// Nested pattern inside must_not (may start with "- ")
|
|
207
|
-
if ((trimmed.startsWith("pattern:") ||
|
|
208
|
-
trimmed.startsWith("- pattern:")) &&
|
|
209
|
-
section === "must_not") {
|
|
210
|
-
// Extract everything after "pattern:" and unquote
|
|
211
|
-
const raw = trimmed.replace(/^-?\s*pattern:\s*/, "").trim();
|
|
212
|
-
const unquoted = raw.replace(/^["']|["']$/g, "");
|
|
213
|
-
if (unquoted) {
|
|
214
|
-
violation = { pattern: unquoted, message: "" };
|
|
215
|
-
}
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
// Section headers
|
|
219
|
-
if (trimmed === "must_not:" || trimmed.startsWith("must_not:")) {
|
|
220
|
-
section = "must_not";
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (trimmed === "must:") {
|
|
224
|
-
section = "must";
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
// Message for current violation (handle nested quotes)
|
|
228
|
-
if (trimmed.startsWith("message:") && violation) {
|
|
229
|
-
// Match "..." or '...' allowing the other quote type inside
|
|
230
|
-
const dquoteMatch = trimmed.match(/message:\s*"([^"]*)"/);
|
|
231
|
-
const squoteMatch = !dquoteMatch
|
|
232
|
-
? trimmed.match(/message:\s*'([^']*)'/)
|
|
233
|
-
: null;
|
|
234
|
-
const match = dquoteMatch || squoteMatch;
|
|
235
|
-
if (match) {
|
|
236
|
-
violation.message = match[1];
|
|
237
|
-
if (rule) {
|
|
238
|
-
rule.must_not = rule.must_not ?? [];
|
|
239
|
-
rule.must_not.push(violation);
|
|
240
|
-
}
|
|
241
|
-
violation = null;
|
|
242
|
-
}
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
// Fix guidance for current violation
|
|
246
|
-
if (trimmed.startsWith("fix:") && violation) {
|
|
247
|
-
const dquoteMatch = trimmed.match(/fix:\s*"([^"]*)"/);
|
|
248
|
-
const squoteMatch = !dquoteMatch
|
|
249
|
-
? trimmed.match(/fix:\s*'([^']*)'/)
|
|
250
|
-
: null;
|
|
251
|
-
const match = dquoteMatch || squoteMatch;
|
|
252
|
-
if (match) {
|
|
253
|
-
violation.fix = match[1];
|
|
254
|
-
}
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
// Note guidance for current violation
|
|
258
|
-
if (trimmed.startsWith("note:") && violation) {
|
|
259
|
-
const dquoteMatch = trimmed.match(/note:\s*"([^"]*)"/);
|
|
260
|
-
const squoteMatch = !dquoteMatch
|
|
261
|
-
? trimmed.match(/note:\s*'([^']*)'/)
|
|
262
|
-
: null;
|
|
263
|
-
const match = dquoteMatch || squoteMatch;
|
|
264
|
-
if (match) {
|
|
265
|
-
violation.note = match[1];
|
|
266
|
-
}
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
// Must items (simple strings)
|
|
270
|
-
if (section === "must" && trimmed.startsWith("- ") && rule) {
|
|
271
|
-
const item = trimmed.slice(2).replace(/^["']|["']$/g, "");
|
|
272
|
-
rule.must = rule.must ?? [];
|
|
273
|
-
rule.must.push(item);
|
|
274
|
-
}
|
|
275
|
-
// max_lines setting
|
|
276
|
-
if (trimmed.startsWith("max_lines:") && rule) {
|
|
277
|
-
const num = parseInt(trimmed.split(":")[1]?.trim(), 10);
|
|
278
|
-
if (!Number.isNaN(num)) {
|
|
279
|
-
rule.max_lines = num;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (rule) {
|
|
284
|
-
config.rules.push(rule);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return config;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
// --- Singleton ---
|
|
291
|
-
const _instance = null;
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AstGrep Client for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* Structural code analysis using ast-grep CLI.
|
|
5
|
-
* Scans files against YAML rule definitions.
|
|
6
|
-
*
|
|
7
|
-
* Requires: npm install -D @ast-grep/cli
|
|
8
|
-
* Rules: ./rules/ directory
|
|
9
|
-
*/
|
|
10
|
-
import { spawnSync } from "node:child_process";
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
import { AstGrepParser } from "./ast-grep-parser.js";
|
|
14
|
-
import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
|
|
15
|
-
import { SgRunner } from "./sg-runner.js";
|
|
16
|
-
const _getExtensionDir = () => {
|
|
17
|
-
if (typeof __dirname !== "undefined") {
|
|
18
|
-
return __dirname;
|
|
19
|
-
}
|
|
20
|
-
return ".";
|
|
21
|
-
};
|
|
22
|
-
// --- Client ---
|
|
23
|
-
export class AstGrepClient {
|
|
24
|
-
constructor(ruleDir, verbose = false) {
|
|
25
|
-
this.available = null;
|
|
26
|
-
this.ruleDir = ruleDir || path.join(process.cwd(), "rules");
|
|
27
|
-
this.log = verbose
|
|
28
|
-
? (msg) => console.error(`[ast-grep] ${msg}`)
|
|
29
|
-
: () => { };
|
|
30
|
-
this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
|
|
31
|
-
this.runner = new SgRunner(verbose);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Check if ast-grep CLI is available, auto-install if not
|
|
35
|
-
*/
|
|
36
|
-
async ensureAvailable() {
|
|
37
|
-
return this.runner.ensureAvailable();
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Check if ast-grep CLI is available (legacy sync method)
|
|
41
|
-
* Prefer ensureAvailable() for auto-install behavior
|
|
42
|
-
*/
|
|
43
|
-
isAvailable() {
|
|
44
|
-
if (this.available !== null)
|
|
45
|
-
return this.available;
|
|
46
|
-
this.available = this.runner.isAvailable();
|
|
47
|
-
if (this.available) {
|
|
48
|
-
this.log("ast-grep available");
|
|
49
|
-
}
|
|
50
|
-
return this.available;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Search for AST patterns in files
|
|
54
|
-
*/
|
|
55
|
-
async search(pattern, lang, paths, options) {
|
|
56
|
-
const args = ["run", "-p", pattern, "--lang", lang, "--json=compact"];
|
|
57
|
-
if (options?.selector) {
|
|
58
|
-
args.push("--selector", options.selector);
|
|
59
|
-
}
|
|
60
|
-
if (options?.context !== undefined) {
|
|
61
|
-
args.push("--context", String(options.context));
|
|
62
|
-
}
|
|
63
|
-
args.push(...paths);
|
|
64
|
-
return this.runner.exec(args);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Search and replace AST patterns
|
|
68
|
-
*/
|
|
69
|
-
async replace(pattern, rewrite, lang, paths, apply = false) {
|
|
70
|
-
const args = [
|
|
71
|
-
"run",
|
|
72
|
-
"-p",
|
|
73
|
-
pattern,
|
|
74
|
-
"-r",
|
|
75
|
-
rewrite,
|
|
76
|
-
"--lang",
|
|
77
|
-
lang,
|
|
78
|
-
"--json=compact",
|
|
79
|
-
];
|
|
80
|
-
if (apply)
|
|
81
|
-
args.push("--update-all");
|
|
82
|
-
args.push(...paths);
|
|
83
|
-
const result = await this.runner.exec(args);
|
|
84
|
-
return { matches: result.matches, applied: apply, error: result.error };
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Run a one-off scan with a temporary rule and configuration
|
|
88
|
-
*/
|
|
89
|
-
runTempScan(dir, ruleId, ruleYaml, timeout = 30000) {
|
|
90
|
-
if (!this.isAvailable())
|
|
91
|
-
return [];
|
|
92
|
-
return this.runner.tempScan(dir, ruleId, ruleYaml, timeout);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Find similar functions by comparing normalized AST structure
|
|
96
|
-
*/
|
|
97
|
-
async findSimilarFunctions(dir, lang = "typescript") {
|
|
98
|
-
const ruleYaml = `id: find-functions
|
|
99
|
-
language: ${lang}
|
|
100
|
-
rule:
|
|
101
|
-
kind: function_declaration
|
|
102
|
-
severity: info
|
|
103
|
-
message: found
|
|
104
|
-
`;
|
|
105
|
-
const matches = this.runTempScan(dir, "find-functions", ruleYaml);
|
|
106
|
-
if (matches.length === 0)
|
|
107
|
-
return [];
|
|
108
|
-
return this.groupSimilarFunctions(matches);
|
|
109
|
-
}
|
|
110
|
-
groupSimilarFunctions(matches) {
|
|
111
|
-
const grouped = new Map();
|
|
112
|
-
for (const item of matches) {
|
|
113
|
-
const name = this.extractFunctionName(item.text);
|
|
114
|
-
if (!name)
|
|
115
|
-
continue;
|
|
116
|
-
const signature = this.normalizeFunction(item.text);
|
|
117
|
-
const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) +
|
|
118
|
-
1;
|
|
119
|
-
const group = grouped.get(signature) ?? [];
|
|
120
|
-
group.push({ name, file: item.file, line });
|
|
121
|
-
grouped.set(signature, group);
|
|
122
|
-
}
|
|
123
|
-
return Array.from(grouped.entries())
|
|
124
|
-
.filter(([_, functions]) => functions.length > 1)
|
|
125
|
-
.map(([pattern, functions]) => ({ pattern, functions }));
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Extract function name from match text
|
|
129
|
-
*/
|
|
130
|
-
extractFunctionName(text) {
|
|
131
|
-
return text.match(/function\s+(\w+)/)?.[1] ?? null;
|
|
132
|
-
}
|
|
133
|
-
normalizeFunction(text) {
|
|
134
|
-
const normalizedText = text
|
|
135
|
-
.replace(/function\s+\w+/, "function FN")
|
|
136
|
-
.replace(/\bconst\b|\blet\b|\bvar\b/g, "VAR")
|
|
137
|
-
.replace(/["'].*?["']/g, "STR")
|
|
138
|
-
.replace(/`[^`]*`/g, "TMPL")
|
|
139
|
-
.replace(/\b\d+\b/g, "NUM")
|
|
140
|
-
.replace(/\btrue\b|\bfalse\b/g, "BOOL")
|
|
141
|
-
.replace(/\/\/.*/g, "")
|
|
142
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
143
|
-
.replace(/\s+/g, " ")
|
|
144
|
-
.trim();
|
|
145
|
-
// Extract just the body structure
|
|
146
|
-
const bodyMatch = normalizedText.match(/\{(.*)\}/);
|
|
147
|
-
const body = bodyMatch ? bodyMatch[1].trim() : normalizedText;
|
|
148
|
-
// Use first 200 chars as signature
|
|
149
|
-
return body.slice(0, 200);
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Scan for exported function names in a directory
|
|
153
|
-
*/
|
|
154
|
-
async scanExports(dir, lang = "typescript") {
|
|
155
|
-
const exports = new Map();
|
|
156
|
-
const ruleYaml = `id: find-functions
|
|
157
|
-
language: ${lang}
|
|
158
|
-
rule:
|
|
159
|
-
kind: function_declaration
|
|
160
|
-
severity: info
|
|
161
|
-
message: found
|
|
162
|
-
`;
|
|
163
|
-
const matches = this.runTempScan(dir, "find-functions", ruleYaml, 15000);
|
|
164
|
-
this.log(`scanExports output length: ${matches.length}`);
|
|
165
|
-
for (const item of matches) {
|
|
166
|
-
const text = item.text || "";
|
|
167
|
-
const nameMatch = text.match(/function\s+(\w+)/);
|
|
168
|
-
if (nameMatch?.[1]) {
|
|
169
|
-
this.log(`scanExports found: ${nameMatch[1]} in ${item.file}`);
|
|
170
|
-
exports.set(nameMatch[1], item.file);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return exports;
|
|
174
|
-
}
|
|
175
|
-
formatMatches(matches, isDryRun = false, showModeIndicator = false) {
|
|
176
|
-
return this.runner.formatMatches(matches, isDryRun, 50, showModeIndicator);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Scan a file against all rules
|
|
180
|
-
*/
|
|
181
|
-
scanFile(filePath) {
|
|
182
|
-
if (!this.isAvailable())
|
|
183
|
-
return [];
|
|
184
|
-
const absolutePath = path.resolve(filePath);
|
|
185
|
-
if (!fs.existsSync(absolutePath))
|
|
186
|
-
return [];
|
|
187
|
-
const configPath = path.join(this.ruleDir, ".sgconfig.yml");
|
|
188
|
-
try {
|
|
189
|
-
const result = spawnSync("npx", ["sg", "scan", "--config", configPath, "--json", absolutePath], {
|
|
190
|
-
encoding: "utf-8",
|
|
191
|
-
timeout: 15000,
|
|
192
|
-
shell: process.platform === "win32",
|
|
193
|
-
});
|
|
194
|
-
// ast-grep exits 1 when it finds issues
|
|
195
|
-
const output = result.stdout || result.stderr || "";
|
|
196
|
-
if (!output.trim())
|
|
197
|
-
return [];
|
|
198
|
-
const parser = new AstGrepParser((id) => this.getRuleDescription(id), (sev) => this.mapSeverity(sev));
|
|
199
|
-
return parser.parseOutput(output, absolutePath);
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
this.log(`Scan error: ${err instanceof Error ? err.message : String(err)}`);
|
|
203
|
-
return [];
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Format diagnostics for LLM consumption
|
|
208
|
-
*/
|
|
209
|
-
formatDiagnostics(diags) {
|
|
210
|
-
if (diags.length === 0)
|
|
211
|
-
return "";
|
|
212
|
-
const errors = diags.filter((d) => d.severity === "error");
|
|
213
|
-
const warnings = diags.filter((d) => d.severity === "warning");
|
|
214
|
-
const hints = diags.filter((d) => d.severity === "hint");
|
|
215
|
-
let output = `[ast-grep] ${diags.length} structural issue(s)`;
|
|
216
|
-
if (errors.length)
|
|
217
|
-
output += ` — ${errors.length} error(s)`;
|
|
218
|
-
if (warnings.length)
|
|
219
|
-
output += ` — ${warnings.length} warning(s)`;
|
|
220
|
-
if (hints.length)
|
|
221
|
-
output += ` — ${hints.length} hint(s)`;
|
|
222
|
-
output += ":\n";
|
|
223
|
-
for (const d of diags.slice(0, 10)) {
|
|
224
|
-
const loc = d.line === d.endLine ? `L${d.line}` : `L${d.line}-${d.endLine}`;
|
|
225
|
-
const ruleInfo = d.ruleDescription
|
|
226
|
-
? `${d.rule}: ${d.ruleDescription.message}`
|
|
227
|
-
: d.rule;
|
|
228
|
-
const fix = d.fix || d.ruleDescription?.note ? " [fixable]" : "";
|
|
229
|
-
output += ` ${ruleInfo} (${loc})${fix}\n`;
|
|
230
|
-
if (d.ruleDescription?.note) {
|
|
231
|
-
const shortNote = d.ruleDescription.note.split("\n")[0];
|
|
232
|
-
output += ` → ${shortNote}\n`;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (diags.length > 10) {
|
|
236
|
-
output += ` ... and ${diags.length - 10} more\n`;
|
|
237
|
-
}
|
|
238
|
-
return output;
|
|
239
|
-
}
|
|
240
|
-
getRuleDescription(ruleId) {
|
|
241
|
-
return this.ruleManager.loadRuleDescriptions().get(ruleId);
|
|
242
|
-
}
|
|
243
|
-
mapSeverity(severity) {
|
|
244
|
-
const lower = severity.toLowerCase();
|
|
245
|
-
if (lower === "error")
|
|
246
|
-
return "error";
|
|
247
|
-
if (lower === "warning")
|
|
248
|
-
return "warning";
|
|
249
|
-
if (lower === "info")
|
|
250
|
-
return "info";
|
|
251
|
-
return "hint";
|
|
252
|
-
}
|
|
253
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
|
-
export class AstGrepParser {
|
|
3
|
-
constructor(getRuleDescription, mapSeverity) {
|
|
4
|
-
this.getRuleDescription = getRuleDescription;
|
|
5
|
-
this.mapSeverity = mapSeverity;
|
|
6
|
-
}
|
|
7
|
-
parseOutput(output, filterFile) {
|
|
8
|
-
const resolvedFilterFile = path.resolve(filterFile);
|
|
9
|
-
try {
|
|
10
|
-
const items = JSON.parse(output);
|
|
11
|
-
if (Array.isArray(items)) {
|
|
12
|
-
return items
|
|
13
|
-
.map((item) => this.parseDiagnostic(item, resolvedFilterFile))
|
|
14
|
-
.filter((d) => d !== null);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
catch (err) {
|
|
18
|
-
void err;
|
|
19
|
-
}
|
|
20
|
-
return output
|
|
21
|
-
.split("\n")
|
|
22
|
-
.filter((l) => l.trim())
|
|
23
|
-
.map((line) => {
|
|
24
|
-
try {
|
|
25
|
-
return this.parseDiagnostic(JSON.parse(line), resolvedFilterFile);
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
void err;
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
.filter((d) => d !== null);
|
|
33
|
-
}
|
|
34
|
-
parseDiagnostic(item, filterFile) {
|
|
35
|
-
if (item.labels?.length) {
|
|
36
|
-
return this.parseNewFormat(item, filterFile);
|
|
37
|
-
}
|
|
38
|
-
if (item.spans?.length) {
|
|
39
|
-
return this.parseLegacyFormat(item, filterFile);
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
parseNewFormat(item, filterFile) {
|
|
44
|
-
const label = item.labels.find((l) => l.style === "primary") || item.labels[0];
|
|
45
|
-
const filePath = path.resolve(label.file || filterFile);
|
|
46
|
-
if (filePath !== filterFile)
|
|
47
|
-
return null;
|
|
48
|
-
const start = label.range?.start || { line: 0, column: 0 };
|
|
49
|
-
const end = label.range?.end || start;
|
|
50
|
-
return {
|
|
51
|
-
line: start.line + 1,
|
|
52
|
-
column: start.column,
|
|
53
|
-
endLine: end.line + 1,
|
|
54
|
-
endColumn: end.column,
|
|
55
|
-
severity: this.mapSeverity(item.severity),
|
|
56
|
-
message: item.message || "Unknown issue",
|
|
57
|
-
rule: item.ruleId || "unknown",
|
|
58
|
-
ruleDescription: this.getRuleDescription(item.ruleId || "unknown"),
|
|
59
|
-
file: filePath,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
parseLegacyFormat(item, filterFile) {
|
|
63
|
-
const span = item.spans?.[0];
|
|
64
|
-
if (!span)
|
|
65
|
-
return null;
|
|
66
|
-
const filePath = path.resolve(span.file || filterFile);
|
|
67
|
-
if (filePath !== filterFile)
|
|
68
|
-
return null;
|
|
69
|
-
const start = span.range?.start || { line: 0, column: 0 };
|
|
70
|
-
const end = span.range?.end || start;
|
|
71
|
-
const ruleId = item.name || item.ruleId || "unknown";
|
|
72
|
-
return {
|
|
73
|
-
line: start.line + 1,
|
|
74
|
-
column: start.column,
|
|
75
|
-
endLine: end.line + 1,
|
|
76
|
-
endColumn: end.column,
|
|
77
|
-
severity: this.mapSeverity(item.severity || item.Severity || "warning"),
|
|
78
|
-
message: item.Message?.text || item.message || "Unknown issue",
|
|
79
|
-
rule: ruleId,
|
|
80
|
-
ruleDescription: this.getRuleDescription(ruleId),
|
|
81
|
-
file: filePath,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|