circle-ir 3.9.8 → 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/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/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/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 +8 -3
- package/dist/analyzer.js +18 -3
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +495 -3
- package/dist/core/circle-ir-core.cjs +2 -1
- package/dist/core/circle-ir-core.js +2 -1
- 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 +1 -0
- package/dist/graph/index.js +1 -0
- package/dist/graph/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -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) {
|
|
@@ -19690,6 +19757,431 @@ var ReactInlineJsxPass = class {
|
|
|
19690
19757
|
}
|
|
19691
19758
|
};
|
|
19692
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
|
+
|
|
19693
20185
|
// src/analysis/metrics/passes/size-metrics-pass.ts
|
|
19694
20186
|
var SizeMetricsPass = class {
|
|
19695
20187
|
name = "size-metrics";
|
|
@@ -20489,7 +20981,7 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
20489
20981
|
enriched: {}
|
|
20490
20982
|
});
|
|
20491
20983
|
const config = options.taintConfig ?? getDefaultConfig();
|
|
20492
|
-
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()).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);
|
|
20493
20985
|
const sinkFilter = results.get("sink-filter");
|
|
20494
20986
|
const interProc = results.get("interprocedural");
|
|
20495
20987
|
const taint = {
|
|
@@ -10591,7 +10591,8 @@ function findSinks(calls, patterns) {
|
|
|
10591
10591
|
cwe: pattern.cwe,
|
|
10592
10592
|
location,
|
|
10593
10593
|
line: call.location.line,
|
|
10594
|
-
confidence
|
|
10594
|
+
confidence,
|
|
10595
|
+
method: call.method_name
|
|
10595
10596
|
});
|
|
10596
10597
|
}
|
|
10597
10598
|
}
|
|
@@ -10526,7 +10526,8 @@ function findSinks(calls, patterns) {
|
|
|
10526
10526
|
cwe: pattern.cwe,
|
|
10527
10527
|
location,
|
|
10528
10528
|
line: call.location.line,
|
|
10529
|
-
confidence
|
|
10529
|
+
confidence,
|
|
10530
|
+
method: call.method_name
|
|
10530
10531
|
});
|
|
10531
10532
|
}
|
|
10532
10533
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExceptionFlowGraph — lightweight wrapper over CFG exception edges.
|
|
3
|
+
*
|
|
4
|
+
* The CFG builder emits edges with `type === 'exception'` connecting the
|
|
5
|
+
* first block of a try body (`from`) to the first block of the corresponding
|
|
6
|
+
* catch handler (`to`). This class indexes those edges so exception-aware
|
|
7
|
+
* passes can query try/catch structure without re-scanning the edge list.
|
|
8
|
+
*/
|
|
9
|
+
import type { CFG, CFGBlock } from '../types/index.js';
|
|
10
|
+
export interface TryCatchInfo {
|
|
11
|
+
tryEntryId: number;
|
|
12
|
+
catchEntryId: number;
|
|
13
|
+
/** First block of the try body. */
|
|
14
|
+
tryBlock: CFGBlock;
|
|
15
|
+
/** First block of the catch handler. */
|
|
16
|
+
catchBlock: CFGBlock;
|
|
17
|
+
}
|
|
18
|
+
export declare class ExceptionFlowGraph {
|
|
19
|
+
/** All try/catch pairs found in the CFG. */
|
|
20
|
+
readonly pairs: TryCatchInfo[];
|
|
21
|
+
/** Block IDs that are catch-handler entry blocks. */
|
|
22
|
+
readonly catchEntryIds: Set<number>;
|
|
23
|
+
/** Block IDs that are try-body entry blocks. */
|
|
24
|
+
readonly tryEntryIds: Set<number>;
|
|
25
|
+
private readonly tryCatchMap;
|
|
26
|
+
private readonly catchTryMap;
|
|
27
|
+
constructor(cfg: CFG, blockById: Map<number, CFGBlock>);
|
|
28
|
+
/** True if at least one try/catch pair was found. */
|
|
29
|
+
get hasTryCatch(): boolean;
|
|
30
|
+
/** True if the given block ID is a catch-handler entry block. */
|
|
31
|
+
isCatchEntry(blockId: number): boolean;
|
|
32
|
+
/** True if the given block ID is a try-body entry block. */
|
|
33
|
+
isTryEntry(blockId: number): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the catch-entry block IDs for the given try-entry block.
|
|
36
|
+
* Multiple values mean multiple catch clauses for the same try.
|
|
37
|
+
*/
|
|
38
|
+
catchBlocksFor(tryEntryId: number): number[];
|
|
39
|
+
/**
|
|
40
|
+
* Returns the try-entry block ID corresponding to a catch-entry block,
|
|
41
|
+
* or `undefined` if the block is not a catch entry.
|
|
42
|
+
*/
|
|
43
|
+
tryBlockFor(catchEntryId: number): number | undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExceptionFlowGraph — lightweight wrapper over CFG exception edges.
|
|
3
|
+
*
|
|
4
|
+
* The CFG builder emits edges with `type === 'exception'` connecting the
|
|
5
|
+
* first block of a try body (`from`) to the first block of the corresponding
|
|
6
|
+
* catch handler (`to`). This class indexes those edges so exception-aware
|
|
7
|
+
* passes can query try/catch structure without re-scanning the edge list.
|
|
8
|
+
*/
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// ExceptionFlowGraph
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export class ExceptionFlowGraph {
|
|
13
|
+
/** All try/catch pairs found in the CFG. */
|
|
14
|
+
pairs;
|
|
15
|
+
/** Block IDs that are catch-handler entry blocks. */
|
|
16
|
+
catchEntryIds;
|
|
17
|
+
/** Block IDs that are try-body entry blocks. */
|
|
18
|
+
tryEntryIds;
|
|
19
|
+
tryCatchMap; // tryEntryId → [catchEntryId, …]
|
|
20
|
+
catchTryMap; // catchEntryId → tryEntryId
|
|
21
|
+
constructor(cfg, blockById) {
|
|
22
|
+
this.pairs = [];
|
|
23
|
+
this.catchEntryIds = new Set();
|
|
24
|
+
this.tryEntryIds = new Set();
|
|
25
|
+
this.tryCatchMap = new Map();
|
|
26
|
+
this.catchTryMap = new Map();
|
|
27
|
+
for (const edge of cfg.edges) {
|
|
28
|
+
if (edge.type !== 'exception')
|
|
29
|
+
continue;
|
|
30
|
+
const tryBlock = blockById.get(edge.from);
|
|
31
|
+
const catchBlock = blockById.get(edge.to);
|
|
32
|
+
if (!tryBlock || !catchBlock)
|
|
33
|
+
continue;
|
|
34
|
+
this.tryEntryIds.add(edge.from);
|
|
35
|
+
this.catchEntryIds.add(edge.to);
|
|
36
|
+
const catches = this.tryCatchMap.get(edge.from) ?? [];
|
|
37
|
+
catches.push(edge.to);
|
|
38
|
+
this.tryCatchMap.set(edge.from, catches);
|
|
39
|
+
this.catchTryMap.set(edge.to, edge.from);
|
|
40
|
+
this.pairs.push({
|
|
41
|
+
tryEntryId: edge.from,
|
|
42
|
+
catchEntryId: edge.to,
|
|
43
|
+
tryBlock,
|
|
44
|
+
catchBlock,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** True if at least one try/catch pair was found. */
|
|
49
|
+
get hasTryCatch() {
|
|
50
|
+
return this.pairs.length > 0;
|
|
51
|
+
}
|
|
52
|
+
/** True if the given block ID is a catch-handler entry block. */
|
|
53
|
+
isCatchEntry(blockId) {
|
|
54
|
+
return this.catchEntryIds.has(blockId);
|
|
55
|
+
}
|
|
56
|
+
/** True if the given block ID is a try-body entry block. */
|
|
57
|
+
isTryEntry(blockId) {
|
|
58
|
+
return this.tryEntryIds.has(blockId);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Returns the catch-entry block IDs for the given try-entry block.
|
|
62
|
+
* Multiple values mean multiple catch clauses for the same try.
|
|
63
|
+
*/
|
|
64
|
+
catchBlocksFor(tryEntryId) {
|
|
65
|
+
return this.tryCatchMap.get(tryEntryId) ?? [];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns the try-entry block ID corresponding to a catch-entry block,
|
|
69
|
+
* or `undefined` if the block is not a catch entry.
|
|
70
|
+
*/
|
|
71
|
+
tryBlockFor(catchEntryId) {
|
|
72
|
+
return this.catchTryMap.get(catchEntryId);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=exception-flow-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exception-flow-graph.js","sourceRoot":"","sources":["../../src/graph/exception-flow-graph.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiBH,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,MAAM,OAAO,kBAAkB;IAC7B,4CAA4C;IACnC,KAAK,CAAiB;IAE/B,qDAAqD;IAC5C,aAAa,CAAc;IAEpC,gDAAgD;IACvC,WAAW,CAAc;IAEjB,WAAW,CAAwB,CAAC,iCAAiC;IACrE,WAAW,CAAsB,CAAG,4BAA4B;IAEjF,YAAY,GAAQ,EAAE,SAAgC;QACpD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,IAAI,CAAC,aAAa,GAAG,IAAI,GAAG,EAAE,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAE7B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;gBAAE,SAAS;YAExC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC1C,IAAI,CAAC,QAAQ,IAAI,CAAC,UAAU;gBAAE,SAAS;YAEvC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAEhC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAEzC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAEzC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBACd,UAAU,EAAE,IAAI,CAAC,IAAI;gBACrB,YAAY,EAAE,IAAI,CAAC,EAAE;gBACrB,QAAQ;gBACR,UAAU;aACX,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,qDAAqD;IACrD,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,iEAAiE;IACjE,YAAY,CAAC,OAAe;QAC1B,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IAED,4DAA4D;IAC5D,UAAU,CAAC,OAAe;QACxB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,cAAc,CAAC,UAAkB;QAC/B,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;IAChD,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,YAAoB;QAC9B,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5C,CAAC;CACF"}
|
package/dist/graph/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { CodeGraph } from './code-graph.js';
|
|
|
2
2
|
export { ProjectGraph } from './project-graph.js';
|
|
3
3
|
export { ImportGraph } from './import-graph.js';
|
|
4
4
|
export { DominatorGraph } from './dominator-graph.js';
|
|
5
|
+
export { ExceptionFlowGraph, type TryCatchInfo } from './exception-flow-graph.js';
|
|
5
6
|
export { AnalysisPipeline, type AnalysisPass, type PassContext, type PipelineRunResult, } from './analysis-pass.js';
|