circle-ir 3.175.0 → 3.177.0

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.
@@ -7415,8 +7415,16 @@ function extractGoCallInfo(node) {
7415
7415
  if (funcNode.type === "selector_expression") {
7416
7416
  const operand = funcNode.childForFieldName("operand");
7417
7417
  const field = funcNode.childForFieldName("field");
7418
- receiver = operand ? getNodeText(operand) : null;
7419
7418
  methodName = field ? getNodeText(field) : getNodeText(funcNode);
7419
+ if (operand) {
7420
+ const opText = getNodeText(operand);
7421
+ if (operand.type === "identifier") {
7422
+ const resolved = resolveGoLocalReceiverType(opText, node);
7423
+ receiver = resolved !== null ? resolved : opText;
7424
+ } else {
7425
+ receiver = opText;
7426
+ }
7427
+ }
7420
7428
  } else if (funcNode.type === "identifier") {
7421
7429
  methodName = getNodeText(funcNode);
7422
7430
  } else {
@@ -7494,6 +7502,80 @@ function findGoEnclosingFunction(node) {
7494
7502
  }
7495
7503
  return null;
7496
7504
  }
7505
+ function resolveGoLocalReceiverType(operandName, callNode) {
7506
+ let cur = callNode.parent;
7507
+ while (cur) {
7508
+ if (cur.type === "method_declaration") {
7509
+ const receiver = cur.childForFieldName("receiver");
7510
+ if (receiver) {
7511
+ const t = extractGoParamTypeForName(receiver, operandName);
7512
+ if (t !== null) return t;
7513
+ }
7514
+ const params = cur.childForFieldName("parameters");
7515
+ if (params) {
7516
+ const t = extractGoParamTypeForName(params, operandName);
7517
+ if (t !== null) return t;
7518
+ }
7519
+ return null;
7520
+ }
7521
+ if (cur.type === "function_declaration" || cur.type === "func_literal") {
7522
+ const params = cur.childForFieldName("parameters");
7523
+ if (params) {
7524
+ const t = extractGoParamTypeForName(params, operandName);
7525
+ if (t !== null) return t;
7526
+ }
7527
+ return null;
7528
+ }
7529
+ cur = cur.parent;
7530
+ }
7531
+ return null;
7532
+ }
7533
+ function extractGoParamTypeForName(list, operandName) {
7534
+ for (let i2 = 0; i2 < list.namedChildCount; i2++) {
7535
+ const child = list.namedChild(i2);
7536
+ if (!child || child.type !== "parameter_declaration") continue;
7537
+ const typeNode = child.childForFieldName("type");
7538
+ if (!typeNode) continue;
7539
+ let matched = false;
7540
+ for (let j = 0; j < child.namedChildCount; j++) {
7541
+ const c = child.namedChild(j);
7542
+ if (!c) continue;
7543
+ if (c.type === "identifier" && getNodeText(c) === operandName) {
7544
+ matched = true;
7545
+ break;
7546
+ }
7547
+ if (c.type === "identifier_list") {
7548
+ for (let k = 0; k < c.namedChildCount; k++) {
7549
+ const id = c.namedChild(k);
7550
+ if (id && id.type === "identifier" && getNodeText(id) === operandName) {
7551
+ matched = true;
7552
+ break;
7553
+ }
7554
+ }
7555
+ if (matched) break;
7556
+ }
7557
+ }
7558
+ if (!matched) continue;
7559
+ return extractGoTypeLastSegment(typeNode);
7560
+ }
7561
+ return null;
7562
+ }
7563
+ function extractGoTypeLastSegment(typeNode) {
7564
+ if (typeNode.type === "pointer_type") {
7565
+ const inner = typeNode.namedChild(0);
7566
+ return inner ? extractGoTypeLastSegment(inner) : null;
7567
+ }
7568
+ if (typeNode.type === "qualified_type") {
7569
+ const name2 = typeNode.childForFieldName("name");
7570
+ if (name2) return getNodeText(name2);
7571
+ const last = typeNode.namedChild(typeNode.namedChildCount - 1);
7572
+ return last ? getNodeText(last) : null;
7573
+ }
7574
+ if (typeNode.type === "type_identifier" || typeNode.type === "identifier") {
7575
+ return getNodeText(typeNode);
7576
+ }
7577
+ return null;
7578
+ }
7497
7579
 
7498
7580
  // src/core/extractors/imports.ts
7499
7581
  function detectLanguage2(tree) {
@@ -8242,10 +8324,10 @@ function buildCFG(tree, language, cache) {
8242
8324
  const allEdges = [];
8243
8325
  let blockIdCounter = 0;
8244
8326
  if (effectiveLanguage === "bash") {
8245
- return buildBashCFG(tree, blockIdCounter);
8327
+ return buildBashCFG(tree, blockIdCounter, cache);
8246
8328
  }
8247
8329
  if (effectiveLanguage === "go") {
8248
- return buildGoCFG(tree, blockIdCounter);
8330
+ return buildGoCFG(tree, blockIdCounter, cache);
8249
8331
  }
8250
8332
  if (isJavaScript) {
8251
8333
  const functions = [
@@ -8602,11 +8684,11 @@ function processSwitchStatement(stmt, startId, blocks, edges, isJavaScript) {
8602
8684
  nextId: currentId
8603
8685
  };
8604
8686
  }
8605
- function buildBashCFG(tree, startId) {
8687
+ function buildBashCFG(tree, startId, cache) {
8606
8688
  const allBlocks = [];
8607
8689
  const allEdges = [];
8608
8690
  let blockIdCounter = startId;
8609
- const functions = findNodes(tree.rootNode, "function_definition");
8691
+ const functions = getNodesFromCache(tree.rootNode, "function_definition", cache);
8610
8692
  for (const func2 of functions) {
8611
8693
  const body2 = func2.childForFieldName("body");
8612
8694
  if (!body2) continue;
@@ -8723,12 +8805,12 @@ function isStatement(node, isJavaScript) {
8723
8805
  ]);
8724
8806
  return isJavaScript ? jsStatementTypes.has(node.type) : javaStatementTypes.has(node.type);
8725
8807
  }
8726
- function buildGoCFG(tree, blockIdCounter) {
8808
+ function buildGoCFG(tree, blockIdCounter, cache) {
8727
8809
  const allBlocks = [];
8728
8810
  const allEdges = [];
8729
8811
  const functions = [
8730
- ...findNodes(tree.rootNode, "function_declaration"),
8731
- ...findNodes(tree.rootNode, "method_declaration")
8812
+ ...getNodesFromCache(tree.rootNode, "function_declaration", cache),
8813
+ ...getNodesFromCache(tree.rootNode, "method_declaration", cache)
8732
8814
  ];
8733
8815
  for (const func2 of functions) {
8734
8816
  const body2 = func2.childForFieldName("body");
@@ -10642,6 +10724,59 @@ var OPEN_REDIRECT_FRAMEWORK_SINKS = [
10642
10724
  { method: "Redirect", class: "Context", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [1], languages: ["go"] },
10643
10725
  { method: "Redirect", class: "Ctx", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["go"] }
10644
10726
  ];
10727
+ var DESERIALIZATION_FRAMEWORK_SINKS = [
10728
+ // --- Python: stdlib + popular third-party --------------------------------
10729
+ // pickle: known-dangerous, any unpickle on untrusted bytes is RCE.
10730
+ { method: "loads", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10731
+ { method: "load", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10732
+ // cPickle alias (Python 2 name, still around in older codebases).
10733
+ { method: "loads", class: "cPickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10734
+ { method: "load", class: "cPickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10735
+ // marshal: stdlib code-object deserializer. Loading a tainted bytestring
10736
+ // as a code object followed by `exec` is arbitrary-code execution.
10737
+ { method: "loads", class: "marshal", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10738
+ { method: "load", class: "marshal", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10739
+ // dill: pickle superset — same RCE profile.
10740
+ { method: "loads", class: "dill", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10741
+ { method: "load", class: "dill", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10742
+ // jsonpickle: JSON wrapper around pickle — trusts `py/object` marker.
10743
+ { method: "decode", class: "jsonpickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10744
+ // --- Go: encoding/gob + yaml.Unmarshal -----------------------------------
10745
+ // gob.NewDecoder(r).Decode(&v): tainted io.Reader → arbitrary Go values.
10746
+ { method: "Decode", class: "Decoder", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], languages: ["go"] },
10747
+ // gopkg.in/yaml.v2 + v3 top-level function; interface{} target is unsafe.
10748
+ { method: "Unmarshal", class: "yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], languages: ["go"] },
10749
+ // --- JS/TS: node-serialize ------------------------------------------------
10750
+ // Known-dangerous — accepts embedded IIFE that runs during deserialize.
10751
+ { method: "unserialize", class: "nodeSerialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] }
10752
+ ];
10753
+ var NOSQL_FRAMEWORK_SINKS = [
10754
+ // --- Python: pymongo Collection ------------------------------------------
10755
+ // Every filter-taking Collection method; filter arg[0] is the query dict.
10756
+ { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10757
+ { method: "find_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10758
+ { method: "aggregate", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10759
+ { method: "update_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["python"] },
10760
+ { method: "update_many", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["python"] },
10761
+ { method: "delete_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10762
+ { method: "delete_many", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10763
+ { method: "count_documents", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10764
+ // --- Java: Spring Data MongoTemplate + native MongoCollection ------------
10765
+ { method: "find", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10766
+ { method: "findOne", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10767
+ { method: "findAll", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10768
+ { method: "find", class: "MongoCollection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10769
+ { method: "aggregate", class: "MongoCollection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10770
+ // --- Go: go.mongodb.org/mongo-driver Collection --------------------------
10771
+ // These fire once Go local-receiver type resolution lands (see
10772
+ // taint-matcher.ts + #240 ship 2 Go receiver work). Same gate as the
10773
+ // gin/fiber Ctx sinks in ship 1.
10774
+ { method: "Find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] },
10775
+ { method: "FindOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] },
10776
+ { method: "UpdateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1, 2], languages: ["go"] },
10777
+ { method: "UpdateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1, 2], languages: ["go"] },
10778
+ { method: "DeleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] }
10779
+ ];
10645
10780
  var TRUST_BOUNDARY_FRAMEWORK_SINKS = [
10646
10781
  // --- Python: Django cache write -----------------------------------------
10647
10782
  { method: "set", class: "cache", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [1], languages: ["python"] },
@@ -12325,7 +12460,12 @@ var DEFAULT_SINKS = [
12325
12460
  // keep the DEFAULT_SINKS literal within TypeScript's union-type
12326
12461
  // inference complexity limit (TS2590). See ~lines 661-752.
12327
12462
  ...OPEN_REDIRECT_FRAMEWORK_SINKS,
12328
- ...TRUST_BOUNDARY_FRAMEWORK_SINKS
12463
+ ...TRUST_BOUNDARY_FRAMEWORK_SINKS,
12464
+ // cognium-dev #240 ship 2 — extended framework sinks for
12465
+ // deserialization (CWE-502) and nosql_injection (CWE-943). Same
12466
+ // pattern: constants defined near the ship-1 blocks and spread here.
12467
+ ...DESERIALIZATION_FRAMEWORK_SINKS,
12468
+ ...NOSQL_FRAMEWORK_SINKS
12329
12469
  ];
12330
12470
  var DEFAULT_SANITIZERS = [
12331
12471
  // SQL Injection - proper parameter binding sanitizes input
@@ -12668,6 +12808,64 @@ function getDefaultConfig() {
12668
12808
  };
12669
12809
  }
12670
12810
 
12811
+ // src/analysis/arg-type-resolver.ts
12812
+ function findEnclosingType(callInMethod, types) {
12813
+ if (!callInMethod) return null;
12814
+ for (const t of types) {
12815
+ for (const m of t.methods) {
12816
+ if (m.name === callInMethod) return t;
12817
+ }
12818
+ }
12819
+ return null;
12820
+ }
12821
+ function resolveIdentifierType(name2, callInMethod, types) {
12822
+ if (!name2) return null;
12823
+ const enclosing = findEnclosingType(callInMethod, types);
12824
+ if (enclosing) {
12825
+ for (const m of enclosing.methods) {
12826
+ if (m.name !== callInMethod) continue;
12827
+ for (const p of m.parameters) {
12828
+ if (p.name === name2 && p.type) return p.type;
12829
+ }
12830
+ }
12831
+ for (const f of enclosing.fields) {
12832
+ if (f.name === name2 && f.type) return f.type;
12833
+ }
12834
+ return null;
12835
+ }
12836
+ for (const t of types) {
12837
+ for (const m of t.methods) {
12838
+ for (const p of m.parameters) {
12839
+ if (p.name === name2 && p.type) return p.type;
12840
+ }
12841
+ }
12842
+ for (const f of t.fields) {
12843
+ if (f.name === name2 && f.type) return f.type;
12844
+ }
12845
+ }
12846
+ return null;
12847
+ }
12848
+ function resolveCallReturnType(calleeName, callInMethod, types) {
12849
+ if (!calleeName) return null;
12850
+ const enclosing = findEnclosingType(callInMethod, types);
12851
+ if (enclosing) {
12852
+ for (const m of enclosing.methods) {
12853
+ if (m.name === calleeName && m.return_type) return m.return_type;
12854
+ }
12855
+ }
12856
+ for (const t of types) {
12857
+ for (const m of t.methods) {
12858
+ if (m.name === calleeName && m.return_type) return m.return_type;
12859
+ }
12860
+ }
12861
+ return null;
12862
+ }
12863
+ function isBoundedClassType(rawType) {
12864
+ if (!rawType) return false;
12865
+ const stripped = rawType.trim().replace(/^(?:java\.lang\.)+/, "");
12866
+ return /^Class(?:\s*<[\s\S]*>)?$/.test(stripped);
12867
+ }
12868
+
12671
12869
  // src/analysis/taint-matcher.ts
12672
12870
  var PYTHON_TAINTED_PATTERNS = [
12673
12871
  { pattern: /\brequest\.args\b/, sourceType: "http_param" },
@@ -12693,7 +12891,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
12693
12891
  const sources = findSources(calls, types, config.sources, sourceLines, language);
12694
12892
  let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
12695
12893
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
12696
- const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines);
12894
+ const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
12697
12895
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
12698
12896
  return { sources, sinks, sanitizers };
12699
12897
  }
@@ -12787,11 +12985,11 @@ function attachSourceLineCode(sources, sinks, code) {
12787
12985
  }
12788
12986
  function findSources(calls, types, patterns, sourceLines, language) {
12789
12987
  const sources = [];
12988
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
12989
+ (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
12990
+ );
12790
12991
  for (const call of calls) {
12791
- for (const pattern of patterns) {
12792
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
12793
- continue;
12794
- }
12992
+ for (const pattern of patternsForLanguage) {
12795
12993
  if (matchesSourcePattern(call, pattern)) {
12796
12994
  sources.push({
12797
12995
  type: pattern.type,
@@ -12807,11 +13005,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
12807
13005
  for (const type of types) {
12808
13006
  for (const method of type.methods) {
12809
13007
  for (const param of method.parameters) {
12810
- for (const pattern of patterns) {
13008
+ for (const pattern of patternsForLanguage) {
12811
13009
  if (pattern.annotation && pattern.param_tainted) {
12812
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
12813
- continue;
12814
- }
12815
13010
  if (matchesAnnotation(param.annotations, pattern.annotation)) {
12816
13011
  const paramLine = param.line ?? method.start_line;
12817
13012
  sources.push({
@@ -12830,11 +13025,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
12830
13025
  }
12831
13026
  for (const type of types) {
12832
13027
  for (const method of type.methods) {
12833
- for (const pattern of patterns) {
13028
+ for (const pattern of patternsForLanguage) {
12834
13029
  if (!pattern.method_annotation) continue;
12835
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
12836
- continue;
12837
- }
12838
13030
  if (!matchesAnnotation(method.annotations, pattern.method_annotation)) continue;
12839
13031
  for (const param of method.parameters) {
12840
13032
  const paramLine = param.line ?? method.start_line;
@@ -13271,13 +13463,30 @@ function isSafeRustCommandCall(call, pattern, language) {
13271
13463
  return false;
13272
13464
  }
13273
13465
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
13274
- function argIsClassLiteral(call, position) {
13466
+ function argIsClassLiteral(call, position, types) {
13275
13467
  const arg = call.arguments.find((a) => a.position === position);
13276
13468
  if (!arg) return false;
13277
13469
  const expr = (arg.literal ?? arg.expression ?? "").trim();
13278
13470
  if (!expr) return false;
13279
13471
  if (CLASS_LITERAL_RE.test(expr)) return true;
13280
- return TYPE_TOKEN_RE.test(expr);
13472
+ if (TYPE_TOKEN_RE.test(expr)) return true;
13473
+ if (!types || types.length === 0) return false;
13474
+ const argExpr = (arg.expression ?? "").trim();
13475
+ const varName = arg.variable ?? "";
13476
+ const isBareIdentifier = varName !== "" && argExpr === varName && !/[(.]/.test(argExpr);
13477
+ if (isBareIdentifier) {
13478
+ const t = resolveIdentifierType(varName, call.in_method, types);
13479
+ if (isBoundedClassType(t)) return true;
13480
+ }
13481
+ const isCallExpr = varName !== "" && new RegExp(`^${escapeRe(varName)}\\s*\\(`).test(argExpr);
13482
+ if (isCallExpr) {
13483
+ const t = resolveCallReturnType(varName, call.in_method, types);
13484
+ if (isBoundedClassType(t)) return true;
13485
+ }
13486
+ return false;
13487
+ }
13488
+ function escapeRe(s) {
13489
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13281
13490
  }
13282
13491
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
13283
13492
  function argIsStringLiteral(call, position) {
@@ -13438,10 +13647,13 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
13438
13647
  }
13439
13648
  return false;
13440
13649
  }
13441
- function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13650
+ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
13651
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
13652
+ (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
13653
+ );
13442
13654
  const sinkMap = /* @__PURE__ */ new Map();
13443
13655
  for (const call of calls) {
13444
- for (const pattern of patterns) {
13656
+ for (const pattern of patternsForLanguage) {
13445
13657
  if (matchesSinkPattern(call, pattern, typeHierarchy, language)) {
13446
13658
  if (isParameterizedQueryCall(call, pattern)) {
13447
13659
  continue;
@@ -13458,7 +13670,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13458
13670
  if (isSafeJSChildProcessCall(call, pattern, language)) {
13459
13671
  continue;
13460
13672
  }
13461
- if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
13673
+ if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
13462
13674
  continue;
13463
13675
  }
13464
13676
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
@@ -13859,7 +14071,20 @@ function matchesAnnotation(annotations, targetAnnotation) {
13859
14071
  }
13860
14072
  return false;
13861
14073
  }
14074
+ var RECEIVER_MIGHT_BE_CLASS_CACHE = /* @__PURE__ */ new Map();
14075
+ var RECEIVER_MIGHT_BE_CLASS_CACHE_CAP = 1e4;
13862
14076
  function receiverMightBeClass(receiver, className) {
14077
+ const key = receiver + "\0" + className;
14078
+ const cached = RECEIVER_MIGHT_BE_CLASS_CACHE.get(key);
14079
+ if (cached !== void 0) return cached;
14080
+ const result = receiverMightBeClassImpl(receiver, className);
14081
+ if (RECEIVER_MIGHT_BE_CLASS_CACHE.size >= RECEIVER_MIGHT_BE_CLASS_CACHE_CAP) {
14082
+ RECEIVER_MIGHT_BE_CLASS_CACHE.clear();
14083
+ }
14084
+ RECEIVER_MIGHT_BE_CLASS_CACHE.set(key, result);
14085
+ return result;
14086
+ }
14087
+ function receiverMightBeClassImpl(receiver, className) {
13863
14088
  if (className.startsWith("*") && className.length > 1) {
13864
14089
  const suffix = className.slice(1).toLowerCase();
13865
14090
  let simpleReceiver = receiver;
@@ -14563,8 +14788,15 @@ var CodeGraph = class {
14563
14788
  };
14564
14789
 
14565
14790
  // src/analysis/dfg-walk.ts
14791
+ var walkBackwardDefsMemo = /* @__PURE__ */ new WeakMap();
14566
14792
  function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
14567
14793
  const maxHops = options.maxHops ?? 32;
14794
+ let perFile = walkBackwardDefsMemo.get(chainsByToDef);
14795
+ if (perFile !== void 0) {
14796
+ const key = `${startDefId}|${maxHops}`;
14797
+ const hit = perFile.get(key);
14798
+ if (hit !== void 0) return hit;
14799
+ }
14568
14800
  const visited = /* @__PURE__ */ new Set();
14569
14801
  const lines = /* @__PURE__ */ new Set();
14570
14802
  let hopCapReached = false;
@@ -14596,7 +14828,13 @@ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
14596
14828
  queue.push(fromId);
14597
14829
  }
14598
14830
  }
14599
- return { visited, lines, hopCapReached };
14831
+ const result = { visited, lines, hopCapReached };
14832
+ if (perFile === void 0) {
14833
+ perFile = /* @__PURE__ */ new Map();
14834
+ walkBackwardDefsMemo.set(chainsByToDef, perFile);
14835
+ }
14836
+ perFile.set(`${startDefId}|${maxHops}`, result);
14837
+ return result;
14600
14838
  }
14601
14839
 
14602
14840
  // src/analysis/sanitizer-index.ts
@@ -15579,7 +15817,7 @@ var ConstantPropagator = class _ConstantPropagator {
15579
15817
  this.constructorParamPositions.clear();
15580
15818
  this.safePatternFieldsCache = null;
15581
15819
  this.isTaintedExpressionCache = null;
15582
- this.collectClassFields(tree.rootNode);
15820
+ const prePassMethods = this.collectClassFieldsAndMethods(tree.rootNode);
15583
15821
  for (const methodName of sanitizerMethods) {
15584
15822
  this.methodReturnsSanitized.add(methodName);
15585
15823
  }
@@ -15587,7 +15825,7 @@ var ConstantPropagator = class _ConstantPropagator {
15587
15825
  this.source,
15588
15826
  (name2) => this.lookupSymbol(name2)
15589
15827
  );
15590
- this.analyzeMethodReturns(tree.rootNode);
15828
+ this.analyzeMethodReturns(prePassMethods);
15591
15829
  this.seedPythonModuleConstants(tree.rootNode);
15592
15830
  this.visit(tree.rootNode);
15593
15831
  this.refineTaintFromConstants();
@@ -15657,8 +15895,7 @@ var ConstantPropagator = class _ConstantPropagator {
15657
15895
  /**
15658
15896
  * Pre-pass: Analyze all methods to detect those that always return constants or sanitized values.
15659
15897
  */
15660
- analyzeMethodReturns(root) {
15661
- const methods = this.findAllMethods(root);
15898
+ analyzeMethodReturns(methods) {
15662
15899
  for (const method of methods) {
15663
15900
  const methodName = this.getMethodName(method);
15664
15901
  if (!methodName) continue;
@@ -15922,11 +16159,15 @@ var ConstantPropagator = class _ConstantPropagator {
15922
16159
  * Collect all class field names (instance/static variables declared at class level).
15923
16160
  * These are variables declared directly in the class body, not inside methods.
15924
16161
  */
15925
- collectClassFields(root) {
16162
+ collectClassFieldsAndMethods(root) {
16163
+ const methods = [];
15926
16164
  const stack = [root];
15927
16165
  while (stack.length > 0) {
15928
16166
  const n = stack.pop();
15929
16167
  if (!n) continue;
16168
+ if (n.type === "method_declaration" || n.type === "function_declaration") {
16169
+ methods.push(n);
16170
+ }
15930
16171
  if (n.type === "class_body") {
15931
16172
  for (const child of n.children) {
15932
16173
  if (child.type === "field_declaration") {
@@ -15940,14 +16181,15 @@ var ConstantPropagator = class _ConstantPropagator {
15940
16181
  }
15941
16182
  }
15942
16183
  }
15943
- stack.push(child);
16184
+ if (child) stack.push(child);
15944
16185
  }
15945
16186
  continue;
15946
16187
  }
15947
16188
  for (const child of n.children) {
15948
- stack.push(child);
16189
+ if (child) stack.push(child);
15949
16190
  }
15950
16191
  }
16192
+ return methods;
15951
16193
  }
15952
16194
  /**
15953
16195
  * Sprint 9 #55 — seed the symbol table with Python module-level constant
@@ -16105,21 +16347,6 @@ var ConstantPropagator = class _ConstantPropagator {
16105
16347
  this.symbols.set(name2, value);
16106
16348
  }
16107
16349
  }
16108
- findAllMethods(node) {
16109
- const methods = [];
16110
- const stack = [node];
16111
- while (stack.length > 0) {
16112
- const n = stack.pop();
16113
- if (!n) continue;
16114
- if (n.type === "method_declaration" || n.type === "function_declaration") {
16115
- methods.push(n);
16116
- }
16117
- for (const child of n.children) {
16118
- if (child) stack.push(child);
16119
- }
16120
- }
16121
- return methods;
16122
- }
16123
16350
  getMethodName(method) {
16124
16351
  const nameNode = method.childForFieldName("name");
16125
16352
  if (nameNode) {