@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.
- package/LICENSE +1 -1
- package/README.md +195 -251
- package/dist/local/analyze/analyzer.js +46 -6
- package/dist/local/analyze/treeSitter/engine.js +172 -17
- package/dist/local/autoProve.js +402 -28
- package/dist/local/cli.js +93 -10
- package/dist/local/cliArgs.js +1 -0
- package/dist/local/generate/runHints.js +9 -4
- package/dist/local/graph/factories.js +5 -2
- package/dist/local/ledger.js +1 -1
- package/dist/local/mcp.js +26 -13
- package/dist/local/operations.js +396 -52
- package/dist/local/pack/coverageReport.js +3 -3
- package/dist/local/proofDoctor.js +312 -0
- package/dist/local/rtm.js +40 -10
- package/dist/local/viz/behaviorReportData.js +79 -7
- package/dist/local/viz/behaviorReportHtml.js +526 -615
- package/dist/local/viz/html.js +1 -1
- package/docs/agent-workflow.md +10 -38
- package/docs/agents/claude-code.md +3 -9
- package/docs/agents/codex.md +3 -20
- package/docs/agents/cursor.md +1 -1
- package/docs/agents/opencode.md +1 -1
- package/docs/agents/vscode.md +1 -1
- package/docs/local-proof-kit.md +52 -19
- package/package.json +39 -6
- package/scripts/spikes/go-dynamic-proof-spike.mjs +637 -0
- package/scripts/spikes/go-mutate.go +182 -0
- package/scripts/spikes/java-dynamic-proof-spike.mjs +571 -0
- package/scripts/spikes/java-mutate.mjs +264 -0
- package/scripts/spikes/python-dynamic-proof-spike.mjs +244 -0
- package/scripts/spikes/python-mutate.py +89 -0
|
@@ -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 || !(
|
|
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 || !(
|
|
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 || !(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|