circle-ir 3.180.0 → 3.182.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.
Files changed (29) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +29 -0
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/dependency-versions.d.ts +37 -0
  5. package/dist/analysis/dependency-versions.d.ts.map +1 -1
  6. package/dist/analysis/dependency-versions.js +70 -0
  7. package/dist/analysis/dependency-versions.js.map +1 -1
  8. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  9. package/dist/analysis/passes/language-sources-pass.js +18 -2
  10. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  11. package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts +47 -31
  12. package/dist/analysis/passes/python-receiver-taint-format-pass.d.ts.map +1 -1
  13. package/dist/analysis/passes/python-receiver-taint-format-pass.js +84 -46
  14. package/dist/analysis/passes/python-receiver-taint-format-pass.js.map +1 -1
  15. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  16. package/dist/analysis/passes/taint-propagation-pass.js +158 -0
  17. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  18. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  19. package/dist/analysis/taint-propagation.js +47 -11
  20. package/dist/analysis/taint-propagation.js.map +1 -1
  21. package/dist/analyzer.d.ts +18 -0
  22. package/dist/analyzer.d.ts.map +1 -1
  23. package/dist/analyzer.js.map +1 -1
  24. package/dist/browser/circle-ir.js +247 -20
  25. package/dist/core/circle-ir-core.cjs +113 -8
  26. package/dist/core/circle-ir-core.js +113 -8
  27. package/dist/core/extractors/dfg.js +99 -0
  28. package/dist/core/extractors/dfg.js.map +1 -1
  29. package/package.json +1 -1
@@ -10066,6 +10066,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
10066
10066
  } else if (child.type === "for_statement") {
10067
10067
  const rangeClause = findChildByTypeGo(child, "range_clause");
10068
10068
  if (rangeClause) {
10069
+ const right = rangeClause.childForFieldName("right");
10070
+ if (right) {
10071
+ extractGoUses(right, uses, scopeStack);
10072
+ }
10069
10073
  const left = rangeClause.childForFieldName("left");
10070
10074
  if (left) {
10071
10075
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
@@ -10073,6 +10077,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
10073
10077
  }
10074
10078
  } else if (child.type === "call_expression") {
10075
10079
  extractGoUses(child, uses, scopeStack);
10080
+ recordGoOpaqueCodecDestDef(child, defs, scopeStack);
10076
10081
  } else if (child.type === "return_statement") {
10077
10082
  for (let i2 = 0; i2 < child.childCount; i2++) {
10078
10083
  const expr = child.child(i2);
@@ -10165,6 +10170,63 @@ function findChildByTypeGo(node, type) {
10165
10170
  }
10166
10171
  return null;
10167
10172
  }
10173
+ var GO_OPAQUE_CODEC_METHODS = /* @__PURE__ */ new Set([
10174
+ "Unmarshal",
10175
+ // json/xml/yaml/toml/gob
10176
+ "Decode",
10177
+ // json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
10178
+ "NewDecoder",
10179
+ // wrapper; handled via chained Decode above
10180
+ "UnmarshalYAML",
10181
+ "UnmarshalJSON",
10182
+ "UnmarshalText",
10183
+ "UnmarshalBinary"
10184
+ ]);
10185
+ function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
10186
+ const fn = call.childForFieldName("function");
10187
+ if (!fn || fn.type !== "selector_expression") return;
10188
+ const fieldNode = fn.childForFieldName("field");
10189
+ if (!fieldNode) return;
10190
+ const method = getNodeText(fieldNode);
10191
+ if (!GO_OPAQUE_CODEC_METHODS.has(method)) return;
10192
+ const argsNode = call.childForFieldName("arguments");
10193
+ if (!argsNode) return;
10194
+ const args2 = [];
10195
+ for (let i2 = 0; i2 < argsNode.childCount; i2++) {
10196
+ const c = argsNode.child(i2);
10197
+ if (!c) continue;
10198
+ if (c.type === "," || c.type === "(" || c.type === ")") continue;
10199
+ args2.push(c);
10200
+ }
10201
+ if (args2.length === 0) return;
10202
+ const destArg = args2.length >= 2 ? args2[1] : args2[0];
10203
+ const destName = extractGoAddressableVarName(destArg);
10204
+ if (!destName || destName === "_") return;
10205
+ const line = call.startPosition.row + 1;
10206
+ const def = {
10207
+ id: defs.length + 1,
10208
+ variable: destName,
10209
+ kind: "local",
10210
+ line
10211
+ };
10212
+ defs.push(def);
10213
+ currentScope(scopeStack).set(destName, def.id);
10214
+ }
10215
+ function extractGoAddressableVarName(node) {
10216
+ if (node.type === "unary_expression") {
10217
+ const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
10218
+ if (operand) return extractGoAddressableVarName(operand);
10219
+ return null;
10220
+ }
10221
+ if (node.type === "identifier") {
10222
+ return getNodeText(node);
10223
+ }
10224
+ if (node.type === "selector_expression") {
10225
+ const field = node.childForFieldName("field");
10226
+ if (field) return getNodeText(field);
10227
+ }
10228
+ return null;
10229
+ }
10168
10230
 
10169
10231
  // src/analysis/config-loader.ts
10170
10232
  function parseConfig(content) {
@@ -10677,6 +10739,35 @@ var DEFAULT_SOURCES = [
10677
10739
  { method: "get", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10678
10740
  { method: "hget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10679
10741
  { method: "mget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10742
+ // --- Serverless transport channels (cognium-dev #213 first slice) ---
10743
+ //
10744
+ // AWS Lambda / API Gateway invocation-event properties. The Lambda
10745
+ // handler signature `(event, context) => …` (JS/TS) or
10746
+ // `def handler(event, context)` (Python) receives an `event` object
10747
+ // whose properties carry the untrusted HTTP-request-shaped payload:
10748
+ //
10749
+ // event.body — request body (string)
10750
+ // event.queryStringParameters — `?a=b` params
10751
+ // event.multiValueQueryStringParameters — `?a=1&a=2` params
10752
+ // event.pathParameters — path template captures
10753
+ // event.headers — request headers
10754
+ // event.multiValueHeaders — repeated headers
10755
+ // event.requestContext — API Gateway request context
10756
+ //
10757
+ // Vercel Serverless Functions and Cloudflare Workers both use `req`
10758
+ // / `request` receivers that are already covered by the Express-
10759
+ // style patterns above (line ~398). This block specifically covers
10760
+ // the `event`-shaped API Gateway convention that those don't reach.
10761
+ { property: "body", object: "event", type: "http_body", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10762
+ { property: "queryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10763
+ { property: "multiValueQueryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10764
+ { property: "pathParameters", object: "event", type: "http_path", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10765
+ { property: "headers", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10766
+ { property: "multiValueHeaders", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10767
+ { property: "body", object: "event", type: "http_body", severity: "high", property_tainted: true, languages: ["python"] },
10768
+ { property: "queryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["python"] },
10769
+ { property: "pathParameters", object: "event", type: "http_path", severity: "high", property_tainted: true, languages: ["python"] },
10770
+ { property: "headers", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["python"] },
10680
10771
  // --- JWT claims (unverified decode — PyJWT / jose / jsonwebtoken / auth0 java-jwt / golang-jwt) ---
10681
10772
  // A JWT's payload is *always* attacker-authored. Even after verification
10682
10773
  // the *contents* of the claims (username, role, custom fields) are not
@@ -14958,15 +15049,21 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
14958
15049
  const callsAtSink = callsByLine.get(sink.line) ?? [];
14959
15050
  for (const call of callsAtSink) {
14960
15051
  for (const arg of call.arguments) {
14961
- if (arg.variable) {
14962
- if (sink.argPositions && sink.argPositions.length > 0) {
14963
- if (!sink.argPositions.includes(arg.position)) {
14964
- continue;
14965
- }
15052
+ if (sink.argPositions && sink.argPositions.length > 0) {
15053
+ if (!sink.argPositions.includes(arg.position)) {
15054
+ continue;
14966
15055
  }
14967
- for (const use of usesAtSink) {
14968
- if (use.variable === arg.variable && use.def_id !== null) {
15056
+ }
15057
+ const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
15058
+ {
15059
+ for (const use of candidateUses) {
15060
+ if (use.def_id !== null) {
14969
15061
  if (allTaintedDefIds.has(use.def_id)) {
15062
+ if (!arg.variable) {
15063
+ if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
15064
+ const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
15065
+ if (!re.test(arg.expression)) continue;
15066
+ }
14970
15067
  const taintInfo = taintByDefId.get(use.def_id);
14971
15068
  if (taintInfo) {
14972
15069
  const isSanitized = checkSanitized(
@@ -15004,7 +15101,15 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
15004
15101
  }
15005
15102
  }
15006
15103
  }
15007
- return { taintedVars, flows, reachableSinks };
15104
+ const seen = /* @__PURE__ */ new Set();
15105
+ const deduped = [];
15106
+ for (const f of flows) {
15107
+ const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
15108
+ if (seen.has(key)) continue;
15109
+ seen.add(key);
15110
+ deduped.push(f);
15111
+ }
15112
+ return { taintedVars, flows: deduped, reachableSinks };
15008
15113
  }
15009
15114
  function findInitialTaint(sources, callsByLine, defsByLine) {
15010
15115
  const tainted = [];
@@ -10000,6 +10000,10 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
10000
10000
  } else if (child.type === "for_statement") {
10001
10001
  const rangeClause = findChildByTypeGo(child, "range_clause");
10002
10002
  if (rangeClause) {
10003
+ const right = rangeClause.childForFieldName("right");
10004
+ if (right) {
10005
+ extractGoUses(right, uses, scopeStack);
10006
+ }
10003
10007
  const left = rangeClause.childForFieldName("left");
10004
10008
  if (left) {
10005
10009
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
@@ -10007,6 +10011,7 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
10007
10011
  }
10008
10012
  } else if (child.type === "call_expression") {
10009
10013
  extractGoUses(child, uses, scopeStack);
10014
+ recordGoOpaqueCodecDestDef(child, defs, scopeStack);
10010
10015
  } else if (child.type === "return_statement") {
10011
10016
  for (let i2 = 0; i2 < child.childCount; i2++) {
10012
10017
  const expr = child.child(i2);
@@ -10099,6 +10104,63 @@ function findChildByTypeGo(node, type) {
10099
10104
  }
10100
10105
  return null;
10101
10106
  }
10107
+ var GO_OPAQUE_CODEC_METHODS = /* @__PURE__ */ new Set([
10108
+ "Unmarshal",
10109
+ // json/xml/yaml/toml/gob
10110
+ "Decode",
10111
+ // json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
10112
+ "NewDecoder",
10113
+ // wrapper; handled via chained Decode above
10114
+ "UnmarshalYAML",
10115
+ "UnmarshalJSON",
10116
+ "UnmarshalText",
10117
+ "UnmarshalBinary"
10118
+ ]);
10119
+ function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
10120
+ const fn = call.childForFieldName("function");
10121
+ if (!fn || fn.type !== "selector_expression") return;
10122
+ const fieldNode = fn.childForFieldName("field");
10123
+ if (!fieldNode) return;
10124
+ const method = getNodeText(fieldNode);
10125
+ if (!GO_OPAQUE_CODEC_METHODS.has(method)) return;
10126
+ const argsNode = call.childForFieldName("arguments");
10127
+ if (!argsNode) return;
10128
+ const args2 = [];
10129
+ for (let i2 = 0; i2 < argsNode.childCount; i2++) {
10130
+ const c = argsNode.child(i2);
10131
+ if (!c) continue;
10132
+ if (c.type === "," || c.type === "(" || c.type === ")") continue;
10133
+ args2.push(c);
10134
+ }
10135
+ if (args2.length === 0) return;
10136
+ const destArg = args2.length >= 2 ? args2[1] : args2[0];
10137
+ const destName = extractGoAddressableVarName(destArg);
10138
+ if (!destName || destName === "_") return;
10139
+ const line = call.startPosition.row + 1;
10140
+ const def = {
10141
+ id: defs.length + 1,
10142
+ variable: destName,
10143
+ kind: "local",
10144
+ line
10145
+ };
10146
+ defs.push(def);
10147
+ currentScope(scopeStack).set(destName, def.id);
10148
+ }
10149
+ function extractGoAddressableVarName(node) {
10150
+ if (node.type === "unary_expression") {
10151
+ const operand = node.childForFieldName("operand") ?? node.child(1) ?? null;
10152
+ if (operand) return extractGoAddressableVarName(operand);
10153
+ return null;
10154
+ }
10155
+ if (node.type === "identifier") {
10156
+ return getNodeText(node);
10157
+ }
10158
+ if (node.type === "selector_expression") {
10159
+ const field = node.childForFieldName("field");
10160
+ if (field) return getNodeText(field);
10161
+ }
10162
+ return null;
10163
+ }
10102
10164
 
10103
10165
  // src/analysis/config-loader.ts
10104
10166
  function parseConfig(content) {
@@ -10611,6 +10673,35 @@ var DEFAULT_SOURCES = [
10611
10673
  { method: "get", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10612
10674
  { method: "hget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10613
10675
  { method: "mget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
10676
+ // --- Serverless transport channels (cognium-dev #213 first slice) ---
10677
+ //
10678
+ // AWS Lambda / API Gateway invocation-event properties. The Lambda
10679
+ // handler signature `(event, context) => …` (JS/TS) or
10680
+ // `def handler(event, context)` (Python) receives an `event` object
10681
+ // whose properties carry the untrusted HTTP-request-shaped payload:
10682
+ //
10683
+ // event.body — request body (string)
10684
+ // event.queryStringParameters — `?a=b` params
10685
+ // event.multiValueQueryStringParameters — `?a=1&a=2` params
10686
+ // event.pathParameters — path template captures
10687
+ // event.headers — request headers
10688
+ // event.multiValueHeaders — repeated headers
10689
+ // event.requestContext — API Gateway request context
10690
+ //
10691
+ // Vercel Serverless Functions and Cloudflare Workers both use `req`
10692
+ // / `request` receivers that are already covered by the Express-
10693
+ // style patterns above (line ~398). This block specifically covers
10694
+ // the `event`-shaped API Gateway convention that those don't reach.
10695
+ { property: "body", object: "event", type: "http_body", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10696
+ { property: "queryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10697
+ { property: "multiValueQueryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10698
+ { property: "pathParameters", object: "event", type: "http_path", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10699
+ { property: "headers", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10700
+ { property: "multiValueHeaders", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["javascript", "typescript"] },
10701
+ { property: "body", object: "event", type: "http_body", severity: "high", property_tainted: true, languages: ["python"] },
10702
+ { property: "queryStringParameters", object: "event", type: "http_query", severity: "high", property_tainted: true, languages: ["python"] },
10703
+ { property: "pathParameters", object: "event", type: "http_path", severity: "high", property_tainted: true, languages: ["python"] },
10704
+ { property: "headers", object: "event", type: "http_header", severity: "high", property_tainted: true, languages: ["python"] },
10614
10705
  // --- JWT claims (unverified decode — PyJWT / jose / jsonwebtoken / auth0 java-jwt / golang-jwt) ---
10615
10706
  // A JWT's payload is *always* attacker-authored. Even after verification
10616
10707
  // the *contents* of the claims (username, role, custom fields) are not
@@ -14892,15 +14983,21 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
14892
14983
  const callsAtSink = callsByLine.get(sink.line) ?? [];
14893
14984
  for (const call of callsAtSink) {
14894
14985
  for (const arg of call.arguments) {
14895
- if (arg.variable) {
14896
- if (sink.argPositions && sink.argPositions.length > 0) {
14897
- if (!sink.argPositions.includes(arg.position)) {
14898
- continue;
14899
- }
14986
+ if (sink.argPositions && sink.argPositions.length > 0) {
14987
+ if (!sink.argPositions.includes(arg.position)) {
14988
+ continue;
14900
14989
  }
14901
- for (const use of usesAtSink) {
14902
- if (use.variable === arg.variable && use.def_id !== null) {
14990
+ }
14991
+ const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
14992
+ {
14993
+ for (const use of candidateUses) {
14994
+ if (use.def_id !== null) {
14903
14995
  if (allTaintedDefIds.has(use.def_id)) {
14996
+ if (!arg.variable) {
14997
+ if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
14998
+ const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
14999
+ if (!re.test(arg.expression)) continue;
15000
+ }
14904
15001
  const taintInfo = taintByDefId.get(use.def_id);
14905
15002
  if (taintInfo) {
14906
15003
  const isSanitized = checkSanitized(
@@ -14938,7 +15035,15 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
14938
15035
  }
14939
15036
  }
14940
15037
  }
14941
- return { taintedVars, flows, reachableSinks };
15038
+ const seen = /* @__PURE__ */ new Set();
15039
+ const deduped = [];
15040
+ for (const f of flows) {
15041
+ const key = `${f.source.line}|${f.sink.line}|${f.sink.type}|${f.path.map((p) => p.variable).join(">")}`;
15042
+ if (seen.has(key)) continue;
15043
+ seen.add(key);
15044
+ deduped.push(f);
15045
+ }
15046
+ return { taintedVars, flows: deduped, reachableSinks };
14942
15047
  }
14943
15048
  function findInitialTaint(sources, callsByLine, defsByLine) {
14944
15049
  const tainted = [];
@@ -1385,6 +1385,14 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
1385
1385
  // range clause: for k, v := range expr
1386
1386
  const rangeClause = findChildByTypeGo(child, 'range_clause');
1387
1387
  if (rangeClause) {
1388
+ const right = rangeClause.childForFieldName('right');
1389
+ // Record uses of the range source on the same line as the loop-var
1390
+ // defs, so `computeChains` links the source def → loop-var def, which
1391
+ // is what carries taint into the loop body. Emit before defs so RHS
1392
+ // uses see the outer binding, not the new loop var. (Issue #243.)
1393
+ if (right) {
1394
+ extractGoUses(right, uses, scopeStack);
1395
+ }
1388
1396
  const left = rangeClause.childForFieldName('left');
1389
1397
  if (left) {
1390
1398
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
@@ -1394,6 +1402,14 @@ function processGoBlock(node, defs, uses, scopeStack, counters) {
1394
1402
  else if (child.type === 'call_expression') {
1395
1403
  // Extract uses from call arguments
1396
1404
  extractGoUses(child, uses, scopeStack);
1405
+ // Opaque-codec destination re-def (cognium-dev #243).
1406
+ //
1407
+ // Calls like `json.Unmarshal(bytes, &dest)`, `xml.Unmarshal(...)`,
1408
+ // `gob.NewDecoder(r).Decode(&dest)`, `yaml.Unmarshal(...)` populate
1409
+ // `dest` via reflection. Model that as a re-definition of `dest` on
1410
+ // this line so `computeChains` links the source-arg use to a fresh
1411
+ // `dest` def and taint propagates through the codec.
1412
+ recordGoOpaqueCodecDestDef(child, defs, scopeStack);
1397
1413
  }
1398
1414
  else if (child.type === 'return_statement') {
1399
1415
  // Extract uses from return expressions
@@ -1514,4 +1530,87 @@ function findChildByTypeGo(node, type) {
1514
1530
  }
1515
1531
  return null;
1516
1532
  }
1533
+ /**
1534
+ * cognium-dev #243 — opaque codec destination re-defs for Go.
1535
+ *
1536
+ * Package/method pairs whose second (or only) arg is a destination that the
1537
+ * call populates via reflection. Modelling them as re-defs of the dest lets
1538
+ * `computeChains` link source-arg → dest and taint propagates through the
1539
+ * codec.
1540
+ */
1541
+ const GO_OPAQUE_CODEC_METHODS = new Set([
1542
+ 'Unmarshal', // json/xml/yaml/toml/gob
1543
+ 'Decode', // json.NewDecoder(r).Decode(&dest), gob.Decoder.Decode
1544
+ 'NewDecoder', // wrapper; handled via chained Decode above
1545
+ 'UnmarshalYAML',
1546
+ 'UnmarshalJSON',
1547
+ 'UnmarshalText',
1548
+ 'UnmarshalBinary',
1549
+ ]);
1550
+ function recordGoOpaqueCodecDestDef(call, defs, scopeStack) {
1551
+ // Only handle direct selector calls: `pkg.Method(...)` or `receiver.Decode(...)`.
1552
+ const fn = call.childForFieldName('function');
1553
+ if (!fn || fn.type !== 'selector_expression')
1554
+ return;
1555
+ const fieldNode = fn.childForFieldName('field');
1556
+ if (!fieldNode)
1557
+ return;
1558
+ const method = getNodeText(fieldNode);
1559
+ if (!GO_OPAQUE_CODEC_METHODS.has(method))
1560
+ return;
1561
+ const argsNode = call.childForFieldName('arguments');
1562
+ if (!argsNode)
1563
+ return;
1564
+ // Positional args (skip commas / parens).
1565
+ const args = [];
1566
+ for (let i = 0; i < argsNode.childCount; i++) {
1567
+ const c = argsNode.child(i);
1568
+ if (!c)
1569
+ continue;
1570
+ if (c.type === ',' || c.type === '(' || c.type === ')')
1571
+ continue;
1572
+ args.push(c);
1573
+ }
1574
+ if (args.length === 0)
1575
+ return;
1576
+ // Destination convention:
1577
+ // - Unmarshal(bytes, &dest) → args[1]
1578
+ // - Decode(&dest) → args[0]
1579
+ const destArg = args.length >= 2 ? args[1] : args[0];
1580
+ const destName = extractGoAddressableVarName(destArg);
1581
+ if (!destName || destName === '_')
1582
+ return;
1583
+ const line = call.startPosition.row + 1;
1584
+ const def = {
1585
+ id: defs.length + 1,
1586
+ variable: destName,
1587
+ kind: 'local',
1588
+ line,
1589
+ };
1590
+ defs.push(def);
1591
+ currentScope(scopeStack).set(destName, def.id);
1592
+ }
1593
+ /**
1594
+ * Return the addressed variable name for an opaque-codec destination arg.
1595
+ * Handles `&x`, bare `x`, and `&pkg.Y`-style receivers (returns `Y` — the
1596
+ * codec still populates that concrete addressable location).
1597
+ */
1598
+ function extractGoAddressableVarName(node) {
1599
+ if (node.type === 'unary_expression') {
1600
+ // &x — operand carries the identifier
1601
+ const operand = node.childForFieldName('operand') ?? node.child(1) ?? null;
1602
+ if (operand)
1603
+ return extractGoAddressableVarName(operand);
1604
+ return null;
1605
+ }
1606
+ if (node.type === 'identifier') {
1607
+ return getNodeText(node);
1608
+ }
1609
+ if (node.type === 'selector_expression') {
1610
+ const field = node.childForFieldName('field');
1611
+ if (field)
1612
+ return getNodeText(field);
1613
+ }
1614
+ return null;
1615
+ }
1517
1616
  //# sourceMappingURL=dfg.js.map