@nudojs/core 0.1.0 → 0.3.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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.d.ts +1633 -11
- package/dist/index.js +9965 -107
- package/package.json +28 -2
- package/src/__tests__/fn-sig-impl.test.ts +57 -0
- package/src/__tests__/ops-refined.test.ts +196 -0
- package/src/__tests__/ops.test.ts +8 -33
- package/src/__tests__/type-value-refined.test.ts +210 -0
- package/src/__tests__/type-value.test.ts +106 -0
- package/src/algebra/__tests__/abs-native.test.ts +176 -0
- package/src/algebra/__tests__/arithmetic.test.ts +170 -0
- package/src/algebra/__tests__/ast-eval.test.ts +154 -0
- package/src/algebra/__tests__/bridge.test.ts +160 -0
- package/src/algebra/__tests__/call-budget.test.ts +117 -0
- package/src/algebra/__tests__/check-case-consistency.test.ts +69 -0
- package/src/algebra/__tests__/check-gold.test.ts +290 -0
- package/src/algebra/__tests__/check-json.test.ts +57 -0
- package/src/algebra/__tests__/check-mini-repo.test.ts +49 -0
- package/src/algebra/__tests__/check-real-commander.test.ts +43 -0
- package/src/algebra/__tests__/check-real-packages.test.ts +165 -0
- package/src/algebra/__tests__/check-recall-gold.test.ts +1024 -0
- package/src/algebra/__tests__/check-require-dep-memo.test.ts +45 -0
- package/src/algebra/__tests__/check-return-contract.test.ts +90 -0
- package/src/algebra/__tests__/check-shape-gold.test.ts +303 -0
- package/src/algebra/__tests__/check-source-memo.test.ts +204 -0
- package/src/algebra/__tests__/check.test.ts +69 -0
- package/src/algebra/__tests__/denote.test.ts +77 -0
- package/src/algebra/__tests__/diagnostics.test.ts +64 -0
- package/src/algebra/__tests__/differential.test.ts +124 -0
- package/src/algebra/__tests__/div-mod.test.ts +38 -0
- package/src/algebra/__tests__/example0.test.ts +103 -0
- package/src/algebra/__tests__/exec-analyze-throws.test.ts +60 -0
- package/src/algebra/__tests__/exec-async.test.ts +46 -0
- package/src/algebra/__tests__/exec-bpath.test.ts +193 -0
- package/src/algebra/__tests__/exec-class.test.ts +56 -0
- package/src/algebra/__tests__/exec-e2e.test.ts +97 -0
- package/src/algebra/__tests__/exec-generator.test.ts +53 -0
- package/src/algebra/__tests__/exec-inherit-default.test.ts +111 -0
- package/src/algebra/__tests__/exec-optional-nested.test.ts +87 -0
- package/src/algebra/__tests__/exec-rest.test.ts +55 -0
- package/src/algebra/__tests__/exec-spread-forof.test.ts +85 -0
- package/src/algebra/__tests__/exec-static-computed.test.ts +61 -0
- package/src/algebra/__tests__/exec-switch.test.ts +74 -0
- package/src/algebra/__tests__/exec-trycatch.test.ts +71 -0
- package/src/algebra/__tests__/fn-fp-memo.test.ts +166 -0
- package/src/algebra/__tests__/fn-fp-multidecl.test.ts +49 -0
- package/src/algebra/__tests__/format.test.ts +34 -0
- package/src/algebra/__tests__/generalize-memo.test.ts +285 -0
- package/src/algebra/__tests__/generalize.test.ts +113 -0
- package/src/algebra/__tests__/hash-source.test.ts +19 -0
- package/src/algebra/__tests__/hof.test.ts +80 -0
- package/src/algebra/__tests__/inlay.test.ts +30 -0
- package/src/algebra/__tests__/language.test.ts +77 -0
- package/src/algebra/__tests__/leq.test.ts +109 -0
- package/src/algebra/__tests__/load-deps-fp-memo.test.ts +132 -0
- package/src/algebra/__tests__/methods.test.ts +37 -0
- package/src/algebra/__tests__/node-types.test.ts +26 -0
- package/src/algebra/__tests__/nudo-constraints.ts +46 -0
- package/src/algebra/__tests__/objects.test.ts +117 -0
- package/src/algebra/__tests__/parse-source-cache.test.ts +42 -0
- package/src/algebra/__tests__/refine-import.test.ts +109 -0
- package/src/algebra/__tests__/stable-source-key-block.test.ts +23 -0
- package/src/algebra/__tests__/stable-source-key.test.ts +31 -0
- package/src/algebra/__tests__/surface.test.ts +41 -0
- package/src/algebra/__tests__/this-inherit.test.ts +130 -0
- package/src/algebra/abs-fn.ts +49 -0
- package/src/algebra/abs-modules.ts +139 -0
- package/src/algebra/abs.ts +206 -0
- package/src/algebra/arithmetic.ts +691 -0
- package/src/algebra/ast-eval.ts +1763 -0
- package/src/algebra/bridge.ts +350 -0
- package/src/algebra/builtins.ts +373 -0
- package/src/algebra/check-report.ts +150 -0
- package/src/algebra/check.ts +1867 -0
- package/src/algebra/constraint.ts +304 -0
- package/src/algebra/denote.ts +166 -0
- package/src/algebra/diagnostics.ts +210 -0
- package/src/algebra/exec/call.ts +29 -0
- package/src/algebra/exec/calls.ts +96 -0
- package/src/algebra/exec/class-registry.ts +29 -0
- package/src/algebra/exec/class.ts +329 -0
- package/src/algebra/exec/index.ts +6 -0
- package/src/algebra/exec/member-diag.ts +141 -0
- package/src/algebra/exec/run.ts +341 -0
- package/src/algebra/exec/runtime.ts +583 -0
- package/src/algebra/exec/transpile.ts +993 -0
- package/src/algebra/fn-fp.ts +294 -0
- package/src/algebra/format.ts +91 -0
- package/src/algebra/generalize.ts +776 -0
- package/src/algebra/hash-source.ts +27 -0
- package/src/algebra/index.ts +34 -0
- package/src/algebra/inlay.ts +198 -0
- package/src/algebra/language.ts +289 -0
- package/src/algebra/leak.ts +68 -0
- package/src/algebra/leq.ts +301 -0
- package/src/algebra/load-deps-fp.ts +97 -0
- package/src/algebra/methods.ts +128 -0
- package/src/algebra/objects.ts +251 -0
- package/src/algebra/parse-source.ts +64 -0
- package/src/algebra/phi.ts +42 -0
- package/src/algebra/pred.ts +395 -0
- package/src/algebra/refine.ts +227 -0
- package/src/algebra/stable-source-key.ts +81 -0
- package/src/algebra/surface.ts +206 -0
- package/src/algebra/template.ts +143 -0
- package/src/algebra/term.ts +119 -0
- package/src/environment.ts +5 -0
- package/src/index.ts +52 -1
- package/src/mock-helpers.ts +166 -0
- package/src/ops.ts +106 -62
- package/src/refinements/range.ts +86 -0
- package/src/refinements/template-predicates.ts +11 -0
- package/src/refinements/template.ts +176 -0
- package/src/strip-types.ts +236 -0
- package/src/type-value.ts +193 -12
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
absToTypeValue,
|
|
4
|
+
typeValueToAbs,
|
|
5
|
+
bridgeIsLossy,
|
|
6
|
+
getTvConfidence,
|
|
7
|
+
numLit,
|
|
8
|
+
numVar,
|
|
9
|
+
gtNum,
|
|
10
|
+
v,
|
|
11
|
+
litValue,
|
|
12
|
+
formatShape,
|
|
13
|
+
analyzeFn,
|
|
14
|
+
objOf,
|
|
15
|
+
strLit,
|
|
16
|
+
boolLit,
|
|
17
|
+
} from "../index.ts";
|
|
18
|
+
import { typeValueToString, T } from "../../type-value.ts";
|
|
19
|
+
|
|
20
|
+
describe("bridge Abs → TypeValue", () => {
|
|
21
|
+
it("literal 4 → literal 4 #exact", () => {
|
|
22
|
+
const tv = absToTypeValue(numLit(4));
|
|
23
|
+
expect(tv.kind).toBe("literal");
|
|
24
|
+
if (tv.kind === "literal") expect(tv.value).toBe(4);
|
|
25
|
+
expect(getTvConfidence(tv)).toBe("exact");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("plain number prim → primitive number", () => {
|
|
29
|
+
const tv = absToTypeValue({
|
|
30
|
+
shape: { k: "prim", type: "number" },
|
|
31
|
+
conf: "exact",
|
|
32
|
+
});
|
|
33
|
+
expect(typeValueToString(tv)).toBe("number");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("x>0 with pred → refined number", () => {
|
|
37
|
+
const a = numVar("x", gtNum(v("x"), 0));
|
|
38
|
+
const tv = absToTypeValue(a);
|
|
39
|
+
// term 是 var,会丢 term;pred 可 encode 为 refined
|
|
40
|
+
expect(tv.kind === "refined" || tv.kind === "primitive").toBe(true);
|
|
41
|
+
const lossy = bridgeIsLossy(a);
|
|
42
|
+
// 丢 term 是预期的
|
|
43
|
+
expect(lossy.reasons.some((r) => r.includes("term"))).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("object slots map to object TypeValue", () => {
|
|
47
|
+
const o = objOf({
|
|
48
|
+
host: { value: strLit("localhost") },
|
|
49
|
+
port: { value: numLit(8080) },
|
|
50
|
+
debug: { value: boolLit(false) },
|
|
51
|
+
});
|
|
52
|
+
const tv = absToTypeValue(o);
|
|
53
|
+
expect(tv.kind).toBe("object");
|
|
54
|
+
expect(typeValueToString(tv)).toContain("host");
|
|
55
|
+
expect(typeValueToString(tv)).toContain("8080");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("bridge TypeValue → Abs", () => {
|
|
60
|
+
it("literal 6 → Abs exact 6", () => {
|
|
61
|
+
const abs = typeValueToAbs(T.literal(6));
|
|
62
|
+
expect(litValue(abs)).toBe(6);
|
|
63
|
+
expect(abs.conf).toBe("exact");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("primitive string → prim string", () => {
|
|
67
|
+
const abs = typeValueToAbs(T.string);
|
|
68
|
+
expect(formatShape(abs)).toBe("string");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("object → obj slots", () => {
|
|
72
|
+
const abs = typeValueToAbs(
|
|
73
|
+
T.object({ a: T.literal(1), b: T.string }),
|
|
74
|
+
);
|
|
75
|
+
expect(abs.shape.k).toBe("obj");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("round-trip literal", () => {
|
|
79
|
+
const tv = absToTypeValue(numLit(42));
|
|
80
|
+
const back = typeValueToAbs(tv);
|
|
81
|
+
expect(litValue(back)).toBe(42);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("round-trip primitive (lossless)", () => {
|
|
85
|
+
const abs0 = {
|
|
86
|
+
shape: { k: "prim" as const, type: "number" as const },
|
|
87
|
+
conf: "exact" as const,
|
|
88
|
+
};
|
|
89
|
+
const tv = absToTypeValue(abs0);
|
|
90
|
+
const back = typeValueToAbs(tv);
|
|
91
|
+
expect(formatShape(back)).toBe("number");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("bridgeIsLossy", () => {
|
|
96
|
+
it("literal is not lossy", () => {
|
|
97
|
+
expect(bridgeIsLossy(numLit(1)).lossy).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it("symbolic term is lossy", () => {
|
|
100
|
+
const src = `
|
|
101
|
+
function scale(x) { return x + 1; }
|
|
102
|
+
`;
|
|
103
|
+
const r = analyzeFn(src, "scale", [numVar("x")]);
|
|
104
|
+
expect(bridgeIsLossy(r).lossy).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("bridge any / conf / refined reverse (rewrite closeout)", () => {
|
|
109
|
+
it("Abs any projects as unknown but keeps path conf (not partial)", () => {
|
|
110
|
+
const a: import("../abs.ts").Abs = { shape: { k: "any" }, conf: "path" };
|
|
111
|
+
const tv = absToTypeValue(a);
|
|
112
|
+
expect(tv.kind).toBe("unknown");
|
|
113
|
+
expect(getTvConfidence(tv)).toBe("path");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("Abs analysis-fail unknown keeps partial conf", () => {
|
|
117
|
+
const a: import("../abs.ts").Abs = { shape: { k: "unknown" }, conf: "partial" };
|
|
118
|
+
const tv = absToTypeValue(a);
|
|
119
|
+
expect(tv.kind).toBe("unknown");
|
|
120
|
+
expect(getTvConfidence(tv)).toBe("partial");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("widened TypeValue does not round-trip as exact", () => {
|
|
124
|
+
const src = `
|
|
125
|
+
function scale(x) { return x + 1; }
|
|
126
|
+
`;
|
|
127
|
+
const r = analyzeFn(src, "scale", [numVar("x")]);
|
|
128
|
+
const tv = absToTypeValue(r);
|
|
129
|
+
const back = typeValueToAbs(tv);
|
|
130
|
+
expect(back.conf === "exact").toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("refined numeric pred reverse-encodes into Abs pred", () => {
|
|
134
|
+
const a = numVar("x", gtNum(v("x"), 0));
|
|
135
|
+
const tv = absToTypeValue(a);
|
|
136
|
+
expect(tv.kind).toBe("refined");
|
|
137
|
+
const back = typeValueToAbs(tv);
|
|
138
|
+
expect(back.shape.k).toBe("prim");
|
|
139
|
+
expect(back.pred).toBeDefined();
|
|
140
|
+
expect(back.pred!.op).toBe("gt");
|
|
141
|
+
// pred 右端恢复为字面量 0
|
|
142
|
+
if (back.pred!.op === "gt") {
|
|
143
|
+
expect(back.pred!.b.op).toBe("lit");
|
|
144
|
+
if (back.pred!.b.op === "lit") expect(back.pred!.b.value).toBe(0);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("refined le reverse-encodes", () => {
|
|
149
|
+
const a: import("../abs.ts").Abs = {
|
|
150
|
+
shape: { k: "prim", type: "number" },
|
|
151
|
+
term: v("n"),
|
|
152
|
+
pred: { op: "le", a: v("n"), b: { op: "lit", value: 10 } },
|
|
153
|
+
conf: "path",
|
|
154
|
+
};
|
|
155
|
+
const tv = absToTypeValue(a);
|
|
156
|
+
expect(tv.kind).toBe("refined");
|
|
157
|
+
const back = typeValueToAbs(tv);
|
|
158
|
+
expect(back.pred?.op).toBe("le");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { checkSource } from "../check.ts";
|
|
3
|
+
import {
|
|
4
|
+
analyzeFn,
|
|
5
|
+
evalProgramAbs,
|
|
6
|
+
resetAbsCallBudget,
|
|
7
|
+
MAX_CALL_DEPTH,
|
|
8
|
+
} from "../ast-eval.ts";
|
|
9
|
+
import { pTrue } from "../pred.ts";
|
|
10
|
+
import { num, unknown } from "../abs.ts";
|
|
11
|
+
import type { Abs } from "../abs.ts";
|
|
12
|
+
|
|
13
|
+
const FAC = `
|
|
14
|
+
function fac(n) {
|
|
15
|
+
if (n <= 1) return 1;
|
|
16
|
+
return n * fac(n - 1);
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const FIB = `
|
|
21
|
+
function fib(n) {
|
|
22
|
+
if (n <= 1) return n;
|
|
23
|
+
return fib(n - 1) + fib(n - 2);
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const MUTUAL = `
|
|
28
|
+
function isEven(n) {
|
|
29
|
+
if (n === 0) return true;
|
|
30
|
+
return isOdd(n - 1);
|
|
31
|
+
}
|
|
32
|
+
function isOdd(n) {
|
|
33
|
+
if (n === 0) return false;
|
|
34
|
+
return isEven(n - 1);
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
describe("Abs call budget (recursion guard)", () => {
|
|
39
|
+
it("analyzeFn on factorial does not stack-overflow", () => {
|
|
40
|
+
resetAbsCallBudget();
|
|
41
|
+
const r = analyzeFn(FAC, "fac", [num()], pTrue);
|
|
42
|
+
expect(r).toBeDefined();
|
|
43
|
+
expect(r.shape.k === "unknown" || r.shape.k === "prim" || r.shape.k === "never").toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("analyzeFn on fib does not stack-overflow", () => {
|
|
47
|
+
resetAbsCallBudget();
|
|
48
|
+
const r = analyzeFn(FIB, "fib", [num()], pTrue);
|
|
49
|
+
expect(r).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("mutual recursion does not stack-overflow", () => {
|
|
53
|
+
resetAbsCallBudget();
|
|
54
|
+
const r = analyzeFn(MUTUAL, "isEven", [num()], pTrue);
|
|
55
|
+
expect(r).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("evalProgramAbs with top-level recursive call does not stack-overflow", () => {
|
|
59
|
+
resetAbsCallBudget();
|
|
60
|
+
const { last } = evalProgramAbs(`
|
|
61
|
+
function count(n) {
|
|
62
|
+
if (n <= 0) return 0;
|
|
63
|
+
return 1 + count(n - 1);
|
|
64
|
+
}
|
|
65
|
+
const x = count(5);
|
|
66
|
+
`);
|
|
67
|
+
expect(last).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("concrete base-case recursion still evaluates", () => {
|
|
71
|
+
// 调用深度在 64 内:count(5) 应给出字面量结果而非 opaque
|
|
72
|
+
resetAbsCallBudget();
|
|
73
|
+
const lit5: Abs = {
|
|
74
|
+
shape: { k: "prim", type: "number" },
|
|
75
|
+
term: { op: "lit", value: 5 },
|
|
76
|
+
conf: "exact",
|
|
77
|
+
};
|
|
78
|
+
const r = analyzeFn(
|
|
79
|
+
`
|
|
80
|
+
function count(n) {
|
|
81
|
+
if (n <= 0) return 0;
|
|
82
|
+
return 1 + count(n - 1);
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
"count",
|
|
86
|
+
[lit5],
|
|
87
|
+
pTrue,
|
|
88
|
+
);
|
|
89
|
+
// 5 层展开应收敛为字面量 5(或至少 number)
|
|
90
|
+
expect(r.shape.k === "prim" || r.shape.k === "never").toBe(true);
|
|
91
|
+
if (r.term && r.term.op === "lit") {
|
|
92
|
+
expect(r.term.value).toBe(5);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("checkSource completes on factorial with structured diagnostics", () => {
|
|
97
|
+
const report = checkSource("/tmp/fac.js", FAC, pTrue, {});
|
|
98
|
+
expect(report).toBeDefined();
|
|
99
|
+
// 不得崩溃;递归截断记 warning
|
|
100
|
+
const codes = report.issues.map((i) => i.code);
|
|
101
|
+
expect(codes).toContain("nudo:recursion-truncated");
|
|
102
|
+
expect(report.issues.every((i) => i.severity !== undefined)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("depth budget constant is aligned with TypeValue evaluator", () => {
|
|
106
|
+
expect(MAX_CALL_DEPTH).toBe(64);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("truncated result uses opaque conf, not partial-any", () => {
|
|
110
|
+
resetAbsCallBudget();
|
|
111
|
+
const r = analyzeFn(FAC, "fac", [num()], pTrue);
|
|
112
|
+
// 截断后应为 unknown+opaque,或仍能算出的 prim;绝不能是 exact 的假结果
|
|
113
|
+
if (r.shape.k === "unknown") {
|
|
114
|
+
expect(r.conf === "opaque" || r.conf === "partial" || r.conf === "widened").toBe(true);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* case ⊄ refine → nudo:case-inconsistency
|
|
3
|
+
* case 是契约的见证,不是另一套前置来源。
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { checkSource, pTrue } from "../index.ts";
|
|
7
|
+
import { withStdImport, stdOpts } from "./nudo-constraints.ts";
|
|
8
|
+
|
|
9
|
+
function issuesOf(src: string) {
|
|
10
|
+
return checkSource("/t/case.js", withStdImport(src), pTrue, stdOpts);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("case vs refine", () => {
|
|
14
|
+
it("ok: case 实参满足契约", () => {
|
|
15
|
+
const r = issuesOf(`
|
|
16
|
+
/**
|
|
17
|
+
* @nudo:refine x positive
|
|
18
|
+
* @nudo:case "ok" (5)
|
|
19
|
+
*/
|
|
20
|
+
function needsPositive(x) {
|
|
21
|
+
return x;
|
|
22
|
+
}
|
|
23
|
+
`);
|
|
24
|
+
expect(r.issues.filter((i) => i.code === "nudo:case-inconsistency")).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("error: case 实参 ⊭ refine", () => {
|
|
28
|
+
const r = issuesOf(`
|
|
29
|
+
/**
|
|
30
|
+
* @nudo:refine x positive
|
|
31
|
+
* @nudo:case "neg" (-1)
|
|
32
|
+
*/
|
|
33
|
+
function needsPositive(x) {
|
|
34
|
+
return x;
|
|
35
|
+
}
|
|
36
|
+
`);
|
|
37
|
+
const err = r.issues.find((i) => i.code === "nudo:case-inconsistency");
|
|
38
|
+
expect(err).toBeDefined();
|
|
39
|
+
expect(err!.severity).toBe("error");
|
|
40
|
+
expect(err!.expected).toContain(">");
|
|
41
|
+
expect(err!.message).toContain("neg");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("error: percent 上界违例", () => {
|
|
45
|
+
const r = issuesOf(`
|
|
46
|
+
/**
|
|
47
|
+
* @nudo:refine n percent
|
|
48
|
+
* @nudo:case "big" (150)
|
|
49
|
+
*/
|
|
50
|
+
function pct(n) {
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
`);
|
|
54
|
+
expect(r.ok).toBe(false);
|
|
55
|
+
expect(r.issues.some((i) => i.code === "nudo:case-inconsistency")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("ok: 无 refine 的 case 不报", () => {
|
|
59
|
+
const r = issuesOf(`
|
|
60
|
+
/**
|
|
61
|
+
* @nudo:case "any" (-1)
|
|
62
|
+
*/
|
|
63
|
+
function id(x) {
|
|
64
|
+
return x;
|
|
65
|
+
}
|
|
66
|
+
`);
|
|
67
|
+
expect(r.issues.filter((i) => i.code === "nudo:case-inconsistency")).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { checkSource, pTrue } from "../index.ts";
|
|
3
|
+
import { withStdImport, stdOpts } from "./nudo-constraints.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* nudo check 金标:多形态 JS 片段上的门禁行为。
|
|
7
|
+
* 每条:source + 期望 ok / 期望出现的 issue code。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Gold = {
|
|
11
|
+
name: string;
|
|
12
|
+
source: string;
|
|
13
|
+
expectOk: boolean;
|
|
14
|
+
expectCode?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const golds: Gold[] = [
|
|
18
|
+
{
|
|
19
|
+
name: "valid positive call",
|
|
20
|
+
source: `
|
|
21
|
+
/**
|
|
22
|
+
* @nudo:refine x positive
|
|
23
|
+
*/
|
|
24
|
+
function needsPositive(x) {
|
|
25
|
+
if (x > 0) return x;
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const r = needsPositive(5);
|
|
29
|
+
`,
|
|
30
|
+
expectOk: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "negative call violates x>0",
|
|
34
|
+
source: `
|
|
35
|
+
/**
|
|
36
|
+
* @nudo:refine x positive
|
|
37
|
+
*/
|
|
38
|
+
function needsPositive(x) {
|
|
39
|
+
if (x > 0) return x;
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
const r = needsPositive(-1);
|
|
43
|
+
`,
|
|
44
|
+
expectOk: false,
|
|
45
|
+
expectCode: "nudo:constraint-violated",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "range guard lo is clamp not violation",
|
|
49
|
+
source: `
|
|
50
|
+
function clampId(id) {
|
|
51
|
+
if (id < 1) return 1;
|
|
52
|
+
if (id > 9999) return 9999;
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
55
|
+
clampId(0);
|
|
56
|
+
`,
|
|
57
|
+
// `id < 1` 是回退守卫,不作为调用前置
|
|
58
|
+
expectOk: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "range guard valid mid",
|
|
62
|
+
source: `
|
|
63
|
+
function clampId(id) {
|
|
64
|
+
if (id < 1) return 1;
|
|
65
|
+
if (id > 9999) return 9999;
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
clampId(50);
|
|
69
|
+
`,
|
|
70
|
+
expectOk: true,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "pure arithmetic no constraints",
|
|
74
|
+
source: `
|
|
75
|
+
function add(a, b) { return a + b; }
|
|
76
|
+
const r = add(1, 2);
|
|
77
|
+
`,
|
|
78
|
+
expectOk: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "ESM export function still listed",
|
|
82
|
+
source: `
|
|
83
|
+
export function scale(x) { return x + 1; }
|
|
84
|
+
`,
|
|
85
|
+
expectOk: true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "async function listed",
|
|
89
|
+
source: `
|
|
90
|
+
export async function load(id) { return id + 1; }
|
|
91
|
+
`,
|
|
92
|
+
expectOk: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "class constructor listed",
|
|
96
|
+
source: `
|
|
97
|
+
class Counter {
|
|
98
|
+
constructor(n) { this.n = n; }
|
|
99
|
+
get() { return this.n; }
|
|
100
|
+
}
|
|
101
|
+
export function main() { return new Counter(1).get(); }
|
|
102
|
+
`,
|
|
103
|
+
expectOk: true,
|
|
104
|
+
},
|
|
105
|
+
// --- 真阳性:必须报 ---
|
|
106
|
+
{
|
|
107
|
+
name: "zero violates x>0",
|
|
108
|
+
source: `
|
|
109
|
+
/**
|
|
110
|
+
* @nudo:refine x positive
|
|
111
|
+
*/
|
|
112
|
+
function needsPositive(x) {
|
|
113
|
+
if (x > 0) return x;
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
needsPositive(0);
|
|
117
|
+
`,
|
|
118
|
+
expectOk: false,
|
|
119
|
+
expectCode: "nudo:constraint-violated",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "unary negative call",
|
|
123
|
+
source: `
|
|
124
|
+
/**
|
|
125
|
+
* @nudo:refine x positive
|
|
126
|
+
*/
|
|
127
|
+
function needsPositive(x) {
|
|
128
|
+
if (x > 0) return x;
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
needsPositive(-3);
|
|
132
|
+
`,
|
|
133
|
+
expectOk: false,
|
|
134
|
+
expectCode: "nudo:constraint-violated",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "x>=1 rejects 0",
|
|
138
|
+
source: `
|
|
139
|
+
/**
|
|
140
|
+
* @nudo:refine i atLeast1
|
|
141
|
+
*/
|
|
142
|
+
function idx(i) {
|
|
143
|
+
if (i >= 1) return i;
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
idx(0);
|
|
147
|
+
`,
|
|
148
|
+
expectOk: false,
|
|
149
|
+
expectCode: "nudo:constraint-violated",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "upper bound x<10 rejects 10",
|
|
153
|
+
source: `
|
|
154
|
+
/**
|
|
155
|
+
* @nudo:refine n small
|
|
156
|
+
*/
|
|
157
|
+
function small(n) {
|
|
158
|
+
if (n < 10) return n;
|
|
159
|
+
return 9;
|
|
160
|
+
}
|
|
161
|
+
small(10);
|
|
162
|
+
`,
|
|
163
|
+
expectOk: false,
|
|
164
|
+
expectCode: "nudo:constraint-violated",
|
|
165
|
+
},
|
|
166
|
+
// --- 真阴性:不得误报(clamp / 无前置) ---
|
|
167
|
+
{
|
|
168
|
+
name: "clamp negative input is not violation",
|
|
169
|
+
source: `
|
|
170
|
+
function clamp(n, lo, hi) {
|
|
171
|
+
if (n < lo) return lo;
|
|
172
|
+
if (n > hi) return hi;
|
|
173
|
+
return n;
|
|
174
|
+
}
|
|
175
|
+
clamp(-5, 0, 100);
|
|
176
|
+
`,
|
|
177
|
+
expectOk: true,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "no guard no constraint",
|
|
181
|
+
source: `
|
|
182
|
+
function double(n) { return n * 2; }
|
|
183
|
+
double(-1);
|
|
184
|
+
`,
|
|
185
|
+
expectOk: true,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "equality guard is not numeric precondition",
|
|
189
|
+
source: `
|
|
190
|
+
function onlyZero(x) {
|
|
191
|
+
if (x === 0) return 0;
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
onlyZero(5);
|
|
195
|
+
`,
|
|
196
|
+
expectOk: true,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "min boundary valid",
|
|
200
|
+
source: `
|
|
201
|
+
function needsPositive(x) {
|
|
202
|
+
if (x > 0) return x;
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
needsPositive(1);
|
|
206
|
+
`,
|
|
207
|
+
expectOk: true,
|
|
208
|
+
},
|
|
209
|
+
// --- 结构 ---
|
|
210
|
+
{
|
|
211
|
+
name: "arrow function constraint",
|
|
212
|
+
source: `
|
|
213
|
+
/**
|
|
214
|
+
* @nudo:refine x positive
|
|
215
|
+
*/
|
|
216
|
+
const needsPositive = (x) => {
|
|
217
|
+
if (x > 0) return x;
|
|
218
|
+
return 0;
|
|
219
|
+
};
|
|
220
|
+
needsPositive(-2);
|
|
221
|
+
`,
|
|
222
|
+
expectOk: false,
|
|
223
|
+
expectCode: "nudo:constraint-violated",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "export default function",
|
|
227
|
+
source: `
|
|
228
|
+
/**
|
|
229
|
+
* @nudo:refine x positive
|
|
230
|
+
*/
|
|
231
|
+
export default function needsPositive(x) {
|
|
232
|
+
if (x > 0) return x;
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
needsPositive(-1);
|
|
236
|
+
`,
|
|
237
|
+
expectOk: false,
|
|
238
|
+
expectCode: "nudo:constraint-violated",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "both bounds mid valid",
|
|
242
|
+
source: `
|
|
243
|
+
/**
|
|
244
|
+
* @nudo:refine n percent
|
|
245
|
+
*/
|
|
246
|
+
function pct(n) {
|
|
247
|
+
if (n >= 0 && n <= 100) return n;
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
pct(50);
|
|
251
|
+
`,
|
|
252
|
+
expectOk: true,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "both bounds high invalid",
|
|
256
|
+
source: `
|
|
257
|
+
/**
|
|
258
|
+
* @nudo:refine n percent
|
|
259
|
+
*/
|
|
260
|
+
function pct(n) {
|
|
261
|
+
if (n >= 0 && n <= 100) return n;
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
pct(150);
|
|
265
|
+
`,
|
|
266
|
+
expectOk: false,
|
|
267
|
+
expectCode: "nudo:constraint-violated",
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
describe("nudo check gold standards", () => {
|
|
272
|
+
for (const g of golds) {
|
|
273
|
+
it(g.name, () => {
|
|
274
|
+
const report = checkSource("gold.js", withStdImport(g.source), pTrue, stdOpts);
|
|
275
|
+
expect(report.ok, formatIssues(report)).toBe(g.expectOk);
|
|
276
|
+
if (g.expectCode) {
|
|
277
|
+
expect(
|
|
278
|
+
report.issues.some((i) => i.code === g.expectCode),
|
|
279
|
+
formatIssues(report),
|
|
280
|
+
).toBe(true);
|
|
281
|
+
}
|
|
282
|
+
// 每个 case 至少能列出函数或明确无函数
|
|
283
|
+
expect(Array.isArray(report.signatures)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
function formatIssues(r: { issues: Array<{ severity: string; code: string; message: string }> }): string {
|
|
289
|
+
return r.issues.map((i) => `${i.severity} ${i.code}: ${i.message}`).join("; ") || "(none)";
|
|
290
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { checkSource, serializeCheckJson, pTrue } from "../index.ts";
|
|
3
|
+
import { withStdImport, stdOpts } from "./nudo-constraints.ts";
|
|
4
|
+
|
|
5
|
+
describe("CheckJson contract v1", () => {
|
|
6
|
+
const src = `
|
|
7
|
+
/**
|
|
8
|
+
* @nudo:refine x positive
|
|
9
|
+
*/
|
|
10
|
+
function needsPositive(x) {
|
|
11
|
+
if (x > 0) return x;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
needsPositive(-1);
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
it("has stable top-level fields", () => {
|
|
18
|
+
const j = serializeCheckJson(checkSource("c.js", withStdImport(src), pTrue, stdOpts));
|
|
19
|
+
expect(j.version).toBe(1);
|
|
20
|
+
expect(j.file).toBe("c.js");
|
|
21
|
+
expect(j.ok).toBe(false);
|
|
22
|
+
expect(j.summary).toMatchObject({
|
|
23
|
+
errors: expect.any(Number),
|
|
24
|
+
warnings: expect.any(Number),
|
|
25
|
+
infos: expect.any(Number),
|
|
26
|
+
functions: expect.any(Number),
|
|
27
|
+
});
|
|
28
|
+
expect(Array.isArray(j.signatures)).toBe(true);
|
|
29
|
+
expect(Array.isArray(j.issues)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("signatures carry abs display + detail", () => {
|
|
33
|
+
const j = serializeCheckJson(checkSource("c.js", withStdImport(src), pTrue, stdOpts));
|
|
34
|
+
const sig = j.signatures.find((s) => s.name === "needsPositive");
|
|
35
|
+
expect(sig).toBeDefined();
|
|
36
|
+
expect(sig!.params).toEqual(["x"]);
|
|
37
|
+
expect(sig!.abs).toContain("#");
|
|
38
|
+
expect(typeof sig!.conf).toBe("string");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("issues carry actual/expected for violations", () => {
|
|
42
|
+
const j = serializeCheckJson(checkSource("c.js", withStdImport(src), pTrue, stdOpts));
|
|
43
|
+
const err = j.issues.find((i) => i.code === "nudo:constraint-violated");
|
|
44
|
+
expect(err).toBeDefined();
|
|
45
|
+
expect(err!.actual).toBeDefined();
|
|
46
|
+
expect(err!.expected).toBeDefined();
|
|
47
|
+
expect(err!.line).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("ok file has ok:true and empty error issues", () => {
|
|
51
|
+
const j = serializeCheckJson(
|
|
52
|
+
checkSource("ok.js", `function f(x){ return x+1; }\nf(1);\n`, pTrue, stdOpts),
|
|
53
|
+
);
|
|
54
|
+
expect(j.ok).toBe(true);
|
|
55
|
+
expect(j.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|