@nudojs/core 1.13.0 → 1.14.1
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/dist/{chunk-V2XC3V72.js → chunk-4GZPH6YL.js} +436 -2
- package/dist/{chunk-A2PLG2QV.js → chunk-JMOQEJTD.js} +15607 -15097
- package/dist/exec.d.ts +206 -57
- package/dist/exec.js +1 -1
- package/dist/index.d.ts +142 -9
- package/dist/index.js +96 -373
- package/dist/internal.d.ts +21 -5
- package/dist/internal.js +6 -2
- package/dist/{projection-budget-DPPRBOLp.d.ts → projection-budget-07VnMktV.d.ts} +44 -2
- package/dist/{run-DhSqP3in.d.ts → run-DfloWX6i.d.ts} +9 -0
- package/package.json +1 -1
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
withEvalBudgetOverride,
|
|
48
48
|
withExecPhi,
|
|
49
49
|
withFnBudgetOverride
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-JMOQEJTD.js";
|
|
51
51
|
|
|
52
52
|
// src/algebra/directive-scan.ts
|
|
53
53
|
function cleanDirectiveLine(raw, kind) {
|
|
@@ -622,6 +622,14 @@ function scanAssumeSegments(lines) {
|
|
|
622
622
|
}
|
|
623
623
|
return out;
|
|
624
624
|
}
|
|
625
|
+
function scanInvariantSegments(lines) {
|
|
626
|
+
const out = [];
|
|
627
|
+
for (const line of lines) {
|
|
628
|
+
const m = line.match(/^@nudo:invariant\s+(.+)$/);
|
|
629
|
+
if (m) out.push(m[1].trim().replace(/\*\/$/, "").trim());
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
625
633
|
function scanThrowsDecl(lines) {
|
|
626
634
|
const kinds = /* @__PURE__ */ new Set();
|
|
627
635
|
let any = false;
|
|
@@ -1516,6 +1524,35 @@ function extractAssumeSegs(source, fnName) {
|
|
|
1516
1524
|
}
|
|
1517
1525
|
return out;
|
|
1518
1526
|
}
|
|
1527
|
+
var INVARIANT_SEG_REGEX = /^([A-Za-z_$][\w$]*)((?:\[\*\]|\.[A-Za-z_$][\w$]*)*)\s*(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)$/;
|
|
1528
|
+
function extractInvariantSegs(source, fnName) {
|
|
1529
|
+
if (!stripStringsKeepComments(source).includes("@nudo:invariant")) return [];
|
|
1530
|
+
const out = [];
|
|
1531
|
+
for (const seg of scanInvariantSegments(fnDirectiveCommentLines(source, fnName))) {
|
|
1532
|
+
const m = seg.match(INVARIANT_SEG_REGEX);
|
|
1533
|
+
if (!m) {
|
|
1534
|
+
out.push({ kind: "bad", raw: seg });
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
const pathStr = m[2] ?? "";
|
|
1538
|
+
const path = [];
|
|
1539
|
+
for (const pm of pathStr.matchAll(/\[\*\]|\.[A-Za-z_$][\w$]*/g)) {
|
|
1540
|
+
const t = pm[0];
|
|
1541
|
+
if (t === "[*]") path.push({ k: "idx" });
|
|
1542
|
+
else path.push({ k: "prop", name: t.slice(1) });
|
|
1543
|
+
}
|
|
1544
|
+
const relTxt = m[3];
|
|
1545
|
+
out.push({
|
|
1546
|
+
kind: "ok",
|
|
1547
|
+
name: m[1],
|
|
1548
|
+
path,
|
|
1549
|
+
rel: relTxt === ">=" ? "ge" : relTxt === "<=" ? "le" : relTxt === ">" ? "gt" : "lt",
|
|
1550
|
+
value: Number(m[4]),
|
|
1551
|
+
display: `${m[1]}${pathStr} ${relTxt} ${m[4]}`
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
return out;
|
|
1555
|
+
}
|
|
1519
1556
|
function fnTopLevelLocalNames(body) {
|
|
1520
1557
|
const block = body;
|
|
1521
1558
|
if (!block || block.type !== "BlockStatement" || !Array.isArray(block.body)) return [];
|
|
@@ -5533,6 +5570,395 @@ function scanLiteralCalls(source, knownFns, phi, opts) {
|
|
|
5533
5570
|
return out;
|
|
5534
5571
|
}
|
|
5535
5572
|
|
|
5573
|
+
// src/algebra/check-report.ts
|
|
5574
|
+
function actionsForIssue(i) {
|
|
5575
|
+
const draft = {
|
|
5576
|
+
kind: "draft",
|
|
5577
|
+
command: "nudo contract --draft",
|
|
5578
|
+
label: "emit a sidecar draft you can edit"
|
|
5579
|
+
};
|
|
5580
|
+
switch (i.code) {
|
|
5581
|
+
case "nudo:constraint-violated":
|
|
5582
|
+
return [
|
|
5583
|
+
{
|
|
5584
|
+
kind: "callsite",
|
|
5585
|
+
label: "use a value satisfying the constraint",
|
|
5586
|
+
...i.expected ? { hint: i.expected } : {}
|
|
5587
|
+
},
|
|
5588
|
+
{
|
|
5589
|
+
kind: "relax",
|
|
5590
|
+
label: "relax the precondition (edit *.nudo.js / @nudo:contract)",
|
|
5591
|
+
...i.expected ? { hint: i.expected } : {}
|
|
5592
|
+
},
|
|
5593
|
+
draft
|
|
5594
|
+
];
|
|
5595
|
+
case "nudo:assign-mismatch":
|
|
5596
|
+
return [
|
|
5597
|
+
{
|
|
5598
|
+
kind: "callsite",
|
|
5599
|
+
label: "align the assigned value with the existing shape",
|
|
5600
|
+
...i.expected ? { hint: i.expected } : {}
|
|
5601
|
+
},
|
|
5602
|
+
draft
|
|
5603
|
+
];
|
|
5604
|
+
case "nudo:entry-may-throw":
|
|
5605
|
+
return [
|
|
5606
|
+
{
|
|
5607
|
+
kind: "draft",
|
|
5608
|
+
command: "nudo contract --draft",
|
|
5609
|
+
label: "narrow the entry with a sidecar fn({ \u2026 }) param contract \u2014 most honest fix for helpers (also tightens the signature)",
|
|
5610
|
+
...i.fn ? { hint: i.fn } : {}
|
|
5611
|
+
},
|
|
5612
|
+
{
|
|
5613
|
+
kind: "relax",
|
|
5614
|
+
label: "declare intentional fail-fast: @nudo:throws Error (or case `!! throws`)",
|
|
5615
|
+
...i.fn ? { hint: i.fn } : {}
|
|
5616
|
+
},
|
|
5617
|
+
{
|
|
5618
|
+
kind: "info",
|
|
5619
|
+
label: "or refine / guard / try-catch the entry so it is total"
|
|
5620
|
+
},
|
|
5621
|
+
{
|
|
5622
|
+
kind: "ignore-throws",
|
|
5623
|
+
command: "nudo check --ignore-throws TypeError",
|
|
5624
|
+
label: "migration switch: ignore this throw class on L2"
|
|
5625
|
+
}
|
|
5626
|
+
];
|
|
5627
|
+
case "nudo:unproven-return":
|
|
5628
|
+
return [
|
|
5629
|
+
{
|
|
5630
|
+
kind: "relax",
|
|
5631
|
+
label: `relax the return contract${i.fn ? ` of ${i.fn}` : ""} to the inferred surface (review the diff \u2014 not a silent green)`,
|
|
5632
|
+
...i.expected ? { hint: i.expected } : {}
|
|
5633
|
+
},
|
|
5634
|
+
draft,
|
|
5635
|
+
{
|
|
5636
|
+
kind: "info",
|
|
5637
|
+
label: "or narrow the return expression / add a precondition so the bound is implied"
|
|
5638
|
+
}
|
|
5639
|
+
];
|
|
5640
|
+
case "nudo:unknown-inference":
|
|
5641
|
+
case "nudo:opaque-result":
|
|
5642
|
+
return [
|
|
5643
|
+
{ kind: "mock", label: "pin the native/unknown face with @nudo:mock" },
|
|
5644
|
+
{ kind: "assume", command: "nudo check --assume", label: "state preconditions via --assume" },
|
|
5645
|
+
{ kind: "info", label: "or add a call site / @nudo:case for evidence" }
|
|
5646
|
+
];
|
|
5647
|
+
case "nudo:arg-opaque":
|
|
5648
|
+
case "nudo:constraint-unproven":
|
|
5649
|
+
return [
|
|
5650
|
+
{ kind: "assume", command: "nudo check --assume", label: "state preconditions via --assume" },
|
|
5651
|
+
{ kind: "info", label: "add a call site or @nudo:case" }
|
|
5652
|
+
];
|
|
5653
|
+
case "nudo:fork-truncated":
|
|
5654
|
+
case "nudo:promise-micro-truncated":
|
|
5655
|
+
case "nudo:promise-micro-error":
|
|
5656
|
+
return [
|
|
5657
|
+
{
|
|
5658
|
+
kind: "info",
|
|
5659
|
+
label: "results widened to unknown \u2014 raise budget (@nudo:budget forks/calls/depth on the function) or narrow control flow"
|
|
5660
|
+
}
|
|
5661
|
+
];
|
|
5662
|
+
case "nudo:loop-truncated":
|
|
5663
|
+
return [
|
|
5664
|
+
{
|
|
5665
|
+
kind: "info",
|
|
5666
|
+
label: "loop state widened \u2014 the exit condition stayed concrete-true past the iteration cap; make the bound data-dependent (abstract) or pin the result with @nudo:case / @nudo:contract"
|
|
5667
|
+
}
|
|
5668
|
+
];
|
|
5669
|
+
case "nudo:recursion-truncated":
|
|
5670
|
+
return [
|
|
5671
|
+
{
|
|
5672
|
+
kind: "info",
|
|
5673
|
+
label: i.reason === "cycle" ? "call cycle: same abstract arguments already on the stack \u2014 raising @nudo:budget cannot lift it; make the arguments vary per recursion level or declare @nudo:contract" : i.reason === "depth" || i.reason === "calls" || i.reason === "budget" ? `results widened to unknown \u2014 raise budget (@nudo:budget ${i.reason === "depth" ? "depth" : i.reason === "calls" ? "calls" : "calls/depth"} on the function) or narrow the recursion` : "results widened to unknown \u2014 raise budget (@nudo:budget calls/depth on the function) or narrow control flow"
|
|
5674
|
+
}
|
|
5675
|
+
];
|
|
5676
|
+
case "nudo:host-effect-blocked":
|
|
5677
|
+
return [
|
|
5678
|
+
{ kind: "mock", label: "pin the host API with @nudo:mock / @nudo:env" },
|
|
5679
|
+
{ kind: "info", label: "results widened to unknown \u2014 or feed the value in from a call site" }
|
|
5680
|
+
];
|
|
5681
|
+
case "nudo:interface-drift":
|
|
5682
|
+
return [
|
|
5683
|
+
{
|
|
5684
|
+
kind: "emit",
|
|
5685
|
+
command: "nudo contract --emit",
|
|
5686
|
+
label: "refresh the @generated segment"
|
|
5687
|
+
}
|
|
5688
|
+
];
|
|
5689
|
+
case "nudo:dual-entry":
|
|
5690
|
+
return [
|
|
5691
|
+
{
|
|
5692
|
+
kind: "info",
|
|
5693
|
+
label: "browser/node records do not cross files \u2014 analyze the entry you ship; mock or skip the other variant"
|
|
5694
|
+
}
|
|
5695
|
+
];
|
|
5696
|
+
default:
|
|
5697
|
+
return i.suggestion ? [{ kind: "info", label: i.suggestion }] : [];
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
function serializeCheckJson(r) {
|
|
5701
|
+
return {
|
|
5702
|
+
version: 1,
|
|
5703
|
+
file: r.file,
|
|
5704
|
+
ok: r.ok,
|
|
5705
|
+
summary: { ...r.summary },
|
|
5706
|
+
signatures: r.signatures.map((s) => ({
|
|
5707
|
+
name: s.name,
|
|
5708
|
+
params: [...s.params],
|
|
5709
|
+
...s.paramTypes ? { paramTypes: [...s.paramTypes] } : {},
|
|
5710
|
+
display: s.display,
|
|
5711
|
+
detail: s.detail,
|
|
5712
|
+
conf: s.conf,
|
|
5713
|
+
abs: formatAbs(s.abs),
|
|
5714
|
+
...s.throws ? { throws: s.throws } : {},
|
|
5715
|
+
...s.entry ? { entry: true } : {}
|
|
5716
|
+
})),
|
|
5717
|
+
issues: r.issues.map((i) => {
|
|
5718
|
+
const acts = i.actions ?? actionsForIssue(i);
|
|
5719
|
+
return {
|
|
5720
|
+
severity: i.severity,
|
|
5721
|
+
code: i.code,
|
|
5722
|
+
message: i.message,
|
|
5723
|
+
...i.fn !== void 0 ? { fn: i.fn } : {},
|
|
5724
|
+
...i.line !== void 0 ? { line: i.line } : {},
|
|
5725
|
+
...i.column !== void 0 ? { column: i.column } : {},
|
|
5726
|
+
...i.actual !== void 0 ? { actual: i.actual } : {},
|
|
5727
|
+
...i.expected !== void 0 ? { expected: i.expected } : {},
|
|
5728
|
+
...i.suggestion !== void 0 ? { suggestion: i.suggestion } : {},
|
|
5729
|
+
...i.reason !== void 0 ? { reason: i.reason } : {},
|
|
5730
|
+
...acts.length > 0 ? { actions: acts } : {}
|
|
5731
|
+
};
|
|
5732
|
+
}),
|
|
5733
|
+
...r.budget ? { budget: { ...r.budget } } : {}
|
|
5734
|
+
};
|
|
5735
|
+
}
|
|
5736
|
+
function serializeCheckJsonMulti(reports) {
|
|
5737
|
+
const summary = {
|
|
5738
|
+
errors: 0,
|
|
5739
|
+
warnings: 0,
|
|
5740
|
+
infos: 0,
|
|
5741
|
+
functions: 0,
|
|
5742
|
+
files: reports.length
|
|
5743
|
+
};
|
|
5744
|
+
let calls = 0;
|
|
5745
|
+
let forks = 0;
|
|
5746
|
+
let maxCalls = 0;
|
|
5747
|
+
let maxForks = 0;
|
|
5748
|
+
let callTruncated = false;
|
|
5749
|
+
let forkTruncated = false;
|
|
5750
|
+
let evalCalls = 0;
|
|
5751
|
+
let maxEvalCalls = 0;
|
|
5752
|
+
let evalCallTruncated = false;
|
|
5753
|
+
let cycleTruncated = false;
|
|
5754
|
+
let loopTruncated = false;
|
|
5755
|
+
const callReasons = /* @__PURE__ */ new Set();
|
|
5756
|
+
const evalReasons = /* @__PURE__ */ new Set();
|
|
5757
|
+
let depth = 0;
|
|
5758
|
+
let maxDepth = 0;
|
|
5759
|
+
let evalDepth = 0;
|
|
5760
|
+
let maxEvalDepth = 0;
|
|
5761
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
5762
|
+
for (const r of reports) {
|
|
5763
|
+
summary.errors += r.summary.errors;
|
|
5764
|
+
summary.warnings += r.summary.warnings;
|
|
5765
|
+
summary.infos += r.summary.infos;
|
|
5766
|
+
summary.functions += r.summary.functions;
|
|
5767
|
+
if (r.budget) {
|
|
5768
|
+
calls += r.budget.calls;
|
|
5769
|
+
forks += r.budget.forks;
|
|
5770
|
+
maxCalls = Math.max(maxCalls, r.budget.maxCalls);
|
|
5771
|
+
maxForks = Math.max(maxForks, r.budget.maxForks);
|
|
5772
|
+
callTruncated = callTruncated || r.budget.callTruncated;
|
|
5773
|
+
forkTruncated = forkTruncated || r.budget.forkTruncated;
|
|
5774
|
+
evalCalls += r.budget.evalCalls ?? 0;
|
|
5775
|
+
maxEvalCalls = Math.max(maxEvalCalls, r.budget.maxEvalCalls ?? 0);
|
|
5776
|
+
evalCallTruncated = evalCallTruncated || (r.budget.evalCallTruncated ?? false);
|
|
5777
|
+
cycleTruncated = cycleTruncated || (r.budget.cycleTruncated ?? false);
|
|
5778
|
+
loopTruncated = loopTruncated || (r.budget.loopTruncated ?? false);
|
|
5779
|
+
for (const x of r.budget.callReasons ?? []) callReasons.add(x);
|
|
5780
|
+
for (const x of r.budget.evalReasons ?? []) evalReasons.add(x);
|
|
5781
|
+
depth = Math.max(depth, r.budget.depth ?? 0);
|
|
5782
|
+
maxDepth = Math.max(maxDepth, r.budget.maxDepth ?? 0);
|
|
5783
|
+
evalDepth = Math.max(evalDepth, r.budget.evalDepth ?? 0);
|
|
5784
|
+
maxEvalDepth = Math.max(maxEvalDepth, r.budget.maxEvalDepth ?? 0);
|
|
5785
|
+
for (const x of r.budget.reasons ?? []) reasons.add(x);
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
const budgetTruncated = callTruncated || evalCallTruncated || forkTruncated || loopTruncated;
|
|
5789
|
+
return {
|
|
5790
|
+
version: 1,
|
|
5791
|
+
kind: "multi",
|
|
5792
|
+
ok: reports.every((r) => r.ok),
|
|
5793
|
+
summary: {
|
|
5794
|
+
...summary,
|
|
5795
|
+
...budgetTruncated ? { budgetTruncated: true } : {}
|
|
5796
|
+
},
|
|
5797
|
+
...budgetTruncated || reports.some((r) => r.budget) ? {
|
|
5798
|
+
budget: {
|
|
5799
|
+
truncated: budgetTruncated,
|
|
5800
|
+
callTruncated,
|
|
5801
|
+
forkTruncated,
|
|
5802
|
+
calls,
|
|
5803
|
+
maxCalls,
|
|
5804
|
+
forks,
|
|
5805
|
+
maxForks,
|
|
5806
|
+
evalCalls,
|
|
5807
|
+
maxEvalCalls,
|
|
5808
|
+
evalCallTruncated,
|
|
5809
|
+
cycleTruncated,
|
|
5810
|
+
loopTruncated,
|
|
5811
|
+
callReasons: [...callReasons].sort(),
|
|
5812
|
+
depth,
|
|
5813
|
+
maxDepth,
|
|
5814
|
+
evalReasons: [...evalReasons].sort(),
|
|
5815
|
+
evalDepth,
|
|
5816
|
+
maxEvalDepth,
|
|
5817
|
+
reasons: [...reasons].sort()
|
|
5818
|
+
}
|
|
5819
|
+
} : {},
|
|
5820
|
+
reports
|
|
5821
|
+
};
|
|
5822
|
+
}
|
|
5823
|
+
var CONTRACT_FIX_CODES = /* @__PURE__ */ new Set([
|
|
5824
|
+
"nudo:constraint-violated",
|
|
5825
|
+
"nudo:assign-mismatch",
|
|
5826
|
+
"nudo:arg-structure",
|
|
5827
|
+
"nudo:interface-domain-exceeds",
|
|
5828
|
+
"nudo:entry-may-throw",
|
|
5829
|
+
"nudo:missing-slot"
|
|
5830
|
+
]);
|
|
5831
|
+
function budgetAttributionSegs(b) {
|
|
5832
|
+
const segs = [];
|
|
5833
|
+
if ((b.callReasons ?? []).includes("calls")) segs.push(`calls ${b.calls}/${b.maxCalls}`);
|
|
5834
|
+
if ((b.callReasons ?? []).includes("depth")) segs.push(`call depth ${b.depth}/${b.maxDepth}`);
|
|
5835
|
+
if ((b.evalReasons ?? []).includes("calls")) {
|
|
5836
|
+
segs.push(`eval calls ${b.evalCalls}/${b.maxEvalCalls}`);
|
|
5837
|
+
}
|
|
5838
|
+
if ((b.evalReasons ?? []).includes("depth")) {
|
|
5839
|
+
segs.push(`eval depth ${b.evalDepth}/${b.maxEvalDepth}`);
|
|
5840
|
+
}
|
|
5841
|
+
if (b.forkTruncated) segs.push(`forks ${b.forks}/${b.maxForks}`);
|
|
5842
|
+
if (b.loopTruncated) segs.push("loop iterations");
|
|
5843
|
+
const cycleOnly = segs.length === 0;
|
|
5844
|
+
if (cycleOnly) segs.push("call cycle \u2014 not a budget limit");
|
|
5845
|
+
return { segs, cycleOnly };
|
|
5846
|
+
}
|
|
5847
|
+
function formatCheckReport(r, opts = {}) {
|
|
5848
|
+
const lines = [];
|
|
5849
|
+
lines.push(`nudo check ${r.file}`);
|
|
5850
|
+
lines.push(r.ok ? "OK" : "FAILED");
|
|
5851
|
+
lines.push(
|
|
5852
|
+
` ${r.summary.errors} error \xB7 ${r.summary.warnings} warning \xB7 ${r.summary.infos} info \xB7 ${r.summary.functions} fn`
|
|
5853
|
+
);
|
|
5854
|
+
if (r.budget?.truncated) {
|
|
5855
|
+
const b = r.budget;
|
|
5856
|
+
const onlyLoop = b.loopTruncated && !b.callTruncated && !b.evalCallTruncated && !b.forkTruncated;
|
|
5857
|
+
const { segs } = budgetAttributionSegs(b);
|
|
5858
|
+
lines.push("");
|
|
5859
|
+
lines.push("budget");
|
|
5860
|
+
lines.push(` truncated ${segs.join(" \xB7 ")}`);
|
|
5861
|
+
lines.push(
|
|
5862
|
+
r.budget.cycleTruncated ? ` \u2192 some results widened to unknown (not a contract failure) \u2014 includes call cycles (same abstract args; a budget cannot lift them)` : onlyLoop ? ` \u2192 loop state widened (iteration budget, not a contract failure) \u2014 @nudo:budget does not apply; make the bound data-dependent or pin with @nudo:case / @nudo:contract` : ` \u2192 some results widened to unknown (not a contract failure) \u2014 raise @nudo:budget / nudo.analysis.maxForks or simplify recursion`
|
|
5863
|
+
);
|
|
5864
|
+
}
|
|
5865
|
+
if (r.signatures.length > 0) {
|
|
5866
|
+
lines.push("");
|
|
5867
|
+
lines.push("signatures");
|
|
5868
|
+
for (const s of r.signatures) {
|
|
5869
|
+
const paramStr = s.paramTypes && s.paramTypes.length > 0 ? s.params.map((p, i) => `${p}: ${s.paramTypes[i] ?? "any"}`).join(", ") : s.params.join(", ");
|
|
5870
|
+
const throwsStr = s.throws ? ` throws ${s.throws}` : "";
|
|
5871
|
+
const displayBase = s.display.replace(/\s+throws\s+[\s\S]+$/, "").trim();
|
|
5872
|
+
const shapeK = s.abs?.shape?.k;
|
|
5873
|
+
const looksLikeFakeAny = shapeK === "any" && displayBase.length > 0 && !/^any(\b|$)/.test(displayBase);
|
|
5874
|
+
const retStr = !looksLikeFakeAny && shapeK ? formatShape(s.abs) || displayBase.replace(/\s+#\w+$/, "").trim() : displayBase.replace(/\s+#\w+$/, "").trim();
|
|
5875
|
+
lines.push(` ${s.name}(${paramStr}) => ${retStr}${throwsStr}`);
|
|
5876
|
+
if (opts.verbose) {
|
|
5877
|
+
for (const ln of s.detail.split("\n").slice(1)) {
|
|
5878
|
+
lines.push(` ${ln}`);
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
if (r.issues.length === 0) {
|
|
5884
|
+
lines.push("");
|
|
5885
|
+
lines.push("(no issues)");
|
|
5886
|
+
} else {
|
|
5887
|
+
lines.push("");
|
|
5888
|
+
lines.push("issues");
|
|
5889
|
+
for (const i of r.issues) {
|
|
5890
|
+
const loc = i.line != null ? `L${i.line}` : "";
|
|
5891
|
+
const head = [i.severity.toUpperCase(), loc, i.fn].filter(Boolean).join(" ");
|
|
5892
|
+
lines.push(` [${head}] ${i.message} (${i.code})`);
|
|
5893
|
+
const showDetail = i.severity === "error" || opts.verbose;
|
|
5894
|
+
if (showDetail && i.actual) lines.push(` actual: ${i.actual}`);
|
|
5895
|
+
if (showDetail && i.expected) lines.push(` expected: ${i.expected}`);
|
|
5896
|
+
if (i.suggestion) {
|
|
5897
|
+
lines.push(` \u2192 ${i.suggestion}`);
|
|
5898
|
+
} else if (CONTRACT_FIX_CODES.has(i.code)) {
|
|
5899
|
+
lines.push(` \u2192 add a refine / sidecar contract, or fix the call-site value`);
|
|
5900
|
+
}
|
|
5901
|
+
if (CONTRACT_FIX_CODES.has(i.code)) {
|
|
5902
|
+
lines.push(` fix: nudo contract --draft (emit a sidecar draft you can edit)`);
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
return lines.join("\n");
|
|
5907
|
+
}
|
|
5908
|
+
function escapeGhaData(s) {
|
|
5909
|
+
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
5910
|
+
}
|
|
5911
|
+
function escapeGhaProp(s) {
|
|
5912
|
+
return escapeGhaData(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
5913
|
+
}
|
|
5914
|
+
function formatGithubAnnotations(r, opts = {}) {
|
|
5915
|
+
const lines = [];
|
|
5916
|
+
let file = r.file;
|
|
5917
|
+
if (opts.workspaceRoot && file.startsWith(opts.workspaceRoot)) {
|
|
5918
|
+
file = file.slice(opts.workspaceRoot.length).replace(/^[/\\]/, "");
|
|
5919
|
+
}
|
|
5920
|
+
for (const i of r.issues) {
|
|
5921
|
+
const level = i.severity === "error" ? "error" : i.severity === "warning" ? "warning" : "notice";
|
|
5922
|
+
const line = i.line ?? 1;
|
|
5923
|
+
const col = (i.column ?? 0) + 1;
|
|
5924
|
+
const title = escapeGhaProp(i.code);
|
|
5925
|
+
const msgBits = [i.message];
|
|
5926
|
+
if (i.actual && i.expected) msgBits.push(`${i.actual} \u22AD ${i.expected}`);
|
|
5927
|
+
else if (i.suggestion) msgBits.push(i.suggestion);
|
|
5928
|
+
if (CONTRACT_FIX_CODES.has(i.code)) {
|
|
5929
|
+
msgBits.push(`fix: nudo contract --draft`);
|
|
5930
|
+
}
|
|
5931
|
+
const msg = escapeGhaData(msgBits.join(" \xB7 "));
|
|
5932
|
+
const props = [
|
|
5933
|
+
`file=${escapeGhaProp(file)}`,
|
|
5934
|
+
`line=${line}`,
|
|
5935
|
+
`col=${col}`,
|
|
5936
|
+
`title=${title}`
|
|
5937
|
+
].join(",");
|
|
5938
|
+
lines.push(`::${level} ${props}::${msg}`);
|
|
5939
|
+
}
|
|
5940
|
+
return lines.join("\n");
|
|
5941
|
+
}
|
|
5942
|
+
function formatGitlabCodeQuality(r, opts = {}) {
|
|
5943
|
+
let file = r.file;
|
|
5944
|
+
if (opts.workspaceRoot && file.startsWith(opts.workspaceRoot)) {
|
|
5945
|
+
file = file.slice(opts.workspaceRoot.length).replace(/^[/\\]/, "");
|
|
5946
|
+
}
|
|
5947
|
+
return r.issues.map((i) => {
|
|
5948
|
+
const begin = i.line ?? 1;
|
|
5949
|
+
const severity = i.severity === "error" ? "major" : i.severity === "warning" ? "minor" : "info";
|
|
5950
|
+
const description = i.actual && i.expected ? `${i.message} (${i.actual} \u22AD ${i.expected})` : i.message;
|
|
5951
|
+
const fingerprint = `${file}:${i.code}:${begin}:${i.message}`;
|
|
5952
|
+
return {
|
|
5953
|
+
description,
|
|
5954
|
+
check_name: i.code,
|
|
5955
|
+
fingerprint,
|
|
5956
|
+
severity,
|
|
5957
|
+
location: { path: file, lines: { begin } }
|
|
5958
|
+
};
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5536
5962
|
export {
|
|
5537
5963
|
listFnDirectiveScopes,
|
|
5538
5964
|
findFnDirectiveScope,
|
|
@@ -5568,6 +5994,7 @@ export {
|
|
|
5568
5994
|
execNudoModule,
|
|
5569
5995
|
resetNudoModuleExecCache,
|
|
5570
5996
|
extractAssumeSegs,
|
|
5997
|
+
extractInvariantSegs,
|
|
5571
5998
|
fnTopLevelLocalNames,
|
|
5572
5999
|
extractRefinesFromSource,
|
|
5573
6000
|
refineToIndexedFull,
|
|
@@ -5610,5 +6037,12 @@ export {
|
|
|
5610
6037
|
stableAnalyzeKeySource,
|
|
5611
6038
|
listTopFunctions,
|
|
5612
6039
|
scanLiteralCalls,
|
|
5613
|
-
checkInjectedDomainEvidence
|
|
6040
|
+
checkInjectedDomainEvidence,
|
|
6041
|
+
actionsForIssue,
|
|
6042
|
+
serializeCheckJson,
|
|
6043
|
+
serializeCheckJsonMulti,
|
|
6044
|
+
budgetAttributionSegs,
|
|
6045
|
+
formatCheckReport,
|
|
6046
|
+
formatGithubAnnotations,
|
|
6047
|
+
formatGitlabCodeQuality
|
|
5614
6048
|
};
|