@haystackeditor/cli 0.7.2 → 0.8.1
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/README.md +59 -12
- package/dist/assets/hooks/agent-context/detect.ts +136 -0
- package/dist/assets/hooks/agent-context/format.ts +99 -0
- package/dist/assets/hooks/agent-context/index.ts +39 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
- package/dist/assets/hooks/agent-context/types.ts +58 -0
- package/dist/assets/hooks/llm-rules-template.md +35 -0
- package/dist/assets/hooks/package.json +11 -0
- package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
- package/dist/assets/hooks/scripts/post-commit.sh +4 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
- package/dist/assets/hooks/scripts/pre-push.sh +5 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
- package/dist/assets/hooks/truncation-checker/index.ts +595 -0
- package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +89 -0
- package/dist/commands/hooks.d.ts +17 -0
- package/dist/commands/hooks.js +269 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +20 -239
- package/dist/commands/secrets.d.ts +15 -0
- package/dist/commands/secrets.js +83 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +215 -0
- package/dist/index.js +107 -7
- package/dist/types.d.ts +32 -8
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +226 -0
- package/dist/utils/skill.d.ts +1 -1
- package/dist/utils/skill.js +481 -13
- package/package.json +2 -2
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter based truncation detection.
|
|
3
|
+
*
|
|
4
|
+
* Detects semantic truncation patterns that regex can't catch:
|
|
5
|
+
* - Truncation in function returns
|
|
6
|
+
* - LLM context variable truncation
|
|
7
|
+
* - Loop-based truncation
|
|
8
|
+
* - Chained truncations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Parser from "tree-sitter";
|
|
12
|
+
import TypeScript from "tree-sitter-typescript";
|
|
13
|
+
import { readFileSync, existsSync } from "fs";
|
|
14
|
+
import { extname } from "path";
|
|
15
|
+
|
|
16
|
+
// Initialize parsers
|
|
17
|
+
const tsxParser = new Parser();
|
|
18
|
+
tsxParser.setLanguage(TypeScript.tsx as unknown as Parser.Language);
|
|
19
|
+
|
|
20
|
+
const tsParser = new Parser();
|
|
21
|
+
tsParser.setLanguage(TypeScript.typescript as unknown as Parser.Language);
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// TYPES
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export interface ASTViolation {
|
|
28
|
+
file: string;
|
|
29
|
+
line: number;
|
|
30
|
+
column: number;
|
|
31
|
+
type: string;
|
|
32
|
+
description: string;
|
|
33
|
+
code: string;
|
|
34
|
+
severity: "error" | "warning";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Variables that likely contain LLM context
|
|
38
|
+
const LLM_CONTEXT_VARS = new Set([
|
|
39
|
+
"context",
|
|
40
|
+
"prompt",
|
|
41
|
+
"messages",
|
|
42
|
+
"output",
|
|
43
|
+
"result",
|
|
44
|
+
"response",
|
|
45
|
+
"content",
|
|
46
|
+
"text",
|
|
47
|
+
"data",
|
|
48
|
+
"input",
|
|
49
|
+
"toolOutput",
|
|
50
|
+
"toolResult",
|
|
51
|
+
"searchResults",
|
|
52
|
+
"fileContent",
|
|
53
|
+
"fileContents",
|
|
54
|
+
"codeContent",
|
|
55
|
+
"sourceCode",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// Truncation method names
|
|
59
|
+
const TRUNCATION_METHODS = new Set([
|
|
60
|
+
"slice",
|
|
61
|
+
"substring",
|
|
62
|
+
"substr",
|
|
63
|
+
"splice",
|
|
64
|
+
"truncate",
|
|
65
|
+
"take",
|
|
66
|
+
"head",
|
|
67
|
+
"limit",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// HELPERS
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
function getParser(filePath: string): Parser | null {
|
|
75
|
+
const ext = extname(filePath);
|
|
76
|
+
if (ext === ".tsx" || ext === ".jsx") return tsxParser;
|
|
77
|
+
if (ext === ".ts" || ext === ".js") return tsParser;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nodeText(node: Parser.SyntaxNode, source: string): string {
|
|
82
|
+
return source.slice(node.startIndex, node.endIndex);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findNodes(
|
|
86
|
+
node: Parser.SyntaxNode,
|
|
87
|
+
types: string[],
|
|
88
|
+
results: Parser.SyntaxNode[] = []
|
|
89
|
+
): Parser.SyntaxNode[] {
|
|
90
|
+
if (types.includes(node.type)) {
|
|
91
|
+
results.push(node);
|
|
92
|
+
}
|
|
93
|
+
for (const child of node.children) {
|
|
94
|
+
findNodes(child, types, results);
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findParent(
|
|
100
|
+
node: Parser.SyntaxNode,
|
|
101
|
+
types: string[]
|
|
102
|
+
): Parser.SyntaxNode | null {
|
|
103
|
+
let current = node.parent;
|
|
104
|
+
while (current) {
|
|
105
|
+
if (types.includes(current.type)) return current;
|
|
106
|
+
current = current.parent;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getLineContent(source: string, line: number): string {
|
|
112
|
+
const lines = source.split("\n");
|
|
113
|
+
return lines[line - 1] || "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// DETECTION: Return statement truncation
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
function detectReturnTruncation(
|
|
121
|
+
tree: Parser.Tree,
|
|
122
|
+
source: string,
|
|
123
|
+
filePath: string
|
|
124
|
+
): ASTViolation[] {
|
|
125
|
+
const violations: ASTViolation[] = [];
|
|
126
|
+
|
|
127
|
+
// Find all return statements
|
|
128
|
+
const returnStatements = findNodes(tree.rootNode, ["return_statement"]);
|
|
129
|
+
|
|
130
|
+
for (const ret of returnStatements) {
|
|
131
|
+
const retText = nodeText(ret, source);
|
|
132
|
+
|
|
133
|
+
// Check if return contains truncation
|
|
134
|
+
const callExpressions = findNodes(ret, ["call_expression"]);
|
|
135
|
+
for (const call of callExpressions) {
|
|
136
|
+
const funcNode = call.childForFieldName("function");
|
|
137
|
+
if (!funcNode) continue;
|
|
138
|
+
|
|
139
|
+
// Check for method calls like .slice(), .substring()
|
|
140
|
+
if (funcNode.type === "member_expression") {
|
|
141
|
+
const property = funcNode.childForFieldName("property");
|
|
142
|
+
if (property && TRUNCATION_METHODS.has(nodeText(property, source))) {
|
|
143
|
+
// Check if it has numeric arguments (indicates truncation)
|
|
144
|
+
const args = call.childForFieldName("arguments");
|
|
145
|
+
if (args) {
|
|
146
|
+
const hasNumericArg = findNodes(args, ["number"]).length > 0;
|
|
147
|
+
const hasLimitVar = /max|limit|MAX|LIMIT/i.test(nodeText(args, source));
|
|
148
|
+
|
|
149
|
+
if (hasNumericArg || hasLimitVar) {
|
|
150
|
+
violations.push({
|
|
151
|
+
file: filePath,
|
|
152
|
+
line: ret.startPosition.row + 1,
|
|
153
|
+
column: ret.startPosition.column + 1,
|
|
154
|
+
type: "return-truncation",
|
|
155
|
+
description: `Function returns truncated data via .${nodeText(property, source)}()`,
|
|
156
|
+
code: retText.slice(0, 100),
|
|
157
|
+
severity: "error",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return violations;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// DETECTION: LLM context variable truncation
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
function detectContextVarTruncation(
|
|
174
|
+
tree: Parser.Tree,
|
|
175
|
+
source: string,
|
|
176
|
+
filePath: string
|
|
177
|
+
): ASTViolation[] {
|
|
178
|
+
const violations: ASTViolation[] = [];
|
|
179
|
+
|
|
180
|
+
// Find all call expressions
|
|
181
|
+
const callExpressions = findNodes(tree.rootNode, ["call_expression"]);
|
|
182
|
+
|
|
183
|
+
for (const call of callExpressions) {
|
|
184
|
+
const funcNode = call.childForFieldName("function");
|
|
185
|
+
if (!funcNode || funcNode.type !== "member_expression") continue;
|
|
186
|
+
|
|
187
|
+
const object = funcNode.childForFieldName("object");
|
|
188
|
+
const property = funcNode.childForFieldName("property");
|
|
189
|
+
if (!object || !property) continue;
|
|
190
|
+
|
|
191
|
+
const methodName = nodeText(property, source);
|
|
192
|
+
if (!TRUNCATION_METHODS.has(methodName)) continue;
|
|
193
|
+
|
|
194
|
+
// Check if the object is an LLM context variable
|
|
195
|
+
const objectName = nodeText(object, source);
|
|
196
|
+
|
|
197
|
+
// Direct variable: context.slice()
|
|
198
|
+
if (LLM_CONTEXT_VARS.has(objectName)) {
|
|
199
|
+
violations.push({
|
|
200
|
+
file: filePath,
|
|
201
|
+
line: call.startPosition.row + 1,
|
|
202
|
+
column: call.startPosition.column + 1,
|
|
203
|
+
type: "context-var-truncation",
|
|
204
|
+
description: `LLM context variable '${objectName}' is truncated via .${methodName}()`,
|
|
205
|
+
code: nodeText(call, source).slice(0, 100),
|
|
206
|
+
severity: "error",
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Property access: result.content.slice()
|
|
212
|
+
if (object.type === "member_expression") {
|
|
213
|
+
const innerProp = object.childForFieldName("property");
|
|
214
|
+
if (innerProp && LLM_CONTEXT_VARS.has(nodeText(innerProp, source))) {
|
|
215
|
+
violations.push({
|
|
216
|
+
file: filePath,
|
|
217
|
+
line: call.startPosition.row + 1,
|
|
218
|
+
column: call.startPosition.column + 1,
|
|
219
|
+
type: "context-var-truncation",
|
|
220
|
+
description: `LLM context property '${nodeText(innerProp, source)}' is truncated via .${methodName}()`,
|
|
221
|
+
code: nodeText(call, source).slice(0, 100),
|
|
222
|
+
severity: "error",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return violations;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// DETECTION: Loop-based truncation (Math.min pattern)
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
function detectLoopTruncation(
|
|
236
|
+
tree: Parser.Tree,
|
|
237
|
+
source: string,
|
|
238
|
+
filePath: string
|
|
239
|
+
): ASTViolation[] {
|
|
240
|
+
const violations: ASTViolation[] = [];
|
|
241
|
+
|
|
242
|
+
// Find for loops
|
|
243
|
+
const forLoops = findNodes(tree.rootNode, ["for_statement"]);
|
|
244
|
+
|
|
245
|
+
for (const loop of forLoops) {
|
|
246
|
+
const loopText = nodeText(loop, source);
|
|
247
|
+
|
|
248
|
+
// Check for Math.min pattern in condition
|
|
249
|
+
if (/Math\.min\s*\([^)]*\.length/.test(loopText)) {
|
|
250
|
+
violations.push({
|
|
251
|
+
file: filePath,
|
|
252
|
+
line: loop.startPosition.row + 1,
|
|
253
|
+
column: loop.startPosition.column + 1,
|
|
254
|
+
type: "loop-truncation",
|
|
255
|
+
description: "Loop uses Math.min() to limit iterations (may truncate data)",
|
|
256
|
+
code: loopText.split("\n")[0].slice(0, 100),
|
|
257
|
+
severity: "warning",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check for hardcoded limit comparison: i < 10
|
|
262
|
+
const condition = loop.children.find((c) => c.type === "binary_expression");
|
|
263
|
+
if (condition) {
|
|
264
|
+
const condText = nodeText(condition, source);
|
|
265
|
+
// Pattern: i < NUMBER where NUMBER is small
|
|
266
|
+
const limitMatch = condText.match(/[a-z_]\w*\s*<\s*(\d+)/i);
|
|
267
|
+
if (limitMatch && parseInt(limitMatch[1]) <= 100) {
|
|
268
|
+
// Check if it's iterating over an array
|
|
269
|
+
if (/\.length|\.size/.test(loopText)) {
|
|
270
|
+
violations.push({
|
|
271
|
+
file: filePath,
|
|
272
|
+
line: loop.startPosition.row + 1,
|
|
273
|
+
column: loop.startPosition.column + 1,
|
|
274
|
+
type: "loop-truncation",
|
|
275
|
+
description: `Loop hardcodes limit of ${limitMatch[1]} iterations`,
|
|
276
|
+
code: loopText.split("\n")[0].slice(0, 100),
|
|
277
|
+
severity: "warning",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return violations;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// DETECTION: Filter/map that drops items
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
function detectFilterTruncation(
|
|
292
|
+
tree: Parser.Tree,
|
|
293
|
+
source: string,
|
|
294
|
+
filePath: string
|
|
295
|
+
): ASTViolation[] {
|
|
296
|
+
const violations: ASTViolation[] = [];
|
|
297
|
+
|
|
298
|
+
const callExpressions = findNodes(tree.rootNode, ["call_expression"]);
|
|
299
|
+
|
|
300
|
+
for (const call of callExpressions) {
|
|
301
|
+
const funcNode = call.childForFieldName("function");
|
|
302
|
+
if (!funcNode || funcNode.type !== "member_expression") continue;
|
|
303
|
+
|
|
304
|
+
const property = funcNode.childForFieldName("property");
|
|
305
|
+
if (!property) continue;
|
|
306
|
+
|
|
307
|
+
const methodName = nodeText(property, source);
|
|
308
|
+
|
|
309
|
+
// Check .filter((_, i) => i < N) pattern
|
|
310
|
+
if (methodName === "filter") {
|
|
311
|
+
const args = call.childForFieldName("arguments");
|
|
312
|
+
if (args) {
|
|
313
|
+
const argsText = nodeText(args, source);
|
|
314
|
+
// Pattern: filter with index comparison
|
|
315
|
+
if (/\(\s*_?\s*,\s*\w+\s*\)\s*=>\s*\w+\s*<\s*\d+/.test(argsText)) {
|
|
316
|
+
violations.push({
|
|
317
|
+
file: filePath,
|
|
318
|
+
line: call.startPosition.row + 1,
|
|
319
|
+
column: call.startPosition.column + 1,
|
|
320
|
+
type: "filter-truncation",
|
|
321
|
+
description: ".filter() uses index to limit results (truncates data)",
|
|
322
|
+
code: nodeText(call, source).slice(0, 100),
|
|
323
|
+
severity: "error",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check for chained .slice() after .map()/.filter()
|
|
330
|
+
if (methodName === "slice") {
|
|
331
|
+
const object = funcNode.childForFieldName("object");
|
|
332
|
+
if (object && object.type === "call_expression") {
|
|
333
|
+
const innerFunc = object.childForFieldName("function");
|
|
334
|
+
if (innerFunc && innerFunc.type === "member_expression") {
|
|
335
|
+
const innerMethod = innerFunc.childForFieldName("property");
|
|
336
|
+
if (innerMethod) {
|
|
337
|
+
const innerMethodName = nodeText(innerMethod, source);
|
|
338
|
+
if (["map", "filter", "flatMap", "reduce"].includes(innerMethodName)) {
|
|
339
|
+
violations.push({
|
|
340
|
+
file: filePath,
|
|
341
|
+
line: call.startPosition.row + 1,
|
|
342
|
+
column: call.startPosition.column + 1,
|
|
343
|
+
type: "chained-truncation",
|
|
344
|
+
description: `.${innerMethodName}() results are truncated with .slice()`,
|
|
345
|
+
code: nodeText(call, source).slice(0, 100),
|
|
346
|
+
severity: "error",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return violations;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// DETECTION: Conditional early return with truncation
|
|
360
|
+
// ============================================================================
|
|
361
|
+
|
|
362
|
+
function detectConditionalTruncation(
|
|
363
|
+
tree: Parser.Tree,
|
|
364
|
+
source: string,
|
|
365
|
+
filePath: string
|
|
366
|
+
): ASTViolation[] {
|
|
367
|
+
const violations: ASTViolation[] = [];
|
|
368
|
+
|
|
369
|
+
// Find if statements
|
|
370
|
+
const ifStatements = findNodes(tree.rootNode, ["if_statement"]);
|
|
371
|
+
|
|
372
|
+
for (const ifStmt of ifStatements) {
|
|
373
|
+
const condition = ifStmt.childForFieldName("condition");
|
|
374
|
+
const consequence = ifStmt.childForFieldName("consequence");
|
|
375
|
+
|
|
376
|
+
if (!condition || !consequence) continue;
|
|
377
|
+
|
|
378
|
+
const condText = nodeText(condition, source);
|
|
379
|
+
|
|
380
|
+
// Check for length comparison in condition
|
|
381
|
+
if (/\.length\s*>\s*\d+/.test(condText)) {
|
|
382
|
+
// Check if consequence contains return with slice
|
|
383
|
+
const returns = findNodes(consequence, ["return_statement"]);
|
|
384
|
+
for (const ret of returns) {
|
|
385
|
+
const retText = nodeText(ret, source);
|
|
386
|
+
if (/\.slice\s*\(/.test(retText)) {
|
|
387
|
+
violations.push({
|
|
388
|
+
file: filePath,
|
|
389
|
+
line: ifStmt.startPosition.row + 1,
|
|
390
|
+
column: ifStmt.startPosition.column + 1,
|
|
391
|
+
type: "conditional-return-truncation",
|
|
392
|
+
description: "Conditional early return truncates data when length exceeds limit",
|
|
393
|
+
code: `if ${condText} { ${retText.slice(0, 50)}... }`,
|
|
394
|
+
severity: "error",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return violations;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// DETECTION: Assignment truncation (result = x.slice())
|
|
406
|
+
// ============================================================================
|
|
407
|
+
|
|
408
|
+
function detectAssignmentTruncation(
|
|
409
|
+
tree: Parser.Tree,
|
|
410
|
+
source: string,
|
|
411
|
+
filePath: string
|
|
412
|
+
): ASTViolation[] {
|
|
413
|
+
const violations: ASTViolation[] = [];
|
|
414
|
+
|
|
415
|
+
// Find assignments and variable declarations
|
|
416
|
+
const assignments = [
|
|
417
|
+
...findNodes(tree.rootNode, ["assignment_expression"]),
|
|
418
|
+
...findNodes(tree.rootNode, ["variable_declarator"]),
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
for (const assign of assignments) {
|
|
422
|
+
// Get the left side (variable name)
|
|
423
|
+
const left =
|
|
424
|
+
assign.childForFieldName("left") || assign.childForFieldName("name");
|
|
425
|
+
if (!left) continue;
|
|
426
|
+
|
|
427
|
+
const varName = nodeText(left, source);
|
|
428
|
+
|
|
429
|
+
// Check if it's an LLM context variable being assigned
|
|
430
|
+
if (!LLM_CONTEXT_VARS.has(varName)) continue;
|
|
431
|
+
|
|
432
|
+
// Get the right side (value)
|
|
433
|
+
const right =
|
|
434
|
+
assign.childForFieldName("right") || assign.childForFieldName("value");
|
|
435
|
+
if (!right) continue;
|
|
436
|
+
|
|
437
|
+
// Check if right side contains truncation
|
|
438
|
+
const calls = findNodes(right, ["call_expression"]);
|
|
439
|
+
for (const call of calls) {
|
|
440
|
+
const funcNode = call.childForFieldName("function");
|
|
441
|
+
if (funcNode && funcNode.type === "member_expression") {
|
|
442
|
+
const property = funcNode.childForFieldName("property");
|
|
443
|
+
if (property && TRUNCATION_METHODS.has(nodeText(property, source))) {
|
|
444
|
+
violations.push({
|
|
445
|
+
file: filePath,
|
|
446
|
+
line: assign.startPosition.row + 1,
|
|
447
|
+
column: assign.startPosition.column + 1,
|
|
448
|
+
type: "assignment-truncation",
|
|
449
|
+
description: `LLM context variable '${varName}' assigned truncated value`,
|
|
450
|
+
code: nodeText(assign, source).slice(0, 100),
|
|
451
|
+
severity: "error",
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return violations;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// MAIN ANALYZER
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
export function analyzeFile(filePath: string): ASTViolation[] {
|
|
466
|
+
if (!existsSync(filePath)) return [];
|
|
467
|
+
|
|
468
|
+
const parser = getParser(filePath);
|
|
469
|
+
if (!parser) return [];
|
|
470
|
+
|
|
471
|
+
const source = readFileSync(filePath, "utf-8");
|
|
472
|
+
const tree = parser.parse(source);
|
|
473
|
+
|
|
474
|
+
const violations: ASTViolation[] = [
|
|
475
|
+
...detectReturnTruncation(tree, source, filePath),
|
|
476
|
+
...detectContextVarTruncation(tree, source, filePath),
|
|
477
|
+
...detectLoopTruncation(tree, source, filePath),
|
|
478
|
+
...detectFilterTruncation(tree, source, filePath),
|
|
479
|
+
...detectConditionalTruncation(tree, source, filePath),
|
|
480
|
+
...detectAssignmentTruncation(tree, source, filePath),
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
return violations;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function analyzeFiles(filePaths: string[]): ASTViolation[] {
|
|
487
|
+
const allViolations: ASTViolation[] = [];
|
|
488
|
+
|
|
489
|
+
for (const filePath of filePaths) {
|
|
490
|
+
const violations = analyzeFile(filePath);
|
|
491
|
+
allViolations.push(...violations);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return allViolations;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function formatASTViolations(violations: ASTViolation[]): string {
|
|
498
|
+
if (violations.length === 0) return "";
|
|
499
|
+
|
|
500
|
+
const lines: string[] = [];
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push("========================================");
|
|
503
|
+
lines.push(" AST-BASED TRUNCATION VIOLATIONS");
|
|
504
|
+
lines.push("========================================");
|
|
505
|
+
lines.push("");
|
|
506
|
+
|
|
507
|
+
// Group by file
|
|
508
|
+
const byFile = new Map<string, ASTViolation[]>();
|
|
509
|
+
for (const v of violations) {
|
|
510
|
+
const existing = byFile.get(v.file) || [];
|
|
511
|
+
existing.push(v);
|
|
512
|
+
byFile.set(v.file, existing);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const [file, fileViolations] of byFile) {
|
|
516
|
+
lines.push(`📄 ${file}`);
|
|
517
|
+
for (const v of fileViolations) {
|
|
518
|
+
const severity = v.severity === "error" ? "❌" : "⚠️";
|
|
519
|
+
lines.push(` ${severity} Line ${v.line}: ${v.type}`);
|
|
520
|
+
lines.push(` ${v.description}`);
|
|
521
|
+
lines.push(` Code: ${v.code}`);
|
|
522
|
+
lines.push("");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
lines.push("========================================");
|
|
527
|
+
return lines.join("\n");
|
|
528
|
+
}
|