@orangepro/orangepro-mcp 0.2.2 → 0.2.3
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/README.md +36 -3
- package/dist/local/analyze/treeSitter/engine.js +60 -1
- package/dist/local/cli.js +1 -1
- package/dist/local/flows/llmFlowDiscovery.js +16 -5
- package/dist/local/mcp.js +1 -1
- package/dist/local/viz/behaviorReportData.js +2 -3
- package/dist/local/viz/behaviorReportHtml.js +9 -8
- package/package.json +6 -2
- package/scripts/spikes/go-mutate.go +43 -0
package/README.md
CHANGED
|
@@ -41,7 +41,38 @@ cd orangepro-mcp && npm ci && npm run build && npm link
|
|
|
41
41
|
|
|
42
42
|
OrangePro runs as an MCP server. Any MCP-compatible agent (Cursor, Claude Code, Codex, Copilot, OpenCode) can drive it.
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### Quick agent setup
|
|
45
|
+
|
|
46
|
+
If you already have `opro` on your PATH, print the exact config for your client:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
opro agent --client codex
|
|
50
|
+
opro agent --client claude-code
|
|
51
|
+
opro agent --client cursor
|
|
52
|
+
opro agent --client opencode
|
|
53
|
+
opro agent --client generic
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
No global install is required. These commands use the published package:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Codex
|
|
60
|
+
npx -y @orangepro/mcp-server@latest agent --client codex
|
|
61
|
+
|
|
62
|
+
# Claude Code
|
|
63
|
+
npx -y @orangepro/mcp-server@latest agent --client claude-code
|
|
64
|
+
|
|
65
|
+
# Cursor
|
|
66
|
+
npx -y @orangepro/mcp-server@latest agent --client cursor
|
|
67
|
+
|
|
68
|
+
# OpenCode
|
|
69
|
+
npx -y @orangepro/mcp-server@latest agent --client opencode
|
|
70
|
+
|
|
71
|
+
# Generic MCP clients, including VS Code/Copilot-style MCP settings
|
|
72
|
+
npx -y @orangepro/mcp-server@latest agent --client generic
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Manual MCP config
|
|
45
76
|
|
|
46
77
|
Add to your client's MCP config:
|
|
47
78
|
|
|
@@ -57,11 +88,13 @@ Add to your client's MCP config:
|
|
|
57
88
|
```
|
|
58
89
|
|
|
59
90
|
| Client | Config location |
|
|
91
|
+
| --- | --- |
|
|
60
92
|
|--------|----------------|
|
|
61
93
|
| Claude Code | `.mcp.json` or `~/.claude.json` |
|
|
62
94
|
| Cursor | `~/.cursor/mcp.json` or Settings → MCP |
|
|
63
|
-
| Codex |
|
|
64
|
-
| VS Code / Copilot | MCP settings |
|
|
95
|
+
| Codex | Config printed by `opro agent --client codex` or `npx -y @orangepro/mcp-server@latest agent --client codex` |
|
|
96
|
+
| VS Code / Copilot | MCP settings; use the `generic` config if your client accepts raw MCP server JSON |
|
|
97
|
+
| OpenCode | Config printed by `opro agent --client opencode` |
|
|
65
98
|
|
|
66
99
|
### The workflow
|
|
67
100
|
|
|
@@ -766,6 +766,17 @@ function goAssertionCalls(node, assertLocals, dotAssertMethods, shadowed) {
|
|
|
766
766
|
return Boolean(fn?.type === "identifier" && !shadowed.has(fn.text) && dotAssertMethods.has(fn.text));
|
|
767
767
|
});
|
|
768
768
|
}
|
|
769
|
+
function goAssertionIdentifierArgs(assertion, testingParams) {
|
|
770
|
+
const sel = callFunctionSelector(assertion);
|
|
771
|
+
const name = sel?.name ?? assertion.childForFieldName("function")?.text;
|
|
772
|
+
const args = namedChildren(assertion.childForFieldName("arguments") ?? assertion).filter((n) => n.type !== "comment");
|
|
773
|
+
if (!name || args.length < 2)
|
|
774
|
+
return [];
|
|
775
|
+
if (!testingParams.has(args[0]?.text ?? ""))
|
|
776
|
+
return [];
|
|
777
|
+
const slots = name === "Equal" || name === "NotEqual" ? [args[1], args[2]] : [args[1]];
|
|
778
|
+
return slots.filter((n) => n?.type === "identifier").map((n) => n.text);
|
|
779
|
+
}
|
|
769
780
|
function goAssertionSubject(assertion, testingParams) {
|
|
770
781
|
const sel = callFunctionSelector(assertion);
|
|
771
782
|
const name = sel?.name ?? assertion.childForFieldName("function")?.text;
|
|
@@ -862,9 +873,36 @@ function extractGoProofCalls(root, imports) {
|
|
|
862
873
|
}
|
|
863
874
|
};
|
|
864
875
|
const processBlock = (block, testName, testingParams, shadowed) => {
|
|
876
|
+
// ONE-HOP block-local dataflow: a standalone `x, err := F(...)` statement is
|
|
877
|
+
// credited when a LATER statement in the SAME block checks a declared name
|
|
878
|
+
// (if-fail condition or assert subject). This covers the dominant real-Go
|
|
879
|
+
// idiom the if-initializer shape misses. Discipline kept: single product
|
|
880
|
+
// call per statement (goShortVarCalls), last write wins, plain reassignment
|
|
881
|
+
// invalidates, never crosses block boundaries. Metadata only — the dynamic
|
|
882
|
+
// oracle still re-verifies every edge before anything is Proven.
|
|
883
|
+
const pending = new Map();
|
|
884
|
+
// Deferred pending credits: witness lines are collected per call and flushed
|
|
885
|
+
// after the block — one witness keeps its exact line (subtest binding),
|
|
886
|
+
// several witnesses drop the line (the oracle's frame-line gate must never
|
|
887
|
+
// refuse a real kill firing at a sibling check).
|
|
888
|
+
const pendingHits = new Map();
|
|
889
|
+
const hitPending = (assertion, calls, line) => {
|
|
890
|
+
for (const c of calls) {
|
|
891
|
+
const key = `${assertion}|${c.qualifier ?? ""}|${c.callee}`;
|
|
892
|
+
const hit = pendingHits.get(key) ?? { assertion, calls: [c], lines: new Set() };
|
|
893
|
+
if (line !== undefined)
|
|
894
|
+
hit.lines.add(line);
|
|
895
|
+
pendingHits.set(key, hit);
|
|
896
|
+
}
|
|
897
|
+
};
|
|
865
898
|
for (const stmt of blockStatements(block)) {
|
|
866
899
|
for (const assertion of goAssertionCalls(stmt, assertLocals, dotAssertMethods, shadowed)) {
|
|
867
|
-
|
|
900
|
+
const subject = goAssertionSubject(assertion, testingParams);
|
|
901
|
+
add(testName, shadowed, "assert_helper", singleGoProductCallIn(subject), assertion.startPosition.row + 1);
|
|
902
|
+
for (const argName of goAssertionIdentifierArgs(assertion, testingParams)) {
|
|
903
|
+
if (pending.has(argName))
|
|
904
|
+
hitPending("assert_helper", pending.get(argName), assertion.startPosition.row + 1);
|
|
905
|
+
}
|
|
868
906
|
}
|
|
869
907
|
if (stmt.type === "if_statement" && hasGoTestingFailure(stmt.childForFieldName("consequence"), testingParams)) {
|
|
870
908
|
const condition = stmt.childForFieldName("condition");
|
|
@@ -876,6 +914,10 @@ function extractGoProofCalls(root, imports) {
|
|
|
876
914
|
const initCalls = init ? goShortVarCalls(init) : null;
|
|
877
915
|
if (initCalls && containsIdentifier(condition, initCalls.names))
|
|
878
916
|
add(testName, shadowed, "testing_fail", initCalls.calls, failLine);
|
|
917
|
+
for (const [name, calls] of pending) {
|
|
918
|
+
if (containsIdentifier(condition, new Set([name])))
|
|
919
|
+
hitPending("testing_fail", calls, failLine);
|
|
920
|
+
}
|
|
879
921
|
}
|
|
880
922
|
for (const child of namedChildren(stmt)) {
|
|
881
923
|
if (child.type === "block")
|
|
@@ -886,6 +928,23 @@ function extractGoProofCalls(root, imports) {
|
|
|
886
928
|
const subTestName = subtest.subName ? `${testName}/${subtest.subName}` : testName;
|
|
887
929
|
processBlock(subtest.body, subTestName, subtest.testingParams, shadowed);
|
|
888
930
|
}
|
|
931
|
+
// Record declarations AFTER uses: a declaration is never its own check.
|
|
932
|
+
const sv = stmt.type === "short_var_declaration" ? goShortVarCalls(stmt) : null;
|
|
933
|
+
if (sv) {
|
|
934
|
+
for (const name of sv.names)
|
|
935
|
+
pending.set(name, sv.calls);
|
|
936
|
+
}
|
|
937
|
+
else if (stmt.type === "assignment_statement") {
|
|
938
|
+
// Plain reassignment kills the binding — the checked value is no longer F's.
|
|
939
|
+
const reassigned = new Set();
|
|
940
|
+
collectNames(stmt.childForFieldName("left") ?? stmt.namedChild(0), reassigned);
|
|
941
|
+
for (const name of reassigned)
|
|
942
|
+
pending.delete(name);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
for (const hit of pendingHits.values()) {
|
|
946
|
+
const line = hit.lines.size === 1 ? [...hit.lines][0] : undefined;
|
|
947
|
+
add(testName, shadowed, hit.assertion, hit.calls, line);
|
|
889
948
|
}
|
|
890
949
|
};
|
|
891
950
|
const processSuiteBlock = (block, testName, receiver, shadowed) => {
|
package/dist/local/cli.js
CHANGED
|
@@ -106,7 +106,7 @@ Usage:
|
|
|
106
106
|
opro ai-links [--all] [--apply] [--provider openai|anthropic|ollama] [--model <name>] [--max-behaviors <n>] [--symbols-per-behavior <n>] [--max-prompt-tokens <n>] [--json]
|
|
107
107
|
# opt-in AI lane: stage weak candidate behavior↔code links in .orangepro/ai/links.json; --apply merges them into candidate_edges only
|
|
108
108
|
opro ai-flows [--apply] [--provider openai|anthropic|ollama] [--model <name>] [--json]
|
|
109
|
-
# opt-in AI lane: stage candidate behavior flows (closed anchor set) in .orangepro/
|
|
109
|
+
# opt-in AI lane: stage candidate behavior flows (closed anchor set) in .orangepro/flows.json; --apply stores them under analysis.candidate_flows only — a verify-these worklist, never evidence
|
|
110
110
|
opro generate [--target REQ-001] [--base <ref>] [--pr <n> [--yes]] [--changed] [--framework playwright] [--limit 3] [--prompt-version v2|v5] [--provider openai|anthropic|ollama|deterministic] [--model <name>] [--single [--raw]] [--background] [--json]
|
|
111
111
|
# default: A/B both arms (prompt-only vs Local KG, same model) scored side by side + writes a fresh report; --single generates one arm only
|
|
112
112
|
# --base <ref>: NON-MUTATING default for PR/branch review — generate only for the behaviors the diff vs <ref> touches (e.g. --base main); read-only \`git diff\`, no checkout
|
|
@@ -19,8 +19,20 @@ const MAX_PROMPT_ENTRIES = 60;
|
|
|
19
19
|
const MAX_PROMPT_SYMBOLS = 300;
|
|
20
20
|
const MAX_COMPLETION_TOKENS = 3000;
|
|
21
21
|
export function aiFlowsPath(root) {
|
|
22
|
+
return join(workspacePaths(root).dir, "flows.json");
|
|
23
|
+
}
|
|
24
|
+
function legacyAiFlowsPath(root) {
|
|
22
25
|
return join(workspacePaths(root).dir, "ai", "flows.json");
|
|
23
26
|
}
|
|
27
|
+
function readAiFlowsArtifact(root) {
|
|
28
|
+
const path = aiFlowsPath(root);
|
|
29
|
+
const current = readArtifact(path);
|
|
30
|
+
if (current.artifact || current.invalid)
|
|
31
|
+
return { ...current, path };
|
|
32
|
+
const legacy = legacyAiFlowsPath(root);
|
|
33
|
+
const fallback = readArtifact(legacy);
|
|
34
|
+
return { ...fallback, path: fallback.artifact || fallback.invalid ? legacy : path };
|
|
35
|
+
}
|
|
24
36
|
function buildAnchorContext(graph) {
|
|
25
37
|
const entries = dedupeEntries([
|
|
26
38
|
...endpointEntries(graph.nodes, graph.edges),
|
|
@@ -210,12 +222,12 @@ export async function generateAiFlows(root, graph, provider, clock) {
|
|
|
210
222
|
node_set_hash: nodeSetHash
|
|
211
223
|
}));
|
|
212
224
|
const path = aiFlowsPath(root);
|
|
213
|
-
const cached =
|
|
225
|
+
const cached = readAiFlowsArtifact(root);
|
|
214
226
|
if (cached.artifact?.cache_key === cacheKey) {
|
|
215
227
|
const hit = cached.artifact;
|
|
216
228
|
return {
|
|
217
229
|
mode: "generate",
|
|
218
|
-
ai_flows_path: path,
|
|
230
|
+
ai_flows_path: cached.path,
|
|
219
231
|
cache_hit: true,
|
|
220
232
|
model_provider: hit.model_provider,
|
|
221
233
|
model_name: hit.model_name,
|
|
@@ -306,8 +318,7 @@ export async function generateAiFlows(root, graph, provider, clock) {
|
|
|
306
318
|
* edges/candidate_edges/nodes/analysis.flows.
|
|
307
319
|
*/
|
|
308
320
|
export function applyAiFlows(root, graph) {
|
|
309
|
-
const path =
|
|
310
|
-
const { artifact, invalid } = readArtifact(path);
|
|
321
|
+
const { path, artifact, invalid } = readAiFlowsArtifact(root);
|
|
311
322
|
if (invalid) {
|
|
312
323
|
throw new Error(`AI flows artifact at ${path} is invalid or corrupted; re-run \`opro ai-flows\` to regenerate it.`);
|
|
313
324
|
}
|
|
@@ -413,7 +424,7 @@ function toCandidateFlow(flow, entriesById, knownEdges, provenance) {
|
|
|
413
424
|
...(flow.rationale ? { rationale: flow.rationale } : {}),
|
|
414
425
|
provenance: {
|
|
415
426
|
source_scope_id: `ai:${provenance.cache_key}`,
|
|
416
|
-
source_ref: ".orangepro/
|
|
427
|
+
source_ref: ".orangepro/flows.json",
|
|
417
428
|
detector: "ai_flows",
|
|
418
429
|
model_provider: provenance.model_provider,
|
|
419
430
|
model_name: provenance.model_name,
|
package/dist/local/mcp.js
CHANGED
|
@@ -407,7 +407,7 @@ export function createLocalServer() {
|
|
|
407
407
|
});
|
|
408
408
|
server.registerTool("orangepro_ai_flows", {
|
|
409
409
|
title: "Stage/apply AI candidate flows",
|
|
410
|
-
description: "Opt-in AI lane: propose candidate behavior-flow chains over existing deterministic entry and CodeSymbol ids. Generate writes .orangepro/
|
|
410
|
+
description: "Opt-in AI lane: propose candidate behavior-flow chains over existing deterministic entry and CodeSymbol ids. Generate writes .orangepro/flows.json only; apply=true stores survivors under analysis.candidate_flows. Candidate flows are a verify-these worklist and never affect Proven, deterministic flow counts, tiers, or coverage.",
|
|
411
411
|
inputSchema: {
|
|
412
412
|
...Workspace,
|
|
413
413
|
apply: z.boolean().optional().describe("Apply staged flows into analysis.candidate_flows. Default false stages flows only."),
|
|
@@ -375,7 +375,6 @@ function riskRows(risks, graph) {
|
|
|
375
375
|
firstRowForFile.set(r.file, r.id);
|
|
376
376
|
return risks.map((risk, idx) => {
|
|
377
377
|
const methodMatch = risk.title.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i);
|
|
378
|
-
const pathMatch = risk.file.match(/\/api\/(.+)$/);
|
|
379
378
|
const tags = [];
|
|
380
379
|
const bucket = riskBucket(risk.risk_score);
|
|
381
380
|
if (bucket)
|
|
@@ -394,8 +393,8 @@ function riskRows(risks, graph) {
|
|
|
394
393
|
applicableCategories: [...new Set(generatedTests.map((t) => t.concern).filter((c) => Boolean(c)))]
|
|
395
394
|
};
|
|
396
395
|
})(),
|
|
397
|
-
verb: methodMatch?.[1]?.toUpperCase() ??
|
|
398
|
-
path: methodMatch?.[2] ??
|
|
396
|
+
verb: methodMatch?.[1]?.toUpperCase() ?? "BEHAVIOR",
|
|
397
|
+
path: methodMatch?.[2] ?? risk.title,
|
|
399
398
|
desc: risk.reasons.join(" · "),
|
|
400
399
|
tags,
|
|
401
400
|
todo: "Write an integration or behavior test that calls this behavior and asserts the observable outcome."
|
|
@@ -470,8 +470,9 @@ if(cf&&cf.flows.length){
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
// risks + generated test samples
|
|
473
|
-
let activeRiskFilter="all";
|
|
474
473
|
const riskList=$("#risk-list"),riskTools=$("#risk-tools");
|
|
474
|
+
const generatedRiskCount=D.risks.filter(r=>r.generatedTests&&r.generatedTests.length).length;
|
|
475
|
+
let activeRiskFilter=generatedRiskCount?"generated":"all";
|
|
475
476
|
function riskMatchesFilter(r){
|
|
476
477
|
const hasGenerated=Boolean(r.generatedTests&&r.generatedTests.length);
|
|
477
478
|
if(activeRiskFilter==="generated")return hasGenerated;
|
|
@@ -481,7 +482,7 @@ function riskMatchesFilter(r){
|
|
|
481
482
|
function renderRiskFilters(){
|
|
482
483
|
const options=[
|
|
483
484
|
["all","All",D.risks.length],
|
|
484
|
-
["generated","
|
|
485
|
+
["generated","Flows with tests",generatedRiskCount],
|
|
485
486
|
["missing","No generated tests",D.risks.filter(r=>!(r.generatedTests&&r.generatedTests.length)).length]
|
|
486
487
|
];
|
|
487
488
|
riskTools.innerHTML=options.map(([key,label,count])=>\`<button class="risk-filter" type="button" data-risk-filter="\${key}" aria-pressed="\${key===activeRiskFilter}">\${label} <span class="gc">\${count}</span></button>\`).join("");
|
|
@@ -519,16 +520,16 @@ function renderRisks(){
|
|
|
519
520
|
riskList.innerHTML="";
|
|
520
521
|
D.risks.filter(riskMatchesFilter).forEach(r=>riskList.append(el("div","risk-card",riskCardHtml(r))));
|
|
521
522
|
if(activeRiskFilter==="all"&&D.generatedTotal){
|
|
522
|
-
const
|
|
523
|
-
const remainingRiskFlows=Math.max(0,D.risks.length-
|
|
523
|
+
const hiddenGeneratedFlows=Math.max(0,generatedRiskCount-D.risks.filter(riskMatchesFilter).length);
|
|
524
|
+
const remainingRiskFlows=Math.max(0,D.risks.length-generatedRiskCount);
|
|
524
525
|
riskList.append(el("div","paywall",
|
|
525
|
-
|
|
526
|
-
? \`<div class="paywall-num">\${
|
|
527
|
-
<div class="paywall-txt">OrangePro
|
|
526
|
+
hiddenGeneratedFlows
|
|
527
|
+
? \`<div class="paywall-num">\${hiddenGeneratedFlows} more flows with tests</div>
|
|
528
|
+
<div class="paywall-txt">OrangePro accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Use the “Flows with tests” filter to review them first.</div>
|
|
528
529
|
<a class="paywall-btn" href="https://app.orangepro.ai" target="_blank">View all on OrangePro Platform →</a>\`
|
|
529
530
|
: remainingRiskFlows
|
|
530
531
|
? \`<div class="paywall-num">\${remainingRiskFlows} high-risk flows left</div>
|
|
531
|
-
<div class="paywall-txt">The local MCP accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"}
|
|
532
|
+
<div class="paywall-txt">The local MCP accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Generate the remaining high-risk flow tests on OrangePro Platform.</div>
|
|
532
533
|
<a class="paywall-btn" href="https://app.orangepro.ai" target="_blank">Generate remaining tests on Platform →</a>\`
|
|
533
534
|
: \`<div class="paywall-num">All generated tests are shown</div>
|
|
534
535
|
<div class="paywall-txt">OrangePro generated tests for every high-risk flow in this report, and every generated test is visible here.</div>\`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OrangePro (`opro`) — a local-first, BYOK CLI + MCP server that builds an evidence graph from a local checkout, ingests runtime coverage, and generates grounded tests. Metadata-only exports; no source upload; generated tests stay local.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -85,7 +85,11 @@
|
|
|
85
85
|
"test:integration:go:auto:slow": "OPRO_SLOW_GO_AUTO=1 vitest run tests/local/autoProveGo.test.ts --pool=forks --poolOptions.forks.maxForks=1",
|
|
86
86
|
"test:integration:java:1": "vitest run tests/local/javaProveIntegration.test.ts -t \"^(?!.*(no-false-Proven|mints the same Proven)).*\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
87
87
|
"test:integration:java:2": "vitest run tests/local/javaProveIntegration.test.ts -t \"no-false-Proven|mints the same Proven\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
88
|
-
"test:integration:java:3": "
|
|
88
|
+
"test:integration:java:3": "npm run test:integration:java:3:unit && npm run test:integration:java:3:prove && npm run test:integration:java:3:survivor && npm run test:integration:java:3:refused",
|
|
89
|
+
"test:integration:java:3:unit": "vitest run tests/local/autoProveJava.test.ts -t \"javaTestForTarget|isEligibleProvableTarget\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
90
|
+
"test:integration:java:3:prove": "vitest run tests/local/autoProveJava.test.ts -t \"mints DP\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
91
|
+
"test:integration:java:3:survivor": "vitest run tests/local/autoProveJava.test.ts -t \"equivalent survivor\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
92
|
+
"test:integration:java:3:refused": "vitest run tests/local/autoProveJava.test.ts -t \"refused-shape\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
89
93
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
90
94
|
"smoke:gap-fill": "npm run build && node scripts/smoke-gap-fill-loop.mjs",
|
|
91
95
|
"smoke:generate-prove": "npm run build && node scripts/smoke-generate-prove-e2e.mjs",
|
|
@@ -35,6 +35,7 @@ import (
|
|
|
35
35
|
"go/printer"
|
|
36
36
|
"go/token"
|
|
37
37
|
"os"
|
|
38
|
+
"strings"
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
// fail prints a stable, machine-readable marker plus a human message and exits
|
|
@@ -104,6 +105,15 @@ func main() {
|
|
|
104
105
|
|
|
105
106
|
if *mode == "sentinel" {
|
|
106
107
|
target.Body = sentinelBody(results)
|
|
108
|
+
// The sentinel body can orphan imports the original body used (Go
|
|
109
|
+
// rejects unused imports, so the mutant would fail to BUILD and the
|
|
110
|
+
// oracle would refuse — an honest but useless verdict for most real
|
|
111
|
+
// functions). Rewrite now-unused imports to blank imports: package
|
|
112
|
+
// init side effects are preserved, no behavior is added, and the
|
|
113
|
+
// repair is fail-safe in both directions — over-blanking a used
|
|
114
|
+
// import still fails the build (refusal, never proof), and a missed
|
|
115
|
+
// unused import is exactly today's behavior.
|
|
116
|
+
blankUnusedImports(astFile)
|
|
107
117
|
}
|
|
108
118
|
// equivalent mode: leave target.Body untouched (semantically identical).
|
|
109
119
|
|
|
@@ -117,6 +127,39 @@ func main() {
|
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
|
|
130
|
+
// blankUnusedImports renames imports whose package qualifier is no longer
|
|
131
|
+
// referenced anywhere in the file to blank imports (`_ "path"`). Dot imports
|
|
132
|
+
// and existing blank imports are left untouched. Qualifier detection is
|
|
133
|
+
// syntactic (selector bases), which over-approximates "used" — the safe
|
|
134
|
+
// direction: we only ever blank an import nothing references.
|
|
135
|
+
func blankUnusedImports(f *ast.File) {
|
|
136
|
+
used := map[string]bool{}
|
|
137
|
+
ast.Inspect(f, func(n ast.Node) bool {
|
|
138
|
+
if sel, ok := n.(*ast.SelectorExpr); ok {
|
|
139
|
+
if id, ok := sel.X.(*ast.Ident); ok {
|
|
140
|
+
used[id.Name] = true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return true
|
|
144
|
+
})
|
|
145
|
+
for _, imp := range f.Imports {
|
|
146
|
+
if imp.Name != nil {
|
|
147
|
+
if imp.Name.Name == "_" || imp.Name.Name == "." {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if !used[imp.Name.Name] {
|
|
151
|
+
imp.Name = ast.NewIdent("_")
|
|
152
|
+
}
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
path := strings.Trim(imp.Path.Value, "\"")
|
|
156
|
+
base := path[strings.LastIndex(path, "/")+1:]
|
|
157
|
+
if !used[base] {
|
|
158
|
+
imp.Name = ast.NewIdent("_")
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
120
163
|
// sentinelBody builds `{ return <zero>, <zero>, ... }` matching the function's
|
|
121
164
|
// result signature so the mutant COMPILES. Zero values are type-derived and
|
|
122
165
|
// deliberately wrong for any function whose real return is non-zero; when a
|