circle-ir 3.19.5 → 3.21.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.
@@ -7450,6 +7450,9 @@ function buildCFG(tree, language) {
7450
7450
  const allBlocks = [];
7451
7451
  const allEdges = [];
7452
7452
  let blockIdCounter = 0;
7453
+ if (effectiveLanguage === "bash") {
7454
+ return buildBashCFG(tree, blockIdCounter);
7455
+ }
7453
7456
  if (isJavaScript) {
7454
7457
  const functions = [
7455
7458
  ...findNodes(tree.rootNode, "function_declaration"),
@@ -7805,6 +7808,81 @@ function processSwitchStatement(stmt, startId, blocks, edges, isJavaScript) {
7805
7808
  nextId: currentId
7806
7809
  };
7807
7810
  }
7811
+ function buildBashCFG(tree, startId) {
7812
+ const allBlocks = [];
7813
+ const allEdges = [];
7814
+ let blockIdCounter = startId;
7815
+ const functions = findNodes(tree.rootNode, "function_definition");
7816
+ for (const func2 of functions) {
7817
+ const body2 = func2.childForFieldName("body");
7818
+ if (!body2) continue;
7819
+ const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, false);
7820
+ allBlocks.push(...blocks);
7821
+ allEdges.push(...edges);
7822
+ blockIdCounter = nextId;
7823
+ }
7824
+ const topLevelStatements = [];
7825
+ for (let i2 = 0; i2 < tree.rootNode.childCount; i2++) {
7826
+ const child = tree.rootNode.child(i2);
7827
+ if (child && child.type !== "function_definition" && isBashStatement(child)) {
7828
+ topLevelStatements.push(child);
7829
+ }
7830
+ }
7831
+ if (topLevelStatements.length > 0) {
7832
+ const entryBlock = {
7833
+ id: blockIdCounter++,
7834
+ type: "entry",
7835
+ start_line: topLevelStatements[0].startPosition.row + 1,
7836
+ end_line: topLevelStatements[0].startPosition.row + 1
7837
+ };
7838
+ allBlocks.push(entryBlock);
7839
+ let lastExitIds = [];
7840
+ let firstBlockId = -1;
7841
+ for (const stmt of topLevelStatements) {
7842
+ const result = processStatement(stmt, blockIdCounter, allBlocks, allEdges, false);
7843
+ blockIdCounter = result.nextId;
7844
+ if (firstBlockId === -1) {
7845
+ firstBlockId = result.entryId;
7846
+ } else {
7847
+ for (const exitId of lastExitIds) {
7848
+ allEdges.push({ from: exitId, to: result.entryId, type: "sequential" });
7849
+ }
7850
+ }
7851
+ lastExitIds = result.exitIds;
7852
+ }
7853
+ if (firstBlockId !== -1) {
7854
+ allEdges.push({ from: entryBlock.id, to: firstBlockId, type: "sequential" });
7855
+ }
7856
+ const exitBlock = {
7857
+ id: blockIdCounter++,
7858
+ type: "exit",
7859
+ start_line: topLevelStatements[topLevelStatements.length - 1].endPosition.row + 1,
7860
+ end_line: topLevelStatements[topLevelStatements.length - 1].endPosition.row + 1
7861
+ };
7862
+ allBlocks.push(exitBlock);
7863
+ for (const exitId of lastExitIds) {
7864
+ allEdges.push({ from: exitId, to: exitBlock.id, type: "sequential" });
7865
+ }
7866
+ }
7867
+ return { blocks: allBlocks, edges: allEdges };
7868
+ }
7869
+ function isBashStatement(node) {
7870
+ const bashStatementTypes = /* @__PURE__ */ new Set([
7871
+ "command",
7872
+ "variable_assignment",
7873
+ "if_statement",
7874
+ "for_statement",
7875
+ "while_statement",
7876
+ "case_statement",
7877
+ "pipeline",
7878
+ "list",
7879
+ "redirected_statement",
7880
+ "compound_statement",
7881
+ "subshell",
7882
+ "declaration_command"
7883
+ ]);
7884
+ return bashStatementTypes.has(node.type);
7885
+ }
7808
7886
  function isStatement(node, isJavaScript) {
7809
7887
  const javaStatementTypes = /* @__PURE__ */ new Set([
7810
7888
  "local_variable_declaration",
@@ -7895,6 +7973,9 @@ function buildDFG(tree, cache, language) {
7895
7973
  if (effectiveLanguage === "rust") {
7896
7974
  return buildRustDFG(tree, cache);
7897
7975
  }
7976
+ if (effectiveLanguage === "bash") {
7977
+ return buildBashDFG(tree);
7978
+ }
7898
7979
  return buildJavaDFG(tree, cache);
7899
7980
  }
7900
7981
  function buildJavaDFG(tree, cache) {
@@ -8621,6 +8702,101 @@ function computeChains(defs, uses) {
8621
8702
  chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
8622
8703
  return chains;
8623
8704
  }
8705
+ function buildBashDFG(tree) {
8706
+ const defs = [];
8707
+ const uses = [];
8708
+ let defIdCounter = 1;
8709
+ let useIdCounter = 1;
8710
+ const scopeStack = [/* @__PURE__ */ new Map()];
8711
+ const positionalParams = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*"];
8712
+ for (const p of positionalParams) {
8713
+ const def = {
8714
+ id: defIdCounter++,
8715
+ variable: p,
8716
+ line: 0,
8717
+ kind: "param"
8718
+ };
8719
+ defs.push(def);
8720
+ currentScope(scopeStack).set(p, def.id);
8721
+ }
8722
+ walkTree(tree.rootNode, (node) => {
8723
+ if (node.type === "variable_assignment") {
8724
+ const nameNode = node.childForFieldName("name");
8725
+ if (nameNode) {
8726
+ const varName = getNodeText(nameNode);
8727
+ const def = {
8728
+ id: defIdCounter++,
8729
+ variable: varName,
8730
+ line: node.startPosition.row + 1,
8731
+ kind: "local"
8732
+ };
8733
+ defs.push(def);
8734
+ currentScope(scopeStack).set(varName, def.id);
8735
+ }
8736
+ } else if (node.type === "command") {
8737
+ const nameNode = node.childForFieldName("name");
8738
+ if (nameNode && getNodeText(nameNode) === "read") {
8739
+ for (let i2 = 0; i2 < node.namedChildCount; i2++) {
8740
+ const arg = node.namedChild(i2);
8741
+ if (!arg || arg === nameNode) continue;
8742
+ if (arg.type === "word") {
8743
+ const text = getNodeText(arg);
8744
+ if (text.startsWith("-")) continue;
8745
+ const def = {
8746
+ id: defIdCounter++,
8747
+ variable: text,
8748
+ line: node.startPosition.row + 1,
8749
+ kind: "local"
8750
+ };
8751
+ defs.push(def);
8752
+ currentScope(scopeStack).set(text, def.id);
8753
+ }
8754
+ }
8755
+ }
8756
+ } else if (node.type === "for_statement") {
8757
+ const varNode = node.childForFieldName("variable");
8758
+ if (varNode) {
8759
+ const varName = getNodeText(varNode);
8760
+ const def = {
8761
+ id: defIdCounter++,
8762
+ variable: varName,
8763
+ line: node.startPosition.row + 1,
8764
+ kind: "local"
8765
+ };
8766
+ defs.push(def);
8767
+ currentScope(scopeStack).set(varName, def.id);
8768
+ }
8769
+ } else if (node.type === "simple_expansion") {
8770
+ const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
8771
+ if (varNameNode) {
8772
+ const varName = getNodeText(varNameNode);
8773
+ if (varName && !varName.startsWith("?") && !varName.startsWith("#")) {
8774
+ const reachingDef = findReachingDef(varName, scopeStack);
8775
+ uses.push({
8776
+ id: useIdCounter++,
8777
+ variable: varName,
8778
+ line: node.startPosition.row + 1,
8779
+ def_id: reachingDef
8780
+ });
8781
+ }
8782
+ }
8783
+ } else if (node.type === "expansion") {
8784
+ const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
8785
+ if (varNameNode && varNameNode.type === "variable_name") {
8786
+ const varName = getNodeText(varNameNode);
8787
+ const reachingDef = findReachingDef(varName, scopeStack);
8788
+ uses.push({
8789
+ id: useIdCounter++,
8790
+ variable: varName,
8791
+ line: node.startPosition.row + 1,
8792
+ def_id: reachingDef
8793
+ });
8794
+ }
8795
+ }
8796
+ });
8797
+ const chains = computeChains(defs, uses);
8798
+ return { defs, uses, chains };
8799
+ }
8624
8800
  function buildRustDFG(tree, cache) {
8625
8801
  const defs = [];
8626
8802
  const uses = [];
@@ -18706,6 +18882,13 @@ var LanguageSourcesPass = class {
18706
18882
  }
18707
18883
  }
18708
18884
  const jsTaintedVars = buildJavaScriptTaintedVars(code, language);
18885
+ if (language === "bash") {
18886
+ additionalSources.push(...findBashTaintSources(code, graph.ir.dfg));
18887
+ const bashFindings = findBashPatternFindings(code, graph.ir.meta.file);
18888
+ for (const finding of bashFindings) {
18889
+ ctx.addFinding(finding);
18890
+ }
18891
+ }
18709
18892
  return { additionalSources, additionalSinks, pyTaintedVars, pySanitizedVars, jsTaintedVars };
18710
18893
  }
18711
18894
  };
@@ -19008,6 +19191,184 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
19008
19191
  }
19009
19192
  return tainted;
19010
19193
  }
19194
+ var BASH_UNTRUSTED_ENV_PATTERNS = [
19195
+ /^USER_INPUT$/i,
19196
+ /^QUERY_STRING$/i,
19197
+ /^REQUEST_/i,
19198
+ /^HTTP_/i,
19199
+ /^REMOTE_/i,
19200
+ /^CONTENT_TYPE$/i,
19201
+ /^CONTENT_LENGTH$/i,
19202
+ /^PATH_INFO$/i,
19203
+ /^SCRIPT_NAME$/i,
19204
+ /^SERVER_NAME$/i
19205
+ ];
19206
+ var BASH_NETWORK_COMMANDS = /* @__PURE__ */ new Set(["curl", "wget", "nc", "ncat"]);
19207
+ var BASH_FILE_COMMANDS = /* @__PURE__ */ new Set(["cat", "head", "tail", "less", "more", "awk", "sed", "cut", "grep"]);
19208
+ function findBashTaintSources(sourceCode, dfg) {
19209
+ const sources = [];
19210
+ const lines = sourceCode.split("\n");
19211
+ const definedVars = new Set(dfg.defs.filter((d) => d.kind === "local").map((d) => d.variable));
19212
+ for (let i2 = 0; i2 < lines.length; i2++) {
19213
+ const line = lines[i2];
19214
+ const trimmed = line.trim();
19215
+ const lineNumber = i2 + 1;
19216
+ if (trimmed.startsWith("#")) continue;
19217
+ const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
19218
+ let m;
19219
+ while ((m = positionalRe.exec(line)) !== null) {
19220
+ const param = m[1] ?? m[2];
19221
+ const alreadyExists = sources.some((s) => s.line === lineNumber && s.variable === param);
19222
+ if (!alreadyExists) {
19223
+ sources.push({
19224
+ type: "io_input",
19225
+ location: `positional parameter $${param}`,
19226
+ severity: "high",
19227
+ line: lineNumber,
19228
+ confidence: 1,
19229
+ variable: param
19230
+ });
19231
+ }
19232
+ }
19233
+ const cmdSubAssign = trimmed.match(/^(\w+)=\$\((\w+)\s/);
19234
+ const cmdSubBacktick = trimmed.match(/^(\w+)=`(\w+)\s/);
19235
+ const csMatch = cmdSubAssign ?? cmdSubBacktick;
19236
+ if (csMatch) {
19237
+ const [, varName, cmd] = csMatch;
19238
+ if (BASH_NETWORK_COMMANDS.has(cmd)) {
19239
+ sources.push({
19240
+ type: "network_input",
19241
+ location: `${varName}=$(${cmd} ...) \u2014 network command output`,
19242
+ severity: "high",
19243
+ line: lineNumber,
19244
+ confidence: 0.9,
19245
+ variable: varName
19246
+ });
19247
+ } else if (BASH_FILE_COMMANDS.has(cmd)) {
19248
+ sources.push({
19249
+ type: "file_input",
19250
+ location: `${varName}=$(${cmd} ...) \u2014 file command output`,
19251
+ severity: "medium",
19252
+ line: lineNumber,
19253
+ confidence: 0.7,
19254
+ variable: varName
19255
+ });
19256
+ }
19257
+ }
19258
+ const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})\}/g;
19259
+ let em;
19260
+ while ((em = envRe.exec(line)) !== null) {
19261
+ const envVar = em[1] ?? em[2];
19262
+ if (!definedVars.has(envVar) && BASH_UNTRUSTED_ENV_PATTERNS.some((p) => p.test(envVar))) {
19263
+ const alreadyExists = sources.some((s) => s.line === lineNumber && s.variable === envVar);
19264
+ if (!alreadyExists) {
19265
+ sources.push({
19266
+ type: "env_input",
19267
+ location: `environment variable $${envVar}`,
19268
+ severity: "medium",
19269
+ line: lineNumber,
19270
+ confidence: 0.8,
19271
+ variable: envVar
19272
+ });
19273
+ }
19274
+ }
19275
+ }
19276
+ }
19277
+ return sources;
19278
+ }
19279
+ var BASH_CREDENTIAL_PATTERN = /^(.*?)(password|passwd|secret|api_?key|token|auth_token|private_key|access_key)\s*=\s*["']?([^"'\s$][^"'\s]*)["']?\s*$/i;
19280
+ function findBashPatternFindings(sourceCode, file) {
19281
+ const findings = [];
19282
+ const lines = sourceCode.split("\n");
19283
+ for (let i2 = 0; i2 < lines.length; i2++) {
19284
+ const line = lines[i2];
19285
+ const trimmed = line.trim();
19286
+ const lineNumber = i2 + 1;
19287
+ if (trimmed.startsWith("#")) continue;
19288
+ const credMatch = trimmed.match(BASH_CREDENTIAL_PATTERN);
19289
+ if (credMatch) {
19290
+ const value = credMatch[3];
19291
+ if (value && !value.startsWith("$") && !value.startsWith("(") && value.length > 1) {
19292
+ findings.push({
19293
+ id: `hardcoded-credential-${file}-${lineNumber}`,
19294
+ pass: "language-sources",
19295
+ category: "security",
19296
+ rule_id: "hardcoded-credential",
19297
+ cwe: "CWE-798",
19298
+ severity: "high",
19299
+ level: "error",
19300
+ message: `Hardcoded credential: ${credMatch[2]} contains a literal value`,
19301
+ file,
19302
+ line: lineNumber,
19303
+ snippet: trimmed.substring(0, 80)
19304
+ });
19305
+ }
19306
+ }
19307
+ if (/\b(curl|wget)\b/.test(trimmed) && /\bhttp:\/\//.test(trimmed)) {
19308
+ findings.push({
19309
+ id: `cleartext-transmission-${file}-${lineNumber}`,
19310
+ pass: "language-sources",
19311
+ category: "security",
19312
+ rule_id: "cleartext-transmission",
19313
+ cwe: "CWE-319",
19314
+ severity: "medium",
19315
+ level: "warning",
19316
+ message: "Cleartext HTTP transmission: use https:// instead of http://",
19317
+ file,
19318
+ line: lineNumber,
19319
+ snippet: trimmed.substring(0, 80)
19320
+ });
19321
+ }
19322
+ const tmpMatch = trimmed.match(/\/tmp\/([^\s"'$]+)/);
19323
+ if (tmpMatch && !/mktemp/.test(trimmed)) {
19324
+ findings.push({
19325
+ id: `predictable-temp-file-${file}-${lineNumber}`,
19326
+ pass: "language-sources",
19327
+ category: "security",
19328
+ rule_id: "predictable-temp-file",
19329
+ cwe: "CWE-377",
19330
+ severity: "medium",
19331
+ level: "warning",
19332
+ message: `Predictable temp file: /tmp/${tmpMatch[1]}. Use mktemp instead`,
19333
+ file,
19334
+ line: lineNumber,
19335
+ snippet: trimmed.substring(0, 80)
19336
+ });
19337
+ }
19338
+ if (/\bchmod\b/.test(trimmed) && /\b(777|666)\b/.test(trimmed)) {
19339
+ const mode = trimmed.match(/\b(777|666)\b/)[1];
19340
+ findings.push({
19341
+ id: `insecure-file-permission-${file}-${lineNumber}`,
19342
+ pass: "language-sources",
19343
+ category: "security",
19344
+ rule_id: "insecure-file-permission",
19345
+ cwe: "CWE-732",
19346
+ severity: "medium",
19347
+ level: "warning",
19348
+ message: `Insecure file permission: chmod ${mode} grants excessive access`,
19349
+ file,
19350
+ line: lineNumber,
19351
+ snippet: trimmed.substring(0, 80)
19352
+ });
19353
+ }
19354
+ if (/\btar\b/.test(trimmed) && /(-x|--extract)/.test(trimmed) && !/--strip-components/.test(trimmed)) {
19355
+ findings.push({
19356
+ id: `unsafe-archive-extraction-${file}-${lineNumber}`,
19357
+ pass: "language-sources",
19358
+ category: "security",
19359
+ rule_id: "unsafe-archive-extraction",
19360
+ cwe: "CWE-22",
19361
+ severity: "medium",
19362
+ level: "warning",
19363
+ message: "Unsafe archive extraction: tar -x without --strip-components may allow path traversal",
19364
+ file,
19365
+ line: lineNumber,
19366
+ snippet: trimmed.substring(0, 80)
19367
+ });
19368
+ }
19369
+ }
19370
+ return findings;
19371
+ }
19011
19372
 
19012
19373
  // src/analysis/passes/sink-filter-pass.ts
19013
19374
  var JS_XSS_SANITIZERS = [
@@ -7526,6 +7526,9 @@ function buildCFG(tree, language) {
7526
7526
  const allBlocks = [];
7527
7527
  const allEdges = [];
7528
7528
  let blockIdCounter = 0;
7529
+ if (effectiveLanguage === "bash") {
7530
+ return buildBashCFG(tree, blockIdCounter);
7531
+ }
7529
7532
  if (isJavaScript) {
7530
7533
  const functions = [
7531
7534
  ...findNodes(tree.rootNode, "function_declaration"),
@@ -7881,6 +7884,81 @@ function processSwitchStatement(stmt, startId, blocks, edges, isJavaScript) {
7881
7884
  nextId: currentId
7882
7885
  };
7883
7886
  }
7887
+ function buildBashCFG(tree, startId) {
7888
+ const allBlocks = [];
7889
+ const allEdges = [];
7890
+ let blockIdCounter = startId;
7891
+ const functions = findNodes(tree.rootNode, "function_definition");
7892
+ for (const func2 of functions) {
7893
+ const body2 = func2.childForFieldName("body");
7894
+ if (!body2) continue;
7895
+ const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, false);
7896
+ allBlocks.push(...blocks);
7897
+ allEdges.push(...edges);
7898
+ blockIdCounter = nextId;
7899
+ }
7900
+ const topLevelStatements = [];
7901
+ for (let i2 = 0; i2 < tree.rootNode.childCount; i2++) {
7902
+ const child = tree.rootNode.child(i2);
7903
+ if (child && child.type !== "function_definition" && isBashStatement(child)) {
7904
+ topLevelStatements.push(child);
7905
+ }
7906
+ }
7907
+ if (topLevelStatements.length > 0) {
7908
+ const entryBlock = {
7909
+ id: blockIdCounter++,
7910
+ type: "entry",
7911
+ start_line: topLevelStatements[0].startPosition.row + 1,
7912
+ end_line: topLevelStatements[0].startPosition.row + 1
7913
+ };
7914
+ allBlocks.push(entryBlock);
7915
+ let lastExitIds = [];
7916
+ let firstBlockId = -1;
7917
+ for (const stmt of topLevelStatements) {
7918
+ const result = processStatement(stmt, blockIdCounter, allBlocks, allEdges, false);
7919
+ blockIdCounter = result.nextId;
7920
+ if (firstBlockId === -1) {
7921
+ firstBlockId = result.entryId;
7922
+ } else {
7923
+ for (const exitId of lastExitIds) {
7924
+ allEdges.push({ from: exitId, to: result.entryId, type: "sequential" });
7925
+ }
7926
+ }
7927
+ lastExitIds = result.exitIds;
7928
+ }
7929
+ if (firstBlockId !== -1) {
7930
+ allEdges.push({ from: entryBlock.id, to: firstBlockId, type: "sequential" });
7931
+ }
7932
+ const exitBlock = {
7933
+ id: blockIdCounter++,
7934
+ type: "exit",
7935
+ start_line: topLevelStatements[topLevelStatements.length - 1].endPosition.row + 1,
7936
+ end_line: topLevelStatements[topLevelStatements.length - 1].endPosition.row + 1
7937
+ };
7938
+ allBlocks.push(exitBlock);
7939
+ for (const exitId of lastExitIds) {
7940
+ allEdges.push({ from: exitId, to: exitBlock.id, type: "sequential" });
7941
+ }
7942
+ }
7943
+ return { blocks: allBlocks, edges: allEdges };
7944
+ }
7945
+ function isBashStatement(node) {
7946
+ const bashStatementTypes = /* @__PURE__ */ new Set([
7947
+ "command",
7948
+ "variable_assignment",
7949
+ "if_statement",
7950
+ "for_statement",
7951
+ "while_statement",
7952
+ "case_statement",
7953
+ "pipeline",
7954
+ "list",
7955
+ "redirected_statement",
7956
+ "compound_statement",
7957
+ "subshell",
7958
+ "declaration_command"
7959
+ ]);
7960
+ return bashStatementTypes.has(node.type);
7961
+ }
7884
7962
  function isStatement(node, isJavaScript) {
7885
7963
  const javaStatementTypes = /* @__PURE__ */ new Set([
7886
7964
  "local_variable_declaration",
@@ -7971,6 +8049,9 @@ function buildDFG(tree, cache, language) {
7971
8049
  if (effectiveLanguage === "rust") {
7972
8050
  return buildRustDFG(tree, cache);
7973
8051
  }
8052
+ if (effectiveLanguage === "bash") {
8053
+ return buildBashDFG(tree);
8054
+ }
7974
8055
  return buildJavaDFG(tree, cache);
7975
8056
  }
7976
8057
  function buildJavaDFG(tree, cache) {
@@ -8697,6 +8778,101 @@ function computeChains(defs, uses) {
8697
8778
  chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
8698
8779
  return chains;
8699
8780
  }
8781
+ function buildBashDFG(tree) {
8782
+ const defs = [];
8783
+ const uses = [];
8784
+ let defIdCounter = 1;
8785
+ let useIdCounter = 1;
8786
+ const scopeStack = [/* @__PURE__ */ new Map()];
8787
+ const positionalParams = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*"];
8788
+ for (const p of positionalParams) {
8789
+ const def = {
8790
+ id: defIdCounter++,
8791
+ variable: p,
8792
+ line: 0,
8793
+ kind: "param"
8794
+ };
8795
+ defs.push(def);
8796
+ currentScope(scopeStack).set(p, def.id);
8797
+ }
8798
+ walkTree(tree.rootNode, (node) => {
8799
+ if (node.type === "variable_assignment") {
8800
+ const nameNode = node.childForFieldName("name");
8801
+ if (nameNode) {
8802
+ const varName = getNodeText(nameNode);
8803
+ const def = {
8804
+ id: defIdCounter++,
8805
+ variable: varName,
8806
+ line: node.startPosition.row + 1,
8807
+ kind: "local"
8808
+ };
8809
+ defs.push(def);
8810
+ currentScope(scopeStack).set(varName, def.id);
8811
+ }
8812
+ } else if (node.type === "command") {
8813
+ const nameNode = node.childForFieldName("name");
8814
+ if (nameNode && getNodeText(nameNode) === "read") {
8815
+ for (let i2 = 0; i2 < node.namedChildCount; i2++) {
8816
+ const arg = node.namedChild(i2);
8817
+ if (!arg || arg === nameNode) continue;
8818
+ if (arg.type === "word") {
8819
+ const text = getNodeText(arg);
8820
+ if (text.startsWith("-")) continue;
8821
+ const def = {
8822
+ id: defIdCounter++,
8823
+ variable: text,
8824
+ line: node.startPosition.row + 1,
8825
+ kind: "local"
8826
+ };
8827
+ defs.push(def);
8828
+ currentScope(scopeStack).set(text, def.id);
8829
+ }
8830
+ }
8831
+ }
8832
+ } else if (node.type === "for_statement") {
8833
+ const varNode = node.childForFieldName("variable");
8834
+ if (varNode) {
8835
+ const varName = getNodeText(varNode);
8836
+ const def = {
8837
+ id: defIdCounter++,
8838
+ variable: varName,
8839
+ line: node.startPosition.row + 1,
8840
+ kind: "local"
8841
+ };
8842
+ defs.push(def);
8843
+ currentScope(scopeStack).set(varName, def.id);
8844
+ }
8845
+ } else if (node.type === "simple_expansion") {
8846
+ const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
8847
+ if (varNameNode) {
8848
+ const varName = getNodeText(varNameNode);
8849
+ if (varName && !varName.startsWith("?") && !varName.startsWith("#")) {
8850
+ const reachingDef = findReachingDef(varName, scopeStack);
8851
+ uses.push({
8852
+ id: useIdCounter++,
8853
+ variable: varName,
8854
+ line: node.startPosition.row + 1,
8855
+ def_id: reachingDef
8856
+ });
8857
+ }
8858
+ }
8859
+ } else if (node.type === "expansion") {
8860
+ const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
8861
+ if (varNameNode && varNameNode.type === "variable_name") {
8862
+ const varName = getNodeText(varNameNode);
8863
+ const reachingDef = findReachingDef(varName, scopeStack);
8864
+ uses.push({
8865
+ id: useIdCounter++,
8866
+ variable: varName,
8867
+ line: node.startPosition.row + 1,
8868
+ def_id: reachingDef
8869
+ });
8870
+ }
8871
+ }
8872
+ });
8873
+ const chains = computeChains(defs, uses);
8874
+ return { defs, uses, chains };
8875
+ }
8700
8876
  function buildRustDFG(tree, cache) {
8701
8877
  const defs = [];
8702
8878
  const uses = [];