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.
@@ -7349,8 +7349,16 @@ function extractGoCallInfo(node) {
7349
7349
  if (funcNode.type === "selector_expression") {
7350
7350
  const operand = funcNode.childForFieldName("operand");
7351
7351
  const field = funcNode.childForFieldName("field");
7352
- receiver = operand ? getNodeText(operand) : null;
7353
7352
  methodName = field ? getNodeText(field) : getNodeText(funcNode);
7353
+ if (operand) {
7354
+ const opText = getNodeText(operand);
7355
+ if (operand.type === "identifier") {
7356
+ const resolved = resolveGoLocalReceiverType(opText, node);
7357
+ receiver = resolved !== null ? resolved : opText;
7358
+ } else {
7359
+ receiver = opText;
7360
+ }
7361
+ }
7354
7362
  } else if (funcNode.type === "identifier") {
7355
7363
  methodName = getNodeText(funcNode);
7356
7364
  } else {
@@ -7428,6 +7436,80 @@ function findGoEnclosingFunction(node) {
7428
7436
  }
7429
7437
  return null;
7430
7438
  }
7439
+ function resolveGoLocalReceiverType(operandName, callNode) {
7440
+ let cur = callNode.parent;
7441
+ while (cur) {
7442
+ if (cur.type === "method_declaration") {
7443
+ const receiver = cur.childForFieldName("receiver");
7444
+ if (receiver) {
7445
+ const t = extractGoParamTypeForName(receiver, operandName);
7446
+ if (t !== null) return t;
7447
+ }
7448
+ const params = cur.childForFieldName("parameters");
7449
+ if (params) {
7450
+ const t = extractGoParamTypeForName(params, operandName);
7451
+ if (t !== null) return t;
7452
+ }
7453
+ return null;
7454
+ }
7455
+ if (cur.type === "function_declaration" || cur.type === "func_literal") {
7456
+ const params = cur.childForFieldName("parameters");
7457
+ if (params) {
7458
+ const t = extractGoParamTypeForName(params, operandName);
7459
+ if (t !== null) return t;
7460
+ }
7461
+ return null;
7462
+ }
7463
+ cur = cur.parent;
7464
+ }
7465
+ return null;
7466
+ }
7467
+ function extractGoParamTypeForName(list, operandName) {
7468
+ for (let i2 = 0; i2 < list.namedChildCount; i2++) {
7469
+ const child = list.namedChild(i2);
7470
+ if (!child || child.type !== "parameter_declaration") continue;
7471
+ const typeNode = child.childForFieldName("type");
7472
+ if (!typeNode) continue;
7473
+ let matched = false;
7474
+ for (let j = 0; j < child.namedChildCount; j++) {
7475
+ const c = child.namedChild(j);
7476
+ if (!c) continue;
7477
+ if (c.type === "identifier" && getNodeText(c) === operandName) {
7478
+ matched = true;
7479
+ break;
7480
+ }
7481
+ if (c.type === "identifier_list") {
7482
+ for (let k = 0; k < c.namedChildCount; k++) {
7483
+ const id = c.namedChild(k);
7484
+ if (id && id.type === "identifier" && getNodeText(id) === operandName) {
7485
+ matched = true;
7486
+ break;
7487
+ }
7488
+ }
7489
+ if (matched) break;
7490
+ }
7491
+ }
7492
+ if (!matched) continue;
7493
+ return extractGoTypeLastSegment(typeNode);
7494
+ }
7495
+ return null;
7496
+ }
7497
+ function extractGoTypeLastSegment(typeNode) {
7498
+ if (typeNode.type === "pointer_type") {
7499
+ const inner = typeNode.namedChild(0);
7500
+ return inner ? extractGoTypeLastSegment(inner) : null;
7501
+ }
7502
+ if (typeNode.type === "qualified_type") {
7503
+ const name2 = typeNode.childForFieldName("name");
7504
+ if (name2) return getNodeText(name2);
7505
+ const last = typeNode.namedChild(typeNode.namedChildCount - 1);
7506
+ return last ? getNodeText(last) : null;
7507
+ }
7508
+ if (typeNode.type === "type_identifier" || typeNode.type === "identifier") {
7509
+ return getNodeText(typeNode);
7510
+ }
7511
+ return null;
7512
+ }
7431
7513
 
7432
7514
  // src/core/extractors/imports.ts
7433
7515
  function detectLanguage2(tree) {
@@ -8176,10 +8258,10 @@ function buildCFG(tree, language, cache) {
8176
8258
  const allEdges = [];
8177
8259
  let blockIdCounter = 0;
8178
8260
  if (effectiveLanguage === "bash") {
8179
- return buildBashCFG(tree, blockIdCounter);
8261
+ return buildBashCFG(tree, blockIdCounter, cache);
8180
8262
  }
8181
8263
  if (effectiveLanguage === "go") {
8182
- return buildGoCFG(tree, blockIdCounter);
8264
+ return buildGoCFG(tree, blockIdCounter, cache);
8183
8265
  }
8184
8266
  if (isJavaScript) {
8185
8267
  const functions = [
@@ -8536,11 +8618,11 @@ function processSwitchStatement(stmt, startId, blocks, edges, isJavaScript) {
8536
8618
  nextId: currentId
8537
8619
  };
8538
8620
  }
8539
- function buildBashCFG(tree, startId) {
8621
+ function buildBashCFG(tree, startId, cache) {
8540
8622
  const allBlocks = [];
8541
8623
  const allEdges = [];
8542
8624
  let blockIdCounter = startId;
8543
- const functions = findNodes(tree.rootNode, "function_definition");
8625
+ const functions = getNodesFromCache(tree.rootNode, "function_definition", cache);
8544
8626
  for (const func2 of functions) {
8545
8627
  const body2 = func2.childForFieldName("body");
8546
8628
  if (!body2) continue;
@@ -8657,12 +8739,12 @@ function isStatement(node, isJavaScript) {
8657
8739
  ]);
8658
8740
  return isJavaScript ? jsStatementTypes.has(node.type) : javaStatementTypes.has(node.type);
8659
8741
  }
8660
- function buildGoCFG(tree, blockIdCounter) {
8742
+ function buildGoCFG(tree, blockIdCounter, cache) {
8661
8743
  const allBlocks = [];
8662
8744
  const allEdges = [];
8663
8745
  const functions = [
8664
- ...findNodes(tree.rootNode, "function_declaration"),
8665
- ...findNodes(tree.rootNode, "method_declaration")
8746
+ ...getNodesFromCache(tree.rootNode, "function_declaration", cache),
8747
+ ...getNodesFromCache(tree.rootNode, "method_declaration", cache)
8666
8748
  ];
8667
8749
  for (const func2 of functions) {
8668
8750
  const body2 = func2.childForFieldName("body");
@@ -10576,6 +10658,59 @@ var OPEN_REDIRECT_FRAMEWORK_SINKS = [
10576
10658
  { method: "Redirect", class: "Context", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [1], languages: ["go"] },
10577
10659
  { method: "Redirect", class: "Ctx", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["go"] }
10578
10660
  ];
10661
+ var DESERIALIZATION_FRAMEWORK_SINKS = [
10662
+ // --- Python: stdlib + popular third-party --------------------------------
10663
+ // pickle: known-dangerous, any unpickle on untrusted bytes is RCE.
10664
+ { method: "loads", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10665
+ { method: "load", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10666
+ // cPickle alias (Python 2 name, still around in older codebases).
10667
+ { method: "loads", class: "cPickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10668
+ { method: "load", class: "cPickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10669
+ // marshal: stdlib code-object deserializer. Loading a tainted bytestring
10670
+ // as a code object followed by `exec` is arbitrary-code execution.
10671
+ { method: "loads", class: "marshal", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10672
+ { method: "load", class: "marshal", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10673
+ // dill: pickle superset — same RCE profile.
10674
+ { method: "loads", class: "dill", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10675
+ { method: "load", class: "dill", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10676
+ // jsonpickle: JSON wrapper around pickle — trusts `py/object` marker.
10677
+ { method: "decode", class: "jsonpickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
10678
+ // --- Go: encoding/gob + yaml.Unmarshal -----------------------------------
10679
+ // gob.NewDecoder(r).Decode(&v): tainted io.Reader → arbitrary Go values.
10680
+ { method: "Decode", class: "Decoder", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], languages: ["go"] },
10681
+ // gopkg.in/yaml.v2 + v3 top-level function; interface{} target is unsafe.
10682
+ { method: "Unmarshal", class: "yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], languages: ["go"] },
10683
+ // --- JS/TS: node-serialize ------------------------------------------------
10684
+ // Known-dangerous — accepts embedded IIFE that runs during deserialize.
10685
+ { method: "unserialize", class: "nodeSerialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] }
10686
+ ];
10687
+ var NOSQL_FRAMEWORK_SINKS = [
10688
+ // --- Python: pymongo Collection ------------------------------------------
10689
+ // Every filter-taking Collection method; filter arg[0] is the query dict.
10690
+ { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10691
+ { method: "find_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10692
+ { method: "aggregate", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10693
+ { method: "update_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["python"] },
10694
+ { method: "update_many", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["python"] },
10695
+ { method: "delete_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10696
+ { method: "delete_many", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10697
+ { method: "count_documents", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
10698
+ // --- Java: Spring Data MongoTemplate + native MongoCollection ------------
10699
+ { method: "find", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10700
+ { method: "findOne", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10701
+ { method: "findAll", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10702
+ { method: "find", class: "MongoCollection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10703
+ { method: "aggregate", class: "MongoCollection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
10704
+ // --- Go: go.mongodb.org/mongo-driver Collection --------------------------
10705
+ // These fire once Go local-receiver type resolution lands (see
10706
+ // taint-matcher.ts + #240 ship 2 Go receiver work). Same gate as the
10707
+ // gin/fiber Ctx sinks in ship 1.
10708
+ { method: "Find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] },
10709
+ { method: "FindOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] },
10710
+ { method: "UpdateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1, 2], languages: ["go"] },
10711
+ { method: "UpdateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1, 2], languages: ["go"] },
10712
+ { method: "DeleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] }
10713
+ ];
10579
10714
  var TRUST_BOUNDARY_FRAMEWORK_SINKS = [
10580
10715
  // --- Python: Django cache write -----------------------------------------
10581
10716
  { method: "set", class: "cache", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [1], languages: ["python"] },
@@ -12259,7 +12394,12 @@ var DEFAULT_SINKS = [
12259
12394
  // keep the DEFAULT_SINKS literal within TypeScript's union-type
12260
12395
  // inference complexity limit (TS2590). See ~lines 661-752.
12261
12396
  ...OPEN_REDIRECT_FRAMEWORK_SINKS,
12262
- ...TRUST_BOUNDARY_FRAMEWORK_SINKS
12397
+ ...TRUST_BOUNDARY_FRAMEWORK_SINKS,
12398
+ // cognium-dev #240 ship 2 — extended framework sinks for
12399
+ // deserialization (CWE-502) and nosql_injection (CWE-943). Same
12400
+ // pattern: constants defined near the ship-1 blocks and spread here.
12401
+ ...DESERIALIZATION_FRAMEWORK_SINKS,
12402
+ ...NOSQL_FRAMEWORK_SINKS
12263
12403
  ];
12264
12404
  var DEFAULT_SANITIZERS = [
12265
12405
  // SQL Injection - proper parameter binding sanitizes input
@@ -12602,6 +12742,64 @@ function getDefaultConfig() {
12602
12742
  };
12603
12743
  }
12604
12744
 
12745
+ // src/analysis/arg-type-resolver.ts
12746
+ function findEnclosingType(callInMethod, types) {
12747
+ if (!callInMethod) return null;
12748
+ for (const t of types) {
12749
+ for (const m of t.methods) {
12750
+ if (m.name === callInMethod) return t;
12751
+ }
12752
+ }
12753
+ return null;
12754
+ }
12755
+ function resolveIdentifierType(name2, callInMethod, types) {
12756
+ if (!name2) return null;
12757
+ const enclosing = findEnclosingType(callInMethod, types);
12758
+ if (enclosing) {
12759
+ for (const m of enclosing.methods) {
12760
+ if (m.name !== callInMethod) continue;
12761
+ for (const p of m.parameters) {
12762
+ if (p.name === name2 && p.type) return p.type;
12763
+ }
12764
+ }
12765
+ for (const f of enclosing.fields) {
12766
+ if (f.name === name2 && f.type) return f.type;
12767
+ }
12768
+ return null;
12769
+ }
12770
+ for (const t of types) {
12771
+ for (const m of t.methods) {
12772
+ for (const p of m.parameters) {
12773
+ if (p.name === name2 && p.type) return p.type;
12774
+ }
12775
+ }
12776
+ for (const f of t.fields) {
12777
+ if (f.name === name2 && f.type) return f.type;
12778
+ }
12779
+ }
12780
+ return null;
12781
+ }
12782
+ function resolveCallReturnType(calleeName, callInMethod, types) {
12783
+ if (!calleeName) return null;
12784
+ const enclosing = findEnclosingType(callInMethod, types);
12785
+ if (enclosing) {
12786
+ for (const m of enclosing.methods) {
12787
+ if (m.name === calleeName && m.return_type) return m.return_type;
12788
+ }
12789
+ }
12790
+ for (const t of types) {
12791
+ for (const m of t.methods) {
12792
+ if (m.name === calleeName && m.return_type) return m.return_type;
12793
+ }
12794
+ }
12795
+ return null;
12796
+ }
12797
+ function isBoundedClassType(rawType) {
12798
+ if (!rawType) return false;
12799
+ const stripped = rawType.trim().replace(/^(?:java\.lang\.)+/, "");
12800
+ return /^Class(?:\s*<[\s\S]*>)?$/.test(stripped);
12801
+ }
12802
+
12605
12803
  // src/analysis/taint-matcher.ts
12606
12804
  var PYTHON_TAINTED_PATTERNS = [
12607
12805
  { pattern: /\brequest\.args\b/, sourceType: "http_param" },
@@ -12627,7 +12825,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
12627
12825
  const sources = findSources(calls, types, config.sources, sourceLines, language);
12628
12826
  let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
12629
12827
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
12630
- const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines);
12828
+ const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
12631
12829
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
12632
12830
  return { sources, sinks, sanitizers };
12633
12831
  }
@@ -12721,11 +12919,11 @@ function attachSourceLineCode(sources, sinks, code) {
12721
12919
  }
12722
12920
  function findSources(calls, types, patterns, sourceLines, language) {
12723
12921
  const sources = [];
12922
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
12923
+ (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
12924
+ );
12724
12925
  for (const call of calls) {
12725
- for (const pattern of patterns) {
12726
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
12727
- continue;
12728
- }
12926
+ for (const pattern of patternsForLanguage) {
12729
12927
  if (matchesSourcePattern(call, pattern)) {
12730
12928
  sources.push({
12731
12929
  type: pattern.type,
@@ -12741,11 +12939,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
12741
12939
  for (const type of types) {
12742
12940
  for (const method of type.methods) {
12743
12941
  for (const param of method.parameters) {
12744
- for (const pattern of patterns) {
12942
+ for (const pattern of patternsForLanguage) {
12745
12943
  if (pattern.annotation && pattern.param_tainted) {
12746
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
12747
- continue;
12748
- }
12749
12944
  if (matchesAnnotation(param.annotations, pattern.annotation)) {
12750
12945
  const paramLine = param.line ?? method.start_line;
12751
12946
  sources.push({
@@ -12764,11 +12959,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
12764
12959
  }
12765
12960
  for (const type of types) {
12766
12961
  for (const method of type.methods) {
12767
- for (const pattern of patterns) {
12962
+ for (const pattern of patternsForLanguage) {
12768
12963
  if (!pattern.method_annotation) continue;
12769
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
12770
- continue;
12771
- }
12772
12964
  if (!matchesAnnotation(method.annotations, pattern.method_annotation)) continue;
12773
12965
  for (const param of method.parameters) {
12774
12966
  const paramLine = param.line ?? method.start_line;
@@ -13205,13 +13397,30 @@ function isSafeRustCommandCall(call, pattern, language) {
13205
13397
  return false;
13206
13398
  }
13207
13399
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
13208
- function argIsClassLiteral(call, position) {
13400
+ function argIsClassLiteral(call, position, types) {
13209
13401
  const arg = call.arguments.find((a) => a.position === position);
13210
13402
  if (!arg) return false;
13211
13403
  const expr = (arg.literal ?? arg.expression ?? "").trim();
13212
13404
  if (!expr) return false;
13213
13405
  if (CLASS_LITERAL_RE.test(expr)) return true;
13214
- return TYPE_TOKEN_RE.test(expr);
13406
+ if (TYPE_TOKEN_RE.test(expr)) return true;
13407
+ if (!types || types.length === 0) return false;
13408
+ const argExpr = (arg.expression ?? "").trim();
13409
+ const varName = arg.variable ?? "";
13410
+ const isBareIdentifier = varName !== "" && argExpr === varName && !/[(.]/.test(argExpr);
13411
+ if (isBareIdentifier) {
13412
+ const t = resolveIdentifierType(varName, call.in_method, types);
13413
+ if (isBoundedClassType(t)) return true;
13414
+ }
13415
+ const isCallExpr = varName !== "" && new RegExp(`^${escapeRe(varName)}\\s*\\(`).test(argExpr);
13416
+ if (isCallExpr) {
13417
+ const t = resolveCallReturnType(varName, call.in_method, types);
13418
+ if (isBoundedClassType(t)) return true;
13419
+ }
13420
+ return false;
13421
+ }
13422
+ function escapeRe(s) {
13423
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13215
13424
  }
13216
13425
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
13217
13426
  function argIsStringLiteral(call, position) {
@@ -13372,10 +13581,13 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
13372
13581
  }
13373
13582
  return false;
13374
13583
  }
13375
- function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13584
+ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
13585
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
13586
+ (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
13587
+ );
13376
13588
  const sinkMap = /* @__PURE__ */ new Map();
13377
13589
  for (const call of calls) {
13378
- for (const pattern of patterns) {
13590
+ for (const pattern of patternsForLanguage) {
13379
13591
  if (matchesSinkPattern(call, pattern, typeHierarchy, language)) {
13380
13592
  if (isParameterizedQueryCall(call, pattern)) {
13381
13593
  continue;
@@ -13392,7 +13604,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13392
13604
  if (isSafeJSChildProcessCall(call, pattern, language)) {
13393
13605
  continue;
13394
13606
  }
13395
- if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
13607
+ if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
13396
13608
  continue;
13397
13609
  }
13398
13610
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
@@ -13793,7 +14005,20 @@ function matchesAnnotation(annotations, targetAnnotation) {
13793
14005
  }
13794
14006
  return false;
13795
14007
  }
14008
+ var RECEIVER_MIGHT_BE_CLASS_CACHE = /* @__PURE__ */ new Map();
14009
+ var RECEIVER_MIGHT_BE_CLASS_CACHE_CAP = 1e4;
13796
14010
  function receiverMightBeClass(receiver, className) {
14011
+ const key = receiver + "\0" + className;
14012
+ const cached = RECEIVER_MIGHT_BE_CLASS_CACHE.get(key);
14013
+ if (cached !== void 0) return cached;
14014
+ const result = receiverMightBeClassImpl(receiver, className);
14015
+ if (RECEIVER_MIGHT_BE_CLASS_CACHE.size >= RECEIVER_MIGHT_BE_CLASS_CACHE_CAP) {
14016
+ RECEIVER_MIGHT_BE_CLASS_CACHE.clear();
14017
+ }
14018
+ RECEIVER_MIGHT_BE_CLASS_CACHE.set(key, result);
14019
+ return result;
14020
+ }
14021
+ function receiverMightBeClassImpl(receiver, className) {
13797
14022
  if (className.startsWith("*") && className.length > 1) {
13798
14023
  const suffix = className.slice(1).toLowerCase();
13799
14024
  let simpleReceiver = receiver;
@@ -14497,8 +14722,15 @@ var CodeGraph = class {
14497
14722
  };
14498
14723
 
14499
14724
  // src/analysis/dfg-walk.ts
14725
+ var walkBackwardDefsMemo = /* @__PURE__ */ new WeakMap();
14500
14726
  function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
14501
14727
  const maxHops = options.maxHops ?? 32;
14728
+ let perFile = walkBackwardDefsMemo.get(chainsByToDef);
14729
+ if (perFile !== void 0) {
14730
+ const key = `${startDefId}|${maxHops}`;
14731
+ const hit = perFile.get(key);
14732
+ if (hit !== void 0) return hit;
14733
+ }
14502
14734
  const visited = /* @__PURE__ */ new Set();
14503
14735
  const lines = /* @__PURE__ */ new Set();
14504
14736
  let hopCapReached = false;
@@ -14530,7 +14762,13 @@ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
14530
14762
  queue.push(fromId);
14531
14763
  }
14532
14764
  }
14533
- return { visited, lines, hopCapReached };
14765
+ const result = { visited, lines, hopCapReached };
14766
+ if (perFile === void 0) {
14767
+ perFile = /* @__PURE__ */ new Map();
14768
+ walkBackwardDefsMemo.set(chainsByToDef, perFile);
14769
+ }
14770
+ perFile.set(`${startDefId}|${maxHops}`, result);
14771
+ return result;
14534
14772
  }
14535
14773
 
14536
14774
  // src/analysis/sanitizer-index.ts
@@ -15513,7 +15751,7 @@ var ConstantPropagator = class _ConstantPropagator {
15513
15751
  this.constructorParamPositions.clear();
15514
15752
  this.safePatternFieldsCache = null;
15515
15753
  this.isTaintedExpressionCache = null;
15516
- this.collectClassFields(tree.rootNode);
15754
+ const prePassMethods = this.collectClassFieldsAndMethods(tree.rootNode);
15517
15755
  for (const methodName of sanitizerMethods) {
15518
15756
  this.methodReturnsSanitized.add(methodName);
15519
15757
  }
@@ -15521,7 +15759,7 @@ var ConstantPropagator = class _ConstantPropagator {
15521
15759
  this.source,
15522
15760
  (name2) => this.lookupSymbol(name2)
15523
15761
  );
15524
- this.analyzeMethodReturns(tree.rootNode);
15762
+ this.analyzeMethodReturns(prePassMethods);
15525
15763
  this.seedPythonModuleConstants(tree.rootNode);
15526
15764
  this.visit(tree.rootNode);
15527
15765
  this.refineTaintFromConstants();
@@ -15591,8 +15829,7 @@ var ConstantPropagator = class _ConstantPropagator {
15591
15829
  /**
15592
15830
  * Pre-pass: Analyze all methods to detect those that always return constants or sanitized values.
15593
15831
  */
15594
- analyzeMethodReturns(root) {
15595
- const methods = this.findAllMethods(root);
15832
+ analyzeMethodReturns(methods) {
15596
15833
  for (const method of methods) {
15597
15834
  const methodName = this.getMethodName(method);
15598
15835
  if (!methodName) continue;
@@ -15856,11 +16093,15 @@ var ConstantPropagator = class _ConstantPropagator {
15856
16093
  * Collect all class field names (instance/static variables declared at class level).
15857
16094
  * These are variables declared directly in the class body, not inside methods.
15858
16095
  */
15859
- collectClassFields(root) {
16096
+ collectClassFieldsAndMethods(root) {
16097
+ const methods = [];
15860
16098
  const stack = [root];
15861
16099
  while (stack.length > 0) {
15862
16100
  const n = stack.pop();
15863
16101
  if (!n) continue;
16102
+ if (n.type === "method_declaration" || n.type === "function_declaration") {
16103
+ methods.push(n);
16104
+ }
15864
16105
  if (n.type === "class_body") {
15865
16106
  for (const child of n.children) {
15866
16107
  if (child.type === "field_declaration") {
@@ -15874,14 +16115,15 @@ var ConstantPropagator = class _ConstantPropagator {
15874
16115
  }
15875
16116
  }
15876
16117
  }
15877
- stack.push(child);
16118
+ if (child) stack.push(child);
15878
16119
  }
15879
16120
  continue;
15880
16121
  }
15881
16122
  for (const child of n.children) {
15882
- stack.push(child);
16123
+ if (child) stack.push(child);
15883
16124
  }
15884
16125
  }
16126
+ return methods;
15885
16127
  }
15886
16128
  /**
15887
16129
  * Sprint 9 #55 — seed the symbol table with Python module-level constant
@@ -16039,21 +16281,6 @@ var ConstantPropagator = class _ConstantPropagator {
16039
16281
  this.symbols.set(name2, value);
16040
16282
  }
16041
16283
  }
16042
- findAllMethods(node) {
16043
- const methods = [];
16044
- const stack = [node];
16045
- while (stack.length > 0) {
16046
- const n = stack.pop();
16047
- if (!n) continue;
16048
- if (n.type === "method_declaration" || n.type === "function_declaration") {
16049
- methods.push(n);
16050
- }
16051
- for (const child of n.children) {
16052
- if (child) stack.push(child);
16053
- }
16054
- }
16055
- return methods;
16056
- }
16057
16284
  getMethodName(method) {
16058
16285
  const nameNode = method.childForFieldName("name");
16059
16286
  if (nameNode) {
@@ -2113,8 +2113,27 @@ function extractGoCallInfo(node) {
2113
2113
  // pkg.Function() or obj.Method()
2114
2114
  const operand = funcNode.childForFieldName('operand');
2115
2115
  const field = funcNode.childForFieldName('field');
2116
- receiver = operand ? getNodeText(operand) : null;
2117
2116
  methodName = field ? getNodeText(field) : getNodeText(funcNode);
2117
+ // cognium-dev #240 ship 2 — Go local-receiver type resolution.
2118
+ // When the operand is a bare identifier and matches a method
2119
+ // receiver or function parameter in the enclosing declaration,
2120
+ // rewrite receiver to the type's last identifier segment. This
2121
+ // unblocks sinks defined with `class: 'Context'` (gin/echo/fiber),
2122
+ // `class: 'Ctx'` (fiber), and `class: 'Collection'` (mongo-driver)
2123
+ // when the source shape is `c.Redirect(...)` / `c.SetCookie(...)` /
2124
+ // `col.Find(...)`. Package-qualified calls like `fmt.Sprintf(...)`
2125
+ // fall through to the original operand text (fmt is not a
2126
+ // parameter / receiver, resolver returns null).
2127
+ if (operand) {
2128
+ const opText = getNodeText(operand);
2129
+ if (operand.type === 'identifier') {
2130
+ const resolved = resolveGoLocalReceiverType(opText, node);
2131
+ receiver = resolved !== null ? resolved : opText;
2132
+ }
2133
+ else {
2134
+ receiver = opText;
2135
+ }
2136
+ }
2118
2137
  }
2119
2138
  else if (funcNode.type === 'identifier') {
2120
2139
  // Plain function call: funcName()
@@ -2213,4 +2232,115 @@ function findGoEnclosingFunction(node) {
2213
2232
  }
2214
2233
  return null;
2215
2234
  }
2235
+ /**
2236
+ * Resolve a Go local-variable operand (bare identifier receiver in a
2237
+ * `selector_expression`) to its declared type's last identifier segment.
2238
+ *
2239
+ * Handles three enclosing-declaration shapes:
2240
+ * 1. Method receiver: `func (c *gin.Context) H() { c.Redirect(...) }`
2241
+ * 2. Function param: `func H(c *gin.Context, url string) { c.Redirect(...) }`
2242
+ * 3. Multi-name param: `func H(a, b *Foo) { a.Bar() }` (identifier_list)
2243
+ *
2244
+ * Returns `null` when the operand does not match any receiver or
2245
+ * parameter in the nearest enclosing `method_declaration` /
2246
+ * `function_declaration` / `func_literal` — the caller then falls back
2247
+ * to the operand text so package-qualified calls like `fmt.Sprintf(...)`
2248
+ * continue to work.
2249
+ *
2250
+ * cognium-dev #240 ship 2.
2251
+ */
2252
+ function resolveGoLocalReceiverType(operandName, callNode) {
2253
+ let cur = callNode.parent;
2254
+ while (cur) {
2255
+ if (cur.type === 'method_declaration') {
2256
+ const receiver = cur.childForFieldName('receiver');
2257
+ if (receiver) {
2258
+ const t = extractGoParamTypeForName(receiver, operandName);
2259
+ if (t !== null)
2260
+ return t;
2261
+ }
2262
+ const params = cur.childForFieldName('parameters');
2263
+ if (params) {
2264
+ const t = extractGoParamTypeForName(params, operandName);
2265
+ if (t !== null)
2266
+ return t;
2267
+ }
2268
+ return null;
2269
+ }
2270
+ if (cur.type === 'function_declaration' || cur.type === 'func_literal') {
2271
+ const params = cur.childForFieldName('parameters');
2272
+ if (params) {
2273
+ const t = extractGoParamTypeForName(params, operandName);
2274
+ if (t !== null)
2275
+ return t;
2276
+ }
2277
+ return null;
2278
+ }
2279
+ cur = cur.parent;
2280
+ }
2281
+ return null;
2282
+ }
2283
+ /**
2284
+ * Scan a Go `parameter_list` for a `parameter_declaration` naming
2285
+ * `operandName` (directly or inside an `identifier_list`) and return
2286
+ * its type's last identifier segment. Returns `null` on miss.
2287
+ */
2288
+ function extractGoParamTypeForName(list, operandName) {
2289
+ for (let i = 0; i < list.namedChildCount; i++) {
2290
+ const child = list.namedChild(i);
2291
+ if (!child || child.type !== 'parameter_declaration')
2292
+ continue;
2293
+ const typeNode = child.childForFieldName('type');
2294
+ if (!typeNode)
2295
+ continue;
2296
+ let matched = false;
2297
+ for (let j = 0; j < child.namedChildCount; j++) {
2298
+ const c = child.namedChild(j);
2299
+ if (!c)
2300
+ continue;
2301
+ if (c.type === 'identifier' && getNodeText(c) === operandName) {
2302
+ matched = true;
2303
+ break;
2304
+ }
2305
+ if (c.type === 'identifier_list') {
2306
+ for (let k = 0; k < c.namedChildCount; k++) {
2307
+ const id = c.namedChild(k);
2308
+ if (id && id.type === 'identifier' && getNodeText(id) === operandName) {
2309
+ matched = true;
2310
+ break;
2311
+ }
2312
+ }
2313
+ if (matched)
2314
+ break;
2315
+ }
2316
+ }
2317
+ if (!matched)
2318
+ continue;
2319
+ return extractGoTypeLastSegment(typeNode);
2320
+ }
2321
+ return null;
2322
+ }
2323
+ /**
2324
+ * Return the last identifier segment of a Go type expression:
2325
+ * `*gin.Context` → `"Context"`; `gin.Context` → `"Context"`;
2326
+ * `*Context` → `"Context"`; `Context` → `"Context"`;
2327
+ * `[]byte` → `null` (structured types not yet handled).
2328
+ */
2329
+ function extractGoTypeLastSegment(typeNode) {
2330
+ if (typeNode.type === 'pointer_type') {
2331
+ const inner = typeNode.namedChild(0);
2332
+ return inner ? extractGoTypeLastSegment(inner) : null;
2333
+ }
2334
+ if (typeNode.type === 'qualified_type') {
2335
+ const name = typeNode.childForFieldName('name');
2336
+ if (name)
2337
+ return getNodeText(name);
2338
+ const last = typeNode.namedChild(typeNode.namedChildCount - 1);
2339
+ return last ? getNodeText(last) : null;
2340
+ }
2341
+ if (typeNode.type === 'type_identifier' || typeNode.type === 'identifier') {
2342
+ return getNodeText(typeNode);
2343
+ }
2344
+ return null;
2345
+ }
2216
2346
  //# sourceMappingURL=calls.js.map