circle-ir 4.9.9 → 4.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.
@@ -5983,6 +5983,26 @@ var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
5983
5983
  "ExecuteReaderAsync",
5984
5984
  "ExecuteNonQueryAsync"
5985
5985
  ]);
5986
+ var CSHARP_RECEIVER_URL_METHODS = /* @__PURE__ */ new Set([
5987
+ "GetAsync",
5988
+ "PostAsync",
5989
+ "PutAsync",
5990
+ "PatchAsync",
5991
+ "DeleteAsync",
5992
+ "HeadAsync",
5993
+ "GetStringAsync",
5994
+ "GetByteArrayAsync",
5995
+ "GetStreamAsync",
5996
+ "GetJsonAsync"
5997
+ ]);
5998
+ var CSHARP_HEADER_RECEIVER_RE = /(^|\.)Headers$/;
5999
+ function csharpBareName(node) {
6000
+ if (node.type === "generic_name") {
6001
+ const id = node.childForFieldName("name") ?? node.namedChild(0);
6002
+ if (id) return getNodeText(id);
6003
+ }
6004
+ return getNodeText(node);
6005
+ }
5986
6006
  function extractCSharpCalls(tree, cache) {
5987
6007
  const calls = [];
5988
6008
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -5994,16 +6014,22 @@ function extractCSharpCalls(tree, cache) {
5994
6014
  if (fn?.type === "member_access_expression") {
5995
6015
  const nameNode = fn.childForFieldName("name");
5996
6016
  const exprNode = fn.childForFieldName("expression");
5997
- methodName = nameNode ? getNodeText(nameNode) : "unknown";
6017
+ methodName = nameNode ? csharpBareName(nameNode) : "unknown";
5998
6018
  receiver = exprNode ? getNodeText(exprNode) : null;
5999
6019
  } else if (fn) {
6000
- methodName = getNodeText(fn);
6020
+ methodName = csharpBareName(fn);
6001
6021
  }
6002
6022
  const argsNode = inv.childForFieldName("arguments");
6003
6023
  let args2 = argsNode ? extractCSharpArguments(argsNode) : [];
6004
6024
  if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
6005
6025
  args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args2];
6006
6026
  }
6027
+ if (methodName === "Add" && receiver && CSHARP_HEADER_RECEIVER_RE.test(receiver)) {
6028
+ methodName = "AddHeader";
6029
+ }
6030
+ if (receiver && args2.length === 0 && CSHARP_RECEIVER_URL_METHODS.has(methodName)) {
6031
+ args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }];
6032
+ }
6007
6033
  calls.push({
6008
6034
  method_name: methodName,
6009
6035
  receiver,
@@ -6019,7 +6045,7 @@ function extractCSharpCalls(tree, cache) {
6019
6045
  const typeNode = creation.childForFieldName("type");
6020
6046
  const argsNode = creation.childForFieldName("arguments");
6021
6047
  calls.push({
6022
- method_name: typeNode ? getNodeText(typeNode) : "unknown",
6048
+ method_name: typeNode ? csharpBareName(typeNode) : "unknown",
6023
6049
  receiver: null,
6024
6050
  receiver_type: null,
6025
6051
  receiver_type_fqn: null,
@@ -6032,6 +6058,39 @@ function extractCSharpCalls(tree, cache) {
6032
6058
  const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
6033
6059
  for (const asn of assignments) {
6034
6060
  const left = asn.childForFieldName("left");
6061
+ if (left?.type === "element_access_expression") {
6062
+ const target = left.childForFieldName("expression");
6063
+ const right2 = asn.childForFieldName("right");
6064
+ if (!target || !right2) continue;
6065
+ if (!CSHARP_HEADER_RECEIVER_RE.test(getNodeText(target))) continue;
6066
+ const subscript = left.childForFieldName("subscript") ?? left.namedChild(1);
6067
+ const rhsText2 = getNodeText(right2);
6068
+ calls.push({
6069
+ method_name: "AddHeader",
6070
+ receiver: getNodeText(target),
6071
+ receiver_type: null,
6072
+ receiver_type_fqn: null,
6073
+ arguments: [
6074
+ {
6075
+ position: 0,
6076
+ expression: subscript ? getNodeText(subscript) : "",
6077
+ variable: null,
6078
+ literal: subscript ? getNodeText(subscript) : null,
6079
+ value: null
6080
+ },
6081
+ {
6082
+ position: 1,
6083
+ expression: rhsText2,
6084
+ variable: right2.type === "identifier" ? rhsText2 : null,
6085
+ literal: right2.type === "string_literal" ? rhsText2 : null,
6086
+ value: null
6087
+ }
6088
+ ],
6089
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6090
+ in_method: findEnclosingMethod(asn)
6091
+ });
6092
+ continue;
6093
+ }
6035
6094
  if (left?.type !== "member_access_expression") continue;
6036
6095
  const nameNode = left.childForFieldName("name");
6037
6096
  if (!nameNode) continue;
@@ -13251,6 +13310,23 @@ var DEFAULT_SINKS = [
13251
13310
  { method: "ExecuteSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13252
13311
  { method: "FromSqlRaw", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13253
13312
  { method: "FromSqlRawAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13313
+ // Dapper micro-ORM (cognium-dev#275). Every Dapper entry point is an
13314
+ // `IDbConnection` extension whose arg 0 is raw SQL text — `conn.Query<T>(sql)`
13315
+ // is exactly as injectable as `new SqlCommand(sql)`. Taint-gated on arg 0, so
13316
+ // a constant/interpolation-free query never fires, and a same-named method on
13317
+ // an unrelated receiver (e.g. RestSharp's `client.Execute(request)`, whose
13318
+ // arg 0 is a request object rather than a tainted string) stays clean.
13319
+ { method: "Query", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13320
+ { method: "QueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13321
+ { method: "QueryFirst", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13322
+ { method: "QueryFirstAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13323
+ { method: "QueryFirstOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13324
+ { method: "QueryFirstOrDefaultAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13325
+ { method: "QuerySingle", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13326
+ { method: "QuerySingleOrDefault", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13327
+ { method: "QueryMultiple", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13328
+ { method: "Execute", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13329
+ { method: "ExecuteAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13254
13330
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13255
13331
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13256
13332
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13316,12 +13392,23 @@ var DEFAULT_SINKS = [
13316
13392
  { method: "DeleteAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13317
13393
  { method: "GetStringAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13318
13394
  { method: "GetByteArrayAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13395
+ // Remaining HttpClient/Flurl verbs (cognium-dev#275). For HttpClient these
13396
+ // take the URL at arg 0; for Flurl's fluent form the receiver is surfaced as
13397
+ // arg 0 by CSHARP_RECEIVER_URL_METHODS. Taint-gated either way.
13398
+ // (GetStreamAsync / PutAsync / DeleteAsync are already registered below.)
13399
+ { method: "GetJsonAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13400
+ { method: "PatchAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13401
+ { method: "HeadAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13319
13402
  { method: "GetStreamAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13320
13403
  { method: "SendAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13321
13404
  { method: "DownloadString", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13322
13405
  { method: "DownloadData", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13323
13406
  { method: "DownloadFile", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [1], languages: ["csharp"] },
13324
13407
  { method: "Create", class: "WebRequest", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13408
+ // RestSharp (cognium-dev#275) — the base URL is fixed at construction, so
13409
+ // `new RestClient(userUrl)` points every later request at attacker-controlled
13410
+ // infrastructure. Ctor arg 0, taint-gated.
13411
+ { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13325
13412
  // C# code injection — dynamic script/assembly loading (CWE-94).
13326
13413
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13327
13414
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13361,6 +13448,11 @@ var DEFAULT_SINKS = [
13361
13448
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13362
13449
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13363
13450
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13451
+ // `return Content(html, "text/html")` on a controller writes arg 0 to the
13452
+ // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13453
+ // `Content(constantString)` and the JSON/plain-text content types are
13454
+ // unaffected because a constant argument carries no taint.
13455
+ { method: "Content", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13364
13456
  // `String.Format(fmt, …)` with an attacker-controlled FORMAT string — CWE-134.
13365
13457
  // Taint-gated on arg 0 (the format), so ordinary `String.Format("...", user)` where
13366
13458
  // only an argument is tainted never fires. .NET composite formatting can't corrupt
@@ -13386,6 +13478,10 @@ var DEFAULT_SINKS = [
13386
13478
  // cognium-dev#273/#275). `BsonDocument.Parse(userJson)` deserializes an
13387
13479
  // attacker-controlled query document. Class-scoped (static receiver resolves).
13388
13480
  { method: "Parse", class: "BsonDocument", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13481
+ // `new BsonJavaScript(userCode)` wraps a server-side JS string — the payload
13482
+ // MongoDB's `$where` / `$function` operators execute. Unconditionally
13483
+ // dangerous when tainted, so no literal gating is needed (cognium-dev#275).
13484
+ { method: "BsonJavaScript", class: "constructor", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["csharp"] },
13389
13485
  // C# log injection — Microsoft.Extensions.Logging `ILogger` (CWE-117,
13390
13486
  // cognium-ai#318). Only the message TEMPLATE (arg[0]) is the injectable
13391
13487
  // position; the structured form `LogInformation("… {Id}", id)` keeps taint in
@@ -13397,6 +13493,16 @@ var DEFAULT_SINKS = [
13397
13493
  { method: "LogDebug", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13398
13494
  { method: "LogCritical", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13399
13495
  { method: "LogTrace", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["csharp"] },
13496
+ // C# ReDoS (CWE-1333, cognium-dev#275). The injectable position is the
13497
+ // PATTERN, not the input: a user-supplied regex can be crafted with
13498
+ // catastrophic backtracking. `Regex.IsMatch(input, pattern)` and friends take
13499
+ // the pattern at arg 1; the `new Regex(pattern)` ctor takes it at arg 0. A
13500
+ // tainted *input* with a constant pattern is not ReDoS and does not fire.
13501
+ { method: "IsMatch", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13502
+ { method: "Match", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13503
+ { method: "Matches", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13504
+ { method: "Replace", class: "Regex", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [1], languages: ["csharp"] },
13505
+ { method: "Regex", class: "constructor", type: "redos", cwe: "CWE-1333", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13400
13506
  // Generic `ILogger.Log(logLevel, message, …)` — class-scoped (`Log` alone is
13401
13507
  // too generic); the message is arg[1].
13402
13508
  { method: "Log", class: "ILogger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1], languages: ["csharp"] },
@@ -13655,6 +13761,15 @@ var DEFAULT_SANITIZERS = [
13655
13761
  // this table; the drift surfaced as a SecuriBench Micro FP on
13656
13762
  // `sanitizers/Sanitizers3.java` (`URLEncoder.encode` + `sendRedirect`).
13657
13763
  { method: "encodeForURL", removes: ["xss", "ssrf", "open_redirect"] },
13764
+ // ASP.NET Core local-redirect guards (cognium-dev#275). `Url.IsLocalUrl(x)`
13765
+ // returns false for any absolute/scheme-relative URL, so a `Redirect(x)` gated
13766
+ // on it cannot leave the site — without this credit the guarded form is a
13767
+ // false positive. `LocalRedirect(x)` is the same check applied by the
13768
+ // framework itself: it THROWS on a non-local URL, which is why it is
13769
+ // deliberately not registered as an open_redirect sink. Recording it here
13770
+ // states that intent in the model rather than leaving it as a silent omission.
13771
+ { method: "IsLocalUrl", removes: ["open_redirect"] },
13772
+ { method: "LocalRedirect", removes: ["open_redirect"] },
13658
13773
  // URL encoding wrapper aliases (common patterns in benchmarks and real-world code)
13659
13774
  { method: "encodeURL", removes: ["xss", "ssrf", "open_redirect"] },
13660
13775
  { method: "urlEncode", removes: ["xss", "ssrf", "open_redirect"] },
@@ -160,6 +160,53 @@ const CSHARP_COMMAND_EXECUTE_METHODS = new Set([
160
160
  'ExecuteScalar', 'ExecuteReader', 'ExecuteNonQuery',
161
161
  'ExecuteScalarAsync', 'ExecuteReaderAsync', 'ExecuteNonQueryAsync',
162
162
  ]);
163
+ /**
164
+ * Flurl fluent HTTP methods (cognium-dev#275). Flurl extends `string`/`Url`, so
165
+ * the request target is the RECEIVER — `userUrl.GetStringAsync()` — which the
166
+ * arg-position sink model cannot address.
167
+ *
168
+ * These names collide with `HttpClient`'s (`client.GetStringAsync(url)`), where
169
+ * the URL is already arg 0 and the receiver is an untainted client. Prepending
170
+ * the receiver unconditionally would shift that URL to index 1 and silently
171
+ * break every existing HttpClient SSRF detection. The two are disambiguated by
172
+ * arity instead: the Flurl fluent form passes no URL argument, HttpClient always
173
+ * does — so the receiver is only surfaced as arg 0 for a ZERO-argument call.
174
+ *
175
+ * Consequence: body-carrying Flurl verbs (`url.PostJsonAsync(dto)`,
176
+ * `url.DownloadFileAsync(dir)`) are NOT covered — they pass an argument, so the
177
+ * guard skips them. Covering those needs real receiver-type resolution to tell a
178
+ * `Url`/`string` receiver from an `HttpClient` one; listing their names here
179
+ * without it would only add entries the guard can never reach.
180
+ */
181
+ const CSHARP_RECEIVER_URL_METHODS = new Set([
182
+ 'GetAsync', 'PostAsync', 'PutAsync', 'PatchAsync', 'DeleteAsync', 'HeadAsync',
183
+ 'GetStringAsync', 'GetByteArrayAsync', 'GetStreamAsync', 'GetJsonAsync',
184
+ ]);
185
+ /**
186
+ * Receiver expressions that denote an ASP.NET header dictionary —
187
+ * `Response.Headers`, `ctx.Response.Headers`, `req.Headers`. Used to normalize
188
+ * `.Headers.Add(k, v)` / `.Headers[k] = v` onto the registered `AddHeader`
189
+ * CRLF sink without registering the ubiquitous bare `Add`. (cognium-dev#275)
190
+ */
191
+ const CSHARP_HEADER_RECEIVER_RE = /(^|\.)Headers$/;
192
+ /**
193
+ * Bare method/type name for a C# node, with any generic type-argument list
194
+ * stripped (cognium-dev#275).
195
+ *
196
+ * tree-sitter-c-sharp represents `conn.Query<User>(sql)` with a `generic_name`
197
+ * in the `name` field, so `getNodeText` yields `"Query<User>"` — which matches
198
+ * no registry entry, making EVERY generic C# call invisible to the sink matcher
199
+ * (Dapper's `Query<T>`/`QueryAsync<T>`, `Deserialize<T>`, `GetService<T>`, …).
200
+ * Taking the identifier child restores the bare name the registry keys on.
201
+ */
202
+ function csharpBareName(node) {
203
+ if (node.type === 'generic_name') {
204
+ const id = node.childForFieldName('name') ?? node.namedChild(0);
205
+ if (id)
206
+ return getNodeText(id);
207
+ }
208
+ return getNodeText(node);
209
+ }
163
210
  function extractCSharpCalls(tree, cache) {
164
211
  const calls = [];
165
212
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -171,11 +218,11 @@ function extractCSharpCalls(tree, cache) {
171
218
  if (fn?.type === 'member_access_expression') {
172
219
  const nameNode = fn.childForFieldName('name');
173
220
  const exprNode = fn.childForFieldName('expression');
174
- methodName = nameNode ? getNodeText(nameNode) : 'unknown';
221
+ methodName = nameNode ? csharpBareName(nameNode) : 'unknown';
175
222
  receiver = exprNode ? getNodeText(exprNode) : null;
176
223
  }
177
224
  else if (fn) {
178
- methodName = getNodeText(fn);
225
+ methodName = csharpBareName(fn);
179
226
  }
180
227
  const argsNode = inv.childForFieldName('arguments');
181
228
  let args = argsNode ? extractCSharpArguments(argsNode) : [];
@@ -187,6 +234,20 @@ function extractCSharpCalls(tree, cache) {
187
234
  if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
188
235
  args = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args];
189
236
  }
237
+ // `Response.Headers.Add(name, value)` is the ASP.NET Core response-header
238
+ // writer, but a bare `Add` is far too common to register as a CRLF sink
239
+ // (List.Add, Dictionary.Add, …). Normalize the header-dictionary form to the
240
+ // already-registered `AddHeader` name instead, gated on the receiver
241
+ // expression ending in `.Headers`, so the existing arg-1 crlf sink matches
242
+ // and nothing else named `Add` is touched. (cognium-dev#275)
243
+ if (methodName === 'Add' && receiver && CSHARP_HEADER_RECEIVER_RE.test(receiver)) {
244
+ methodName = 'AddHeader';
245
+ }
246
+ // Flurl fluent form only — see CSHARP_RECEIVER_URL_METHODS. The arity guard
247
+ // is what keeps `client.GetStringAsync(url)` (HttpClient) untouched.
248
+ if (receiver && args.length === 0 && CSHARP_RECEIVER_URL_METHODS.has(methodName)) {
249
+ args = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }];
250
+ }
190
251
  calls.push({
191
252
  method_name: methodName,
192
253
  receiver,
@@ -202,7 +263,7 @@ function extractCSharpCalls(tree, cache) {
202
263
  const typeNode = creation.childForFieldName('type');
203
264
  const argsNode = creation.childForFieldName('arguments');
204
265
  calls.push({
205
- method_name: typeNode ? getNodeText(typeNode) : 'unknown',
266
+ method_name: typeNode ? csharpBareName(typeNode) : 'unknown',
206
267
  receiver: null,
207
268
  receiver_type: null,
208
269
  receiver_type_fqn: null,
@@ -221,6 +282,44 @@ function extractCSharpCalls(tree, cache) {
221
282
  const assignments = getNodesFromCache(tree.rootNode, 'assignment_expression', cache);
222
283
  for (const asn of assignments) {
223
284
  const left = asn.childForFieldName('left');
285
+ // `Response.Headers["X-Foo"] = tainted` — the indexer form of the header
286
+ // write above. Emitted as `AddHeader(name, value)` so the same arg-1 crlf
287
+ // sink matches. (cognium-dev#275)
288
+ if (left?.type === 'element_access_expression') {
289
+ const target = left.childForFieldName('expression');
290
+ const right = asn.childForFieldName('right');
291
+ if (!target || !right)
292
+ continue;
293
+ if (!CSHARP_HEADER_RECEIVER_RE.test(getNodeText(target)))
294
+ continue;
295
+ const subscript = left.childForFieldName('subscript') ?? left.namedChild(1);
296
+ const rhsText = getNodeText(right);
297
+ calls.push({
298
+ method_name: 'AddHeader',
299
+ receiver: getNodeText(target),
300
+ receiver_type: null,
301
+ receiver_type_fqn: null,
302
+ arguments: [
303
+ {
304
+ position: 0,
305
+ expression: subscript ? getNodeText(subscript) : '',
306
+ variable: null,
307
+ literal: subscript ? getNodeText(subscript) : null,
308
+ value: null,
309
+ },
310
+ {
311
+ position: 1,
312
+ expression: rhsText,
313
+ variable: right.type === 'identifier' ? rhsText : null,
314
+ literal: right.type === 'string_literal' ? rhsText : null,
315
+ value: null,
316
+ },
317
+ ],
318
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
319
+ in_method: findEnclosingMethod(asn),
320
+ });
321
+ continue;
322
+ }
224
323
  if (left?.type !== 'member_access_expression')
225
324
  continue;
226
325
  const nameNode = left.childForFieldName('name');