@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,45 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { checkSource, resetCheckSourceMemo, pTrue } from "../index.ts";
|
|
3
|
+
import { STD_NUDO_SRC } from "./nudo-constraints.ts";
|
|
4
|
+
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
resetCheckSourceMemo();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("check memo covers regular require deps", () => {
|
|
10
|
+
it("misses when require target refine changes (parent source unchanged)", () => {
|
|
11
|
+
let dep = `
|
|
12
|
+
/// @nudo:import { positive } from "./std.nudo.js"
|
|
13
|
+
/**
|
|
14
|
+
* @nudo:refine x positive
|
|
15
|
+
*/
|
|
16
|
+
function needsPositive(x) {
|
|
17
|
+
if (x > 0) return x;
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
module.exports = { needsPositive };
|
|
21
|
+
`;
|
|
22
|
+
const loadModule = (spec: string) => {
|
|
23
|
+
if (spec.includes("std.nudo")) return STD_NUDO_SRC;
|
|
24
|
+
return dep;
|
|
25
|
+
};
|
|
26
|
+
const opts = { loadModule, fromFile: "/t/a.js" };
|
|
27
|
+
const src = `
|
|
28
|
+
const { needsPositive } = require("./v.js");
|
|
29
|
+
needsPositive(-1);
|
|
30
|
+
`;
|
|
31
|
+
const r1 = checkSource("/t/a.js", src, pTrue, opts);
|
|
32
|
+
expect(r1.ok).toBe(false);
|
|
33
|
+
expect(r1.issues.some((i) => i.code === "nudo:constraint-violated")).toBe(true);
|
|
34
|
+
|
|
35
|
+
dep = `
|
|
36
|
+
function needsPositive(x) {
|
|
37
|
+
if (x > 0) return x;
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
module.exports = { needsPositive };
|
|
41
|
+
`;
|
|
42
|
+
const r2 = checkSource("/t/a.js", src, pTrue, opts);
|
|
43
|
+
expect(r2.ok).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nudo:refine return 后置契约:推断返回值 ⊭ 声明。
|
|
3
|
+
* 与 @nudo:refine(前置)对偶;契约仍来自 *.nudo.js 模板。
|
|
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/ret.js", withStdImport(src), pTrue, stdOpts);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("@nudo:refine return", () => {
|
|
14
|
+
it("ok: 返回值满足 positive", () => {
|
|
15
|
+
const r = issuesOf(`
|
|
16
|
+
/**
|
|
17
|
+
* @nudo:refine x positive
|
|
18
|
+
* @nudo:refine return positive
|
|
19
|
+
*/
|
|
20
|
+
function inc(x) {
|
|
21
|
+
return x + 1;
|
|
22
|
+
}
|
|
23
|
+
`);
|
|
24
|
+
expect(r.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("error: 字面量返回 ⊭ positive", () => {
|
|
28
|
+
const r = issuesOf(`
|
|
29
|
+
/**
|
|
30
|
+
* @nudo:refine return positive
|
|
31
|
+
*/
|
|
32
|
+
function bad() {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
`);
|
|
36
|
+
expect(r.ok).toBe(false);
|
|
37
|
+
const err = r.issues.find((i) => i.message.includes("@nudo:refine return"));
|
|
38
|
+
expect(err).toBeDefined();
|
|
39
|
+
expect(err!.expected).toContain(">");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("error: 返回类型 ⊭ string", () => {
|
|
43
|
+
const r = issuesOf(`
|
|
44
|
+
/**
|
|
45
|
+
* @nudo:refine return notAString
|
|
46
|
+
*/
|
|
47
|
+
function n() {
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
`);
|
|
51
|
+
// notAString 不在 std 里 → 无契约可查,不报
|
|
52
|
+
expect(r.issues.filter((i) => i.message.includes("@nudo:refine return"))).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ok: 无 @nudo:refine return 不猜后置", () => {
|
|
56
|
+
const r = issuesOf(`
|
|
57
|
+
function free() {
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
`);
|
|
61
|
+
expect(r.issues.filter((i) => i.message.includes("@nudo:refine return"))).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("error: percent 上界", () => {
|
|
65
|
+
const r = issuesOf(`
|
|
66
|
+
/**
|
|
67
|
+
* @nudo:refine return percent
|
|
68
|
+
*/
|
|
69
|
+
function big() {
|
|
70
|
+
return 150;
|
|
71
|
+
}
|
|
72
|
+
`);
|
|
73
|
+
expect(r.ok).toBe(false);
|
|
74
|
+
expect(r.issues.some((i) => i.message.includes("@nudo:refine return"))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("ok: any/符号返回不误报", () => {
|
|
78
|
+
const r = issuesOf(`
|
|
79
|
+
/**
|
|
80
|
+
* @nudo:refine x positive
|
|
81
|
+
* @nudo:refine return positive
|
|
82
|
+
*/
|
|
83
|
+
function keep(x) {
|
|
84
|
+
return x;
|
|
85
|
+
}
|
|
86
|
+
`);
|
|
87
|
+
// 符号返回带 pred x>0,应满足
|
|
88
|
+
expect(r.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shape 约束金标:契约规范 object 形状,无需 interface/type。
|
|
3
|
+
* 唯一 refine 形态:@nudo:refine <param> <shapeName>
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { checkSource, pTrue } from "../index.ts";
|
|
7
|
+
import {
|
|
8
|
+
shape,
|
|
9
|
+
number,
|
|
10
|
+
string,
|
|
11
|
+
isNudoConstraint,
|
|
12
|
+
instantiateConstraint,
|
|
13
|
+
} from "../constraint.ts";
|
|
14
|
+
import { predToString } from "../pred.ts";
|
|
15
|
+
import { execNudoModule } from "../refine.ts";
|
|
16
|
+
import { withStdImport, stdOpts } from "./nudo-constraints.ts";
|
|
17
|
+
|
|
18
|
+
function issuesOf(src: string) {
|
|
19
|
+
return checkSource("/t/shape.js", withStdImport(src), pTrue, stdOpts);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function errorCodes(src: string): string[] {
|
|
23
|
+
return issuesOf(src)
|
|
24
|
+
.issues.filter((i) => i.severity === "error")
|
|
25
|
+
.map((i) => i.code);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("shape() constraint builder", () => {
|
|
29
|
+
it("builds field map from number()/string()", () => {
|
|
30
|
+
const c = shape({ id: number().gt(0), name: string() });
|
|
31
|
+
expect(isNudoConstraint(c)).toBe(true);
|
|
32
|
+
expect(Object.keys(c.fields ?? {}).sort()).toEqual(["id", "name"]);
|
|
33
|
+
expect(c.fields!.id!.constraint.prim).toBe("number");
|
|
34
|
+
expect(c.fields!.name!.constraint.prim).toBe("string");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("instantiates to field-access Preds", () => {
|
|
38
|
+
const c = shape({ id: number().gt(0), name: string() });
|
|
39
|
+
const p = instantiateConstraint(c, "u");
|
|
40
|
+
const s = predToString(p);
|
|
41
|
+
expect(s).toContain("u.id");
|
|
42
|
+
expect(s).toContain(">");
|
|
43
|
+
expect(s).toContain("typeof u.name");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("optional() marks field", () => {
|
|
47
|
+
const c = shape({ label: string().optional() });
|
|
48
|
+
expect(c.fields!.label!.optional).toBe(true);
|
|
49
|
+
expect(c.fields!.label!.constraint.isOptional).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("executes shape in .nudo.js", () => {
|
|
53
|
+
const src = `
|
|
54
|
+
export const user = shape({ id: number().gt(0), name: string() });
|
|
55
|
+
`;
|
|
56
|
+
const exp = execNudoModule(src);
|
|
57
|
+
expect(isNudoConstraint(exp.user)).toBe(true);
|
|
58
|
+
expect(Object.keys((exp.user as { fields: object }).fields).sort()).toEqual([
|
|
59
|
+
"id",
|
|
60
|
+
"name",
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("check: object literal ⊭ shape", () => {
|
|
66
|
+
it("ok: valid object", () => {
|
|
67
|
+
const r = issuesOf(`
|
|
68
|
+
/**
|
|
69
|
+
* @nudo:refine u userShape
|
|
70
|
+
*/
|
|
71
|
+
function register(u) {
|
|
72
|
+
return u.name;
|
|
73
|
+
}
|
|
74
|
+
register({ id: 1, name: "a" });
|
|
75
|
+
`);
|
|
76
|
+
expect(r.ok).toBe(true);
|
|
77
|
+
expect(r.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("error: field bound violated (id > 0)", () => {
|
|
81
|
+
const r = issuesOf(`
|
|
82
|
+
/**
|
|
83
|
+
* @nudo:refine u userShape
|
|
84
|
+
*/
|
|
85
|
+
function register(u) {
|
|
86
|
+
return u.name;
|
|
87
|
+
}
|
|
88
|
+
register({ id: -1, name: "a" });
|
|
89
|
+
`);
|
|
90
|
+
expect(r.ok).toBe(false);
|
|
91
|
+
const err = r.issues.find((i) => i.code === "nudo:constraint-violated");
|
|
92
|
+
expect(err).toBeDefined();
|
|
93
|
+
expect(err!.expected).toContain("u.id");
|
|
94
|
+
expect(err!.expected).toContain(">");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("error: missing required field", () => {
|
|
98
|
+
// body 不访问 name,仅契约声明要求该字段
|
|
99
|
+
const r = issuesOf(`
|
|
100
|
+
/**
|
|
101
|
+
* @nudo:refine u userShape
|
|
102
|
+
*/
|
|
103
|
+
function register(u) {
|
|
104
|
+
return u.id;
|
|
105
|
+
}
|
|
106
|
+
register({ id: 1 });
|
|
107
|
+
`);
|
|
108
|
+
expect(r.ok).toBe(false);
|
|
109
|
+
const err = r.issues.find((i) => i.code === "nudo:constraint-violated");
|
|
110
|
+
expect(err).toBeDefined();
|
|
111
|
+
expect(err!.expected).toContain("missing");
|
|
112
|
+
expect(err!.expected).toContain("name");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("error: field prim mismatch (name is number)", () => {
|
|
116
|
+
const r = issuesOf(`
|
|
117
|
+
/**
|
|
118
|
+
* @nudo:refine u userShape
|
|
119
|
+
*/
|
|
120
|
+
function register(u) {
|
|
121
|
+
return u.name;
|
|
122
|
+
}
|
|
123
|
+
register({ id: 1, name: 2 });
|
|
124
|
+
`);
|
|
125
|
+
expect(r.ok).toBe(false);
|
|
126
|
+
const err = r.issues.find((i) => i.code === "nudo:constraint-violated");
|
|
127
|
+
expect(err).toBeDefined();
|
|
128
|
+
expect(err!.expected).toContain("string");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("ok: optional field omitted", () => {
|
|
132
|
+
const r = issuesOf(`
|
|
133
|
+
/**
|
|
134
|
+
* @nudo:refine c configShape
|
|
135
|
+
*/
|
|
136
|
+
function setup(c) {
|
|
137
|
+
return c.retries;
|
|
138
|
+
}
|
|
139
|
+
setup({ retries: 3 });
|
|
140
|
+
`);
|
|
141
|
+
expect(r.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("error: optional field present but wrong type", () => {
|
|
145
|
+
const r = issuesOf(`
|
|
146
|
+
/**
|
|
147
|
+
* @nudo:refine c configShape
|
|
148
|
+
*/
|
|
149
|
+
function setup(c) {
|
|
150
|
+
return c.retries;
|
|
151
|
+
}
|
|
152
|
+
setup({ retries: 3, label: 9 });
|
|
153
|
+
`);
|
|
154
|
+
expect(r.ok).toBe(false);
|
|
155
|
+
expect(
|
|
156
|
+
r.issues.some(
|
|
157
|
+
(i) =>
|
|
158
|
+
i.code === "nudo:constraint-violated" &&
|
|
159
|
+
(i.expected ?? "").includes("string"),
|
|
160
|
+
),
|
|
161
|
+
).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("error: numeric upper bound on shape field", () => {
|
|
165
|
+
const r = issuesOf(`
|
|
166
|
+
/**
|
|
167
|
+
* @nudo:refine c configShape
|
|
168
|
+
*/
|
|
169
|
+
function setup(c) {
|
|
170
|
+
return c.retries;
|
|
171
|
+
}
|
|
172
|
+
setup({ retries: 99 });
|
|
173
|
+
`);
|
|
174
|
+
expect(r.ok).toBe(false);
|
|
175
|
+
const err = r.issues.find((i) => i.code === "nudo:constraint-violated");
|
|
176
|
+
expect(err).toBeDefined();
|
|
177
|
+
expect(err!.expected).toContain("≤");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("ok: unknown identifier arg (no false positive)", () => {
|
|
181
|
+
const r = issuesOf(`
|
|
182
|
+
/**
|
|
183
|
+
* @nudo:refine u userShape
|
|
184
|
+
*/
|
|
185
|
+
function register(u) {
|
|
186
|
+
return u.name;
|
|
187
|
+
}
|
|
188
|
+
const payload = getPayload();
|
|
189
|
+
register(payload);
|
|
190
|
+
`);
|
|
191
|
+
expect(r.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("ok: no refine → no shape gate", () => {
|
|
195
|
+
const r = issuesOf(`
|
|
196
|
+
function open(u) {
|
|
197
|
+
return u;
|
|
198
|
+
}
|
|
199
|
+
open({ id: -1 });
|
|
200
|
+
`);
|
|
201
|
+
expect(r.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("ok: nested shape + array fields", () => {
|
|
205
|
+
const r = issuesOf(`
|
|
206
|
+
/**
|
|
207
|
+
* @nudo:refine o orderShape
|
|
208
|
+
*/
|
|
209
|
+
function place(o) {
|
|
210
|
+
return o.user.id;
|
|
211
|
+
}
|
|
212
|
+
place({ user: { id: 1, name: "a" }, tags: ["x"] });
|
|
213
|
+
`);
|
|
214
|
+
expect(r.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("error: nested shape field bound", () => {
|
|
218
|
+
const r = issuesOf(`
|
|
219
|
+
/**
|
|
220
|
+
* @nudo:refine o orderShape
|
|
221
|
+
*/
|
|
222
|
+
function place(o) {
|
|
223
|
+
return o.user.id;
|
|
224
|
+
}
|
|
225
|
+
place({ user: { id: -1, name: "a" }, tags: ["x"] });
|
|
226
|
+
`);
|
|
227
|
+
expect(r.ok).toBe(false);
|
|
228
|
+
expect(r.issues.some((i) => i.code === "nudo:constraint-violated")).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("error: array element min length", () => {
|
|
232
|
+
const r = issuesOf(`
|
|
233
|
+
/**
|
|
234
|
+
* @nudo:refine o orderShape
|
|
235
|
+
*/
|
|
236
|
+
function place(o) {
|
|
237
|
+
return o.tags;
|
|
238
|
+
}
|
|
239
|
+
place({ user: { id: 1, name: "a" }, tags: [""] });
|
|
240
|
+
`);
|
|
241
|
+
expect(r.ok).toBe(false);
|
|
242
|
+
expect(r.issues.some((i) => i.code === "nudo:constraint-violated")).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("ok: int refine", () => {
|
|
246
|
+
const ok = issuesOf(`
|
|
247
|
+
/**
|
|
248
|
+
* @nudo:refine n intId
|
|
249
|
+
*/
|
|
250
|
+
function take(n) { return n; }
|
|
251
|
+
take(3);
|
|
252
|
+
`);
|
|
253
|
+
expect(ok.issues.filter((i) => i.severity === "error")).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("error: int refine rejects non-integer", () => {
|
|
257
|
+
const r = issuesOf(`
|
|
258
|
+
/**
|
|
259
|
+
* @nudo:refine n intId
|
|
260
|
+
*/
|
|
261
|
+
function take(n) { return n; }
|
|
262
|
+
take(1.5);
|
|
263
|
+
`);
|
|
264
|
+
expect(r.ok).toBe(false);
|
|
265
|
+
expect(r.issues.some((i) => i.expected?.includes("int"))).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("error: array refine rejects non-array", () => {
|
|
269
|
+
const r = issuesOf(`
|
|
270
|
+
/**
|
|
271
|
+
* @nudo:refine xs positives
|
|
272
|
+
*/
|
|
273
|
+
function take(xs) { return xs; }
|
|
274
|
+
take(1);
|
|
275
|
+
`);
|
|
276
|
+
expect(r.ok).toBe(false);
|
|
277
|
+
expect(r.issues.some((i) => i.expected?.includes("array"))).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("error: array element bound", () => {
|
|
281
|
+
const r = issuesOf(`
|
|
282
|
+
/**
|
|
283
|
+
* @nudo:refine xs positives
|
|
284
|
+
*/
|
|
285
|
+
function take(xs) { return xs; }
|
|
286
|
+
take([-1]);
|
|
287
|
+
`);
|
|
288
|
+
expect(r.ok).toBe(false);
|
|
289
|
+
expect(r.issues.some((i) => i.code === "nudo:constraint-violated")).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("error: string min length", () => {
|
|
293
|
+
const r = issuesOf(`
|
|
294
|
+
/**
|
|
295
|
+
* @nudo:refine s shortName
|
|
296
|
+
*/
|
|
297
|
+
function take(s) { return s; }
|
|
298
|
+
take("");
|
|
299
|
+
`);
|
|
300
|
+
expect(r.ok).toBe(false);
|
|
301
|
+
expect(r.issues.some((i) => i.expected?.includes("length"))).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
checkSource,
|
|
4
|
+
resetCheckSourceMemo,
|
|
5
|
+
getCheckSourceMemoSize,
|
|
6
|
+
evictCheckSourceMemoForPaths,
|
|
7
|
+
resetGeneralizeMemo,
|
|
8
|
+
pTrue,
|
|
9
|
+
} from "../index.ts";
|
|
10
|
+
import { STD_NUDO_SRC, withStdImport } from "./nudo-constraints.ts";
|
|
11
|
+
|
|
12
|
+
const SRC = `
|
|
13
|
+
function add(a, b) {
|
|
14
|
+
return a + b;
|
|
15
|
+
}
|
|
16
|
+
function needsPositive(x) {
|
|
17
|
+
/** @nudo:refine x positive */
|
|
18
|
+
return x + 1;
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
resetCheckSourceMemo();
|
|
24
|
+
resetGeneralizeMemo();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("checkSource whole-file memo", () => {
|
|
28
|
+
it("hit returns equivalent report for same source", () => {
|
|
29
|
+
const a = checkSource("/t/a.js", SRC, pTrue, {});
|
|
30
|
+
const b = checkSource("/t/a.js", SRC, pTrue, {});
|
|
31
|
+
expect(getCheckSourceMemoSize()).toBe(1);
|
|
32
|
+
expect(b.ok).toBe(a.ok);
|
|
33
|
+
expect(b.signatures.map((s) => s.name)).toEqual(a.signatures.map((s) => s.name));
|
|
34
|
+
expect(b.issues.map((i) => i.code)).toEqual(a.issues.map((i) => i.code));
|
|
35
|
+
expect(b.summary).toEqual(a.summary);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("hits when only trailing non-nudo comments change", () => {
|
|
39
|
+
checkSource("/t/a.js", SRC, pTrue, {});
|
|
40
|
+
checkSource("/t/a.js", SRC + "\n// touch\n", pTrue, {});
|
|
41
|
+
expect(getCheckSourceMemoSize()).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("misses when source body changes", () => {
|
|
45
|
+
checkSource("/t/a.js", SRC, pTrue, {});
|
|
46
|
+
checkSource("/t/a.js", SRC + "\nconst zzz = 1;\n", pTrue, {});
|
|
47
|
+
expect(getCheckSourceMemoSize()).toBe(2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("misses when filePath changes", () => {
|
|
51
|
+
checkSource("/t/a.js", SRC, pTrue, {});
|
|
52
|
+
checkSource("/t/b.js", SRC, pTrue, {});
|
|
53
|
+
expect(getCheckSourceMemoSize()).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("misses when loadModule identity changes", () => {
|
|
57
|
+
const l1 = () => undefined;
|
|
58
|
+
const l2 = () => undefined;
|
|
59
|
+
checkSource("/t/a.js", SRC, pTrue, { loadModule: l1, fromFile: "/t/a.js" });
|
|
60
|
+
checkSource("/t/a.js", SRC, pTrue, { loadModule: l2, fromFile: "/t/a.js" });
|
|
61
|
+
expect(getCheckSourceMemoSize()).toBe(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("misses when dep content changes", () => {
|
|
65
|
+
let dep = "export const positive = number().gt(0);\n";
|
|
66
|
+
const loadModule = () => dep;
|
|
67
|
+
const opts = { loadModule, fromFile: "/t/a.js" };
|
|
68
|
+
const src = `
|
|
69
|
+
/// @nudo:import { positive } from "./shapes.nudo.js"
|
|
70
|
+
function needsPositive(x) {
|
|
71
|
+
/** @nudo:refine x positive */
|
|
72
|
+
return x + 1;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
checkSource("/t/a.js", src, pTrue, opts);
|
|
76
|
+
dep = "export const positive = number().ge(0);\n";
|
|
77
|
+
checkSource("/t/a.js", src, pTrue, opts);
|
|
78
|
+
expect(getCheckSourceMemoSize()).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("evictCheckSourceMemoForPaths drops dependents of that dep", () => {
|
|
82
|
+
const loadModule = () => "export const positive = number().gt(0);\n";
|
|
83
|
+
const opts = { loadModule, fromFile: "/t/a.js" };
|
|
84
|
+
const src = `
|
|
85
|
+
/// @nudo:import { positive } from "./shapes.nudo.js"
|
|
86
|
+
function needsPositive(x) {
|
|
87
|
+
/** @nudo:refine x positive */
|
|
88
|
+
return x + 1;
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
checkSource("/t/a.js", src, pTrue, opts);
|
|
92
|
+
checkSource("/t/b.js", SRC, pTrue, {});
|
|
93
|
+
const before = getCheckSourceMemoSize();
|
|
94
|
+
const n = evictCheckSourceMemoForPaths(["/t/shapes.nudo.js"]);
|
|
95
|
+
expect(n).toBeGreaterThan(0);
|
|
96
|
+
expect(getCheckSourceMemoSize()).toBeLessThan(before);
|
|
97
|
+
// 无关文件仍在
|
|
98
|
+
expect(getCheckSourceMemoSize()).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("reset drops all entries", () => {
|
|
102
|
+
checkSource("/t/a.js", SRC, pTrue, {});
|
|
103
|
+
expect(getCheckSourceMemoSize()).toBeGreaterThan(0);
|
|
104
|
+
resetCheckSourceMemo();
|
|
105
|
+
expect(getCheckSourceMemoSize()).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("mutating returned issues does not corrupt cache", () => {
|
|
109
|
+
const a = checkSource("/t/a.js", SRC, pTrue, {});
|
|
110
|
+
a.issues.push({
|
|
111
|
+
severity: "error",
|
|
112
|
+
code: "x",
|
|
113
|
+
message: "mutated",
|
|
114
|
+
});
|
|
115
|
+
a.signatures.length = 0;
|
|
116
|
+
const b = checkSource("/t/a.js", SRC, pTrue, {});
|
|
117
|
+
expect(b.issues.some((i) => i.message === "mutated")).toBe(false);
|
|
118
|
+
expect(b.signatures.length).toBeGreaterThan(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("same loadModule identity hits memo (wrapper must not defeat key)", () => {
|
|
122
|
+
const loadModule = () => undefined;
|
|
123
|
+
const opts = { loadModule, fromFile: "/t/a.js" };
|
|
124
|
+
checkSource("/t/a.js", SRC, pTrue, opts);
|
|
125
|
+
const size1 = getCheckSourceMemoSize();
|
|
126
|
+
checkSource("/t/a.js", SRC, pTrue, opts);
|
|
127
|
+
checkSource("/t/a.js", SRC, pTrue, opts);
|
|
128
|
+
expect(size1).toBe(1);
|
|
129
|
+
expect(getCheckSourceMemoSize()).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("hits when only trailing non-nudo comments change even with loadModule", () => {
|
|
133
|
+
const loadModule = () => undefined;
|
|
134
|
+
const opts = { loadModule, fromFile: "/t/a.js" };
|
|
135
|
+
checkSource("/t/a.js", SRC, pTrue, opts);
|
|
136
|
+
checkSource("/t/a.js", SRC + "\n// touch\n", pTrue, opts);
|
|
137
|
+
expect(getCheckSourceMemoSize()).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("reports constraint violation for const-assigned literal call", () => {
|
|
141
|
+
const loadModule = (spec: string) =>
|
|
142
|
+
spec.includes("std") ? STD_NUDO_SRC : undefined;
|
|
143
|
+
const src = `${withStdImport(`
|
|
144
|
+
/**
|
|
145
|
+
* @nudo:refine x positive
|
|
146
|
+
*/
|
|
147
|
+
function needsPositive(x) {
|
|
148
|
+
if (x > 0) return x;
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
const bad = needsPositive(-1);
|
|
152
|
+
`)}`;
|
|
153
|
+
const r = checkSource("/t/scan.js", src, pTrue, { loadModule, fromFile: "/t/scan.js" });
|
|
154
|
+
expect(r.issues.some((i) => i.code === "nudo:constraint-violated")).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("reports constraint violation for if-condition literal call", () => {
|
|
158
|
+
const loadModule = (spec: string) =>
|
|
159
|
+
spec.includes("std") ? STD_NUDO_SRC : undefined;
|
|
160
|
+
const src = `${withStdImport(`
|
|
161
|
+
/**
|
|
162
|
+
* @nudo:refine x positive
|
|
163
|
+
*/
|
|
164
|
+
function needsPositive(x) {
|
|
165
|
+
if (x > 0) return x;
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
if (needsPositive(-1)) {}
|
|
169
|
+
`)}`;
|
|
170
|
+
const r = checkSource("/t/scan-if.js", src, pTrue, { loadModule, fromFile: "/t/scan-if.js" });
|
|
171
|
+
expect(r.issues.some((i) => i.code === "nudo:constraint-violated")).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("fingerprints transitive require deps (grandchild change misses)", () => {
|
|
175
|
+
const files: Record<string, string> = {
|
|
176
|
+
"/t/w.js": "module.exports = { n: 1 };\n",
|
|
177
|
+
"/t/v.js": 'const w = require("./w.js");\nmodule.exports = { fromW: w.n };\n',
|
|
178
|
+
};
|
|
179
|
+
const loadModule = (spec: string, fromFile: string) => {
|
|
180
|
+
if (spec === "./w.js") return files["/t/w.js"];
|
|
181
|
+
if (spec === "./v.js") return files["/t/v.js"];
|
|
182
|
+
if (spec === "./w.js" && fromFile.endsWith("v.js")) return files["/t/w.js"];
|
|
183
|
+
return undefined;
|
|
184
|
+
};
|
|
185
|
+
// resolve relative to the dep file when nested
|
|
186
|
+
const load = (spec: string, fromFile: string) => {
|
|
187
|
+
if (fromFile === "/t/a.js" && spec === "./v.js") return files["/t/v.js"];
|
|
188
|
+
if (fromFile === "/t/v.js" && spec === "./w.js") return files["/t/w.js"];
|
|
189
|
+
return loadModule(spec, fromFile);
|
|
190
|
+
};
|
|
191
|
+
const opts = { loadModule: load, fromFile: "/t/a.js" };
|
|
192
|
+
const src = `
|
|
193
|
+
const v = require("./v.js");
|
|
194
|
+
function id(x) { return x; }
|
|
195
|
+
id(v.fromW);
|
|
196
|
+
`;
|
|
197
|
+
checkSource("/t/a.js", src, pTrue, opts);
|
|
198
|
+
expect(getCheckSourceMemoSize()).toBe(1);
|
|
199
|
+
// grandchild content change, parent + child sources unchanged
|
|
200
|
+
files["/t/w.js"] = "module.exports = { n: 2 };\n";
|
|
201
|
+
checkSource("/t/a.js", src, pTrue, opts);
|
|
202
|
+
expect(getCheckSourceMemoSize()).toBe(2);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { checkSource, formatCheckReport, pTrue } from "../index.ts";
|
|
3
|
+
import { withStdImport, stdOpts } from "./nudo-constraints.ts";
|
|
4
|
+
|
|
5
|
+
describe("nudo check gate", () => {
|
|
6
|
+
it("literal call violating constraint is error", () => {
|
|
7
|
+
// @nudo:refine 声明契约;调用 -1 应报错
|
|
8
|
+
const src = `
|
|
9
|
+
/**
|
|
10
|
+
* @nudo:refine x positive
|
|
11
|
+
*/
|
|
12
|
+
function needsPositive(x) {
|
|
13
|
+
if (x > 0) return x;
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
const r = needsPositive(-1);
|
|
17
|
+
`;
|
|
18
|
+
const report = checkSource("t.js", withStdImport(src), pTrue, stdOpts);
|
|
19
|
+
expect(report.ok).toBe(false);
|
|
20
|
+
const err = report.issues.find((i) => i.severity === "error");
|
|
21
|
+
expect(err).toBeDefined();
|
|
22
|
+
expect(err!.code).toBe("nudo:constraint-violated");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("valid literal call is ok", () => {
|
|
26
|
+
const src = `
|
|
27
|
+
/**
|
|
28
|
+
* @nudo:refine x positive
|
|
29
|
+
*/
|
|
30
|
+
function needsPositive(x) {
|
|
31
|
+
if (x > 0) return x;
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
const r = needsPositive(5);
|
|
35
|
+
`;
|
|
36
|
+
const report = checkSource("t.js", withStdImport(src), pTrue, stdOpts);
|
|
37
|
+
expect(report.ok).toBe(true);
|
|
38
|
+
expect(report.signatures.some((f) => f.name === "needsPositive")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("reports intensional signatures", () => {
|
|
42
|
+
const src = `
|
|
43
|
+
function add(a, b) { return a + b; }
|
|
44
|
+
function scale(x) { return add(x, 1); }
|
|
45
|
+
`;
|
|
46
|
+
const report = checkSource("t.js", withStdImport(src), pTrue, stdOpts);
|
|
47
|
+
const scale = report.signatures.find((f) => f.name === "scale");
|
|
48
|
+
expect(scale!.display).toContain("A1");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("formatCheckReport is Nudo-native (signatures + issues)", () => {
|
|
52
|
+
const bad = checkSource(
|
|
53
|
+
"t.js",
|
|
54
|
+
withStdImport(`/**
|
|
55
|
+
* @nudo:refine x positive
|
|
56
|
+
*/
|
|
57
|
+
function f(x){ if (x>0) return x; return 0; }
|
|
58
|
+
f(-1);
|
|
59
|
+
`),
|
|
60
|
+
pTrue,
|
|
61
|
+
stdOpts,
|
|
62
|
+
);
|
|
63
|
+
const text = formatCheckReport(bad);
|
|
64
|
+
expect(text).toContain("FAILED");
|
|
65
|
+
expect(text).toContain("signatures");
|
|
66
|
+
expect(text).toContain("actual:");
|
|
67
|
+
expect(text).toContain("expected:");
|
|
68
|
+
});
|
|
69
|
+
});
|