circle-ir 3.3.3 → 3.6.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.
@@ -5470,6 +5470,9 @@ function extractCalls(tree, cache, language) {
5470
5470
  const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5471
5471
  const isPython = detectedLanguage === "python";
5472
5472
  const isRust = detectedLanguage === "rust";
5473
+ if (detectedLanguage === "bash") {
5474
+ return extractBashCalls(tree, cache);
5475
+ }
5473
5476
  if (isRust) {
5474
5477
  return extractRustCalls(tree, cache);
5475
5478
  }
@@ -6159,6 +6162,88 @@ function inferTypeFromReceiverName(receiver) {
6159
6162
  const lowerReceiver = receiver.toLowerCase();
6160
6163
  return patterns[lowerReceiver] ?? null;
6161
6164
  }
6165
+ function extractBashCalls(tree, cache) {
6166
+ const calls = [];
6167
+ const commands = getNodesFromCache(tree.rootNode, "command", cache);
6168
+ for (const cmd of commands) {
6169
+ const callInfo = extractBashCommandInfo(cmd);
6170
+ if (callInfo) {
6171
+ calls.push(callInfo);
6172
+ }
6173
+ }
6174
+ return calls;
6175
+ }
6176
+ function extractBashCommandInfo(node) {
6177
+ const nameNode = node.childForFieldName("name");
6178
+ if (!nameNode) return null;
6179
+ const commandName = getNodeText(nameNode);
6180
+ if (!commandName) return null;
6181
+ const args2 = [];
6182
+ let position = 0;
6183
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6184
+ const child = node.child(i2);
6185
+ if (!child) continue;
6186
+ if (child === nameNode) continue;
6187
+ if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
6188
+ continue;
6189
+ }
6190
+ const expression = getNodeText(child);
6191
+ if (!expression.trim()) continue;
6192
+ const variable = extractBashVariableRef(child);
6193
+ args2.push({
6194
+ position: position++,
6195
+ expression,
6196
+ variable,
6197
+ literal: null
6198
+ });
6199
+ }
6200
+ const inMethod = findBashEnclosingFunction(node);
6201
+ return {
6202
+ method_name: commandName,
6203
+ receiver: null,
6204
+ arguments: args2,
6205
+ location: {
6206
+ line: node.startPosition.row + 1,
6207
+ column: node.startPosition.column
6208
+ },
6209
+ in_method: inMethod,
6210
+ resolved: false,
6211
+ resolution: { status: "external_method" }
6212
+ };
6213
+ }
6214
+ function extractBashVariableRef(node) {
6215
+ const type = node.type;
6216
+ if (type === "simple_expansion" || type === "expansion") {
6217
+ return getNodeText(node).replace(/^\$\{?/, "").replace(/\}$/, "");
6218
+ }
6219
+ if (type === "string" || type === "concatenation") {
6220
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6221
+ const child = node.child(i2);
6222
+ if (!child) continue;
6223
+ if (child.type === "simple_expansion" || child.type === "expansion") {
6224
+ return getNodeText(child).replace(/^\$\{?/, "").replace(/\}$/, "");
6225
+ }
6226
+ }
6227
+ }
6228
+ if (type === "word") {
6229
+ const text = getNodeText(node);
6230
+ if (text.startsWith("$")) {
6231
+ return text.slice(1).replace(/^\{/, "").replace(/\}$/, "");
6232
+ }
6233
+ }
6234
+ return null;
6235
+ }
6236
+ function findBashEnclosingFunction(node) {
6237
+ let current = node.parent;
6238
+ while (current) {
6239
+ if (current.type === "function_definition") {
6240
+ const nameNode = current.childForFieldName("name");
6241
+ return nameNode ? getNodeText(nameNode) : null;
6242
+ }
6243
+ current = current.parent;
6244
+ }
6245
+ return null;
6246
+ }
6162
6247
  function extractPythonCalls(tree, cache) {
6163
6248
  const calls = [];
6164
6249
  const context = buildPythonResolutionContext(tree, cache);
@@ -13896,6 +13981,9 @@ function getLanguageRegistry() {
13896
13981
  function registerLanguage(plugin) {
13897
13982
  getLanguageRegistry().register(plugin);
13898
13983
  }
13984
+ function getLanguagePlugin(language) {
13985
+ return getLanguageRegistry().get(language);
13986
+ }
13899
13987
 
13900
13988
  // src/languages/plugins/base.ts
13901
13989
  var BaseLanguagePlugin = class {
@@ -14497,6 +14585,80 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
14497
14585
  confidence: 0.85,
14498
14586
  returnTainted: true
14499
14587
  },
14588
+ // Fastify-specific sources (request object)
14589
+ {
14590
+ method: "raw",
14591
+ class: "request",
14592
+ type: "http_param",
14593
+ severity: "high",
14594
+ confidence: 0.85,
14595
+ returnTainted: true
14596
+ },
14597
+ {
14598
+ method: "hostname",
14599
+ class: "request",
14600
+ type: "http_header",
14601
+ severity: "medium",
14602
+ confidence: 0.8,
14603
+ returnTainted: true
14604
+ },
14605
+ // Koa context sources (ctx.* and ctx.request.*)
14606
+ {
14607
+ method: "header",
14608
+ class: "ctx",
14609
+ type: "http_header",
14610
+ severity: "high",
14611
+ confidence: 0.85,
14612
+ returnTainted: true
14613
+ },
14614
+ {
14615
+ method: "headers",
14616
+ class: "ctx",
14617
+ type: "http_header",
14618
+ severity: "high",
14619
+ confidence: 0.85,
14620
+ returnTainted: true
14621
+ },
14622
+ {
14623
+ method: "host",
14624
+ class: "ctx",
14625
+ type: "http_header",
14626
+ severity: "medium",
14627
+ confidence: 0.8,
14628
+ returnTainted: true
14629
+ },
14630
+ {
14631
+ method: "hostname",
14632
+ class: "ctx",
14633
+ type: "http_header",
14634
+ severity: "medium",
14635
+ confidence: 0.8,
14636
+ returnTainted: true
14637
+ },
14638
+ {
14639
+ method: "path",
14640
+ class: "ctx",
14641
+ type: "http_path",
14642
+ severity: "high",
14643
+ confidence: 0.85,
14644
+ returnTainted: true
14645
+ },
14646
+ {
14647
+ method: "url",
14648
+ class: "ctx",
14649
+ type: "http_path",
14650
+ severity: "high",
14651
+ confidence: 0.85,
14652
+ returnTainted: true
14653
+ },
14654
+ {
14655
+ method: "querystring",
14656
+ class: "ctx",
14657
+ type: "http_param",
14658
+ severity: "high",
14659
+ confidence: 0.85,
14660
+ returnTainted: true
14661
+ },
14500
14662
  // DOM sources (for browser code)
14501
14663
  {
14502
14664
  method: "location",
@@ -14839,6 +15001,21 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
14839
15001
  severity: "critical",
14840
15002
  argPositions: [0]
14841
15003
  },
15004
+ // Prisma ORM - unsafe raw query methods ($executeRaw/$queryRaw with template literals are safe/parameterized)
15005
+ {
15006
+ method: "$executeRawUnsafe",
15007
+ type: "sql_injection",
15008
+ cwe: "CWE-89",
15009
+ severity: "critical",
15010
+ argPositions: [0]
15011
+ },
15012
+ {
15013
+ method: "$queryRawUnsafe",
15014
+ type: "sql_injection",
15015
+ cwe: "CWE-89",
15016
+ severity: "critical",
15017
+ argPositions: [0]
15018
+ },
14842
15019
  // SSRF
14843
15020
  {
14844
15021
  method: "fetch",
@@ -15944,12 +16121,241 @@ var RustPlugin = class extends BaseLanguagePlugin {
15944
16121
  }
15945
16122
  };
15946
16123
 
16124
+ // src/languages/plugins/bash.ts
16125
+ var BashPlugin = class extends BaseLanguagePlugin {
16126
+ id = "bash";
16127
+ name = "Bash/Shell";
16128
+ extensions = [".sh", ".bash", ".zsh", ".ksh"];
16129
+ wasmPath = "tree-sitter-bash.wasm";
16130
+ nodeTypes = {
16131
+ // Type declarations — shell has no OOP types
16132
+ classDeclaration: [],
16133
+ interfaceDeclaration: [],
16134
+ enumDeclaration: [],
16135
+ functionDeclaration: ["function_definition"],
16136
+ methodDeclaration: ["function_definition"],
16137
+ // Expressions — commands are treated as calls
16138
+ methodCall: ["command"],
16139
+ functionCall: ["command"],
16140
+ assignment: ["variable_assignment"],
16141
+ variableDeclaration: ["variable_assignment", "declaration_command"],
16142
+ // Parameters and arguments — positional args are child words
16143
+ parameter: [],
16144
+ argument: [],
16145
+ // Annotations/decorators — none in shell
16146
+ annotation: [],
16147
+ decorator: [],
16148
+ // Imports — shell uses `source` / `.` but no formal import system
16149
+ importStatement: [],
16150
+ // Control flow
16151
+ ifStatement: ["if_statement"],
16152
+ forStatement: ["for_statement", "c_style_for_statement"],
16153
+ whileStatement: ["while_statement"],
16154
+ tryStatement: [],
16155
+ returnStatement: []
16156
+ };
16157
+ /**
16158
+ * Shell scripts don't have a formal framework concept.
16159
+ */
16160
+ detectFramework(_context) {
16161
+ return void 0;
16162
+ }
16163
+ /**
16164
+ * Bash taint source patterns.
16165
+ * In shell, tainted data enters via `read` (stdin).
16166
+ * curl/wget are excluded as sources (see comment in implementation).
16167
+ */
16168
+ getBuiltinSources() {
16169
+ return [
16170
+ // read built-in reads user input from stdin.
16171
+ // curl/wget are intentionally excluded: they're also registered as sinks (SSRF),
16172
+ // and without DFG tracking of $() command substitution, including them as sources
16173
+ // would generate false positives for safe curl calls.
16174
+ {
16175
+ method: "read",
16176
+ type: "io_input",
16177
+ severity: "high",
16178
+ confidence: 0.9,
16179
+ returnTainted: true
16180
+ }
16181
+ ];
16182
+ }
16183
+ /**
16184
+ * Bash taint sink patterns.
16185
+ * Key sinks: eval (CWE-94), bash/sh -c (CWE-78), DB clients (CWE-89),
16186
+ * file operations (CWE-22), SSRF via curl/wget (CWE-918).
16187
+ */
16188
+ getBuiltinSinks() {
16189
+ return [
16190
+ // Code / command injection via eval
16191
+ {
16192
+ method: "eval",
16193
+ type: "code_injection",
16194
+ cwe: "CWE-94",
16195
+ severity: "critical",
16196
+ argPositions: [0]
16197
+ },
16198
+ // Command injection: spawning a sub-shell with -c flag
16199
+ {
16200
+ method: "bash",
16201
+ type: "command_injection",
16202
+ cwe: "CWE-78",
16203
+ severity: "critical",
16204
+ argPositions: [1]
16205
+ },
16206
+ {
16207
+ method: "sh",
16208
+ type: "command_injection",
16209
+ cwe: "CWE-78",
16210
+ severity: "critical",
16211
+ argPositions: [1]
16212
+ },
16213
+ {
16214
+ method: "zsh",
16215
+ type: "command_injection",
16216
+ cwe: "CWE-78",
16217
+ severity: "critical",
16218
+ argPositions: [1]
16219
+ },
16220
+ {
16221
+ method: "ksh",
16222
+ type: "command_injection",
16223
+ cwe: "CWE-78",
16224
+ severity: "critical",
16225
+ argPositions: [1]
16226
+ },
16227
+ // SQL injection via DB CLI clients (first arg is query/expression)
16228
+ {
16229
+ method: "mysql",
16230
+ type: "sql_injection",
16231
+ cwe: "CWE-89",
16232
+ severity: "critical",
16233
+ argPositions: [1]
16234
+ },
16235
+ {
16236
+ method: "psql",
16237
+ type: "sql_injection",
16238
+ cwe: "CWE-89",
16239
+ severity: "critical",
16240
+ argPositions: [1]
16241
+ },
16242
+ {
16243
+ method: "sqlite3",
16244
+ type: "sql_injection",
16245
+ cwe: "CWE-89",
16246
+ severity: "critical",
16247
+ argPositions: [1]
16248
+ },
16249
+ // Path traversal via file operations (first arg is path)
16250
+ {
16251
+ method: "cat",
16252
+ type: "path_traversal",
16253
+ cwe: "CWE-22",
16254
+ severity: "high",
16255
+ argPositions: [0]
16256
+ },
16257
+ {
16258
+ method: "rm",
16259
+ type: "path_traversal",
16260
+ cwe: "CWE-22",
16261
+ severity: "high",
16262
+ argPositions: [0]
16263
+ },
16264
+ {
16265
+ method: "cp",
16266
+ type: "path_traversal",
16267
+ cwe: "CWE-22",
16268
+ severity: "high",
16269
+ argPositions: [0]
16270
+ },
16271
+ {
16272
+ method: "mv",
16273
+ type: "path_traversal",
16274
+ cwe: "CWE-22",
16275
+ severity: "high",
16276
+ argPositions: [0]
16277
+ },
16278
+ {
16279
+ method: "chmod",
16280
+ type: "path_traversal",
16281
+ cwe: "CWE-22",
16282
+ severity: "medium",
16283
+ argPositions: [1]
16284
+ },
16285
+ {
16286
+ method: "chown",
16287
+ type: "path_traversal",
16288
+ cwe: "CWE-22",
16289
+ severity: "medium",
16290
+ argPositions: [1]
16291
+ },
16292
+ // SSRF — curl/wget with externally-controlled URL
16293
+ {
16294
+ method: "curl",
16295
+ type: "ssrf",
16296
+ cwe: "CWE-918",
16297
+ severity: "high",
16298
+ argPositions: [0]
16299
+ },
16300
+ {
16301
+ method: "wget",
16302
+ type: "ssrf",
16303
+ cwe: "CWE-918",
16304
+ severity: "high",
16305
+ argPositions: [0]
16306
+ }
16307
+ ];
16308
+ }
16309
+ /**
16310
+ * Shell has no OOP receiver types.
16311
+ */
16312
+ getReceiverType(_node, _context) {
16313
+ return void 0;
16314
+ }
16315
+ /**
16316
+ * Bash string literals: quoted strings and raw ($'...') strings.
16317
+ */
16318
+ isStringLiteral(node) {
16319
+ return node.type === "string" || node.type === "raw_string" || node.type === "ansi_c_string";
16320
+ }
16321
+ /**
16322
+ * Extract string value from bash string literal, stripping quotes.
16323
+ */
16324
+ getStringValue(node) {
16325
+ if (!this.isStringLiteral(node)) return void 0;
16326
+ const text = node.text;
16327
+ if (node.type === "raw_string") {
16328
+ return text.slice(1, -1);
16329
+ }
16330
+ if (node.type === "ansi_c_string") {
16331
+ return text.slice(2, -1);
16332
+ }
16333
+ const match = text.match(/^"(.*)"$/s);
16334
+ if (match) return match[1];
16335
+ return text;
16336
+ }
16337
+ // Extraction methods — delegate to base extractors via generic walker
16338
+ extractTypes(_context) {
16339
+ return [];
16340
+ }
16341
+ extractCalls(_context) {
16342
+ return [];
16343
+ }
16344
+ extractImports(_context) {
16345
+ return [];
16346
+ }
16347
+ extractPackage(_context) {
16348
+ return void 0;
16349
+ }
16350
+ };
16351
+
15947
16352
  // src/languages/plugins/index.ts
15948
16353
  function registerBuiltinPlugins() {
15949
16354
  registerLanguage(new JavaPlugin());
15950
16355
  registerLanguage(new JavaScriptPlugin());
15951
16356
  registerLanguage(new PythonPlugin());
15952
16357
  registerLanguage(new RustPlugin());
16358
+ registerLanguage(new BashPlugin());
15953
16359
  }
15954
16360
 
15955
16361
  // src/utils/logger.ts
@@ -16278,6 +16684,18 @@ async function analyze(code, filePath, language, options = {}) {
16278
16684
  "member_expression",
16279
16685
  "assignment_expression"
16280
16686
  ]);
16687
+ } else if (language === "bash") {
16688
+ nodeTypesToCollect = /* @__PURE__ */ new Set([
16689
+ // Bash AST nodes
16690
+ "command",
16691
+ "function_definition",
16692
+ "variable_assignment",
16693
+ "declaration_command",
16694
+ "if_statement",
16695
+ "for_statement",
16696
+ "c_style_for_statement",
16697
+ "while_statement"
16698
+ ]);
16281
16699
  } else {
16282
16700
  nodeTypesToCollect = /* @__PURE__ */ new Set([
16283
16701
  // Java AST nodes
@@ -16308,7 +16726,41 @@ async function analyze(code, filePath, language, options = {}) {
16308
16726
  }
16309
16727
  }
16310
16728
  }
16311
- const baseConfig = options.taintConfig ?? getDefaultConfig();
16729
+ let baseConfig = options.taintConfig ?? getDefaultConfig();
16730
+ if (!options.taintConfig) {
16731
+ const plugin = getLanguagePlugin(language);
16732
+ if (plugin) {
16733
+ const pluginSources = plugin.getBuiltinSources();
16734
+ const pluginSinks = plugin.getBuiltinSinks();
16735
+ if (pluginSources.length > 0 || pluginSinks.length > 0) {
16736
+ baseConfig = {
16737
+ ...baseConfig,
16738
+ sources: [
16739
+ ...baseConfig.sources,
16740
+ ...pluginSources.map((s) => ({
16741
+ method: s.method,
16742
+ class: s.class,
16743
+ annotation: s.annotation,
16744
+ type: s.type,
16745
+ severity: s.severity,
16746
+ return_tainted: s.returnTainted ?? false
16747
+ }))
16748
+ ],
16749
+ sinks: [
16750
+ ...baseConfig.sinks,
16751
+ ...pluginSinks.map((s) => ({
16752
+ method: s.method,
16753
+ class: s.class,
16754
+ type: s.type,
16755
+ cwe: s.cwe,
16756
+ severity: s.severity,
16757
+ arg_positions: s.argPositions
16758
+ }))
16759
+ ]
16760
+ };
16761
+ }
16762
+ }
16763
+ }
16312
16764
  const preliminaryTaint = analyzeTaint(calls, types, baseConfig);
16313
16765
  const taintedParameters = [];
16314
16766
  for (const source of preliminaryTaint.sources) {
@@ -5546,6 +5546,9 @@ function extractCalls(tree, cache, language) {
5546
5546
  const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5547
5547
  const isPython = detectedLanguage === "python";
5548
5548
  const isRust = detectedLanguage === "rust";
5549
+ if (detectedLanguage === "bash") {
5550
+ return extractBashCalls(tree, cache);
5551
+ }
5549
5552
  if (isRust) {
5550
5553
  return extractRustCalls(tree, cache);
5551
5554
  }
@@ -6235,6 +6238,88 @@ function inferTypeFromReceiverName(receiver) {
6235
6238
  const lowerReceiver = receiver.toLowerCase();
6236
6239
  return patterns[lowerReceiver] ?? null;
6237
6240
  }
6241
+ function extractBashCalls(tree, cache) {
6242
+ const calls = [];
6243
+ const commands = getNodesFromCache(tree.rootNode, "command", cache);
6244
+ for (const cmd of commands) {
6245
+ const callInfo = extractBashCommandInfo(cmd);
6246
+ if (callInfo) {
6247
+ calls.push(callInfo);
6248
+ }
6249
+ }
6250
+ return calls;
6251
+ }
6252
+ function extractBashCommandInfo(node) {
6253
+ const nameNode = node.childForFieldName("name");
6254
+ if (!nameNode) return null;
6255
+ const commandName = getNodeText(nameNode);
6256
+ if (!commandName) return null;
6257
+ const args2 = [];
6258
+ let position = 0;
6259
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6260
+ const child = node.child(i2);
6261
+ if (!child) continue;
6262
+ if (child === nameNode) continue;
6263
+ if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
6264
+ continue;
6265
+ }
6266
+ const expression = getNodeText(child);
6267
+ if (!expression.trim()) continue;
6268
+ const variable = extractBashVariableRef(child);
6269
+ args2.push({
6270
+ position: position++,
6271
+ expression,
6272
+ variable,
6273
+ literal: null
6274
+ });
6275
+ }
6276
+ const inMethod = findBashEnclosingFunction(node);
6277
+ return {
6278
+ method_name: commandName,
6279
+ receiver: null,
6280
+ arguments: args2,
6281
+ location: {
6282
+ line: node.startPosition.row + 1,
6283
+ column: node.startPosition.column
6284
+ },
6285
+ in_method: inMethod,
6286
+ resolved: false,
6287
+ resolution: { status: "external_method" }
6288
+ };
6289
+ }
6290
+ function extractBashVariableRef(node) {
6291
+ const type = node.type;
6292
+ if (type === "simple_expansion" || type === "expansion") {
6293
+ return getNodeText(node).replace(/^\$\{?/, "").replace(/\}$/, "");
6294
+ }
6295
+ if (type === "string" || type === "concatenation") {
6296
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6297
+ const child = node.child(i2);
6298
+ if (!child) continue;
6299
+ if (child.type === "simple_expansion" || child.type === "expansion") {
6300
+ return getNodeText(child).replace(/^\$\{?/, "").replace(/\}$/, "");
6301
+ }
6302
+ }
6303
+ }
6304
+ if (type === "word") {
6305
+ const text = getNodeText(node);
6306
+ if (text.startsWith("$")) {
6307
+ return text.slice(1).replace(/^\{/, "").replace(/\}$/, "");
6308
+ }
6309
+ }
6310
+ return null;
6311
+ }
6312
+ function findBashEnclosingFunction(node) {
6313
+ let current = node.parent;
6314
+ while (current) {
6315
+ if (current.type === "function_definition") {
6316
+ const nameNode = current.childForFieldName("name");
6317
+ return nameNode ? getNodeText(nameNode) : null;
6318
+ }
6319
+ current = current.parent;
6320
+ }
6321
+ return null;
6322
+ }
6238
6323
  function extractPythonCalls(tree, cache) {
6239
6324
  const calls = [];
6240
6325
  const context = buildPythonResolutionContext(tree, cache);
@@ -5481,6 +5481,9 @@ function extractCalls(tree, cache, language) {
5481
5481
  const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5482
5482
  const isPython = detectedLanguage === "python";
5483
5483
  const isRust = detectedLanguage === "rust";
5484
+ if (detectedLanguage === "bash") {
5485
+ return extractBashCalls(tree, cache);
5486
+ }
5484
5487
  if (isRust) {
5485
5488
  return extractRustCalls(tree, cache);
5486
5489
  }
@@ -6170,6 +6173,88 @@ function inferTypeFromReceiverName(receiver) {
6170
6173
  const lowerReceiver = receiver.toLowerCase();
6171
6174
  return patterns[lowerReceiver] ?? null;
6172
6175
  }
6176
+ function extractBashCalls(tree, cache) {
6177
+ const calls = [];
6178
+ const commands = getNodesFromCache(tree.rootNode, "command", cache);
6179
+ for (const cmd of commands) {
6180
+ const callInfo = extractBashCommandInfo(cmd);
6181
+ if (callInfo) {
6182
+ calls.push(callInfo);
6183
+ }
6184
+ }
6185
+ return calls;
6186
+ }
6187
+ function extractBashCommandInfo(node) {
6188
+ const nameNode = node.childForFieldName("name");
6189
+ if (!nameNode) return null;
6190
+ const commandName = getNodeText(nameNode);
6191
+ if (!commandName) return null;
6192
+ const args2 = [];
6193
+ let position = 0;
6194
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6195
+ const child = node.child(i2);
6196
+ if (!child) continue;
6197
+ if (child === nameNode) continue;
6198
+ if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
6199
+ continue;
6200
+ }
6201
+ const expression = getNodeText(child);
6202
+ if (!expression.trim()) continue;
6203
+ const variable = extractBashVariableRef(child);
6204
+ args2.push({
6205
+ position: position++,
6206
+ expression,
6207
+ variable,
6208
+ literal: null
6209
+ });
6210
+ }
6211
+ const inMethod = findBashEnclosingFunction(node);
6212
+ return {
6213
+ method_name: commandName,
6214
+ receiver: null,
6215
+ arguments: args2,
6216
+ location: {
6217
+ line: node.startPosition.row + 1,
6218
+ column: node.startPosition.column
6219
+ },
6220
+ in_method: inMethod,
6221
+ resolved: false,
6222
+ resolution: { status: "external_method" }
6223
+ };
6224
+ }
6225
+ function extractBashVariableRef(node) {
6226
+ const type = node.type;
6227
+ if (type === "simple_expansion" || type === "expansion") {
6228
+ return getNodeText(node).replace(/^\$\{?/, "").replace(/\}$/, "");
6229
+ }
6230
+ if (type === "string" || type === "concatenation") {
6231
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6232
+ const child = node.child(i2);
6233
+ if (!child) continue;
6234
+ if (child.type === "simple_expansion" || child.type === "expansion") {
6235
+ return getNodeText(child).replace(/^\$\{?/, "").replace(/\}$/, "");
6236
+ }
6237
+ }
6238
+ }
6239
+ if (type === "word") {
6240
+ const text = getNodeText(node);
6241
+ if (text.startsWith("$")) {
6242
+ return text.slice(1).replace(/^\{/, "").replace(/\}$/, "");
6243
+ }
6244
+ }
6245
+ return null;
6246
+ }
6247
+ function findBashEnclosingFunction(node) {
6248
+ let current = node.parent;
6249
+ while (current) {
6250
+ if (current.type === "function_definition") {
6251
+ const nameNode = current.childForFieldName("name");
6252
+ return nameNode ? getNodeText(nameNode) : null;
6253
+ }
6254
+ current = current.parent;
6255
+ }
6256
+ return null;
6257
+ }
6173
6258
  function extractPythonCalls(tree, cache) {
6174
6259
  const calls = [];
6175
6260
  const context = buildPythonResolutionContext(tree, cache);