pi-lens 2.0.36 → 2.0.38
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/clients/architect-client.js +50 -20
- package/clients/architect-client.ts +61 -22
- package/clients/ast-grep-client.js +15 -106
- package/clients/ast-grep-client.test.js +2 -14
- package/clients/ast-grep-client.test.ts +2 -16
- package/clients/ast-grep-client.ts +34 -150
- package/clients/auto-loop.js +117 -0
- package/clients/auto-loop.ts +171 -0
- package/clients/biome-client.ts +28 -25
- package/clients/complexity-client.js +27 -16
- package/clients/complexity-client.ts +149 -105
- package/clients/dependency-checker.ts +15 -35
- package/clients/fix-scanners.js +195 -0
- package/clients/fix-scanners.ts +297 -0
- package/clients/interviewer-templates.js +75 -0
- package/clients/interviewer-templates.ts +90 -0
- package/clients/interviewer.js +73 -101
- package/clients/interviewer.ts +195 -140
- package/clients/knip-client.js +12 -4
- package/clients/knip-client.ts +21 -16
- package/clients/scan-architectural-debt.js +62 -72
- package/clients/scan-architectural-debt.ts +79 -64
- package/clients/scan-utils.js +98 -0
- package/clients/scan-utils.ts +112 -0
- package/clients/sg-runner.js +138 -0
- package/clients/sg-runner.ts +168 -0
- package/clients/subprocess-client.js +0 -37
- package/clients/subprocess-client.ts +0 -60
- package/clients/ts-service.ts +1 -7
- package/clients/type-safety-client.js +3 -8
- package/clients/type-safety-client.ts +10 -14
- package/clients/typescript-client.js +55 -56
- package/clients/typescript-client.ts +94 -52
- package/default-architect.yaml +87 -0
- package/index.ts +107 -1162
- package/package.json +2 -1
- package/rules/ast-grep-rules/rules/large-class.yml +6 -2
- package/rules/ast-grep-rules/rules/long-method.yml +1 -1
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
- package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
|
@@ -15,36 +15,60 @@ import { minimatch } from "minimatch";
|
|
|
15
15
|
export class ArchitectClient {
|
|
16
16
|
constructor(verbose = false) {
|
|
17
17
|
this.config = null;
|
|
18
|
-
this.
|
|
18
|
+
this.isUserConfig = false;
|
|
19
19
|
this.log = verbose
|
|
20
20
|
? (msg) => console.error(`[architect] ${msg}`)
|
|
21
21
|
: () => { };
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Load architect config from project root
|
|
24
|
+
* Load architect config from project root.
|
|
25
|
+
* Falls back to built-in default if no user config exists.
|
|
25
26
|
*/
|
|
26
27
|
loadConfig(projectRoot) {
|
|
27
|
-
// Try
|
|
28
|
-
const
|
|
28
|
+
// Try user config locations first
|
|
29
|
+
const userCandidates = [
|
|
29
30
|
path.join(projectRoot, ".pi-lens", "architect.yaml"),
|
|
30
31
|
path.join(projectRoot, "architect.yaml"),
|
|
31
32
|
path.join(projectRoot, ".pi-lens", "architect.yml"),
|
|
32
33
|
];
|
|
33
|
-
for (const configPath of
|
|
34
|
+
for (const configPath of userCandidates) {
|
|
34
35
|
try {
|
|
35
36
|
const content = fs.readFileSync(configPath, "utf-8");
|
|
36
37
|
this.config = this.parseYaml(content);
|
|
37
38
|
this.configPath = configPath;
|
|
38
|
-
this.
|
|
39
|
+
this.isUserConfig = true;
|
|
40
|
+
this.log(`Loaded user architect config from ${configPath}`);
|
|
39
41
|
return true;
|
|
40
42
|
}
|
|
41
43
|
catch (error) {
|
|
42
44
|
this.log(`Could not read ${configPath}: ${error}`);
|
|
43
|
-
continue;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// Fall back to built-in default
|
|
48
|
+
try {
|
|
49
|
+
// Handle both CommonJS and ESM environments
|
|
50
|
+
let currentDir = ".";
|
|
51
|
+
if (typeof __dirname !== "undefined") {
|
|
52
|
+
currentDir = __dirname;
|
|
53
|
+
}
|
|
54
|
+
const defaultPath = path.join(currentDir, "..", "default-architect.yaml");
|
|
55
|
+
const content = fs.readFileSync(defaultPath, "utf-8");
|
|
56
|
+
this.config = this.parseYaml(content);
|
|
57
|
+
this.configPath = defaultPath;
|
|
58
|
+
this.isUserConfig = false;
|
|
59
|
+
this.log("Using default architect rules (create .pi-lens/architect.yaml to customize)");
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
this.log("No architect config available");
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if the loaded config is user-defined (not default)
|
|
69
|
+
*/
|
|
70
|
+
isUserDefined() {
|
|
71
|
+
return this.isUserConfig;
|
|
48
72
|
}
|
|
49
73
|
/**
|
|
50
74
|
* Check if config is loaded
|
|
@@ -77,12 +101,22 @@ export class ArchitectClient {
|
|
|
77
101
|
if (!rule.must_not)
|
|
78
102
|
continue;
|
|
79
103
|
for (const check of rule.must_not) {
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
// We use 'g' to find all occurrences and correctly report line numbers
|
|
105
|
+
const regex = new RegExp(check.pattern, "gi");
|
|
106
|
+
let match;
|
|
107
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: RegExp.exec iteration
|
|
108
|
+
while ((match = regex.exec(content)) !== null) {
|
|
109
|
+
// Convert index to line number
|
|
110
|
+
const lineNum = content.slice(0, match.index).split("\n").length;
|
|
82
111
|
violations.push({
|
|
83
112
|
pattern: rule.pattern,
|
|
84
113
|
message: check.message,
|
|
114
|
+
line: lineNum,
|
|
85
115
|
});
|
|
116
|
+
// Prevent infinite loop on empty matches
|
|
117
|
+
if (match.index === regex.lastIndex) {
|
|
118
|
+
regex.lastIndex++;
|
|
119
|
+
}
|
|
86
120
|
}
|
|
87
121
|
}
|
|
88
122
|
}
|
|
@@ -132,7 +166,7 @@ export class ArchitectClient {
|
|
|
132
166
|
parseYaml(content) {
|
|
133
167
|
const config = { rules: [] };
|
|
134
168
|
// Split into top-level rule blocks (4-space indent "- pattern:")
|
|
135
|
-
const ruleBlocks = content.split(/(?=^
|
|
169
|
+
const ruleBlocks = content.split(/(?=^ {2}- pattern:)/m);
|
|
136
170
|
for (const block of ruleBlocks) {
|
|
137
171
|
const lines = block.split("\n");
|
|
138
172
|
let rule = null;
|
|
@@ -154,7 +188,9 @@ export class ArchitectClient {
|
|
|
154
188
|
continue;
|
|
155
189
|
}
|
|
156
190
|
// Nested pattern inside must_not (may start with "- ")
|
|
157
|
-
if ((trimmed.startsWith("pattern:") ||
|
|
191
|
+
if ((trimmed.startsWith("pattern:") ||
|
|
192
|
+
trimmed.startsWith("- pattern:")) &&
|
|
193
|
+
section === "must_not") {
|
|
158
194
|
// Extract everything after "pattern:" and unquote
|
|
159
195
|
const raw = trimmed.replace(/^-?\s*pattern:\s*/, "").trim();
|
|
160
196
|
const unquoted = raw.replace(/^["']|["']$/g, "");
|
|
@@ -207,10 +243,4 @@ export class ArchitectClient {
|
|
|
207
243
|
}
|
|
208
244
|
}
|
|
209
245
|
// --- Singleton ---
|
|
210
|
-
|
|
211
|
-
export function getArchitectClient(verbose = false) {
|
|
212
|
-
if (!instance) {
|
|
213
|
-
instance = new ArchitectClient(verbose);
|
|
214
|
-
}
|
|
215
|
-
return instance;
|
|
216
|
-
}
|
|
246
|
+
const _instance = null;
|
|
@@ -18,6 +18,7 @@ import { minimatch } from "minimatch";
|
|
|
18
18
|
export interface ArchitectViolation {
|
|
19
19
|
pattern: string;
|
|
20
20
|
message: string;
|
|
21
|
+
line?: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface ArchitectRule {
|
|
@@ -43,7 +44,7 @@ export interface FileArchitectResult {
|
|
|
43
44
|
|
|
44
45
|
export class ArchitectClient {
|
|
45
46
|
private config: ArchitectConfig | null = null;
|
|
46
|
-
private
|
|
47
|
+
private isUserConfig: boolean = false;
|
|
47
48
|
private log: (msg: string) => void;
|
|
48
49
|
|
|
49
50
|
constructor(verbose = false) {
|
|
@@ -53,31 +54,57 @@ export class ArchitectClient {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* Load architect config from project root
|
|
57
|
+
* Load architect config from project root.
|
|
58
|
+
* Falls back to built-in default if no user config exists.
|
|
57
59
|
*/
|
|
58
60
|
loadConfig(projectRoot: string): boolean {
|
|
59
|
-
// Try
|
|
60
|
-
const
|
|
61
|
+
// Try user config locations first
|
|
62
|
+
const userCandidates = [
|
|
61
63
|
path.join(projectRoot, ".pi-lens", "architect.yaml"),
|
|
62
64
|
path.join(projectRoot, "architect.yaml"),
|
|
63
65
|
path.join(projectRoot, ".pi-lens", "architect.yml"),
|
|
64
66
|
];
|
|
65
67
|
|
|
66
|
-
for (const configPath of
|
|
68
|
+
for (const configPath of userCandidates) {
|
|
67
69
|
try {
|
|
68
70
|
const content = fs.readFileSync(configPath, "utf-8");
|
|
69
71
|
this.config = this.parseYaml(content);
|
|
70
72
|
this.configPath = configPath;
|
|
71
|
-
this.
|
|
73
|
+
this.isUserConfig = true;
|
|
74
|
+
this.log(`Loaded user architect config from ${configPath}`);
|
|
72
75
|
return true;
|
|
73
76
|
} catch (error) {
|
|
74
77
|
this.log(`Could not read ${configPath}: ${error}`);
|
|
75
|
-
continue;
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
// Fall back to built-in default
|
|
82
|
+
try {
|
|
83
|
+
// Handle both CommonJS and ESM environments
|
|
84
|
+
let currentDir = ".";
|
|
85
|
+
if (typeof __dirname !== "undefined") {
|
|
86
|
+
currentDir = __dirname;
|
|
87
|
+
}
|
|
88
|
+
const defaultPath = path.join(currentDir, "..", "default-architect.yaml");
|
|
89
|
+
const content = fs.readFileSync(defaultPath, "utf-8");
|
|
90
|
+
this.config = this.parseYaml(content);
|
|
91
|
+
this.configPath = defaultPath;
|
|
92
|
+
this.isUserConfig = false;
|
|
93
|
+
this.log(
|
|
94
|
+
"Using default architect rules (create .pi-lens/architect.yaml to customize)",
|
|
95
|
+
);
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
this.log("No architect config available");
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if the loaded config is user-defined (not default)
|
|
105
|
+
*/
|
|
106
|
+
isUserDefined(): boolean {
|
|
107
|
+
return this.isUserConfig;
|
|
81
108
|
}
|
|
82
109
|
|
|
83
110
|
/**
|
|
@@ -114,12 +141,24 @@ export class ArchitectClient {
|
|
|
114
141
|
if (!rule.must_not) continue;
|
|
115
142
|
|
|
116
143
|
for (const check of rule.must_not) {
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
// We use 'g' to find all occurrences and correctly report line numbers
|
|
145
|
+
const regex = new RegExp(check.pattern, "gi");
|
|
146
|
+
let match: RegExpExecArray | null;
|
|
147
|
+
|
|
148
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: RegExp.exec iteration
|
|
149
|
+
while ((match = regex.exec(content)) !== null) {
|
|
150
|
+
// Convert index to line number
|
|
151
|
+
const lineNum = content.slice(0, match.index).split("\n").length;
|
|
119
152
|
violations.push({
|
|
120
153
|
pattern: rule.pattern,
|
|
121
154
|
message: check.message,
|
|
155
|
+
line: lineNum,
|
|
122
156
|
});
|
|
157
|
+
|
|
158
|
+
// Prevent infinite loop on empty matches
|
|
159
|
+
if (match.index === regex.lastIndex) {
|
|
160
|
+
regex.lastIndex++;
|
|
161
|
+
}
|
|
123
162
|
}
|
|
124
163
|
}
|
|
125
164
|
}
|
|
@@ -131,7 +170,10 @@ export class ArchitectClient {
|
|
|
131
170
|
* Check file size against max_lines rule
|
|
132
171
|
* Returns violation if file exceeds the limit
|
|
133
172
|
*/
|
|
134
|
-
checkFileSize(
|
|
173
|
+
checkFileSize(
|
|
174
|
+
filePath: string,
|
|
175
|
+
lineCount: number,
|
|
176
|
+
): ArchitectViolation | null {
|
|
135
177
|
const rules = this.getRulesForFile(filePath);
|
|
136
178
|
|
|
137
179
|
for (const rule of rules) {
|
|
@@ -177,7 +219,7 @@ export class ArchitectClient {
|
|
|
177
219
|
const config: ArchitectConfig = { rules: [] };
|
|
178
220
|
|
|
179
221
|
// Split into top-level rule blocks (4-space indent "- pattern:")
|
|
180
|
-
const ruleBlocks = content.split(/(?=^
|
|
222
|
+
const ruleBlocks = content.split(/(?=^ {2}- pattern:)/m);
|
|
181
223
|
|
|
182
224
|
for (const block of ruleBlocks) {
|
|
183
225
|
const lines = block.split("\n");
|
|
@@ -202,7 +244,11 @@ export class ArchitectClient {
|
|
|
202
244
|
continue;
|
|
203
245
|
}
|
|
204
246
|
// Nested pattern inside must_not (may start with "- ")
|
|
205
|
-
if (
|
|
247
|
+
if (
|
|
248
|
+
(trimmed.startsWith("pattern:") ||
|
|
249
|
+
trimmed.startsWith("- pattern:")) &&
|
|
250
|
+
section === "must_not"
|
|
251
|
+
) {
|
|
206
252
|
// Extract everything after "pattern:" and unquote
|
|
207
253
|
const raw = trimmed.replace(/^-?\s*pattern:\s*/, "").trim();
|
|
208
254
|
const unquoted = raw.replace(/^["']|["']$/g, "");
|
|
@@ -263,11 +309,4 @@ export class ArchitectClient {
|
|
|
263
309
|
|
|
264
310
|
// --- Singleton ---
|
|
265
311
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
export function getArchitectClient(verbose = false): ArchitectClient {
|
|
269
|
-
if (!instance) {
|
|
270
|
-
instance = new ArchitectClient(verbose);
|
|
271
|
-
}
|
|
272
|
-
return instance;
|
|
273
|
-
}
|
|
312
|
+
const _instance: ArchitectClient | null = null;
|
|
@@ -7,22 +7,28 @@
|
|
|
7
7
|
* Requires: npm install -D @ast-grep/cli
|
|
8
8
|
* Rules: ./rules/ directory
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { AstGrepParser } from "./ast-grep-parser.js";
|
|
14
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
|
+
};
|
|
15
22
|
// --- Client ---
|
|
16
23
|
export class AstGrepClient {
|
|
17
24
|
constructor(ruleDir, verbose = false) {
|
|
18
25
|
this.available = null;
|
|
19
|
-
this.ruleDir =
|
|
20
|
-
ruleDir ||
|
|
21
|
-
path.join(typeof __dirname !== "undefined" ? __dirname : ".", "..", "rules");
|
|
26
|
+
this.ruleDir = ruleDir || path.join(process.cwd(), "rules");
|
|
22
27
|
this.log = verbose
|
|
23
28
|
? (msg) => console.error(`[ast-grep] ${msg}`)
|
|
24
29
|
: () => { };
|
|
25
30
|
this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
|
|
31
|
+
this.runner = new SgRunner(verbose);
|
|
26
32
|
}
|
|
27
33
|
/**
|
|
28
34
|
* Check if ast-grep CLI is available
|
|
@@ -30,12 +36,7 @@ export class AstGrepClient {
|
|
|
30
36
|
isAvailable() {
|
|
31
37
|
if (this.available !== null)
|
|
32
38
|
return this.available;
|
|
33
|
-
|
|
34
|
-
encoding: "utf-8",
|
|
35
|
-
timeout: 10000,
|
|
36
|
-
shell: true,
|
|
37
|
-
});
|
|
38
|
-
this.available = !result.error && result.status === 0;
|
|
39
|
+
this.available = this.runner.isAvailable();
|
|
39
40
|
if (this.available) {
|
|
40
41
|
this.log("ast-grep available");
|
|
41
42
|
}
|
|
@@ -45,7 +46,7 @@ export class AstGrepClient {
|
|
|
45
46
|
* Search for AST patterns in files
|
|
46
47
|
*/
|
|
47
48
|
async search(pattern, lang, paths) {
|
|
48
|
-
return this.
|
|
49
|
+
return this.runner.exec([
|
|
49
50
|
"run",
|
|
50
51
|
"-p",
|
|
51
52
|
pattern,
|
|
@@ -72,7 +73,7 @@ export class AstGrepClient {
|
|
|
72
73
|
if (apply)
|
|
73
74
|
args.push("--update-all");
|
|
74
75
|
args.push(...paths);
|
|
75
|
-
const result = await this.
|
|
76
|
+
const result = await this.runner.exec(args);
|
|
76
77
|
return { matches: result.matches, applied: apply, error: result.error };
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
@@ -81,39 +82,7 @@ export class AstGrepClient {
|
|
|
81
82
|
runTempScan(dir, ruleId, ruleYaml, timeout = 30000) {
|
|
82
83
|
if (!this.isAvailable())
|
|
83
84
|
return [];
|
|
84
|
-
|
|
85
|
-
const ts = Date.now();
|
|
86
|
-
const sessionDir = path.join(tmpDir, `pi-lens-temp-${ruleId}-${ts}`);
|
|
87
|
-
const rulesSubdir = path.join(sessionDir, "rules");
|
|
88
|
-
const ruleFile = path.join(rulesSubdir, `${ruleId}.yml`);
|
|
89
|
-
const configFile = path.join(sessionDir, ".sgconfig.yml");
|
|
90
|
-
try {
|
|
91
|
-
fs.mkdirSync(rulesSubdir, { recursive: true });
|
|
92
|
-
fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
|
|
93
|
-
fs.writeFileSync(ruleFile, ruleYaml);
|
|
94
|
-
const result = spawnSync("npx", ["sg", "scan", "--config", configFile, "--json", dir], {
|
|
95
|
-
encoding: "utf-8",
|
|
96
|
-
timeout,
|
|
97
|
-
shell: true,
|
|
98
|
-
});
|
|
99
|
-
const output = result.stdout || result.stderr || "";
|
|
100
|
-
if (!output.trim())
|
|
101
|
-
return [];
|
|
102
|
-
const items = JSON.parse(output);
|
|
103
|
-
return Array.isArray(items) ? items : [items];
|
|
104
|
-
}
|
|
105
|
-
catch (err) {
|
|
106
|
-
void err;
|
|
107
|
-
return [];
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
try {
|
|
111
|
-
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
void err;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
85
|
+
return this.runner.tempScan(dir, ruleId, ruleYaml, timeout);
|
|
117
86
|
}
|
|
118
87
|
/**
|
|
119
88
|
* Find similar functions by comparing normalized AST structure
|
|
@@ -199,68 +168,8 @@ message: found
|
|
|
199
168
|
}
|
|
200
169
|
return exports;
|
|
201
170
|
}
|
|
202
|
-
runSg(args) {
|
|
203
|
-
return new Promise((resolve) => {
|
|
204
|
-
const proc = spawn("npx", ["sg", ...args], {
|
|
205
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
206
|
-
shell: true,
|
|
207
|
-
});
|
|
208
|
-
let stdout = "";
|
|
209
|
-
let stderr = "";
|
|
210
|
-
proc.stdout.on("data", (data) => (stdout += data.toString()));
|
|
211
|
-
proc.stderr.on("data", (data) => (stderr += data.toString()));
|
|
212
|
-
proc.on("error", (err) => {
|
|
213
|
-
if (err.message.includes("ENOENT")) {
|
|
214
|
-
resolve({
|
|
215
|
-
matches: [],
|
|
216
|
-
error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli",
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
resolve({ matches: [], error: err.message });
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
proc.on("close", (code) => {
|
|
224
|
-
if (code !== 0 && !stdout.trim()) {
|
|
225
|
-
resolve({
|
|
226
|
-
matches: [],
|
|
227
|
-
error: stderr.includes("No files found")
|
|
228
|
-
? undefined
|
|
229
|
-
: stderr.trim() || `Exit code ${code}`,
|
|
230
|
-
});
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (!stdout.trim()) {
|
|
234
|
-
resolve({ matches: [] });
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
const parsed = JSON.parse(stdout);
|
|
239
|
-
const matches = Array.isArray(parsed) ? parsed : [parsed];
|
|
240
|
-
resolve({ matches });
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
void err;
|
|
244
|
-
resolve({ matches: [], error: "Failed to parse output" });
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
171
|
formatMatches(matches, isDryRun = false) {
|
|
250
|
-
|
|
251
|
-
return "No matches found";
|
|
252
|
-
const MAX = 50;
|
|
253
|
-
const shown = matches.slice(0, MAX);
|
|
254
|
-
const lines = shown.map((m) => {
|
|
255
|
-
const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
|
|
256
|
-
const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
|
|
257
|
-
return isDryRun && m.replacement
|
|
258
|
-
? `${loc}\n - ${text}\n + ${m.replacement}`
|
|
259
|
-
: `${loc}: ${text}`;
|
|
260
|
-
});
|
|
261
|
-
if (matches.length > MAX)
|
|
262
|
-
lines.unshift(`Found ${matches.length} matches (showing first ${MAX}):`);
|
|
263
|
-
return lines.join("\n");
|
|
172
|
+
return this.runner.formatMatches(matches, isDryRun);
|
|
264
173
|
}
|
|
265
174
|
/**
|
|
266
175
|
* Scan a file against all rules
|
|
@@ -25,18 +25,6 @@ describe("AstGrepClient", () => {
|
|
|
25
25
|
const result = client.scanFile("/nonexistent/file.ts");
|
|
26
26
|
expect(result).toEqual([]);
|
|
27
27
|
});
|
|
28
|
-
it("should detect var usage (no-var rule)", () => {
|
|
29
|
-
if (!client.isAvailable())
|
|
30
|
-
return;
|
|
31
|
-
const content = `
|
|
32
|
-
var x = 1;
|
|
33
|
-
var y = 2;
|
|
34
|
-
`;
|
|
35
|
-
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
36
|
-
const result = client.scanFile(filePath);
|
|
37
|
-
// Should detect var usage
|
|
38
|
-
expect(result.some((d) => d.rule === "no-var")).toBe(true);
|
|
39
|
-
});
|
|
40
28
|
it("should detect console.log usage", () => {
|
|
41
29
|
if (!client.isAvailable())
|
|
42
30
|
return;
|
|
@@ -45,8 +33,8 @@ console.log("test");
|
|
|
45
33
|
`;
|
|
46
34
|
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
47
35
|
const result = client.scanFile(filePath);
|
|
48
|
-
//
|
|
49
|
-
expect(
|
|
36
|
+
// Should detect console.log
|
|
37
|
+
expect(result.some((d) => d.rule === "no-console-log")).toBe(true);
|
|
50
38
|
});
|
|
51
39
|
});
|
|
52
40
|
describe("formatDiagnostics", () => {
|
|
@@ -30,20 +30,6 @@ describe("AstGrepClient", () => {
|
|
|
30
30
|
expect(result).toEqual([]);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it("should detect var usage (no-var rule)", () => {
|
|
34
|
-
if (!client.isAvailable()) return;
|
|
35
|
-
|
|
36
|
-
const content = `
|
|
37
|
-
var x = 1;
|
|
38
|
-
var y = 2;
|
|
39
|
-
`;
|
|
40
|
-
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
41
|
-
const result = client.scanFile(filePath);
|
|
42
|
-
|
|
43
|
-
// Should detect var usage
|
|
44
|
-
expect(result.some((d) => d.rule === "no-var")).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
33
|
it("should detect console.log usage", () => {
|
|
48
34
|
if (!client.isAvailable()) return;
|
|
49
35
|
|
|
@@ -53,8 +39,8 @@ console.log("test");
|
|
|
53
39
|
const filePath = createTempFile(tmpDir, "test.ts", content);
|
|
54
40
|
const result = client.scanFile(filePath);
|
|
55
41
|
|
|
56
|
-
//
|
|
57
|
-
expect(
|
|
42
|
+
// Should detect console.log
|
|
43
|
+
expect(result.some((d) => d.rule === "no-console-log")).toBe(true);
|
|
58
44
|
});
|
|
59
45
|
});
|
|
60
46
|
|