circle-ir 3.192.0 → 3.194.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.
@@ -9935,6 +9935,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
9935
9935
  function buildGoDFG(tree) {
9936
9936
  const defs = [];
9937
9937
  const uses = [];
9938
+ const extraChains = [];
9938
9939
  let defIdCounter = 1;
9939
9940
  let useIdCounter = 1;
9940
9941
  const scopeStack = [/* @__PURE__ */ new Map()];
@@ -9956,7 +9957,7 @@ function buildGoDFG(tree) {
9956
9957
  }
9957
9958
  const body2 = func2.childForFieldName("body");
9958
9959
  if (body2) {
9959
- processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
9960
+ processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
9960
9961
  defIdCounter = defs.length + 1;
9961
9962
  useIdCounter = uses.length + 1;
9962
9963
  }
@@ -9971,6 +9972,15 @@ function buildGoDFG(tree) {
9971
9972
  }
9972
9973
  }
9973
9974
  const chains = computeChains(defs, uses);
9975
+ const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
9976
+ for (const ch of extraChains) {
9977
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
9978
+ if (!seen.has(key)) {
9979
+ seen.add(key);
9980
+ chains.push(ch);
9981
+ }
9982
+ }
9983
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
9974
9984
  return { defs, uses, chains };
9975
9985
  }
9976
9986
  function extractGoParamDefs(params, defs, _startId, scopeStack) {
@@ -9995,28 +10005,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
9995
10005
  }
9996
10006
  }
9997
10007
  }
9998
- function processGoBlock(node, defs, uses, scopeStack, counters) {
10008
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
10009
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
10010
+ if (right.endPosition.row <= right.startPosition.row) return;
10011
+ for (let ui = useStart; ui < useEnd; ui++) {
10012
+ const u = uses[ui];
10013
+ if (!u || u.def_id === null) continue;
10014
+ for (let di = defStart; di < defEnd; di++) {
10015
+ const d = defs[di];
10016
+ if (d && d.kind === "local" && d.id !== u.def_id) {
10017
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
10018
+ }
10019
+ }
10020
+ }
10021
+ };
9999
10022
  walkTree(node, (child) => {
10000
10023
  if (child.type === "short_var_declaration") {
10001
10024
  const left = child.childForFieldName("left");
10002
10025
  const right = child.childForFieldName("right");
10026
+ const usesBefore = uses.length;
10003
10027
  if (right) {
10004
10028
  extractGoUses(right, uses, scopeStack);
10005
10029
  }
10030
+ const defsBefore = defs.length;
10006
10031
  if (left) {
10007
10032
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10008
10033
  }
10034
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10009
10035
  } else if (child.type === "var_declaration") {
10010
10036
  processGoVarDecl(child, defs, scopeStack, counters);
10011
10037
  } else if (child.type === "assignment_statement") {
10012
10038
  const left = child.childForFieldName("left");
10013
10039
  const right = child.childForFieldName("right");
10040
+ const usesBefore = uses.length;
10014
10041
  if (right) {
10015
10042
  extractGoUses(right, uses, scopeStack);
10016
10043
  }
10044
+ const defsBefore = defs.length;
10017
10045
  if (left) {
10018
10046
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10019
10047
  }
10048
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10020
10049
  } else if (child.type === "for_statement") {
10021
10050
  const rangeClause = findChildByTypeGo(child, "range_clause");
10022
10051
  if (rangeClause) {
@@ -28591,12 +28620,12 @@ function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
28591
28620
  function findJavaPathGetFileNameSanitizers(code) {
28592
28621
  const sanitizers = [];
28593
28622
  const lines = code.split("\n");
28594
- const assignmentRe = /^\s*(?:(?:final\s+)?[A-Za-z_][\w.<>?,\s\[\]]*?\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
28623
+ const assignmentRe2 = /^\s*(?:(?:final\s+)?[A-Za-z_][\w.<>?,\s\[\]]*?\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
28595
28624
  const rhsHasGetFileNameRe = /\.\s*getFileName\s*\(\s*\)/;
28596
28625
  const rhsHasPathsChainRe = /\b(?:Paths\s*\.\s*get|Path\s*\.\s*of)\s*\(/;
28597
28626
  const candidates = [];
28598
28627
  for (let i2 = 0; i2 < lines.length; i2++) {
28599
- const m = assignmentRe.exec(lines[i2]);
28628
+ const m = assignmentRe2.exec(lines[i2]);
28600
28629
  if (!m) continue;
28601
28630
  const rhs = m[2];
28602
28631
  if (!rhsHasGetFileNameRe.test(rhs)) continue;
@@ -33831,6 +33860,333 @@ var DeserializationSafetyGatePass = class {
33831
33860
  }
33832
33861
  };
33833
33862
 
33863
+ // src/analysis/passes/prompt-injection-safety-gate-pass.ts
33864
+ function isDelimiterLiteral(lit) {
33865
+ const s = lit.trim();
33866
+ if (s.length === 0) return true;
33867
+ return /^<\/?[\w.-]+\s*\/?>$/.test(s) || /^\[\/?[\w.-]+\]$/.test(s) || /^`{1,3}[\w-]*$/.test(s) || /^"""[\w-]*$/.test(s) || /^[#*=_~-]{2,}$/.test(s) || /^<{2,}[\w-]*$/.test(s) || /^[\w-]*>{2,}$/.test(s);
33868
+ }
33869
+ function scanValue(code, start2) {
33870
+ let depth = 0;
33871
+ let str = null;
33872
+ let out2 = "";
33873
+ for (let i2 = start2; i2 < code.length; i2++) {
33874
+ const c = code[i2];
33875
+ if (str) {
33876
+ out2 += c;
33877
+ if (c === str && code[i2 - 1] !== "\\") str = null;
33878
+ continue;
33879
+ }
33880
+ if (c === '"' || c === "'" || c === "`") {
33881
+ str = c;
33882
+ out2 += c;
33883
+ continue;
33884
+ }
33885
+ if (c === "(" || c === "[" || c === "{") {
33886
+ depth++;
33887
+ out2 += c;
33888
+ continue;
33889
+ }
33890
+ if (c === ")" || c === "]" || c === "}") {
33891
+ if (depth === 0) break;
33892
+ depth--;
33893
+ out2 += c;
33894
+ continue;
33895
+ }
33896
+ if (c === "," && depth === 0) break;
33897
+ out2 += c;
33898
+ }
33899
+ return out2.trim();
33900
+ }
33901
+ function stringLiterals(expr) {
33902
+ return [...expr.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
33903
+ }
33904
+ function hasIdentifier(expr) {
33905
+ return /[A-Za-z_$][\w$]*/.test(expr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " "));
33906
+ }
33907
+ function isPureLiteral(valueExpr) {
33908
+ const stripped = valueExpr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " ");
33909
+ return !/[A-Za-z_$][\w$]*/.test(stripped);
33910
+ }
33911
+ function isInstructionConcat(valueExpr) {
33912
+ if (!valueExpr.includes("+")) return false;
33913
+ if (!hasIdentifier(valueExpr)) return false;
33914
+ const lits = stringLiterals(valueExpr);
33915
+ if (lits.length === 0) return false;
33916
+ return !lits.every(isDelimiterLiteral);
33917
+ }
33918
+ function isMixedTemplate(v) {
33919
+ const fstr = v.match(/\bf(["'])((?:\\.|(?!\1).)*)\1/);
33920
+ if (fstr) {
33921
+ const body2 = fstr[2];
33922
+ if (/\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\{[^}]*\}/g, ""))) return true;
33923
+ }
33924
+ const tmpl = v.match(/`([^`]*)`/);
33925
+ if (tmpl) {
33926
+ const body2 = tmpl[1];
33927
+ if (/\$\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\$\{[^}]*\}/g, ""))) return true;
33928
+ }
33929
+ return false;
33930
+ }
33931
+ function assignmentRe(varName, flags2 = "") {
33932
+ return new RegExp(`\\b${varName}\\s*(?::=|(?<![=<>!])=(?!=))\\s*`, flags2);
33933
+ }
33934
+ function resolveAssignmentRHS(varName, codeLines) {
33935
+ const re = assignmentRe(varName);
33936
+ for (const line of codeLines) {
33937
+ const m = line.match(re);
33938
+ if (m && m.index !== void 0) {
33939
+ return scanValue(line, m.index + m[0].length);
33940
+ }
33941
+ }
33942
+ return void 0;
33943
+ }
33944
+ function classifyExpr(v) {
33945
+ if (isMixedTemplate(v)) return "unsafe";
33946
+ if (v.includes("+")) {
33947
+ if (isInstructionConcat(v)) return "unsafe";
33948
+ const lits = stringLiterals(v);
33949
+ if (lits.length > 0 && lits.every(isDelimiterLiteral) && hasIdentifier(v)) return "safe";
33950
+ return "unknown";
33951
+ }
33952
+ return "unknown";
33953
+ }
33954
+ function classifyContentPlacement(val, codeLines) {
33955
+ const v = val.trim();
33956
+ if (/^[A-Za-z_$][\w$]*$/.test(v)) {
33957
+ const rhs = resolveAssignmentRHS(v, codeLines);
33958
+ if (rhs === void 0) return "unknown";
33959
+ return classifyExpr(rhs);
33960
+ }
33961
+ return classifyExpr(v);
33962
+ }
33963
+ function classifyPromptCall(callCode, codeLines) {
33964
+ const contentRe = /["']?[Cc]ontent["']?\s*[:=]\s*/g;
33965
+ let m;
33966
+ let sawDynamic = false;
33967
+ let allSafe = true;
33968
+ while ((m = contentRe.exec(callCode)) !== null) {
33969
+ const val = scanValue(callCode, m.index + m[0].length);
33970
+ if (val === "" || isPureLiteral(val)) continue;
33971
+ sawDynamic = true;
33972
+ const ctx = callCode.slice(Math.max(0, m.index - 80), m.index);
33973
+ const roleM = ctx.match(/["']?[Rr]ole["']?\s*[:=]\s*["'](\w+)["'][^"']*$/);
33974
+ const role = roleM ? roleM[1].toLowerCase() : "user";
33975
+ if (role === "system" || role === "assistant") return "unsafe";
33976
+ const placement = classifyContentPlacement(val, codeLines);
33977
+ if (placement === "unsafe") return "unsafe";
33978
+ if (placement !== "safe") allSafe = false;
33979
+ }
33980
+ if (!sawDynamic) return "unknown";
33981
+ return allSafe ? "safe" : "unknown";
33982
+ }
33983
+ function scanRhsAcrossLines(text, start2) {
33984
+ let depth = 0;
33985
+ let str = null;
33986
+ let out2 = "";
33987
+ for (let i2 = start2; i2 < text.length; i2++) {
33988
+ const c = text[i2];
33989
+ if (str) {
33990
+ out2 += c;
33991
+ if (c === str && text[i2 - 1] !== "\\") str = null;
33992
+ continue;
33993
+ }
33994
+ if (c === '"' || c === "'" || c === "`") {
33995
+ str = c;
33996
+ out2 += c;
33997
+ continue;
33998
+ }
33999
+ if (c === "(" || c === "[" || c === "{") {
34000
+ depth++;
34001
+ out2 += c;
34002
+ continue;
34003
+ }
34004
+ if (c === ")" || c === "]" || c === "}") {
34005
+ if (depth === 0) break;
34006
+ depth--;
34007
+ out2 += c;
34008
+ continue;
34009
+ }
34010
+ if (c === "\n" && depth === 0) break;
34011
+ out2 += c;
34012
+ }
34013
+ return out2.trim();
34014
+ }
34015
+ function resolveBuilderRhs(varName, joined) {
34016
+ const re = assignmentRe(varName, "g");
34017
+ const m = re.exec(joined);
34018
+ if (m) return scanRhsAcrossLines(joined, m.index + m[0].length);
34019
+ return void 0;
34020
+ }
34021
+ function collectBuilderRegion(callCode, joined) {
34022
+ const parts2 = [];
34023
+ const seen = /* @__PURE__ */ new Set();
34024
+ const stack = [...callCode.matchAll(/[A-Za-z_$][\w$]*/g)].map((x) => x[0]);
34025
+ let budget = 40;
34026
+ while (stack.length > 0 && budget-- > 0) {
34027
+ const v = stack.pop();
34028
+ if (seen.has(v)) continue;
34029
+ seen.add(v);
34030
+ const rhs = resolveBuilderRhs(v, joined);
34031
+ if (rhs) {
34032
+ parts2.push(rhs);
34033
+ for (const mm of rhs.matchAll(/[A-Za-z_$][\w$]*/g)) stack.push(mm[0]);
34034
+ }
34035
+ }
34036
+ return parts2.join("\n");
34037
+ }
34038
+ function classifyPromptSink(callCode, codeLines) {
34039
+ const direct = classifyPromptCall(callCode, codeLines);
34040
+ if (direct !== "unknown") return direct;
34041
+ const region = collectBuilderRegion(callCode, codeLines.join("\n"));
34042
+ if (!region) return "unknown";
34043
+ return classifyPromptCall(region, codeLines);
34044
+ }
34045
+ var PromptInjectionSafetyGatePass = class {
34046
+ name = "prompt-injection-safety-gate";
34047
+ category = "security";
34048
+ run(ctx) {
34049
+ const { graph, code } = ctx;
34050
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
34051
+ if (sinks.length === 0) return { droppedSafe: 0 };
34052
+ const codeLines = code.split("\n");
34053
+ let droppedSafe = 0;
34054
+ const kept = sinks.filter((sink) => {
34055
+ if (sink.type !== "prompt_injection") return true;
34056
+ const callCode = sink.code ?? codeLines[sink.line - 1] ?? "";
34057
+ if (!callCode) return true;
34058
+ if (classifyPromptSink(callCode, codeLines) === "safe") {
34059
+ droppedSafe++;
34060
+ return false;
34061
+ }
34062
+ return true;
34063
+ });
34064
+ if (droppedSafe > 0) {
34065
+ sinks.length = 0;
34066
+ sinks.push(...kept);
34067
+ }
34068
+ return { droppedSafe };
34069
+ }
34070
+ };
34071
+
34072
+ // src/analysis/passes/speculative-prompt-param-source-pass.ts
34073
+ function returnOrAssignRhs(text) {
34074
+ const t = text.trim();
34075
+ const ret = t.match(/^return\s+(.+?);?$/);
34076
+ if (ret) return ret[1];
34077
+ const asg = t.match(/^[A-Za-z_$][\w$.]*\s*(?::=|(?<![=<>!])=(?!=))\s*(.+?);?$/);
34078
+ if (asg) return asg[1];
34079
+ return void 0;
34080
+ }
34081
+ function looksLikePromptText(lit) {
34082
+ const s = lit.trim();
34083
+ if (/\b(you are|you're|assistant|system prompt|follow (the )?(policy|instructions|rules)|act as|your task|instructions?\s*:|ignore (all )?previous|do not reveal|respond (to|with|as)|answer the (question|user)|you (must|should|will)|helpful (ai|assistant))\b/i.test(s)) {
34084
+ return true;
34085
+ }
34086
+ const words = s.split(/\s+/).filter(Boolean);
34087
+ return words.length >= 4 && s.length >= 20;
34088
+ }
34089
+ function literalsOf(expr) {
34090
+ return [...expr.matchAll(/"([^"]*)"|'([^']*)'|`([^`]*)`/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
34091
+ }
34092
+ function detectPromptConstructionFlows(codeLines, types) {
34093
+ const flows = [];
34094
+ const seen = /* @__PURE__ */ new Set();
34095
+ for (const type of types) {
34096
+ for (const method of type.methods) {
34097
+ const params = (method.parameters ?? []).map((p) => ({ name: p.name, line: p.line ?? method.start_line })).filter((p) => p.name && p.name !== "_");
34098
+ if (params.length === 0) continue;
34099
+ for (let line = method.start_line; line <= method.end_line; line++) {
34100
+ const text = codeLines[line - 1] ?? "";
34101
+ const rhs = returnOrAssignRhs(text);
34102
+ if (!rhs || !isInstructionConcat(rhs)) continue;
34103
+ if (!literalsOf(rhs).some(looksLikePromptText)) continue;
34104
+ const param = params.find((p) => new RegExp(`\\b${p.name}\\b`).test(rhs));
34105
+ if (!param) continue;
34106
+ const key = `${param.line}|${line}`;
34107
+ if (seen.has(key)) continue;
34108
+ seen.add(key);
34109
+ flows.push({
34110
+ source_line: param.line,
34111
+ sink_line: line,
34112
+ source_type: "http_body",
34113
+ sink_type: "prompt_injection",
34114
+ path: [
34115
+ { variable: param.name, line: param.line, type: "source" },
34116
+ { variable: param.name, line, type: "sink" }
34117
+ ],
34118
+ confidence: 1,
34119
+ sanitized: false
34120
+ });
34121
+ }
34122
+ }
34123
+ }
34124
+ return flows;
34125
+ }
34126
+ function looksLikeTextParam(type) {
34127
+ if (!type) return true;
34128
+ const t = type.trim();
34129
+ if (t.startsWith("*") || t.startsWith("&")) return false;
34130
+ if (/\b(Client|Context|Writer|Reader|Conn|DB|Logger|Handler|Server|Request|Response|Pool|Session|Engine|Service|Repository|Config)\b/.test(t)) {
34131
+ return false;
34132
+ }
34133
+ return /(?:^|\b)(string|str|String|text|any|object|interface\{\}|\[\]byte|\[\]string|List\[str\]|Optional\[str\])(?:\b|$)/i.test(t) || // TS unions / plain identifiers without a handle keyword default to text.
34134
+ /^[A-Za-z_$][\w$<>[\], |]*$/.test(t);
34135
+ }
34136
+ var SpeculativePromptParamSourcePass = class {
34137
+ constructor(enabled) {
34138
+ this.enabled = enabled;
34139
+ }
34140
+ enabled;
34141
+ name = "speculative-prompt-param-source";
34142
+ category = "security";
34143
+ run(ctx) {
34144
+ if (!this.enabled) return { added: 0 };
34145
+ if (!ctx.hasResult("sink-filter")) return { added: 0 };
34146
+ const sinkFilter = ctx.getResult("sink-filter");
34147
+ const { types } = ctx.graph.ir;
34148
+ const promptSinks = sinkFilter.sinks.filter(
34149
+ (s) => s.type === "prompt_injection"
34150
+ );
34151
+ if (promptSinks.length === 0) return { added: 0 };
34152
+ const existing = new Set(
34153
+ sinkFilter.sources.filter((s) => typeof s.variable === "string").map((s) => `${s.variable}:${s.line}`)
34154
+ );
34155
+ const added = [];
34156
+ for (const type of types) {
34157
+ for (const method of type.methods) {
34158
+ const hasPromptSink = promptSinks.some(
34159
+ (s) => s.line >= method.start_line && s.line <= method.end_line
34160
+ );
34161
+ if (!hasPromptSink) continue;
34162
+ for (const param of method.parameters) {
34163
+ if (!param.name || param.name === "_") continue;
34164
+ if (!looksLikeTextParam(param.type)) continue;
34165
+ const line = param.line ?? method.start_line;
34166
+ const key = `${param.name}:${line}`;
34167
+ if (existing.has(key)) continue;
34168
+ existing.add(key);
34169
+ added.push({
34170
+ // `http_body` reaches `prompt_injection` via the flow
34171
+ // generators; a generic untrusted request-shaped type.
34172
+ type: "http_body",
34173
+ location: `speculative untrusted parameter '${param.name}' in ${method.name}`,
34174
+ severity: "high",
34175
+ line,
34176
+ confidence: 1,
34177
+ variable: param.name,
34178
+ in_method: method.name
34179
+ });
34180
+ }
34181
+ }
34182
+ }
34183
+ if (added.length > 0) {
34184
+ sinkFilter.sources.push(...added);
34185
+ }
34186
+ return { added: added.length };
34187
+ }
34188
+ };
34189
+
33834
34190
  // src/analysis/passes/cli-main-reflection-suppress-pass.ts
33835
34191
  var REFLECTION_SINK_METHODS = /* @__PURE__ */ new Set([
33836
34192
  "forName",
@@ -44077,6 +44433,9 @@ async function analyze(code, filePath, language, options = {}) {
44077
44433
  if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
44078
44434
  if (!disabledPasses.has("deserialization-safety-gate"))
44079
44435
  pipeline.add(new DeserializationSafetyGatePass(options.dependencyContext));
44436
+ if (!disabledPasses.has("prompt-injection-safety-gate"))
44437
+ pipeline.add(new PromptInjectionSafetyGatePass());
44438
+ pipeline.add(new SpeculativePromptParamSourcePass(options.speculativeParamSources === true));
44080
44439
  if (!disabledPasses.has("cli-main-reflection-suppress"))
44081
44440
  pipeline.add(new CliMainReflectionSuppressPass());
44082
44441
  if (!disabledPasses.has("library-profile-sink-gate"))
@@ -44156,6 +44515,17 @@ async function analyze(code, filePath, language, options = {}) {
44156
44515
  flows: interProc.additionalFlows,
44157
44516
  interprocedural: interProc.interprocedural
44158
44517
  };
44518
+ if (options.speculativeParamSources === true) {
44519
+ const existingFlowKeys = new Set(
44520
+ (taint.flows ?? []).map((f) => `${f.source_line}|${f.sink_line}|${f.sink_type}`)
44521
+ );
44522
+ const constructionFlows = detectPromptConstructionFlows(code.split("\n"), types).filter(
44523
+ (f) => !existingFlowKeys.has(`${f.source_line}|${f.sink_line}|${f.sink_type}`)
44524
+ );
44525
+ if (constructionFlows.length > 0) {
44526
+ taint.flows = [...taint.flows ?? [], ...constructionFlows];
44527
+ }
44528
+ }
44159
44529
  if (taint.flows && taint.flows.length > 0 && taint.sinks.length > 0) {
44160
44530
  const sinkTagsByKey = /* @__PURE__ */ new Map();
44161
44531
  for (const s of taint.sinks) {
@@ -9981,6 +9981,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
9981
9981
  function buildGoDFG(tree) {
9982
9982
  const defs = [];
9983
9983
  const uses = [];
9984
+ const extraChains = [];
9984
9985
  let defIdCounter = 1;
9985
9986
  let useIdCounter = 1;
9986
9987
  const scopeStack = [/* @__PURE__ */ new Map()];
@@ -10002,7 +10003,7 @@ function buildGoDFG(tree) {
10002
10003
  }
10003
10004
  const body2 = func2.childForFieldName("body");
10004
10005
  if (body2) {
10005
- processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
10006
+ processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
10006
10007
  defIdCounter = defs.length + 1;
10007
10008
  useIdCounter = uses.length + 1;
10008
10009
  }
@@ -10017,6 +10018,15 @@ function buildGoDFG(tree) {
10017
10018
  }
10018
10019
  }
10019
10020
  const chains = computeChains(defs, uses);
10021
+ const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
10022
+ for (const ch of extraChains) {
10023
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
10024
+ if (!seen.has(key)) {
10025
+ seen.add(key);
10026
+ chains.push(ch);
10027
+ }
10028
+ }
10029
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
10020
10030
  return { defs, uses, chains };
10021
10031
  }
10022
10032
  function extractGoParamDefs(params, defs, _startId, scopeStack) {
@@ -10041,28 +10051,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
10041
10051
  }
10042
10052
  }
10043
10053
  }
10044
- function processGoBlock(node, defs, uses, scopeStack, counters) {
10054
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
10055
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
10056
+ if (right.endPosition.row <= right.startPosition.row) return;
10057
+ for (let ui = useStart; ui < useEnd; ui++) {
10058
+ const u = uses[ui];
10059
+ if (!u || u.def_id === null) continue;
10060
+ for (let di = defStart; di < defEnd; di++) {
10061
+ const d = defs[di];
10062
+ if (d && d.kind === "local" && d.id !== u.def_id) {
10063
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
10064
+ }
10065
+ }
10066
+ }
10067
+ };
10045
10068
  walkTree(node, (child) => {
10046
10069
  if (child.type === "short_var_declaration") {
10047
10070
  const left = child.childForFieldName("left");
10048
10071
  const right = child.childForFieldName("right");
10072
+ const usesBefore = uses.length;
10049
10073
  if (right) {
10050
10074
  extractGoUses(right, uses, scopeStack);
10051
10075
  }
10076
+ const defsBefore = defs.length;
10052
10077
  if (left) {
10053
10078
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10054
10079
  }
10080
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10055
10081
  } else if (child.type === "var_declaration") {
10056
10082
  processGoVarDecl(child, defs, scopeStack, counters);
10057
10083
  } else if (child.type === "assignment_statement") {
10058
10084
  const left = child.childForFieldName("left");
10059
10085
  const right = child.childForFieldName("right");
10086
+ const usesBefore = uses.length;
10060
10087
  if (right) {
10061
10088
  extractGoUses(right, uses, scopeStack);
10062
10089
  }
10090
+ const defsBefore = defs.length;
10063
10091
  if (left) {
10064
10092
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
10065
10093
  }
10094
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10066
10095
  } else if (child.type === "for_statement") {
10067
10096
  const rangeClause = findChildByTypeGo(child, "range_clause");
10068
10097
  if (rangeClause) {
@@ -9915,6 +9915,7 @@ var GO_KEYWORDS = /* @__PURE__ */ new Set([
9915
9915
  function buildGoDFG(tree) {
9916
9916
  const defs = [];
9917
9917
  const uses = [];
9918
+ const extraChains = [];
9918
9919
  let defIdCounter = 1;
9919
9920
  let useIdCounter = 1;
9920
9921
  const scopeStack = [/* @__PURE__ */ new Map()];
@@ -9936,7 +9937,7 @@ function buildGoDFG(tree) {
9936
9937
  }
9937
9938
  const body2 = func2.childForFieldName("body");
9938
9939
  if (body2) {
9939
- processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
9940
+ processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
9940
9941
  defIdCounter = defs.length + 1;
9941
9942
  useIdCounter = uses.length + 1;
9942
9943
  }
@@ -9951,6 +9952,15 @@ function buildGoDFG(tree) {
9951
9952
  }
9952
9953
  }
9953
9954
  const chains = computeChains(defs, uses);
9955
+ const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
9956
+ for (const ch of extraChains) {
9957
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
9958
+ if (!seen.has(key)) {
9959
+ seen.add(key);
9960
+ chains.push(ch);
9961
+ }
9962
+ }
9963
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
9954
9964
  return { defs, uses, chains };
9955
9965
  }
9956
9966
  function extractGoParamDefs(params, defs, _startId, scopeStack) {
@@ -9975,28 +9985,47 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
9975
9985
  }
9976
9986
  }
9977
9987
  }
9978
- function processGoBlock(node, defs, uses, scopeStack, counters) {
9988
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
9989
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
9990
+ if (right.endPosition.row <= right.startPosition.row) return;
9991
+ for (let ui = useStart; ui < useEnd; ui++) {
9992
+ const u = uses[ui];
9993
+ if (!u || u.def_id === null) continue;
9994
+ for (let di = defStart; di < defEnd; di++) {
9995
+ const d = defs[di];
9996
+ if (d && d.kind === "local" && d.id !== u.def_id) {
9997
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
9998
+ }
9999
+ }
10000
+ }
10001
+ };
9979
10002
  walkTree(node, (child) => {
9980
10003
  if (child.type === "short_var_declaration") {
9981
10004
  const left = child.childForFieldName("left");
9982
10005
  const right = child.childForFieldName("right");
10006
+ const usesBefore = uses.length;
9983
10007
  if (right) {
9984
10008
  extractGoUses(right, uses, scopeStack);
9985
10009
  }
10010
+ const defsBefore = defs.length;
9986
10011
  if (left) {
9987
10012
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
9988
10013
  }
10014
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
9989
10015
  } else if (child.type === "var_declaration") {
9990
10016
  processGoVarDecl(child, defs, scopeStack, counters);
9991
10017
  } else if (child.type === "assignment_statement") {
9992
10018
  const left = child.childForFieldName("left");
9993
10019
  const right = child.childForFieldName("right");
10020
+ const usesBefore = uses.length;
9994
10021
  if (right) {
9995
10022
  extractGoUses(right, uses, scopeStack);
9996
10023
  }
10024
+ const defsBefore = defs.length;
9997
10025
  if (left) {
9998
10026
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
9999
10027
  }
10028
+ if (right) linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
10000
10029
  } else if (child.type === "for_statement") {
10001
10030
  const rangeClause = findChildByTypeGo(child, "range_clause");
10002
10031
  if (rangeClause) {