@orangepro/orangepro-mcp 0.1.0 → 0.2.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.
@@ -333,6 +333,14 @@ export function analyzeRepo(root, opts = {}) {
333
333
  // file, and every CodeSymbol external_id that made it into the graph (so a
334
334
  // confirmed-but-capped symbol downgrades instead of COVERS-ing the file).
335
335
  const eligibleSymbolsByFile = new Map();
336
+ // Proof-edge eligibility for non-TS languages. A hard TESTED_BY/COVERS proof edge is REAL
337
+ // derivable evidence (a test the repo already runs), independent of the entry-point-adjacent
338
+ // DENOMINATOR bar. It admits behavior-callable symbols that are `eligible` OR merely
339
+ // `not_entry_point_adjacent` (e.g. a Formatter/Converter SPI method like PetTypeFormatter#print)
340
+ // but still EXCLUDES infra plumbing (behaviorSurfaceExcluded) and boilerplate/generated. Used
341
+ // only by the Go/Java/Python proof-target resolvers; `eligibleSymbolsByFile` (the denominator)
342
+ // is unchanged, so this never widens the denominator.
343
+ const proofEligibleSymbolsByFile = new Map();
336
344
  const codeSymbolIds = new Set();
337
345
  let testFiles = 0;
338
346
  let flowsTruncated = 0;
@@ -798,6 +806,15 @@ export function analyzeRepo(root, opts = {}) {
798
806
  else
799
807
  eligibleSymbolsByFile.set(file.relPath, [sym.name]);
800
808
  }
809
+ // Proof-edge eligibility: eligible OR not_entry_point_adjacent, but never infra
810
+ // (behaviorSurfaceExcluded). Denominator-neutral — only the non-TS proof resolvers read it.
811
+ if (eligible || notEntryPointAdjacent) {
812
+ const list = proofEligibleSymbolsByFile.get(file.relPath);
813
+ if (list)
814
+ list.push(sym.name);
815
+ else
816
+ proofEligibleSymbolsByFile.set(file.relPath, [sym.name]);
817
+ }
801
818
  symbolCount++;
802
819
  }
803
820
  const contractsForFile = behaviorContractsByFile.get(file.relPath) ?? [];
@@ -1512,13 +1529,13 @@ export function analyzeRepo(root, opts = {}) {
1512
1529
  return matches.length === 1 ? matches[0] : null;
1513
1530
  };
1514
1531
  const eligibleGoSymbol = (targetRel, name) => {
1515
- if (!targetRel || !(eligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1532
+ if (!targetRel || !(proofEligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1516
1533
  return null;
1517
1534
  const symId = `sym:${targetRel}#${name}`;
1518
1535
  return codeSymbolIds.has(symId) ? symId : null;
1519
1536
  };
1520
1537
  const eligiblePythonSymbol = (targetRel, name) => {
1521
- if (!targetRel || !(eligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1538
+ if (!targetRel || !(proofEligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1522
1539
  return null;
1523
1540
  const symId = `sym:${targetRel}#${name}`;
1524
1541
  return codeSymbolIds.has(symId) ? symId : null;
@@ -1530,7 +1547,7 @@ export function analyzeRepo(root, opts = {}) {
1530
1547
  return Boolean(nonTsStructureByFile.get(targetRel)?.structure.javaClasses?.some((c) => c.name === className && c.methods.includes(methodName)));
1531
1548
  };
1532
1549
  const eligibleJavaSymbol = (targetRel, name, expectedKind) => {
1533
- if (!targetRel || !(eligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1550
+ if (!targetRel || !(proofEligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
1534
1551
  return null;
1535
1552
  if (symbolKind(targetRel, name) !== expectedKind)
1536
1553
  return null;
@@ -1720,6 +1737,19 @@ export function analyzeRepo(root, opts = {}) {
1720
1737
  continue;
1721
1738
  seenGoProof.add(edgeKey);
1722
1739
  goProofConfirmedPairs++;
1740
+ // `test_name` = the enclosing `func TestXxx` (or its literal-named subtest path,
1741
+ // `TestXxx/sub`) where the assertion witnessed the target. STRUCTURAL METADATA ONLY
1742
+ // (never proof) — it lets Go auto-drive pick the exact `go test -run` for THIS target
1743
+ // even when the file has many tests, where the file-level `test_names[]` cannot
1744
+ // disambiguate. Only `[A-Za-z0-9_]` path segments are accepted (defensive; the
1745
+ // extractor already drops runtime/unsafe subtest names to the bare parent).
1746
+ // `assertion_line` (Slice 2) = the 1-based test-source line of the assertion that
1747
+ // witnessed the target. STRUCTURAL METADATA ONLY — it lets the Go oracle bind a
1748
+ // runtime-named subtest's mutant failure to THIS exact assertion (a sibling subtest
1749
+ // asserting at a different line is refused). Emitted only alongside a valid test_name.
1750
+ const goEdgeProps = /^Test[A-Za-z0-9_]+(\/[A-Za-z0-9_]+)*$/.test(proof.testName)
1751
+ ? { test_name: proof.testName, ...(typeof proof.assertionLine === "number" ? { assertion_line: proof.assertionLine } : {}) }
1752
+ : undefined;
1723
1753
  edges.push(makeEdge({
1724
1754
  from_external_id: symId,
1725
1755
  to_external_id: testExternalId,
@@ -1727,7 +1757,8 @@ export function analyzeRepo(root, opts = {}) {
1727
1757
  evidence_strength: "hard",
1728
1758
  review_status: "auto_detected",
1729
1759
  provenance: prov(testRel, hashString(`${symId}:${proof.assertion}`)),
1730
- last_verified: proofVerifiedAt
1760
+ last_verified: proofVerifiedAt,
1761
+ ...(goEdgeProps ? { properties: goEdgeProps } : {})
1731
1762
  }));
1732
1763
  edges.push(makeEdge({
1733
1764
  from_external_id: testExternalId,
@@ -1736,7 +1767,8 @@ export function analyzeRepo(root, opts = {}) {
1736
1767
  evidence_strength: "hard",
1737
1768
  review_status: "auto_detected",
1738
1769
  provenance: prov(testRel, hashString(`${symId}:${proof.assertion}`)),
1739
- last_verified: proofVerifiedAt
1770
+ last_verified: proofVerifiedAt,
1771
+ ...(goEdgeProps ? { properties: goEdgeProps } : {})
1740
1772
  }));
1741
1773
  }
1742
1774
  }
@@ -1753,11 +1785,19 @@ export function analyzeRepo(root, opts = {}) {
1753
1785
  continue;
1754
1786
  seenJavaProof.add(edgeKey);
1755
1787
  javaProofConfirmedPairs++;
1788
+ // `test_name` = the enclosing `@Test` method where the assertion witnessed the
1789
+ // target. STRUCTURAL METADATA ONLY (never proof) — it lets Java auto-drive pick
1790
+ // the exact `mvn test -Dtest=Class#method` for THIS target even when the test
1791
+ // class has many @Test methods, where the file-level `test_names[]` cannot
1792
+ // disambiguate. Only a plain Java identifier is recorded (defensive; the
1793
+ // extractor already yields one). Provenance hash is unchanged.
1794
+ const javaEdgeProps = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(proof.testName) ? { test_name: proof.testName } : undefined;
1756
1795
  edges.push(...makeProofEdges({
1757
1796
  testRel,
1758
1797
  symId,
1759
1798
  provenance: prov(testRel, hashString(`${symId}:${proof.assertion}:${proof.className}.${proof.callee}`)),
1760
- lastVerified: proofVerifiedAt
1799
+ lastVerified: proofVerifiedAt,
1800
+ ...(javaEdgeProps ? { properties: javaEdgeProps } : {})
1761
1801
  }));
1762
1802
  }
1763
1803
  }
@@ -749,6 +749,14 @@ function hasGoTestingFailure(node, testingParams) {
749
749
  return Boolean(sel && testingParams.has(sel.qualifier) && GO_TEST_FAIL_METHODS.has(sel.name));
750
750
  });
751
751
  }
752
+ /** 1-based source line of the first `t.Error*`/`t.Fatal*` call in a consequence (undefined if none). */
753
+ function goTestingFailureCallLine(node, testingParams) {
754
+ const call = callExpressions(node).find((c) => {
755
+ const sel = callFunctionSelector(c);
756
+ return Boolean(sel && testingParams.has(sel.qualifier) && GO_TEST_FAIL_METHODS.has(sel.name));
757
+ });
758
+ return call ? call.startPosition.row + 1 : undefined;
759
+ }
752
760
  function goAssertionCalls(node, assertLocals, dotAssertMethods, shadowed) {
753
761
  return callExpressions(node).filter((call) => {
754
762
  const sel = callFunctionSelector(call);
@@ -808,13 +816,27 @@ function goSubtestBody(stmt, testingParams) {
808
816
  });
809
817
  if (!call)
810
818
  return null;
811
- const fn = namedChildren(call.childForFieldName("arguments") ?? call).find((n) => n.type === "func_literal");
819
+ const args = namedChildren(call.childForFieldName("arguments") ?? call);
820
+ const fn = args.find((n) => n.type === "func_literal");
812
821
  const body = fn?.childForFieldName("body");
813
822
  if (!fn || !body)
814
823
  return null;
815
824
  const nextTestingParams = new Set(testingParams);
816
825
  collectGoTestingParamNames(fn.childForFieldName("parameters"), nextTestingParams);
817
- return nextTestingParams.size > testingParams.size ? { body, testingParams: nextTestingParams } : null;
826
+ if (nextTestingParams.size <= testingParams.size)
827
+ return null;
828
+ // Only a STRING-LITERAL subtest name with no chars Go would rewrite in the `-run`
829
+ // path (letters/digits/underscore) is recorded — that segment matches the go-test
830
+ // JSON `Test` field verbatim (`TestX/sub`). A runtime name (`tc.Name`) or a literal
831
+ // needing sanitization yields no subName → the caller keeps the parent `TestX`, whose
832
+ // exact-match oracle safely refuses the runtime subtest frames rather than false-prove.
833
+ const nameArg = args.find((n) => n.type !== "func_literal");
834
+ // A double-quoted `interpreted_string_literal` text is the source token WITH quotes
835
+ // (e.g. `"basic"`); strip them and accept only a `-run`-safe identifier segment.
836
+ const raw = nameArg?.type === "interpreted_string_literal" ? nameArg.text : undefined;
837
+ const inner = raw && raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : undefined;
838
+ const subName = inner && /^[A-Za-z0-9_]+$/.test(inner) ? inner : undefined;
839
+ return { body, testingParams: nextTestingParams, ...(subName ? { subName } : {}) };
818
840
  }
819
841
  function goShortVarCalls(stmt) {
820
842
  if (stmt.type !== "short_var_declaration")
@@ -830,35 +852,40 @@ function extractGoProofCalls(root, imports) {
830
852
  const assertLocals = goAssertLocals(imports);
831
853
  const dotAssertMethods = goDotAssertMethods(root);
832
854
  const suiteTypes = goCanonicalSuiteTypes(root, goSuiteLocals(imports));
833
- const add = (testName, shadowed, assertion, calls) => {
855
+ const add = (testName, shadowed, assertion, calls, assertionLine) => {
834
856
  for (const c of calls) {
835
857
  const key = `${testName}|${c.qualifier ?? ""}|${c.callee}|${assertion}`;
836
858
  if (seen.has(key))
837
859
  continue;
838
860
  seen.add(key);
839
- out.push({ caller: testName, testName, ...c, shadowed: [...shadowed], assertion });
861
+ out.push({ caller: testName, testName, ...c, shadowed: [...shadowed], assertion, ...(assertionLine ? { assertionLine } : {}) });
840
862
  }
841
863
  };
842
864
  const processBlock = (block, testName, testingParams, shadowed) => {
843
865
  for (const stmt of blockStatements(block)) {
844
866
  for (const assertion of goAssertionCalls(stmt, assertLocals, dotAssertMethods, shadowed)) {
845
- add(testName, shadowed, "assert_helper", singleGoProductCallIn(goAssertionSubject(assertion, testingParams)));
867
+ add(testName, shadowed, "assert_helper", singleGoProductCallIn(goAssertionSubject(assertion, testingParams)), assertion.startPosition.row + 1);
846
868
  }
847
869
  if (stmt.type === "if_statement" && hasGoTestingFailure(stmt.childForFieldName("consequence"), testingParams)) {
848
870
  const condition = stmt.childForFieldName("condition");
849
- add(testName, shadowed, "testing_fail", singleGoProductCallIn(condition));
871
+ // Go reports the failing frame at the `t.Error*`/`t.Fatal*` CALL inside the
872
+ // consequence, not the `if` line — bind the line there so the subtest gate matches.
873
+ const failLine = goTestingFailureCallLine(stmt.childForFieldName("consequence"), testingParams);
874
+ add(testName, shadowed, "testing_fail", singleGoProductCallIn(condition), failLine);
850
875
  const init = stmt.childForFieldName("initializer");
851
876
  const initCalls = init ? goShortVarCalls(init) : null;
852
877
  if (initCalls && containsIdentifier(condition, initCalls.names))
853
- add(testName, shadowed, "testing_fail", initCalls.calls);
878
+ add(testName, shadowed, "testing_fail", initCalls.calls, failLine);
854
879
  }
855
880
  for (const child of namedChildren(stmt)) {
856
881
  if (child.type === "block")
857
882
  processBlock(child, testName, testingParams, shadowed);
858
883
  }
859
884
  const subtest = goSubtestBody(stmt, testingParams);
860
- if (subtest)
861
- processBlock(subtest.body, testName, subtest.testingParams, shadowed);
885
+ if (subtest) {
886
+ const subTestName = subtest.subName ? `${testName}/${subtest.subName}` : testName;
887
+ processBlock(subtest.body, subTestName, subtest.testingParams, shadowed);
888
+ }
862
889
  }
863
890
  };
864
891
  const processSuiteBlock = (block, testName, receiver, shadowed) => {
@@ -913,6 +940,7 @@ const JAVA_ASSERT_ACTUAL_ARG = new Set([
913
940
  ]);
914
941
  const JAVA_ASSERT_SUBJECT_ARG = new Set(["assertFalse", "assertNotNull", "assertNull", "assertTrue"]);
915
942
  const JAVA_ASSERT_METHODS = new Set([...JAVA_ASSERT_ACTUAL_ARG, ...JAVA_ASSERT_SUBJECT_ARG]);
943
+ const JAVA_ASSERTJ_ASSERT_CLASS = "org.assertj.core.api.Assertions";
916
944
  function addJavaAssertionImport(out, name, assertion) {
917
945
  if (!JAVA_ASSERT_METHODS.has(name))
918
946
  return;
@@ -944,6 +972,57 @@ function javaJunitAssertImports(root) {
944
972
  }
945
973
  return out;
946
974
  }
975
+ /**
976
+ * True when `assertThat` is statically imported from AssertJ (`org.assertj.core.api.Assertions`,
977
+ * star or explicit) — the canonical Spring/Mockito unit assertion. AssertJ chains on a subject
978
+ * (`assertThat(x).isEqualTo(...)`), so recognizing the `assertThat(x)` head lets the extractor see
979
+ * the target call that produced `x`. Trust is unchanged: the emitted edge only NAMES a candidate
980
+ * test; the frozen dynamic oracle still re-runs it and refuses/survives if the target isn't proven.
981
+ */
982
+ function javaHasAssertjAssertThat(root) {
983
+ for (const node of root.descendantsOfType("import_declaration")) {
984
+ if (!node)
985
+ continue;
986
+ if (!/^import\s+static\b/.test(node.text))
987
+ continue;
988
+ const spec = namedChildren(node).find((n) => n.type.endsWith("identifier"))?.text;
989
+ if (!spec)
990
+ continue;
991
+ const star = namedChildren(node).some((n) => n.type === "asterisk");
992
+ if (star && spec === JAVA_ASSERTJ_ASSERT_CLASS)
993
+ return true;
994
+ if (!star && spec === `${JAVA_ASSERTJ_ASSERT_CLASS}.assertThat`)
995
+ return true;
996
+ }
997
+ return false;
998
+ }
999
+ /**
1000
+ * Map each declared FIELD name → its declared type simple name, for a test class. The declared
1001
+ * type IS the receiver's static type (no dataflow needed) — so `private PetTypeFormatter fmt;`
1002
+ * yields `fmt → PetTypeFormatter`. Used to resolve the className when an assertion's target call
1003
+ * has a field receiver (`this.fmt.method(...)` or the bare `fmt.method(...)`), the canonical
1004
+ * `@BeforeEach`/`@Autowired`-injected Spring unit shape.
1005
+ */
1006
+ function javaFieldTypes(root) {
1007
+ const out = new Map();
1008
+ for (const node of root.descendantsOfType("field_declaration")) {
1009
+ if (!node)
1010
+ continue;
1011
+ const className = simpleJavaClassName(node.childForFieldName("type")?.text);
1012
+ if (!className)
1013
+ continue;
1014
+ for (const child of namedChildren(node)) {
1015
+ if (child.type !== "variable_declarator")
1016
+ continue;
1017
+ const name = child.childForFieldName("name")?.text;
1018
+ // First declaration wins; a name declared with two different types is left unresolved
1019
+ // (fields don't legally redeclare, but guard against odd trees).
1020
+ if (name && !out.has(name))
1021
+ out.set(name, className);
1022
+ }
1023
+ }
1024
+ return out;
1025
+ }
947
1026
  function javaJunitTestAnnotationLocals(imports) {
948
1027
  const out = new Set();
949
1028
  for (const i of imports) {
@@ -1071,7 +1150,28 @@ function javaObjectCreationClassName(node) {
1071
1150
  return null;
1072
1151
  return simpleJavaClassName(node.childForFieldName("type")?.text);
1073
1152
  }
1074
- function javaDirectProofTarget(subject) {
1153
+ /**
1154
+ * Resolve a method-invocation receiver `object` node to the target class simple name.
1155
+ * - `Foo.bar()` static-style: a class-shaped bare identifier → the identifier itself.
1156
+ * - `fmt.bar()` bare FIELD identifier → the field's declared type (from `fieldTypes`).
1157
+ * - `this.fmt.bar()` field access → the field's declared type.
1158
+ * A bare identifier that is a known field is resolved as a field (its declared type), NOT as a
1159
+ * class name — the field-type map is authoritative for receivers it knows.
1160
+ */
1161
+ function javaReceiverClassName(object, fieldTypes) {
1162
+ if (!object)
1163
+ return null;
1164
+ if (object.type === "identifier") {
1165
+ const field = fieldTypes.get(object.text);
1166
+ return field ?? simpleJavaClassName(object.text);
1167
+ }
1168
+ if (object.type === "field_access" && object.childForFieldName("object")?.type === "this") {
1169
+ const fieldName = object.childForFieldName("field")?.text;
1170
+ return fieldName ? (fieldTypes.get(fieldName) ?? null) : null;
1171
+ }
1172
+ return null;
1173
+ }
1174
+ function javaDirectProofTarget(subject, fieldTypes) {
1075
1175
  if (!subject || javaCallLikeCount(subject) !== 1)
1076
1176
  return null;
1077
1177
  if (subject.type === "object_creation_expression") {
@@ -1081,8 +1181,7 @@ function javaDirectProofTarget(subject) {
1081
1181
  if (subject.type !== "method_invocation")
1082
1182
  return null;
1083
1183
  const callee = subject.childForFieldName("name")?.text;
1084
- const object = subject.childForFieldName("object");
1085
- const className = object?.type === "identifier" ? simpleJavaClassName(object.text) : null;
1184
+ const className = javaReceiverClassName(subject.childForFieldName("object"), fieldTypes);
1086
1185
  return callee && className ? { className, callee, target_kind: "method" } : null;
1087
1186
  }
1088
1187
  function processJavaNestedBlocks(block, testName, shadowed, processBlock) {
@@ -1098,12 +1197,51 @@ function processJavaNestedBlocks(block, testName, shadowed, processBlock) {
1098
1197
  };
1099
1198
  walk(block);
1100
1199
  }
1200
+ /**
1201
+ * Map each local variable in a @Test method body → the single method_invocation that initialized
1202
+ * it (e.g. `String r = this.fmt.print(x);` → `r → this.fmt.print(x)`). Lets an AssertJ chain whose
1203
+ * subject is a local (`assertThat(r).isEqualTo(...)`) resolve back to the target call that produced
1204
+ * it. Only the DIRECT initializer of a `local_variable_declaration` is recorded; reassignment is
1205
+ * ignored (fail-closed: an ambiguous local simply yields no edge).
1206
+ */
1207
+ function javaLocalVarInits(body) {
1208
+ const out = new Map();
1209
+ for (const decl of body.descendantsOfType("local_variable_declaration")) {
1210
+ if (!decl)
1211
+ continue;
1212
+ for (const child of namedChildren(decl)) {
1213
+ if (child.type !== "variable_declarator")
1214
+ continue;
1215
+ const name = child.childForFieldName("name")?.text;
1216
+ const value = child.childForFieldName("value");
1217
+ if (name && value && !out.has(name))
1218
+ out.set(name, value);
1219
+ }
1220
+ }
1221
+ return out;
1222
+ }
1223
+ /**
1224
+ * The subject expression an AssertJ chain asserts on, i.e. the sole argument of the `assertThat(x)`
1225
+ * head of a chain. Returns null when it isn't an `assertThat(...)` call with exactly one argument.
1226
+ */
1227
+ function javaAssertjSubjectArg(call, shadowed) {
1228
+ if (call.childForFieldName("name")?.text !== "assertThat")
1229
+ return null;
1230
+ if (call.childForFieldName("object"))
1231
+ return null; // must be the bare imported free call
1232
+ if (shadowed.has("assertThat"))
1233
+ return null;
1234
+ const args = namedChildren(call.childForFieldName("arguments") ?? call).filter((n) => n.type !== "comment");
1235
+ return args.length === 1 ? (args[0] ?? null) : null;
1236
+ }
1101
1237
  function extractJavaProofCalls(root, imports) {
1102
1238
  const out = [];
1103
1239
  const seen = new Set();
1104
1240
  const assertImports = javaJunitAssertImports(root);
1105
- if (assertImports.size === 0)
1241
+ const hasAssertj = javaHasAssertjAssertThat(root);
1242
+ if (assertImports.size === 0 && !hasAssertj)
1106
1243
  return out;
1244
+ const fieldTypes = javaFieldTypes(root);
1107
1245
  const testAnnotationLocals = javaJunitTestAnnotationLocals(imports);
1108
1246
  const declaredMethods = javaDeclaredMethodNames(root);
1109
1247
  const add = (testName, shadowed, assertions, target) => {
@@ -1116,12 +1254,29 @@ function extractJavaProofCalls(root, imports) {
1116
1254
  seen.add(key);
1117
1255
  out.push({ testName, ...target, assertion, shadowed: [...shadowed] });
1118
1256
  };
1119
- const processBlock = (block, testName, shadowed) => {
1257
+ // AssertJ edges carry no static junit4/5 flavor; label them junit5 so `add`'s dedup/hash key is
1258
+ // stable. The value only feeds the provenance hash + edge-props — never a trust decision (the
1259
+ // frozen oracle re-runs the test regardless).
1260
+ const assertjAssertions = new Set(["junit5"]);
1261
+ const processBlock = (block, testName, shadowed, localInits) => {
1120
1262
  for (const stmt of blockStatements(block)) {
1121
1263
  for (const assertion of javaAssertionCalls(stmt, assertImports, shadowed)) {
1122
- add(testName, shadowed, assertion.assertions, javaDirectProofTarget(javaAssertionSubject(assertion.call, assertion.assertions)));
1264
+ add(testName, shadowed, assertion.assertions, javaDirectProofTarget(javaAssertionSubject(assertion.call, assertion.assertions), fieldTypes));
1265
+ }
1266
+ if (hasAssertj) {
1267
+ for (const call of stmt.descendantsOfType("method_invocation")) {
1268
+ if (!call || hasAncestorTypeBefore(call, stmt, "lambda_expression"))
1269
+ continue;
1270
+ const arg = javaAssertjSubjectArg(call, shadowed);
1271
+ if (!arg)
1272
+ continue;
1273
+ // Subject is either the target call directly (`assertThat(this.fmt.m(...))`) or a local
1274
+ // whose initializer is the target call (`String r = this.fmt.m(...); assertThat(r)...`).
1275
+ const subject = arg.type === "identifier" ? (localInits.get(arg.text) ?? null) : arg;
1276
+ add(testName, shadowed, assertjAssertions, javaDirectProofTarget(subject, fieldTypes));
1277
+ }
1123
1278
  }
1124
- processJavaNestedBlocks(stmt, testName, shadowed, processBlock);
1279
+ processJavaNestedBlocks(stmt, testName, shadowed, (b, t, s) => processBlock(b, t, s, localInits));
1125
1280
  }
1126
1281
  };
1127
1282
  for (const method of root.descendantsOfType("method_declaration")) {
@@ -1137,7 +1292,7 @@ function extractJavaProofCalls(root, imports) {
1137
1292
  for (const declared of declaredMethods) {
1138
1293
  shadowed.add(declared);
1139
1294
  }
1140
- processBlock(body, name, shadowed);
1295
+ processBlock(body, name, shadowed, javaLocalVarInits(body));
1141
1296
  }
1142
1297
  return out;
1143
1298
  }