circle-ir 3.9.7 → 3.9.10
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/dist/analysis/passes/broad-catch-pass.d.ts +29 -0
- package/dist/analysis/passes/broad-catch-pass.js +79 -0
- package/dist/analysis/passes/broad-catch-pass.js.map +1 -0
- package/dist/analysis/passes/deep-inheritance-pass.d.ts +30 -0
- package/dist/analysis/passes/deep-inheritance-pass.js +82 -0
- package/dist/analysis/passes/deep-inheritance-pass.js.map +1 -0
- package/dist/analysis/passes/double-close-pass.d.ts +33 -0
- package/dist/analysis/passes/double-close-pass.js +109 -0
- package/dist/analysis/passes/double-close-pass.js.map +1 -0
- package/dist/analysis/passes/infinite-loop-pass.d.ts +31 -0
- package/dist/analysis/passes/infinite-loop-pass.js +126 -0
- package/dist/analysis/passes/infinite-loop-pass.js.map +1 -0
- package/dist/analysis/passes/react-inline-jsx-pass.d.ts +36 -0
- package/dist/analysis/passes/react-inline-jsx-pass.js +140 -0
- package/dist/analysis/passes/react-inline-jsx-pass.js.map +1 -0
- package/dist/analysis/passes/redundant-loop-pass.d.ts +30 -0
- package/dist/analysis/passes/redundant-loop-pass.js +146 -0
- package/dist/analysis/passes/redundant-loop-pass.js.map +1 -0
- package/dist/analysis/passes/serial-await-pass.d.ts +36 -0
- package/dist/analysis/passes/serial-await-pass.js +132 -0
- package/dist/analysis/passes/serial-await-pass.js.map +1 -0
- package/dist/analysis/passes/sink-filter-pass.js +7 -1
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/swallowed-exception-pass.d.ts +35 -0
- package/dist/analysis/passes/swallowed-exception-pass.js +103 -0
- package/dist/analysis/passes/swallowed-exception-pass.js.map +1 -0
- package/dist/analysis/passes/unbounded-collection-pass.d.ts +32 -0
- package/dist/analysis/passes/unbounded-collection-pass.js +128 -0
- package/dist/analysis/passes/unbounded-collection-pass.js.map +1 -0
- package/dist/analysis/passes/unhandled-exception-pass.d.ts +34 -0
- package/dist/analysis/passes/unhandled-exception-pass.js +123 -0
- package/dist/analysis/passes/unhandled-exception-pass.js.map +1 -0
- package/dist/analysis/passes/use-after-close-pass.d.ts +30 -0
- package/dist/analysis/passes/use-after-close-pass.js +100 -0
- package/dist/analysis/passes/use-after-close-pass.js.map +1 -0
- package/dist/analysis/taint-matcher.js +1 -0
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +12 -1
- package/dist/analyzer.js +34 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +1035 -3
- package/dist/core/circle-ir-core.cjs +2 -1
- package/dist/core/circle-ir-core.js +2 -1
- package/dist/graph/dominator-graph.d.ts +53 -0
- package/dist/graph/dominator-graph.js +256 -0
- package/dist/graph/dominator-graph.js.map +1 -0
- package/dist/graph/exception-flow-graph.d.ts +44 -0
- package/dist/graph/exception-flow-graph.js +75 -0
- package/dist/graph/exception-flow-graph.js.map +1 -0
- package/dist/graph/index.d.ts +2 -0
- package/dist/graph/index.js +2 -0
- package/dist/graph/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -10483,7 +10483,8 @@ function findSinks(calls, patterns) {
|
|
|
10483
10483
|
cwe: pattern.cwe,
|
|
10484
10484
|
location,
|
|
10485
10485
|
line: call.location.line,
|
|
10486
|
-
confidence
|
|
10486
|
+
confidence,
|
|
10487
|
+
method: call.method_name
|
|
10487
10488
|
});
|
|
10488
10489
|
}
|
|
10489
10490
|
}
|
|
@@ -11278,6 +11279,71 @@ var CodeGraph = class {
|
|
|
11278
11279
|
}
|
|
11279
11280
|
};
|
|
11280
11281
|
|
|
11282
|
+
// src/graph/exception-flow-graph.ts
|
|
11283
|
+
var ExceptionFlowGraph = class {
|
|
11284
|
+
/** All try/catch pairs found in the CFG. */
|
|
11285
|
+
pairs;
|
|
11286
|
+
/** Block IDs that are catch-handler entry blocks. */
|
|
11287
|
+
catchEntryIds;
|
|
11288
|
+
/** Block IDs that are try-body entry blocks. */
|
|
11289
|
+
tryEntryIds;
|
|
11290
|
+
tryCatchMap;
|
|
11291
|
+
// tryEntryId → [catchEntryId, …]
|
|
11292
|
+
catchTryMap;
|
|
11293
|
+
// catchEntryId → tryEntryId
|
|
11294
|
+
constructor(cfg, blockById) {
|
|
11295
|
+
this.pairs = [];
|
|
11296
|
+
this.catchEntryIds = /* @__PURE__ */ new Set();
|
|
11297
|
+
this.tryEntryIds = /* @__PURE__ */ new Set();
|
|
11298
|
+
this.tryCatchMap = /* @__PURE__ */ new Map();
|
|
11299
|
+
this.catchTryMap = /* @__PURE__ */ new Map();
|
|
11300
|
+
for (const edge of cfg.edges) {
|
|
11301
|
+
if (edge.type !== "exception") continue;
|
|
11302
|
+
const tryBlock = blockById.get(edge.from);
|
|
11303
|
+
const catchBlock = blockById.get(edge.to);
|
|
11304
|
+
if (!tryBlock || !catchBlock) continue;
|
|
11305
|
+
this.tryEntryIds.add(edge.from);
|
|
11306
|
+
this.catchEntryIds.add(edge.to);
|
|
11307
|
+
const catches = this.tryCatchMap.get(edge.from) ?? [];
|
|
11308
|
+
catches.push(edge.to);
|
|
11309
|
+
this.tryCatchMap.set(edge.from, catches);
|
|
11310
|
+
this.catchTryMap.set(edge.to, edge.from);
|
|
11311
|
+
this.pairs.push({
|
|
11312
|
+
tryEntryId: edge.from,
|
|
11313
|
+
catchEntryId: edge.to,
|
|
11314
|
+
tryBlock,
|
|
11315
|
+
catchBlock
|
|
11316
|
+
});
|
|
11317
|
+
}
|
|
11318
|
+
}
|
|
11319
|
+
/** True if at least one try/catch pair was found. */
|
|
11320
|
+
get hasTryCatch() {
|
|
11321
|
+
return this.pairs.length > 0;
|
|
11322
|
+
}
|
|
11323
|
+
/** True if the given block ID is a catch-handler entry block. */
|
|
11324
|
+
isCatchEntry(blockId) {
|
|
11325
|
+
return this.catchEntryIds.has(blockId);
|
|
11326
|
+
}
|
|
11327
|
+
/** True if the given block ID is a try-body entry block. */
|
|
11328
|
+
isTryEntry(blockId) {
|
|
11329
|
+
return this.tryEntryIds.has(blockId);
|
|
11330
|
+
}
|
|
11331
|
+
/**
|
|
11332
|
+
* Returns the catch-entry block IDs for the given try-entry block.
|
|
11333
|
+
* Multiple values mean multiple catch clauses for the same try.
|
|
11334
|
+
*/
|
|
11335
|
+
catchBlocksFor(tryEntryId) {
|
|
11336
|
+
return this.tryCatchMap.get(tryEntryId) ?? [];
|
|
11337
|
+
}
|
|
11338
|
+
/**
|
|
11339
|
+
* Returns the try-entry block ID corresponding to a catch-entry block,
|
|
11340
|
+
* or `undefined` if the block is not a catch entry.
|
|
11341
|
+
*/
|
|
11342
|
+
tryBlockFor(catchEntryId) {
|
|
11343
|
+
return this.catchTryMap.get(catchEntryId);
|
|
11344
|
+
}
|
|
11345
|
+
};
|
|
11346
|
+
|
|
11281
11347
|
// src/graph/analysis-pass.ts
|
|
11282
11348
|
var AnalysisPipeline = class {
|
|
11283
11349
|
passes = [];
|
|
@@ -17422,7 +17488,8 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
17422
17488
|
return sinks.filter((sink) => {
|
|
17423
17489
|
const callsAtSink = callsByLine.get(sink.line) ?? [];
|
|
17424
17490
|
const isInSynchronizedBlock = synchronizedLines?.has(sink.line) ?? false;
|
|
17425
|
-
|
|
17491
|
+
const relevantCalls = sink.method ? callsAtSink.filter((c) => c.method_name === sink.method) : callsAtSink;
|
|
17492
|
+
for (const call of relevantCalls) {
|
|
17426
17493
|
let allArgsAreClean = true;
|
|
17427
17494
|
const methodName = call.in_method;
|
|
17428
17495
|
for (const arg of call.arguments) {
|
|
@@ -19150,6 +19217,971 @@ var StaleDocRefPass = class {
|
|
|
19150
19217
|
}
|
|
19151
19218
|
};
|
|
19152
19219
|
|
|
19220
|
+
// src/analysis/passes/infinite-loop-pass.ts
|
|
19221
|
+
var EXIT_KEYWORDS = /\b(return|throw|raise|break|System\.exit|process\.exit|os\._exit|exit!\()\b/;
|
|
19222
|
+
var InfiniteLoopPass = class {
|
|
19223
|
+
name = "infinite-loop";
|
|
19224
|
+
category = "reliability";
|
|
19225
|
+
run(ctx) {
|
|
19226
|
+
const { graph, code, language } = ctx;
|
|
19227
|
+
if (language === "bash") {
|
|
19228
|
+
return { potentialInfiniteLoops: [] };
|
|
19229
|
+
}
|
|
19230
|
+
const { blocks, edges } = graph.ir.cfg;
|
|
19231
|
+
if (blocks.length === 0) return { potentialInfiniteLoops: [] };
|
|
19232
|
+
const file = graph.ir.meta.file;
|
|
19233
|
+
const codeLines = code.split("\n");
|
|
19234
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
19235
|
+
for (const edge of edges) {
|
|
19236
|
+
const list = outgoing.get(edge.from) ?? [];
|
|
19237
|
+
list.push({ to: edge.to, type: edge.type });
|
|
19238
|
+
outgoing.set(edge.from, list);
|
|
19239
|
+
}
|
|
19240
|
+
const backEdges = edges.filter((e) => e.type === "back");
|
|
19241
|
+
const potentialInfiniteLoops = [];
|
|
19242
|
+
const reportedHeaders = /* @__PURE__ */ new Set();
|
|
19243
|
+
for (const backEdge of backEdges) {
|
|
19244
|
+
const headerId = backEdge.to;
|
|
19245
|
+
const tailId = backEdge.from;
|
|
19246
|
+
const header = graph.blockById.get(headerId);
|
|
19247
|
+
const tail = graph.blockById.get(tailId);
|
|
19248
|
+
if (!header || !tail) continue;
|
|
19249
|
+
if (reportedHeaders.has(headerId)) continue;
|
|
19250
|
+
const bodyIds = /* @__PURE__ */ new Set();
|
|
19251
|
+
const queue = [headerId];
|
|
19252
|
+
bodyIds.add(headerId);
|
|
19253
|
+
while (queue.length > 0) {
|
|
19254
|
+
const cur = queue.shift();
|
|
19255
|
+
for (const { to, type } of outgoing.get(cur) ?? []) {
|
|
19256
|
+
if (type === "back") continue;
|
|
19257
|
+
if (!bodyIds.has(to)) {
|
|
19258
|
+
bodyIds.add(to);
|
|
19259
|
+
queue.push(to);
|
|
19260
|
+
}
|
|
19261
|
+
}
|
|
19262
|
+
if (cur === tailId) break;
|
|
19263
|
+
}
|
|
19264
|
+
let hasExit = false;
|
|
19265
|
+
for (const bodyId of bodyIds) {
|
|
19266
|
+
for (const { to, type } of outgoing.get(bodyId) ?? []) {
|
|
19267
|
+
if (type === "back") continue;
|
|
19268
|
+
if (!bodyIds.has(to)) {
|
|
19269
|
+
hasExit = true;
|
|
19270
|
+
break;
|
|
19271
|
+
}
|
|
19272
|
+
}
|
|
19273
|
+
if (hasExit) break;
|
|
19274
|
+
}
|
|
19275
|
+
if (hasExit) continue;
|
|
19276
|
+
const bodyStart = header.start_line;
|
|
19277
|
+
const bodyEnd = tail.end_line;
|
|
19278
|
+
let hasKeywordExit = false;
|
|
19279
|
+
for (let ln = bodyStart; ln <= bodyEnd && ln <= codeLines.length; ln++) {
|
|
19280
|
+
if (EXIT_KEYWORDS.test(codeLines[ln - 1] ?? "")) {
|
|
19281
|
+
hasKeywordExit = true;
|
|
19282
|
+
break;
|
|
19283
|
+
}
|
|
19284
|
+
}
|
|
19285
|
+
if (hasKeywordExit) continue;
|
|
19286
|
+
reportedHeaders.add(headerId);
|
|
19287
|
+
potentialInfiniteLoops.push({ headerLine: header.start_line, bodyEndLine: bodyEnd });
|
|
19288
|
+
const loc = bodyStart === bodyEnd ? `line ${bodyStart}` : `lines ${bodyStart}\u2013${bodyEnd}`;
|
|
19289
|
+
ctx.addFinding({
|
|
19290
|
+
id: `infinite-loop-${file}-${header.start_line}`,
|
|
19291
|
+
pass: this.name,
|
|
19292
|
+
category: this.category,
|
|
19293
|
+
rule_id: this.name,
|
|
19294
|
+
cwe: "CWE-835",
|
|
19295
|
+
severity: "medium",
|
|
19296
|
+
level: "warning",
|
|
19297
|
+
message: `Potential infinite loop: no reachable break, return, or throw found in loop body (${loc})`,
|
|
19298
|
+
file,
|
|
19299
|
+
line: header.start_line,
|
|
19300
|
+
end_line: bodyEnd > header.start_line ? bodyEnd : void 0,
|
|
19301
|
+
fix: "Ensure the loop has a reachable exit condition (break, return, or throw) on all paths"
|
|
19302
|
+
});
|
|
19303
|
+
}
|
|
19304
|
+
return { potentialInfiniteLoops };
|
|
19305
|
+
}
|
|
19306
|
+
};
|
|
19307
|
+
|
|
19308
|
+
// src/analysis/passes/deep-inheritance-pass.ts
|
|
19309
|
+
var DEPTH_THRESHOLD = 5;
|
|
19310
|
+
var CYCLE_GUARD = 20;
|
|
19311
|
+
var DeepInheritancePass = class {
|
|
19312
|
+
name = "deep-inheritance";
|
|
19313
|
+
category = "architecture";
|
|
19314
|
+
run(ctx) {
|
|
19315
|
+
const { graph, language } = ctx;
|
|
19316
|
+
if (language === "rust" || language === "bash") {
|
|
19317
|
+
return { deepClasses: [] };
|
|
19318
|
+
}
|
|
19319
|
+
const file = graph.ir.meta.file;
|
|
19320
|
+
const types = graph.ir.types;
|
|
19321
|
+
if (types.length === 0) return { deepClasses: [] };
|
|
19322
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
19323
|
+
for (const typeInfo of types) {
|
|
19324
|
+
if (typeInfo.extends) {
|
|
19325
|
+
const parentName = typeInfo.extends.replace(/<.*>/, "").trim();
|
|
19326
|
+
if (parentName) {
|
|
19327
|
+
parentMap.set(typeInfo.name, parentName);
|
|
19328
|
+
}
|
|
19329
|
+
}
|
|
19330
|
+
}
|
|
19331
|
+
const deepClasses = [];
|
|
19332
|
+
for (const typeInfo of types) {
|
|
19333
|
+
if (typeInfo.kind !== "class") continue;
|
|
19334
|
+
if (typeInfo.start_line <= 0) continue;
|
|
19335
|
+
let depth = 0;
|
|
19336
|
+
let current = parentMap.get(typeInfo.name);
|
|
19337
|
+
const visited = /* @__PURE__ */ new Set([typeInfo.name]);
|
|
19338
|
+
while (current !== void 0 && depth < CYCLE_GUARD) {
|
|
19339
|
+
depth++;
|
|
19340
|
+
if (visited.has(current)) break;
|
|
19341
|
+
visited.add(current);
|
|
19342
|
+
current = parentMap.get(current);
|
|
19343
|
+
}
|
|
19344
|
+
if (depth > DEPTH_THRESHOLD) {
|
|
19345
|
+
deepClasses.push({ className: typeInfo.name, depth, line: typeInfo.start_line });
|
|
19346
|
+
ctx.addFinding({
|
|
19347
|
+
id: `deep-inheritance-${file}-${typeInfo.start_line}`,
|
|
19348
|
+
pass: this.name,
|
|
19349
|
+
category: this.category,
|
|
19350
|
+
rule_id: this.name,
|
|
19351
|
+
cwe: "CWE-1086",
|
|
19352
|
+
severity: "low",
|
|
19353
|
+
level: "warning",
|
|
19354
|
+
message: `Deep inheritance: class \`${typeInfo.name}\` has inheritance depth ${depth} (threshold: ${DEPTH_THRESHOLD})`,
|
|
19355
|
+
file,
|
|
19356
|
+
line: typeInfo.start_line,
|
|
19357
|
+
fix: `Refactor to prefer composition over inheritance. Consider extracting shared behaviour into interfaces or mixins.`,
|
|
19358
|
+
evidence: { depth, threshold: DEPTH_THRESHOLD }
|
|
19359
|
+
});
|
|
19360
|
+
}
|
|
19361
|
+
}
|
|
19362
|
+
return { deepClasses };
|
|
19363
|
+
}
|
|
19364
|
+
};
|
|
19365
|
+
|
|
19366
|
+
// src/analysis/passes/redundant-loop-pass.ts
|
|
19367
|
+
var LENGTH_PATTERN = /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\.\s*(?:length|size\(\)|count\(\))/g;
|
|
19368
|
+
var OBJECT_STATIC_PATTERN = /\bObject\s*\.\s*(?:keys|values|entries)\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*\)/g;
|
|
19369
|
+
var MATH_PATTERN = /\bMath\s*\.\s*(?:sqrt|pow|abs|floor|ceil|round|log|log2|log10)\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*[,)]/g;
|
|
19370
|
+
var RedundantLoopPass = class {
|
|
19371
|
+
name = "redundant-loop-computation";
|
|
19372
|
+
category = "performance";
|
|
19373
|
+
run(ctx) {
|
|
19374
|
+
const { graph, code, language } = ctx;
|
|
19375
|
+
if (language === "bash") {
|
|
19376
|
+
return { invariants: [] };
|
|
19377
|
+
}
|
|
19378
|
+
const file = graph.ir.meta.file;
|
|
19379
|
+
const codeLines = code.split("\n");
|
|
19380
|
+
const loops = graph.loopBodies();
|
|
19381
|
+
if (loops.length === 0) return { invariants: [] };
|
|
19382
|
+
const invariants = [];
|
|
19383
|
+
const reported = /* @__PURE__ */ new Set();
|
|
19384
|
+
for (const loop of loops) {
|
|
19385
|
+
const { start_line, end_line } = loop;
|
|
19386
|
+
const modifiedVars = /* @__PURE__ */ new Set();
|
|
19387
|
+
for (const def of graph.ir.dfg.defs) {
|
|
19388
|
+
if (def.line >= start_line && def.line <= end_line) {
|
|
19389
|
+
modifiedVars.add(def.variable);
|
|
19390
|
+
}
|
|
19391
|
+
}
|
|
19392
|
+
for (let ln = start_line; ln <= end_line && ln <= codeLines.length; ln++) {
|
|
19393
|
+
const lineText = codeLines[ln - 1] ?? "";
|
|
19394
|
+
if (lineText.trim() === "") continue;
|
|
19395
|
+
LENGTH_PATTERN.lastIndex = 0;
|
|
19396
|
+
let m;
|
|
19397
|
+
while ((m = LENGTH_PATTERN.exec(lineText)) !== null) {
|
|
19398
|
+
const varName = m[1];
|
|
19399
|
+
if (modifiedVars.has(varName)) continue;
|
|
19400
|
+
const expr = m[0];
|
|
19401
|
+
const key = `${ln}-${expr}`;
|
|
19402
|
+
if (reported.has(key)) continue;
|
|
19403
|
+
reported.add(key);
|
|
19404
|
+
invariants.push({ line: ln, expression: expr, variable: varName });
|
|
19405
|
+
ctx.addFinding({
|
|
19406
|
+
id: `redundant-loop-computation-${file}-${ln}`,
|
|
19407
|
+
pass: this.name,
|
|
19408
|
+
category: this.category,
|
|
19409
|
+
rule_id: this.name,
|
|
19410
|
+
cwe: "CWE-1050",
|
|
19411
|
+
severity: "low",
|
|
19412
|
+
level: "note",
|
|
19413
|
+
message: `Loop-invariant computation: \`${expr}\` is recomputed on every iteration; hoist outside loop`,
|
|
19414
|
+
file,
|
|
19415
|
+
line: ln,
|
|
19416
|
+
snippet: lineText.trim(),
|
|
19417
|
+
fix: `Compute \`${expr}\` once before the loop and use the cached value inside.`,
|
|
19418
|
+
evidence: { variable: varName, loop_start: start_line, loop_end: end_line }
|
|
19419
|
+
});
|
|
19420
|
+
}
|
|
19421
|
+
OBJECT_STATIC_PATTERN.lastIndex = 0;
|
|
19422
|
+
while ((m = OBJECT_STATIC_PATTERN.exec(lineText)) !== null) {
|
|
19423
|
+
const varName = m[1];
|
|
19424
|
+
if (modifiedVars.has(varName)) continue;
|
|
19425
|
+
const expr = m[0];
|
|
19426
|
+
const key = `${ln}-${expr}`;
|
|
19427
|
+
if (reported.has(key)) continue;
|
|
19428
|
+
reported.add(key);
|
|
19429
|
+
invariants.push({ line: ln, expression: expr, variable: varName });
|
|
19430
|
+
ctx.addFinding({
|
|
19431
|
+
id: `redundant-loop-computation-${file}-${ln}-obj`,
|
|
19432
|
+
pass: this.name,
|
|
19433
|
+
category: this.category,
|
|
19434
|
+
rule_id: this.name,
|
|
19435
|
+
cwe: "CWE-1050",
|
|
19436
|
+
severity: "low",
|
|
19437
|
+
level: "note",
|
|
19438
|
+
message: `Loop-invariant computation: \`${expr}\` allocates a new array on every iteration; hoist outside loop`,
|
|
19439
|
+
file,
|
|
19440
|
+
line: ln,
|
|
19441
|
+
snippet: lineText.trim(),
|
|
19442
|
+
fix: `Compute \`${expr}\` once before the loop.`,
|
|
19443
|
+
evidence: { variable: varName, loop_start: start_line, loop_end: end_line }
|
|
19444
|
+
});
|
|
19445
|
+
}
|
|
19446
|
+
MATH_PATTERN.lastIndex = 0;
|
|
19447
|
+
while ((m = MATH_PATTERN.exec(lineText)) !== null) {
|
|
19448
|
+
const varName = m[1];
|
|
19449
|
+
if (modifiedVars.has(varName)) continue;
|
|
19450
|
+
const expr = m[0].replace(/[,)]?\s*$/, ")");
|
|
19451
|
+
const key = `${ln}-${expr}`;
|
|
19452
|
+
if (reported.has(key)) continue;
|
|
19453
|
+
reported.add(key);
|
|
19454
|
+
invariants.push({ line: ln, expression: expr, variable: varName });
|
|
19455
|
+
ctx.addFinding({
|
|
19456
|
+
id: `redundant-loop-computation-${file}-${ln}-math`,
|
|
19457
|
+
pass: this.name,
|
|
19458
|
+
category: this.category,
|
|
19459
|
+
rule_id: this.name,
|
|
19460
|
+
cwe: "CWE-1050",
|
|
19461
|
+
severity: "low",
|
|
19462
|
+
level: "note",
|
|
19463
|
+
message: `Loop-invariant computation: \`${expr}\` is recomputed on every iteration; hoist outside loop`,
|
|
19464
|
+
file,
|
|
19465
|
+
line: ln,
|
|
19466
|
+
snippet: lineText.trim(),
|
|
19467
|
+
fix: `Compute \`${expr}\` once before the loop.`,
|
|
19468
|
+
evidence: { variable: varName, loop_start: start_line, loop_end: end_line }
|
|
19469
|
+
});
|
|
19470
|
+
}
|
|
19471
|
+
}
|
|
19472
|
+
}
|
|
19473
|
+
return { invariants };
|
|
19474
|
+
}
|
|
19475
|
+
};
|
|
19476
|
+
|
|
19477
|
+
// src/analysis/passes/unbounded-collection-pass.ts
|
|
19478
|
+
var GROW_METHODS = {
|
|
19479
|
+
java: /* @__PURE__ */ new Set(["add", "put", "offer", "push", "addAll", "addFirst", "addLast", "enqueue", "insert"]),
|
|
19480
|
+
javascript: /* @__PURE__ */ new Set(["push", "set", "add", "unshift", "append", "prepend"]),
|
|
19481
|
+
typescript: /* @__PURE__ */ new Set(["push", "set", "add", "unshift", "append", "prepend"]),
|
|
19482
|
+
python: /* @__PURE__ */ new Set(["append", "extend", "update", "add", "insert"]),
|
|
19483
|
+
rust: /* @__PURE__ */ new Set(["push", "insert", "push_back", "push_front"])
|
|
19484
|
+
};
|
|
19485
|
+
var SHRINK_METHODS = /* @__PURE__ */ new Set([
|
|
19486
|
+
"clear",
|
|
19487
|
+
"remove",
|
|
19488
|
+
"delete",
|
|
19489
|
+
"shift",
|
|
19490
|
+
"pop",
|
|
19491
|
+
"removeFirst",
|
|
19492
|
+
"removeLast",
|
|
19493
|
+
"poll",
|
|
19494
|
+
"pollFirst",
|
|
19495
|
+
"pollLast",
|
|
19496
|
+
"dequeue",
|
|
19497
|
+
"discard",
|
|
19498
|
+
"drain"
|
|
19499
|
+
]);
|
|
19500
|
+
var SIZE_LIMIT_RE = /\b(?:size|length|count|len)\s*\(\)?\s*[<>]=?\s*\d|\b(?:MAX|LIMIT|CAPACITY|MAX_SIZE)\b/i;
|
|
19501
|
+
var UnboundedCollectionPass = class {
|
|
19502
|
+
name = "unbounded-collection";
|
|
19503
|
+
category = "performance";
|
|
19504
|
+
run(ctx) {
|
|
19505
|
+
const { graph, code, language } = ctx;
|
|
19506
|
+
if (language === "bash") {
|
|
19507
|
+
return { unboundedCollections: [] };
|
|
19508
|
+
}
|
|
19509
|
+
const growMethods = GROW_METHODS[language] ?? GROW_METHODS["javascript"];
|
|
19510
|
+
const file = graph.ir.meta.file;
|
|
19511
|
+
const codeLines = code.split("\n");
|
|
19512
|
+
const loops = graph.loopBodies();
|
|
19513
|
+
if (loops.length === 0) return { unboundedCollections: [] };
|
|
19514
|
+
const unboundedCollections = [];
|
|
19515
|
+
const reported = /* @__PURE__ */ new Set();
|
|
19516
|
+
for (const loop of loops) {
|
|
19517
|
+
const { start_line, end_line } = loop;
|
|
19518
|
+
const loopSource = codeLines.slice(start_line - 1, end_line).join("\n");
|
|
19519
|
+
const growCalls = [];
|
|
19520
|
+
for (const call of graph.ir.calls) {
|
|
19521
|
+
const ln = call.location.line;
|
|
19522
|
+
if (ln < start_line || ln > end_line) continue;
|
|
19523
|
+
if (!growMethods.has(call.method_name)) continue;
|
|
19524
|
+
if (!call.receiver) continue;
|
|
19525
|
+
if (call.receiver === "this" || call.receiver === "self") continue;
|
|
19526
|
+
growCalls.push({ receiver: call.receiver, line: ln });
|
|
19527
|
+
}
|
|
19528
|
+
if (growCalls.length === 0) continue;
|
|
19529
|
+
const receiverLines = /* @__PURE__ */ new Map();
|
|
19530
|
+
for (const { receiver, line } of growCalls) {
|
|
19531
|
+
if (!receiverLines.has(receiver)) {
|
|
19532
|
+
receiverLines.set(receiver, line);
|
|
19533
|
+
}
|
|
19534
|
+
}
|
|
19535
|
+
for (const [receiver, firstGrowLine] of receiverLines.entries()) {
|
|
19536
|
+
let hasShrink = false;
|
|
19537
|
+
for (const call of graph.ir.calls) {
|
|
19538
|
+
const ln = call.location.line;
|
|
19539
|
+
if (ln < start_line || ln > end_line) continue;
|
|
19540
|
+
if (call.receiver !== receiver) continue;
|
|
19541
|
+
if (SHRINK_METHODS.has(call.method_name)) {
|
|
19542
|
+
hasShrink = true;
|
|
19543
|
+
break;
|
|
19544
|
+
}
|
|
19545
|
+
}
|
|
19546
|
+
if (hasShrink) continue;
|
|
19547
|
+
if (SIZE_LIMIT_RE.test(loopSource)) continue;
|
|
19548
|
+
const key = `${receiver}-${start_line}`;
|
|
19549
|
+
if (reported.has(key)) continue;
|
|
19550
|
+
reported.add(key);
|
|
19551
|
+
unboundedCollections.push({
|
|
19552
|
+
receiver,
|
|
19553
|
+
line: firstGrowLine,
|
|
19554
|
+
loopStart: start_line,
|
|
19555
|
+
loopEnd: end_line
|
|
19556
|
+
});
|
|
19557
|
+
ctx.addFinding({
|
|
19558
|
+
id: `unbounded-collection-${file}-${firstGrowLine}`,
|
|
19559
|
+
pass: this.name,
|
|
19560
|
+
category: this.category,
|
|
19561
|
+
rule_id: this.name,
|
|
19562
|
+
cwe: "CWE-770",
|
|
19563
|
+
severity: "medium",
|
|
19564
|
+
level: "warning",
|
|
19565
|
+
message: `Unbounded collection: \`${receiver}\` grows inside a loop (lines ${start_line}\u2013${end_line}) with no size limit or clear`,
|
|
19566
|
+
file,
|
|
19567
|
+
line: firstGrowLine,
|
|
19568
|
+
fix: `Add a size limit check (e.g., \`if (${receiver}.size() >= MAX) break;\`) or periodically clear/drain \`${receiver}\`.`,
|
|
19569
|
+
evidence: { receiver, loop_start: start_line, loop_end: end_line }
|
|
19570
|
+
});
|
|
19571
|
+
}
|
|
19572
|
+
}
|
|
19573
|
+
return { unboundedCollections };
|
|
19574
|
+
}
|
|
19575
|
+
};
|
|
19576
|
+
|
|
19577
|
+
// src/analysis/passes/serial-await-pass.ts
|
|
19578
|
+
var AWAIT_ASSIGN_RE = /(?:const|let|var)?\s*(\w+)\s*=\s*await\s/;
|
|
19579
|
+
var AWAIT_RE = /\bawait\s/;
|
|
19580
|
+
var SerialAwaitPass = class {
|
|
19581
|
+
name = "serial-await";
|
|
19582
|
+
category = "performance";
|
|
19583
|
+
run(ctx) {
|
|
19584
|
+
const { graph, code, language } = ctx;
|
|
19585
|
+
if (language !== "javascript" && language !== "typescript") {
|
|
19586
|
+
return { serialAwaits: [] };
|
|
19587
|
+
}
|
|
19588
|
+
const file = graph.ir.meta.file;
|
|
19589
|
+
const codeLines = code.split("\n");
|
|
19590
|
+
const totalLines = codeLines.length;
|
|
19591
|
+
const serialAwaits = [];
|
|
19592
|
+
const reportedFunctions = /* @__PURE__ */ new Set();
|
|
19593
|
+
const awaitLines = [];
|
|
19594
|
+
for (let i2 = 0; i2 < totalLines; i2++) {
|
|
19595
|
+
const lineText = codeLines[i2];
|
|
19596
|
+
if (!AWAIT_RE.test(lineText)) continue;
|
|
19597
|
+
const m = AWAIT_ASSIGN_RE.exec(lineText);
|
|
19598
|
+
const boundVar = m ? m[1] : null;
|
|
19599
|
+
awaitLines.push({ line: i2 + 1, boundVar });
|
|
19600
|
+
}
|
|
19601
|
+
if (awaitLines.length < 2) return { serialAwaits: [] };
|
|
19602
|
+
for (let i2 = 0; i2 + 1 < awaitLines.length; i2++) {
|
|
19603
|
+
const a1 = awaitLines[i2];
|
|
19604
|
+
const a2 = awaitLines[i2 + 1];
|
|
19605
|
+
const method1 = graph.methodAtLine(a1.line);
|
|
19606
|
+
const method2 = graph.methodAtLine(a2.line);
|
|
19607
|
+
const methodKey1 = method1 ? `${method1.type.name}.${method1.method.name}.${method1.method.start_line}` : `top.${a1.line}`;
|
|
19608
|
+
const methodKey2 = method2 ? `${method2.type.name}.${method2.method.name}.${method2.method.start_line}` : `top.${a2.line}`;
|
|
19609
|
+
if (methodKey1 !== methodKey2) continue;
|
|
19610
|
+
if (a2.line - a1.line > 4) continue;
|
|
19611
|
+
const line2Text = codeLines[a2.line - 1] ?? "";
|
|
19612
|
+
const line1Text = codeLines[a1.line - 1] ?? "";
|
|
19613
|
+
let dependent = false;
|
|
19614
|
+
if (a1.boundVar && new RegExp(`\\b${a1.boundVar}\\b`).test(line2Text)) {
|
|
19615
|
+
dependent = true;
|
|
19616
|
+
}
|
|
19617
|
+
if (!dependent && a2.boundVar && new RegExp(`\\b${a2.boundVar}\\b`).test(line1Text)) {
|
|
19618
|
+
dependent = true;
|
|
19619
|
+
}
|
|
19620
|
+
if (!dependent) {
|
|
19621
|
+
const defs1 = graph.defsAtLine(a1.line);
|
|
19622
|
+
const defs2 = graph.defsAtLine(a2.line);
|
|
19623
|
+
for (const d1 of defs1) {
|
|
19624
|
+
for (const d2 of defs2) {
|
|
19625
|
+
if (d1.variable === d2.variable) {
|
|
19626
|
+
dependent = true;
|
|
19627
|
+
break;
|
|
19628
|
+
}
|
|
19629
|
+
}
|
|
19630
|
+
if (dependent) break;
|
|
19631
|
+
}
|
|
19632
|
+
}
|
|
19633
|
+
if (dependent) continue;
|
|
19634
|
+
if (reportedFunctions.has(methodKey1)) continue;
|
|
19635
|
+
reportedFunctions.add(methodKey1);
|
|
19636
|
+
const funcLine = method1?.method.start_line ?? a1.line;
|
|
19637
|
+
serialAwaits.push({ functionLine: funcLine, firstAwaitLine: a1.line, secondAwaitLine: a2.line });
|
|
19638
|
+
const expr1 = line1Text.trim().replace(/^(?:const|let|var)\s+/, "");
|
|
19639
|
+
const expr2 = line2Text.trim().replace(/^(?:const|let|var)\s+/, "");
|
|
19640
|
+
ctx.addFinding({
|
|
19641
|
+
id: `serial-await-${file}-${a1.line}`,
|
|
19642
|
+
pass: this.name,
|
|
19643
|
+
category: this.category,
|
|
19644
|
+
rule_id: this.name,
|
|
19645
|
+
cwe: void 0,
|
|
19646
|
+
severity: "low",
|
|
19647
|
+
level: "note",
|
|
19648
|
+
message: `Serial awaits: \`${expr1}\` (line ${a1.line}) and \`${expr2}\` (line ${a2.line}) have no data dependency; consider using Promise.all()`,
|
|
19649
|
+
file,
|
|
19650
|
+
line: a1.line,
|
|
19651
|
+
end_line: a2.line,
|
|
19652
|
+
fix: `const [result1, result2] = await Promise.all([operation1, operation2]);`,
|
|
19653
|
+
evidence: {
|
|
19654
|
+
first_await_line: a1.line,
|
|
19655
|
+
second_await_line: a2.line,
|
|
19656
|
+
function_line: funcLine
|
|
19657
|
+
}
|
|
19658
|
+
});
|
|
19659
|
+
}
|
|
19660
|
+
return { serialAwaits };
|
|
19661
|
+
}
|
|
19662
|
+
};
|
|
19663
|
+
|
|
19664
|
+
// src/analysis/passes/react-inline-jsx-pass.ts
|
|
19665
|
+
var JSX_COMPONENT_RE = /<[A-Z][A-Za-z0-9]*/;
|
|
19666
|
+
var INLINE_OBJECT_RE = /\s([A-Za-z][A-Za-z0-9_]*)=\{\{/g;
|
|
19667
|
+
var INLINE_ARROW_RE = /\s([A-Za-z][A-Za-z0-9_]*)=\{(?:\(|[A-Za-z_$]).*?=>/g;
|
|
19668
|
+
var INLINE_FUNCTION_RE = /\s([A-Za-z][A-Za-z0-9_]*)=\{function\s*\(/g;
|
|
19669
|
+
var SKIP_PROPS = /* @__PURE__ */ new Set(["style", "key", "ref", "className", "id"]);
|
|
19670
|
+
var ReactInlineJsxPass = class {
|
|
19671
|
+
name = "react-inline-jsx";
|
|
19672
|
+
category = "performance";
|
|
19673
|
+
run(ctx) {
|
|
19674
|
+
const { graph, code, language } = ctx;
|
|
19675
|
+
if (language !== "javascript" && language !== "typescript") {
|
|
19676
|
+
return { inlineProps: [] };
|
|
19677
|
+
}
|
|
19678
|
+
if (!JSX_COMPONENT_RE.test(code)) {
|
|
19679
|
+
return { inlineProps: [] };
|
|
19680
|
+
}
|
|
19681
|
+
const file = graph.ir.meta.file;
|
|
19682
|
+
const codeLines = code.split("\n");
|
|
19683
|
+
const inlineProps = [];
|
|
19684
|
+
for (let i2 = 0; i2 < codeLines.length; i2++) {
|
|
19685
|
+
const lineText = codeLines[i2];
|
|
19686
|
+
const ln = i2 + 1;
|
|
19687
|
+
const trimmed = lineText.trimStart();
|
|
19688
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
19689
|
+
continue;
|
|
19690
|
+
}
|
|
19691
|
+
INLINE_OBJECT_RE.lastIndex = 0;
|
|
19692
|
+
let m;
|
|
19693
|
+
while ((m = INLINE_OBJECT_RE.exec(lineText)) !== null) {
|
|
19694
|
+
const propName = m[1];
|
|
19695
|
+
if (SKIP_PROPS.has(propName)) continue;
|
|
19696
|
+
if (propName.startsWith("data-")) continue;
|
|
19697
|
+
inlineProps.push({ line: ln, propName, kind: "object" });
|
|
19698
|
+
ctx.addFinding({
|
|
19699
|
+
id: `react-inline-jsx-obj-${file}-${ln}`,
|
|
19700
|
+
pass: this.name,
|
|
19701
|
+
category: this.category,
|
|
19702
|
+
rule_id: this.name,
|
|
19703
|
+
cwe: void 0,
|
|
19704
|
+
severity: "low",
|
|
19705
|
+
level: "note",
|
|
19706
|
+
message: `Inline object in JSX prop \`${propName}\` creates a new reference on every render, defeating memoization`,
|
|
19707
|
+
file,
|
|
19708
|
+
line: ln,
|
|
19709
|
+
snippet: lineText.trim(),
|
|
19710
|
+
fix: `Extract the object literal into a \`useMemo\` hook or a module-level constant, then pass the reference: \`${propName}={myConstObject}\`.`
|
|
19711
|
+
});
|
|
19712
|
+
}
|
|
19713
|
+
INLINE_ARROW_RE.lastIndex = 0;
|
|
19714
|
+
while ((m = INLINE_ARROW_RE.exec(lineText)) !== null) {
|
|
19715
|
+
const propName = m[1];
|
|
19716
|
+
if (SKIP_PROPS.has(propName)) continue;
|
|
19717
|
+
if (propName.startsWith("data")) continue;
|
|
19718
|
+
inlineProps.push({ line: ln, propName, kind: "arrow" });
|
|
19719
|
+
ctx.addFinding({
|
|
19720
|
+
id: `react-inline-jsx-arrow-${file}-${ln}`,
|
|
19721
|
+
pass: this.name,
|
|
19722
|
+
category: this.category,
|
|
19723
|
+
rule_id: this.name,
|
|
19724
|
+
cwe: void 0,
|
|
19725
|
+
severity: "low",
|
|
19726
|
+
level: "note",
|
|
19727
|
+
message: `Inline arrow function in JSX prop \`${propName}\` creates a new function reference on every render, defeating memoization`,
|
|
19728
|
+
file,
|
|
19729
|
+
line: ln,
|
|
19730
|
+
snippet: lineText.trim(),
|
|
19731
|
+
fix: `Wrap the handler with \`useCallback\` or define it outside the component: \`const handle${propName.charAt(0).toUpperCase()}${propName.slice(1)} = useCallback(...)\`.`
|
|
19732
|
+
});
|
|
19733
|
+
}
|
|
19734
|
+
INLINE_FUNCTION_RE.lastIndex = 0;
|
|
19735
|
+
while ((m = INLINE_FUNCTION_RE.exec(lineText)) !== null) {
|
|
19736
|
+
const propName = m[1];
|
|
19737
|
+
if (SKIP_PROPS.has(propName)) continue;
|
|
19738
|
+
if (propName.startsWith("data")) continue;
|
|
19739
|
+
inlineProps.push({ line: ln, propName, kind: "function" });
|
|
19740
|
+
ctx.addFinding({
|
|
19741
|
+
id: `react-inline-jsx-fn-${file}-${ln}`,
|
|
19742
|
+
pass: this.name,
|
|
19743
|
+
category: this.category,
|
|
19744
|
+
rule_id: this.name,
|
|
19745
|
+
cwe: void 0,
|
|
19746
|
+
severity: "low",
|
|
19747
|
+
level: "note",
|
|
19748
|
+
message: `Inline function expression in JSX prop \`${propName}\` creates a new function reference on every render, defeating memoization`,
|
|
19749
|
+
file,
|
|
19750
|
+
line: ln,
|
|
19751
|
+
snippet: lineText.trim(),
|
|
19752
|
+
fix: `Wrap the handler with \`useCallback\` or define it outside the component: \`const handle${propName.charAt(0).toUpperCase()}${propName.slice(1)} = useCallback(...)\`.`
|
|
19753
|
+
});
|
|
19754
|
+
}
|
|
19755
|
+
}
|
|
19756
|
+
return { inlineProps };
|
|
19757
|
+
}
|
|
19758
|
+
};
|
|
19759
|
+
|
|
19760
|
+
// src/analysis/passes/swallowed-exception-pass.ts
|
|
19761
|
+
var MEANINGFUL_ACTION_RE = /\b(throw|raise|log|logger|console\.(error|warn|log|debug|info)|System\.(out|err)\.|print(?:ln|f)?|warn|error|debug|info|fatal|LOGGER|LOG|logging\.(warning|error|debug|info|critical))\b|\breturn\s+\S/;
|
|
19762
|
+
var SwallowedExceptionPass = class {
|
|
19763
|
+
name = "swallowed-exception";
|
|
19764
|
+
category = "reliability";
|
|
19765
|
+
run(ctx) {
|
|
19766
|
+
const { graph, code, language } = ctx;
|
|
19767
|
+
if (language === "rust" || language === "bash") {
|
|
19768
|
+
return { swallowed: [] };
|
|
19769
|
+
}
|
|
19770
|
+
const { cfg } = graph.ir;
|
|
19771
|
+
if (cfg.blocks.length === 0) return { swallowed: [] };
|
|
19772
|
+
const exGraph = new ExceptionFlowGraph(cfg, graph.blockById);
|
|
19773
|
+
if (!exGraph.hasTryCatch) return { swallowed: [] };
|
|
19774
|
+
const file = graph.ir.meta.file;
|
|
19775
|
+
const codeLines = code.split("\n");
|
|
19776
|
+
const swallowed = [];
|
|
19777
|
+
const reported = /* @__PURE__ */ new Set();
|
|
19778
|
+
for (const pair of exGraph.pairs) {
|
|
19779
|
+
const catchLine = pair.catchBlock.start_line;
|
|
19780
|
+
if (reported.has(catchLine)) continue;
|
|
19781
|
+
const methodInfo = graph.methodAtLine(catchLine);
|
|
19782
|
+
const scanEnd = methodInfo ? methodInfo.method.end_line : codeLines.length;
|
|
19783
|
+
const catchBodyEnd = this.findCatchBodyEnd(codeLines, catchLine, scanEnd);
|
|
19784
|
+
let hasAction = false;
|
|
19785
|
+
for (let ln = catchLine; ln <= catchBodyEnd && ln <= codeLines.length; ln++) {
|
|
19786
|
+
if (MEANINGFUL_ACTION_RE.test(codeLines[ln - 1] ?? "")) {
|
|
19787
|
+
hasAction = true;
|
|
19788
|
+
break;
|
|
19789
|
+
}
|
|
19790
|
+
}
|
|
19791
|
+
if (!hasAction) {
|
|
19792
|
+
reported.add(catchLine);
|
|
19793
|
+
swallowed.push({ line: catchLine });
|
|
19794
|
+
const snippet = (codeLines[catchLine - 1] ?? "").trim();
|
|
19795
|
+
ctx.addFinding({
|
|
19796
|
+
id: `swallowed-exception-${file}-${catchLine}`,
|
|
19797
|
+
pass: this.name,
|
|
19798
|
+
category: this.category,
|
|
19799
|
+
rule_id: this.name,
|
|
19800
|
+
cwe: "CWE-390",
|
|
19801
|
+
severity: "medium",
|
|
19802
|
+
level: "warning",
|
|
19803
|
+
message: `Swallowed exception: catch block at line ${catchLine} has no throw, log, or return \u2014 the exception is silently discarded`,
|
|
19804
|
+
file,
|
|
19805
|
+
line: catchLine,
|
|
19806
|
+
snippet,
|
|
19807
|
+
fix: "At minimum log the exception, or re-throw it; never silently discard exceptions"
|
|
19808
|
+
});
|
|
19809
|
+
}
|
|
19810
|
+
}
|
|
19811
|
+
return { swallowed };
|
|
19812
|
+
}
|
|
19813
|
+
/**
|
|
19814
|
+
* Walks source lines starting at `startLine` counting brace depth.
|
|
19815
|
+
* Returns the line where the brace depth first returns to zero after
|
|
19816
|
+
* the opening brace (i.e., the closing brace of the catch block).
|
|
19817
|
+
* Capped at `maxLine`.
|
|
19818
|
+
*/
|
|
19819
|
+
findCatchBodyEnd(lines, startLine, maxLine) {
|
|
19820
|
+
let depth = 0;
|
|
19821
|
+
let started = false;
|
|
19822
|
+
for (let ln = startLine; ln <= maxLine && ln <= lines.length; ln++) {
|
|
19823
|
+
const text = lines[ln - 1] ?? "";
|
|
19824
|
+
for (const ch of text) {
|
|
19825
|
+
if (ch === "{") {
|
|
19826
|
+
depth++;
|
|
19827
|
+
started = true;
|
|
19828
|
+
} else if (ch === "}" && started) {
|
|
19829
|
+
depth--;
|
|
19830
|
+
}
|
|
19831
|
+
}
|
|
19832
|
+
if (started && depth <= 0) return ln;
|
|
19833
|
+
}
|
|
19834
|
+
return maxLine;
|
|
19835
|
+
}
|
|
19836
|
+
};
|
|
19837
|
+
|
|
19838
|
+
// src/analysis/passes/broad-catch-pass.ts
|
|
19839
|
+
var JAVA_BROAD_RE = /catch\s*\(\s*(Exception|Throwable|RuntimeException|Error)\s/;
|
|
19840
|
+
var PYTHON_BROAD_RE = /^\s*except\s*:|except\s+(Exception|BaseException)\b/;
|
|
19841
|
+
var BroadCatchPass = class {
|
|
19842
|
+
name = "broad-catch";
|
|
19843
|
+
category = "reliability";
|
|
19844
|
+
run(ctx) {
|
|
19845
|
+
const { graph, code, language } = ctx;
|
|
19846
|
+
if (language !== "java" && language !== "python") {
|
|
19847
|
+
return { broadCatches: [] };
|
|
19848
|
+
}
|
|
19849
|
+
const { cfg } = graph.ir;
|
|
19850
|
+
if (cfg.blocks.length === 0) return { broadCatches: [] };
|
|
19851
|
+
const exGraph = new ExceptionFlowGraph(cfg, graph.blockById);
|
|
19852
|
+
if (!exGraph.hasTryCatch) return { broadCatches: [] };
|
|
19853
|
+
const file = graph.ir.meta.file;
|
|
19854
|
+
const codeLines = code.split("\n");
|
|
19855
|
+
const broadCatches = [];
|
|
19856
|
+
const reported = /* @__PURE__ */ new Set();
|
|
19857
|
+
const pattern = language === "java" ? JAVA_BROAD_RE : PYTHON_BROAD_RE;
|
|
19858
|
+
for (const pair of exGraph.pairs) {
|
|
19859
|
+
const catchLine = pair.catchBlock.start_line;
|
|
19860
|
+
if (reported.has(catchLine)) continue;
|
|
19861
|
+
const lineText = codeLines[catchLine - 1] ?? "";
|
|
19862
|
+
const match = pattern.exec(lineText);
|
|
19863
|
+
if (!match) continue;
|
|
19864
|
+
const caughtType = match[1] ?? "Exception";
|
|
19865
|
+
reported.add(catchLine);
|
|
19866
|
+
broadCatches.push({ line: catchLine, type: caughtType });
|
|
19867
|
+
const snippet = lineText.trim();
|
|
19868
|
+
ctx.addFinding({
|
|
19869
|
+
id: `broad-catch-${file}-${catchLine}`,
|
|
19870
|
+
pass: this.name,
|
|
19871
|
+
category: this.category,
|
|
19872
|
+
rule_id: this.name,
|
|
19873
|
+
cwe: "CWE-396",
|
|
19874
|
+
severity: "low",
|
|
19875
|
+
level: "warning",
|
|
19876
|
+
message: `Broad catch: catching \`${caughtType}\` at line ${catchLine} suppresses unexpected errors and hides bugs`,
|
|
19877
|
+
file,
|
|
19878
|
+
line: catchLine,
|
|
19879
|
+
snippet,
|
|
19880
|
+
fix: language === "java" ? `Catch the specific exception types your code can handle (e.g., \`IOException\`, \`SQLException\`)` : `Catch the specific exception types your code can handle (e.g., \`ValueError\`, \`KeyError\`)`,
|
|
19881
|
+
evidence: { caughtType }
|
|
19882
|
+
});
|
|
19883
|
+
}
|
|
19884
|
+
return { broadCatches };
|
|
19885
|
+
}
|
|
19886
|
+
};
|
|
19887
|
+
|
|
19888
|
+
// src/analysis/passes/unhandled-exception-pass.ts
|
|
19889
|
+
var JS_THROW_RE = /^\s*throw\s+/;
|
|
19890
|
+
var PYTHON_RAISE_RE = /^\s*raise\b/;
|
|
19891
|
+
var UnhandledExceptionPass = class {
|
|
19892
|
+
name = "unhandled-exception";
|
|
19893
|
+
category = "reliability";
|
|
19894
|
+
run(ctx) {
|
|
19895
|
+
const { graph, code, language } = ctx;
|
|
19896
|
+
if (language !== "javascript" && language !== "typescript" && language !== "python") {
|
|
19897
|
+
return { unhandled: [] };
|
|
19898
|
+
}
|
|
19899
|
+
const { cfg } = graph.ir;
|
|
19900
|
+
const file = graph.ir.meta.file;
|
|
19901
|
+
const codeLines = code.split("\n");
|
|
19902
|
+
const exGraph = new ExceptionFlowGraph(cfg, graph.blockById);
|
|
19903
|
+
const coveredRanges = [];
|
|
19904
|
+
for (const pair of exGraph.pairs) {
|
|
19905
|
+
if (pair.catchBlock.start_line > pair.tryBlock.start_line) {
|
|
19906
|
+
coveredRanges.push({
|
|
19907
|
+
start: pair.tryBlock.start_line,
|
|
19908
|
+
end: pair.catchBlock.start_line - 1
|
|
19909
|
+
});
|
|
19910
|
+
}
|
|
19911
|
+
}
|
|
19912
|
+
const catchStarts = new Set(
|
|
19913
|
+
exGraph.pairs.map((p) => p.catchBlock.start_line)
|
|
19914
|
+
);
|
|
19915
|
+
const throwRe = language === "python" ? PYTHON_RAISE_RE : JS_THROW_RE;
|
|
19916
|
+
const unhandled = [];
|
|
19917
|
+
const reportedMethods = /* @__PURE__ */ new Set();
|
|
19918
|
+
for (let ln = 1; ln <= codeLines.length; ln++) {
|
|
19919
|
+
const lineText = codeLines[ln - 1] ?? "";
|
|
19920
|
+
if (!throwRe.test(lineText)) continue;
|
|
19921
|
+
let inCatch = false;
|
|
19922
|
+
for (const cs of catchStarts) {
|
|
19923
|
+
if (ln >= cs) {
|
|
19924
|
+
inCatch = true;
|
|
19925
|
+
break;
|
|
19926
|
+
}
|
|
19927
|
+
}
|
|
19928
|
+
inCatch = false;
|
|
19929
|
+
for (const pair of exGraph.pairs) {
|
|
19930
|
+
if (ln >= pair.catchBlock.start_line) {
|
|
19931
|
+
const mThrow = graph.methodAtLine(ln);
|
|
19932
|
+
const mCatch = graph.methodAtLine(pair.catchBlock.start_line);
|
|
19933
|
+
if (mThrow && mCatch && mThrow.method.start_line === mCatch.method.start_line) {
|
|
19934
|
+
inCatch = true;
|
|
19935
|
+
break;
|
|
19936
|
+
}
|
|
19937
|
+
}
|
|
19938
|
+
}
|
|
19939
|
+
if (inCatch) continue;
|
|
19940
|
+
const isCovered = coveredRanges.some((r) => ln >= r.start && ln <= r.end);
|
|
19941
|
+
if (isCovered) continue;
|
|
19942
|
+
const methodInfo = graph.methodAtLine(ln);
|
|
19943
|
+
const methodKey = methodInfo ? `${methodInfo.method.start_line}-${methodInfo.method.end_line}` : `global-${ln}`;
|
|
19944
|
+
if (reportedMethods.has(methodKey)) continue;
|
|
19945
|
+
reportedMethods.add(methodKey);
|
|
19946
|
+
const methodName = methodInfo?.method.name ?? "<anonymous>";
|
|
19947
|
+
unhandled.push({ line: ln, method: methodName });
|
|
19948
|
+
const snippet = lineText.trim();
|
|
19949
|
+
ctx.addFinding({
|
|
19950
|
+
id: `unhandled-exception-${file}-${ln}`,
|
|
19951
|
+
pass: this.name,
|
|
19952
|
+
category: this.category,
|
|
19953
|
+
rule_id: this.name,
|
|
19954
|
+
cwe: "CWE-390",
|
|
19955
|
+
severity: "medium",
|
|
19956
|
+
level: "warning",
|
|
19957
|
+
message: `Unhandled exception: \`throw\` at line ${ln} in \`${methodName}\` is not inside a try/catch \u2014 callers receive an unexpected exception`,
|
|
19958
|
+
file,
|
|
19959
|
+
line: ln,
|
|
19960
|
+
snippet,
|
|
19961
|
+
fix: "Wrap throwing code in a try/catch, or document the exception in the function signature",
|
|
19962
|
+
evidence: { method: methodName }
|
|
19963
|
+
});
|
|
19964
|
+
}
|
|
19965
|
+
return { unhandled };
|
|
19966
|
+
}
|
|
19967
|
+
};
|
|
19968
|
+
|
|
19969
|
+
// src/analysis/passes/double-close-pass.ts
|
|
19970
|
+
var RESOURCE_CTORS2 = /* @__PURE__ */ new Set([
|
|
19971
|
+
"FileInputStream",
|
|
19972
|
+
"FileOutputStream",
|
|
19973
|
+
"FileReader",
|
|
19974
|
+
"FileWriter",
|
|
19975
|
+
"BufferedReader",
|
|
19976
|
+
"BufferedWriter",
|
|
19977
|
+
"PrintWriter",
|
|
19978
|
+
"InputStreamReader",
|
|
19979
|
+
"OutputStreamWriter",
|
|
19980
|
+
"RandomAccessFile",
|
|
19981
|
+
"DataInputStream",
|
|
19982
|
+
"DataOutputStream",
|
|
19983
|
+
"ObjectInputStream",
|
|
19984
|
+
"ObjectOutputStream",
|
|
19985
|
+
"ZipInputStream",
|
|
19986
|
+
"ZipOutputStream",
|
|
19987
|
+
"JarInputStream",
|
|
19988
|
+
"JarOutputStream",
|
|
19989
|
+
"GZIPInputStream",
|
|
19990
|
+
"GZIPOutputStream",
|
|
19991
|
+
"FileChannel",
|
|
19992
|
+
"Socket",
|
|
19993
|
+
"ServerSocket",
|
|
19994
|
+
"DatagramSocket"
|
|
19995
|
+
]);
|
|
19996
|
+
var RESOURCE_FACTORY_METHODS2 = /* @__PURE__ */ new Set([
|
|
19997
|
+
"openConnection",
|
|
19998
|
+
"openStream",
|
|
19999
|
+
"newInputStream",
|
|
20000
|
+
"newOutputStream",
|
|
20001
|
+
"newBufferedReader",
|
|
20002
|
+
"newBufferedWriter",
|
|
20003
|
+
"newByteChannel",
|
|
20004
|
+
"open",
|
|
20005
|
+
"createReadStream",
|
|
20006
|
+
"createWriteStream",
|
|
20007
|
+
"createConnection"
|
|
20008
|
+
]);
|
|
20009
|
+
var CLOSE_METHODS2 = /* @__PURE__ */ new Set([
|
|
20010
|
+
"close",
|
|
20011
|
+
"dispose",
|
|
20012
|
+
"shutdown",
|
|
20013
|
+
"disconnect",
|
|
20014
|
+
"release",
|
|
20015
|
+
"destroy",
|
|
20016
|
+
"free",
|
|
20017
|
+
"shutdownNow",
|
|
20018
|
+
"terminate"
|
|
20019
|
+
]);
|
|
20020
|
+
var DoubleClosePass = class {
|
|
20021
|
+
name = "double-close";
|
|
20022
|
+
category = "reliability";
|
|
20023
|
+
run(ctx) {
|
|
20024
|
+
const { graph, code } = ctx;
|
|
20025
|
+
if (ctx.language === "bash") return { doubleCloses: [] };
|
|
20026
|
+
const file = graph.ir.meta.file;
|
|
20027
|
+
const codeLines = code.split("\n");
|
|
20028
|
+
const doubleCloses = [];
|
|
20029
|
+
for (const call of graph.ir.calls) {
|
|
20030
|
+
const name2 = call.method_name;
|
|
20031
|
+
const isConstructor = call.is_constructor === true && RESOURCE_CTORS2.has(name2);
|
|
20032
|
+
const isFactory = !call.is_constructor && RESOURCE_FACTORY_METHODS2.has(name2);
|
|
20033
|
+
if (!isConstructor && !isFactory) continue;
|
|
20034
|
+
const openLine = call.location.line;
|
|
20035
|
+
const defs = graph.defsAtLine(openLine);
|
|
20036
|
+
if (defs.length === 0) continue;
|
|
20037
|
+
const resourceVar = defs[0].variable;
|
|
20038
|
+
const methodInfo = graph.methodAtLine(openLine);
|
|
20039
|
+
if (!methodInfo) continue;
|
|
20040
|
+
const { start_line: methodStart, end_line: methodEnd } = methodInfo.method;
|
|
20041
|
+
const closeCalls = graph.ir.calls.filter(
|
|
20042
|
+
(c) => CLOSE_METHODS2.has(c.method_name) && c.receiver === resourceVar && c.location.line > openLine && c.location.line <= methodEnd
|
|
20043
|
+
);
|
|
20044
|
+
if (closeCalls.length < 2) continue;
|
|
20045
|
+
const closeLines = closeCalls.map((c) => c.location.line);
|
|
20046
|
+
const allInFinally = closeLines.every(
|
|
20047
|
+
(cl) => this.isInFinallyBlock(codeLines, cl, methodStart, methodEnd)
|
|
20048
|
+
);
|
|
20049
|
+
if (allInFinally) continue;
|
|
20050
|
+
doubleCloses.push({ openLine, closeLines, variable: resourceVar });
|
|
20051
|
+
const snippet = (codeLines[openLine - 1] ?? "").trim();
|
|
20052
|
+
const linesStr = closeLines.join(" and ");
|
|
20053
|
+
ctx.addFinding({
|
|
20054
|
+
id: `double-close-${file}-${openLine}`,
|
|
20055
|
+
pass: this.name,
|
|
20056
|
+
category: this.category,
|
|
20057
|
+
rule_id: this.name,
|
|
20058
|
+
cwe: "CWE-675",
|
|
20059
|
+
severity: "medium",
|
|
20060
|
+
level: "warning",
|
|
20061
|
+
message: `Double close: \`${resourceVar}\` is closed at lines ${linesStr} \u2014 closing an already-closed resource may throw`,
|
|
20062
|
+
file,
|
|
20063
|
+
line: openLine,
|
|
20064
|
+
snippet,
|
|
20065
|
+
fix: `Close the resource exactly once in a finally block; add a null/isClosed guard before the second close if closing on multiple paths`,
|
|
20066
|
+
evidence: { variable: resourceVar, close_lines: closeLines }
|
|
20067
|
+
});
|
|
20068
|
+
}
|
|
20069
|
+
return { doubleCloses };
|
|
20070
|
+
}
|
|
20071
|
+
/** True if the given line is inside a `finally` block in the method. */
|
|
20072
|
+
isInFinallyBlock(lines, targetLine, methodStart, methodEnd) {
|
|
20073
|
+
for (let ln = methodStart; ln <= targetLine && ln <= methodEnd && ln <= lines.length; ln++) {
|
|
20074
|
+
if (/\bfinally\b/.test(lines[ln - 1] ?? "")) return true;
|
|
20075
|
+
}
|
|
20076
|
+
return false;
|
|
20077
|
+
}
|
|
20078
|
+
};
|
|
20079
|
+
|
|
20080
|
+
// src/analysis/passes/use-after-close-pass.ts
|
|
20081
|
+
var RESOURCE_CTORS3 = /* @__PURE__ */ new Set([
|
|
20082
|
+
"FileInputStream",
|
|
20083
|
+
"FileOutputStream",
|
|
20084
|
+
"FileReader",
|
|
20085
|
+
"FileWriter",
|
|
20086
|
+
"BufferedReader",
|
|
20087
|
+
"BufferedWriter",
|
|
20088
|
+
"PrintWriter",
|
|
20089
|
+
"InputStreamReader",
|
|
20090
|
+
"OutputStreamWriter",
|
|
20091
|
+
"RandomAccessFile",
|
|
20092
|
+
"DataInputStream",
|
|
20093
|
+
"DataOutputStream",
|
|
20094
|
+
"ObjectInputStream",
|
|
20095
|
+
"ObjectOutputStream",
|
|
20096
|
+
"ZipInputStream",
|
|
20097
|
+
"ZipOutputStream",
|
|
20098
|
+
"JarInputStream",
|
|
20099
|
+
"JarOutputStream",
|
|
20100
|
+
"GZIPInputStream",
|
|
20101
|
+
"GZIPOutputStream",
|
|
20102
|
+
"FileChannel",
|
|
20103
|
+
"Socket",
|
|
20104
|
+
"ServerSocket",
|
|
20105
|
+
"DatagramSocket"
|
|
20106
|
+
]);
|
|
20107
|
+
var RESOURCE_FACTORY_METHODS3 = /* @__PURE__ */ new Set([
|
|
20108
|
+
"openConnection",
|
|
20109
|
+
"openStream",
|
|
20110
|
+
"newInputStream",
|
|
20111
|
+
"newOutputStream",
|
|
20112
|
+
"newBufferedReader",
|
|
20113
|
+
"newBufferedWriter",
|
|
20114
|
+
"newByteChannel",
|
|
20115
|
+
"open",
|
|
20116
|
+
"createReadStream",
|
|
20117
|
+
"createWriteStream",
|
|
20118
|
+
"createConnection"
|
|
20119
|
+
]);
|
|
20120
|
+
var CLOSE_METHODS3 = /* @__PURE__ */ new Set([
|
|
20121
|
+
"close",
|
|
20122
|
+
"dispose",
|
|
20123
|
+
"shutdown",
|
|
20124
|
+
"disconnect",
|
|
20125
|
+
"release",
|
|
20126
|
+
"destroy",
|
|
20127
|
+
"free",
|
|
20128
|
+
"shutdownNow",
|
|
20129
|
+
"terminate"
|
|
20130
|
+
]);
|
|
20131
|
+
var UseAfterClosePass = class {
|
|
20132
|
+
name = "use-after-close";
|
|
20133
|
+
category = "reliability";
|
|
20134
|
+
run(ctx) {
|
|
20135
|
+
const { graph, code } = ctx;
|
|
20136
|
+
if (ctx.language === "bash") return { useAfterCloses: [] };
|
|
20137
|
+
const file = graph.ir.meta.file;
|
|
20138
|
+
const codeLines = code.split("\n");
|
|
20139
|
+
const useAfterCloses = [];
|
|
20140
|
+
for (const call of graph.ir.calls) {
|
|
20141
|
+
const name2 = call.method_name;
|
|
20142
|
+
const isConstructor = call.is_constructor === true && RESOURCE_CTORS3.has(name2);
|
|
20143
|
+
const isFactory = !call.is_constructor && RESOURCE_FACTORY_METHODS3.has(name2);
|
|
20144
|
+
if (!isConstructor && !isFactory) continue;
|
|
20145
|
+
const openLine = call.location.line;
|
|
20146
|
+
const defs = graph.defsAtLine(openLine);
|
|
20147
|
+
if (defs.length === 0) continue;
|
|
20148
|
+
const resourceVar = defs[0].variable;
|
|
20149
|
+
const methodInfo = graph.methodAtLine(openLine);
|
|
20150
|
+
if (!methodInfo) continue;
|
|
20151
|
+
const methodEnd = methodInfo.method.end_line;
|
|
20152
|
+
const firstClose = graph.ir.calls.filter(
|
|
20153
|
+
(c) => CLOSE_METHODS3.has(c.method_name) && c.receiver === resourceVar && c.location.line > openLine && c.location.line <= methodEnd
|
|
20154
|
+
).sort((a, b) => a.location.line - b.location.line)[0];
|
|
20155
|
+
if (!firstClose) continue;
|
|
20156
|
+
const closeLine = firstClose.location.line;
|
|
20157
|
+
const usesAfterClose = graph.ir.calls.filter(
|
|
20158
|
+
(c) => c.receiver === resourceVar && c.location.line > closeLine && c.location.line <= methodEnd && !CLOSE_METHODS3.has(c.method_name)
|
|
20159
|
+
);
|
|
20160
|
+
for (const use of usesAfterClose) {
|
|
20161
|
+
const useLine = use.location.line;
|
|
20162
|
+
useAfterCloses.push({ openLine, closeLine, useLine, variable: resourceVar });
|
|
20163
|
+
const snippet = (codeLines[useLine - 1] ?? "").trim();
|
|
20164
|
+
ctx.addFinding({
|
|
20165
|
+
id: `use-after-close-${file}-${useLine}`,
|
|
20166
|
+
pass: this.name,
|
|
20167
|
+
category: this.category,
|
|
20168
|
+
rule_id: this.name,
|
|
20169
|
+
cwe: "CWE-672",
|
|
20170
|
+
severity: "high",
|
|
20171
|
+
level: "error",
|
|
20172
|
+
message: `Use after close: \`${resourceVar}.${use.method_name}()\` at line ${useLine} is called after \`${resourceVar}.close()\` at line ${closeLine}`,
|
|
20173
|
+
file,
|
|
20174
|
+
line: useLine,
|
|
20175
|
+
snippet,
|
|
20176
|
+
fix: `Do not use a resource after closing it; keep \`${resourceVar}\` open until all uses are complete`,
|
|
20177
|
+
evidence: { variable: resourceVar, close_line: closeLine, open_line: openLine }
|
|
20178
|
+
});
|
|
20179
|
+
}
|
|
20180
|
+
}
|
|
20181
|
+
return { useAfterCloses };
|
|
20182
|
+
}
|
|
20183
|
+
};
|
|
20184
|
+
|
|
19153
20185
|
// src/analysis/metrics/passes/size-metrics-pass.ts
|
|
19154
20186
|
var SizeMetricsPass = class {
|
|
19155
20187
|
name = "size-metrics";
|
|
@@ -19949,7 +20981,7 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
19949
20981
|
enriched: {}
|
|
19950
20982
|
});
|
|
19951
20983
|
const config = options.taintConfig ?? getDefaultConfig();
|
|
19952
|
-
const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).run(graph, code, language, config);
|
|
20984
|
+
const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).add(new InfiniteLoopPass()).add(new DeepInheritancePass()).add(new RedundantLoopPass()).add(new UnboundedCollectionPass()).add(new SerialAwaitPass()).add(new ReactInlineJsxPass()).add(new SwallowedExceptionPass()).add(new BroadCatchPass()).add(new UnhandledExceptionPass()).add(new DoubleClosePass()).add(new UseAfterClosePass()).run(graph, code, language, config);
|
|
19953
20985
|
const sinkFilter = results.get("sink-filter");
|
|
19954
20986
|
const interProc = results.get("interprocedural");
|
|
19955
20987
|
const taint = {
|