@nudojs/core 0.3.0 → 0.3.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-BU4A6XJ3.js +6856 -0
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +168 -0
- package/dist/index-CVYjXnwU.d.ts +902 -0
- package/dist/index.d.ts +64 -710
- package/dist/index.js +3258 -8426
- package/package.json +18 -5
- package/CHANGELOG.md +0 -34
- package/src/__tests__/environment.test.ts +0 -48
- package/src/__tests__/fn-sig-impl.test.ts +0 -57
- package/src/__tests__/ops-refined.test.ts +0 -196
- package/src/__tests__/ops.test.ts +0 -84
- package/src/__tests__/type-value-phase2.test.ts +0 -117
- package/src/__tests__/type-value-phase3.test.ts +0 -94
- package/src/__tests__/type-value-refined.test.ts +0 -210
- package/src/__tests__/type-value.test.ts +0 -319
- package/src/algebra/__tests__/abs-native.test.ts +0 -176
- package/src/algebra/__tests__/arithmetic.test.ts +0 -170
- package/src/algebra/__tests__/ast-eval.test.ts +0 -154
- package/src/algebra/__tests__/bridge.test.ts +0 -160
- package/src/algebra/__tests__/call-budget.test.ts +0 -117
- package/src/algebra/__tests__/check-case-consistency.test.ts +0 -69
- package/src/algebra/__tests__/check-gold.test.ts +0 -290
- package/src/algebra/__tests__/check-json.test.ts +0 -57
- package/src/algebra/__tests__/check-mini-repo.test.ts +0 -49
- package/src/algebra/__tests__/check-real-commander.test.ts +0 -43
- package/src/algebra/__tests__/check-real-packages.test.ts +0 -165
- package/src/algebra/__tests__/check-recall-gold.test.ts +0 -1024
- package/src/algebra/__tests__/check-require-dep-memo.test.ts +0 -45
- package/src/algebra/__tests__/check-return-contract.test.ts +0 -90
- package/src/algebra/__tests__/check-shape-gold.test.ts +0 -303
- package/src/algebra/__tests__/check-source-memo.test.ts +0 -204
- package/src/algebra/__tests__/check.test.ts +0 -69
- package/src/algebra/__tests__/denote.test.ts +0 -77
- package/src/algebra/__tests__/diagnostics.test.ts +0 -64
- package/src/algebra/__tests__/differential.test.ts +0 -124
- package/src/algebra/__tests__/div-mod.test.ts +0 -38
- package/src/algebra/__tests__/example0.test.ts +0 -103
- package/src/algebra/__tests__/exec-analyze-throws.test.ts +0 -60
- package/src/algebra/__tests__/exec-async.test.ts +0 -46
- package/src/algebra/__tests__/exec-bpath.test.ts +0 -193
- package/src/algebra/__tests__/exec-class.test.ts +0 -56
- package/src/algebra/__tests__/exec-e2e.test.ts +0 -97
- package/src/algebra/__tests__/exec-generator.test.ts +0 -53
- package/src/algebra/__tests__/exec-inherit-default.test.ts +0 -111
- package/src/algebra/__tests__/exec-optional-nested.test.ts +0 -87
- package/src/algebra/__tests__/exec-rest.test.ts +0 -55
- package/src/algebra/__tests__/exec-spread-forof.test.ts +0 -85
- package/src/algebra/__tests__/exec-static-computed.test.ts +0 -61
- package/src/algebra/__tests__/exec-switch.test.ts +0 -74
- package/src/algebra/__tests__/exec-trycatch.test.ts +0 -71
- package/src/algebra/__tests__/fn-fp-memo.test.ts +0 -166
- package/src/algebra/__tests__/fn-fp-multidecl.test.ts +0 -49
- package/src/algebra/__tests__/format.test.ts +0 -34
- package/src/algebra/__tests__/generalize-memo.test.ts +0 -285
- package/src/algebra/__tests__/generalize.test.ts +0 -113
- package/src/algebra/__tests__/hash-source.test.ts +0 -19
- package/src/algebra/__tests__/hof.test.ts +0 -80
- package/src/algebra/__tests__/inlay.test.ts +0 -30
- package/src/algebra/__tests__/language.test.ts +0 -77
- package/src/algebra/__tests__/leq.test.ts +0 -109
- package/src/algebra/__tests__/load-deps-fp-memo.test.ts +0 -132
- package/src/algebra/__tests__/methods.test.ts +0 -37
- package/src/algebra/__tests__/node-types.test.ts +0 -26
- package/src/algebra/__tests__/nudo-constraints.ts +0 -46
- package/src/algebra/__tests__/objects.test.ts +0 -117
- package/src/algebra/__tests__/parse-source-cache.test.ts +0 -42
- package/src/algebra/__tests__/refine-import.test.ts +0 -109
- package/src/algebra/__tests__/stable-source-key-block.test.ts +0 -23
- package/src/algebra/__tests__/stable-source-key.test.ts +0 -31
- package/src/algebra/__tests__/surface.test.ts +0 -41
- package/src/algebra/__tests__/this-inherit.test.ts +0 -130
- package/src/algebra/abs-fn.ts +0 -49
- package/src/algebra/abs-modules.ts +0 -139
- package/src/algebra/abs.ts +0 -206
- package/src/algebra/arithmetic.ts +0 -691
- package/src/algebra/ast-eval.ts +0 -1763
- package/src/algebra/bridge.ts +0 -350
- package/src/algebra/builtins.ts +0 -373
- package/src/algebra/check-report.ts +0 -150
- package/src/algebra/check.ts +0 -1867
- package/src/algebra/constraint.ts +0 -304
- package/src/algebra/denote.ts +0 -166
- package/src/algebra/diagnostics.ts +0 -210
- package/src/algebra/exec/call.ts +0 -29
- package/src/algebra/exec/calls.ts +0 -96
- package/src/algebra/exec/class-registry.ts +0 -29
- package/src/algebra/exec/class.ts +0 -329
- package/src/algebra/exec/index.ts +0 -6
- package/src/algebra/exec/member-diag.ts +0 -141
- package/src/algebra/exec/run.ts +0 -341
- package/src/algebra/exec/runtime.ts +0 -583
- package/src/algebra/exec/transpile.ts +0 -993
- package/src/algebra/fn-fp.ts +0 -294
- package/src/algebra/format.ts +0 -91
- package/src/algebra/generalize.ts +0 -776
- package/src/algebra/hash-source.ts +0 -27
- package/src/algebra/index.ts +0 -34
- package/src/algebra/inlay.ts +0 -198
- package/src/algebra/language.ts +0 -289
- package/src/algebra/leak.ts +0 -68
- package/src/algebra/leq.ts +0 -301
- package/src/algebra/load-deps-fp.ts +0 -97
- package/src/algebra/methods.ts +0 -128
- package/src/algebra/objects.ts +0 -251
- package/src/algebra/parse-source.ts +0 -64
- package/src/algebra/phi.ts +0 -42
- package/src/algebra/pred.ts +0 -395
- package/src/algebra/refine.ts +0 -227
- package/src/algebra/stable-source-key.ts +0 -81
- package/src/algebra/surface.ts +0 -206
- package/src/algebra/template.ts +0 -143
- package/src/algebra/term.ts +0 -119
- package/src/environment.ts +0 -74
- package/src/index.ts +0 -73
- package/src/mock-helpers.ts +0 -166
- package/src/ops.ts +0 -191
- package/src/refinements/range.ts +0 -86
- package/src/refinements/template-predicates.ts +0 -11
- package/src/refinements/template.ts +0 -176
- package/src/strip-types.ts +0 -236
- package/src/type-value.ts +0 -513
- package/tsconfig.json +0 -8
package/src/algebra/check.ts
DELETED
|
@@ -1,1867 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* nudo check 门禁:对源码跑代数分析,产出 error/warning。
|
|
3
|
-
*
|
|
4
|
-
* 报告是 **Nudo 原生格式**(Abs 优先),不是 TS 诊断的换皮:
|
|
5
|
-
* - 签名表给出无损 Abs(shape/term/pred/conf)
|
|
6
|
-
* - 违例写清 actual ⊭ expected(实参 Abs vs 前置 Pred)
|
|
7
|
-
* - dts/TS 兼容不是本报告的职责
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { parseSource as parse } from "./parse-source.ts";
|
|
11
|
-
import type { Node } from "@babel/types";
|
|
12
|
-
import {
|
|
13
|
-
analyzeFn,
|
|
14
|
-
evalProgramAbs,
|
|
15
|
-
evalNode,
|
|
16
|
-
emptyEnv,
|
|
17
|
-
setAbsAssignCollector,
|
|
18
|
-
setAbsTruncationCollector,
|
|
19
|
-
resetAbsCallBudget,
|
|
20
|
-
type AbsAssignRecord,
|
|
21
|
-
} from "./ast-eval.ts";
|
|
22
|
-
import { defaultLeakBudget } from "./leak.ts";
|
|
23
|
-
import { leqAbs } from "./leq.ts";
|
|
24
|
-
import {
|
|
25
|
-
refineToIndexedFull,
|
|
26
|
-
extractRefineReturnFromSource,
|
|
27
|
-
type RefineEntry,
|
|
28
|
-
} from "./refine.ts";
|
|
29
|
-
import type { NudoConstraint, NudoField } from "./constraint.ts";
|
|
30
|
-
import { generalizeFromAst } from "./generalize.ts";
|
|
31
|
-
import { canSkipLiteralCallScan } from "./fn-fp.ts";
|
|
32
|
-
import { stableAnalyzeKeySource } from "./stable-source-key.ts";
|
|
33
|
-
import { hashSource, resetHashSourceCache } from "./hash-source.ts";
|
|
34
|
-
import {
|
|
35
|
-
extractAllLoadSpecs,
|
|
36
|
-
loadModuleDepsFingerprint,
|
|
37
|
-
normPath,
|
|
38
|
-
type LoadDepsFingerprint,
|
|
39
|
-
} from "./load-deps-fp.ts";
|
|
40
|
-
import { numLit, unknown, abs as makeAbs } from "./abs.ts";
|
|
41
|
-
import type { Abs } from "./abs.ts";
|
|
42
|
-
import type { Phi, Pred } from "./pred.ts";
|
|
43
|
-
import { pTrue, predToString } from "./pred.ts";
|
|
44
|
-
import { termToString } from "./term.ts";
|
|
45
|
-
import { litValue } from "./abs.ts";
|
|
46
|
-
import { formatAbs, formatAbsMultiline, formatShape } from "./format.ts";
|
|
47
|
-
import { checkCall } from "./diagnostics.ts";
|
|
48
|
-
import type { CheckIssue, CheckReport, NudoSig } from "./check-report.ts";
|
|
49
|
-
|
|
50
|
-
// --- 整文件 CheckReport memo(LSP/CI 重复 check → O(1)) ---
|
|
51
|
-
|
|
52
|
-
const checkReportMemo = new Map<string, CheckReport>();
|
|
53
|
-
const checkKeyDeps = new Map<string, string[]>();
|
|
54
|
-
const checkDepIndex = new Map<string, Set<string>>();
|
|
55
|
-
const loadModuleIds = new WeakMap<object, number>();
|
|
56
|
-
let nextLoadModuleId = 1;
|
|
57
|
-
const MAX_CHECK_MEMO = 256;
|
|
58
|
-
|
|
59
|
-
export function resetCheckSourceMemo(): void {
|
|
60
|
-
checkReportMemo.clear();
|
|
61
|
-
checkKeyDeps.clear();
|
|
62
|
-
checkDepIndex.clear();
|
|
63
|
-
resetHashSourceCache();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getCheckSourceMemoSize(): number {
|
|
67
|
-
return checkReportMemo.size;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function loadModuleId(fn?: (spec: string, fromFile: string) => string | undefined): number {
|
|
71
|
-
if (!fn) return 0;
|
|
72
|
-
let id = loadModuleIds.get(fn);
|
|
73
|
-
if (id === undefined) {
|
|
74
|
-
id = nextLoadModuleId++;
|
|
75
|
-
loadModuleIds.set(fn, id);
|
|
76
|
-
}
|
|
77
|
-
return id;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function checkDepsFingerprint(
|
|
81
|
-
source: string,
|
|
82
|
-
opts: CheckOptions,
|
|
83
|
-
): LoadDepsFingerprint {
|
|
84
|
-
if (!opts.loadModule || !opts.fromFile) {
|
|
85
|
-
return { fp: "-", paths: [], truncated: false };
|
|
86
|
-
}
|
|
87
|
-
return loadModuleDepsFingerprint(source, opts.loadModule, opts.fromFile);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function unindexCheckKey(key: string): void {
|
|
91
|
-
const deps = checkKeyDeps.get(key);
|
|
92
|
-
if (!deps) return;
|
|
93
|
-
for (const p of deps) {
|
|
94
|
-
const set = checkDepIndex.get(p);
|
|
95
|
-
if (!set) continue;
|
|
96
|
-
set.delete(key);
|
|
97
|
-
if (set.size === 0) checkDepIndex.delete(p);
|
|
98
|
-
}
|
|
99
|
-
checkKeyDeps.delete(key);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** `*.nudo.js` 变更后定向逐出依赖它的整文件 check 缓存 */
|
|
103
|
-
export function evictCheckSourceMemoForPaths(paths: string[]): number {
|
|
104
|
-
let n = 0;
|
|
105
|
-
for (const raw of paths) {
|
|
106
|
-
const p = normPath(raw);
|
|
107
|
-
const keys = checkDepIndex.get(p);
|
|
108
|
-
if (!keys) continue;
|
|
109
|
-
for (const key of [...keys]) {
|
|
110
|
-
checkReportMemo.delete(key);
|
|
111
|
-
unindexCheckKey(key);
|
|
112
|
-
n++;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return n;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function cloneCheckReport(r: CheckReport): CheckReport {
|
|
119
|
-
return {
|
|
120
|
-
file: r.file,
|
|
121
|
-
issues: r.issues.map((i) => ({ ...i })),
|
|
122
|
-
ok: r.ok,
|
|
123
|
-
signatures: r.signatures.map((s) => ({
|
|
124
|
-
...s,
|
|
125
|
-
params: [...s.params],
|
|
126
|
-
})),
|
|
127
|
-
summary: { ...r.summary },
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function checkMemoKey(
|
|
132
|
-
filePath: string,
|
|
133
|
-
source: string,
|
|
134
|
-
identityOpts: CheckOptions,
|
|
135
|
-
deps: LoadDepsFingerprint,
|
|
136
|
-
): string {
|
|
137
|
-
// identity must stay the caller's raw loadModule or every checkSource
|
|
138
|
-
// allocates a new loadModuleId and memo never hits.
|
|
139
|
-
return [
|
|
140
|
-
hashSource(source),
|
|
141
|
-
filePath,
|
|
142
|
-
`${loadModuleId(identityOpts.loadModule)}:${identityOpts.fromFile ?? ""}`,
|
|
143
|
-
deps.fp,
|
|
144
|
-
].join("|");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function checkMemoGet(key: string): CheckReport | null {
|
|
148
|
-
if (!checkReportMemo.has(key)) return null;
|
|
149
|
-
const v = checkReportMemo.get(key)!;
|
|
150
|
-
checkReportMemo.delete(key);
|
|
151
|
-
checkReportMemo.set(key, v);
|
|
152
|
-
return v;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function checkMemoSet(key: string, value: CheckReport, depPaths: string[]): void {
|
|
156
|
-
if (checkReportMemo.size >= MAX_CHECK_MEMO) {
|
|
157
|
-
const oldest = checkReportMemo.keys().next().value;
|
|
158
|
-
if (oldest !== undefined) {
|
|
159
|
-
checkReportMemo.delete(oldest);
|
|
160
|
-
unindexCheckKey(oldest);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
checkReportMemo.set(key, value);
|
|
164
|
-
if (depPaths.length > 0) {
|
|
165
|
-
checkKeyDeps.set(key, depPaths);
|
|
166
|
-
for (const p of depPaths) {
|
|
167
|
-
let set = checkDepIndex.get(p);
|
|
168
|
-
if (!set) {
|
|
169
|
-
set = new Set();
|
|
170
|
-
checkDepIndex.set(p, set);
|
|
171
|
-
}
|
|
172
|
-
set.add(key);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* check 选项:core 不碰 fs;host 用 loadModule 喂 require 目标源码。
|
|
179
|
-
*/
|
|
180
|
-
export type CheckOptions = {
|
|
181
|
-
/** 相对/绝对 require 说明符 → 模块源码;undefined = 解析失败 */
|
|
182
|
-
loadModule?: (spec: string, fromFile: string) => string | undefined;
|
|
183
|
-
/** 当前文件路径(供 loadModule 解析相对 spec) */
|
|
184
|
-
fromFile?: string;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
function listTopFunctions(source: string, file?: ReturnType<typeof parse>): string[] {
|
|
188
|
-
const f = file ?? parse(source);
|
|
189
|
-
const names: string[] = [];
|
|
190
|
-
for (const stmt of f.program.body) {
|
|
191
|
-
let decl: Node = stmt;
|
|
192
|
-
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
|
|
193
|
-
decl = stmt.declaration;
|
|
194
|
-
}
|
|
195
|
-
if (stmt.type === "ExportDefaultDeclaration" && stmt.declaration) {
|
|
196
|
-
decl = stmt.declaration;
|
|
197
|
-
}
|
|
198
|
-
if (decl.type === "FunctionDeclaration" && decl.id) names.push(decl.id.name);
|
|
199
|
-
if (decl.type === "VariableDeclaration") {
|
|
200
|
-
for (const d of decl.declarations) {
|
|
201
|
-
if (
|
|
202
|
-
d.id.type === "Identifier" &&
|
|
203
|
-
d.init &&
|
|
204
|
-
(d.init.type === "ArrowFunctionExpression" ||
|
|
205
|
-
d.init.type === "FunctionExpression")
|
|
206
|
-
) {
|
|
207
|
-
names.push(d.id.name);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return names;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* 检查一个文件:
|
|
217
|
-
* - 每个顶层函数 generalize;无法得到任何签名 → warning
|
|
218
|
-
* - 若 source 中有字面量调用 `f(负数)` 且 f 有 `>0` 约束 → error
|
|
219
|
-
* - entry 结果 partial/opaque → info
|
|
220
|
-
*
|
|
221
|
-
* 默认 Φ(pTrue)走整文件 memo:同 source + deps + loadModule 身份 → O(1)。
|
|
222
|
-
*/
|
|
223
|
-
export function checkSource(
|
|
224
|
-
filePath: string,
|
|
225
|
-
source: string,
|
|
226
|
-
phi: Phi = pTrue,
|
|
227
|
-
opts: CheckOptions = {},
|
|
228
|
-
): CheckReport {
|
|
229
|
-
// Per-call loadModule cache: dep fingerprint + scan/refine share one read.
|
|
230
|
-
// Identity for memo keys stays on the caller's raw loadModule — a fresh
|
|
231
|
-
// wrapper each call would make loadModuleId() unique and defeat the memo.
|
|
232
|
-
let callOpts = opts;
|
|
233
|
-
if (opts.loadModule && opts.fromFile) {
|
|
234
|
-
const loadCache = new Map<string, string | undefined>();
|
|
235
|
-
const raw = opts.loadModule;
|
|
236
|
-
const fromFile = opts.fromFile;
|
|
237
|
-
callOpts = {
|
|
238
|
-
...opts,
|
|
239
|
-
loadModule: (spec, from) => {
|
|
240
|
-
const k = `${from}\0${spec}`;
|
|
241
|
-
if (!loadCache.has(k)) loadCache.set(k, raw(spec, from));
|
|
242
|
-
return loadCache.get(k);
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const useMemo = phi.op === "true";
|
|
248
|
-
let memoKey: string | undefined;
|
|
249
|
-
// 尾部无 @nudo 的注释/空行不进键:comment-only 编辑复用 CheckReport
|
|
250
|
-
const stable = stableAnalyzeKeySource(source);
|
|
251
|
-
// 一次指纹:check 整文件 memo + 所有 generalize L0 共用(避免 per-fn 重读 dep)
|
|
252
|
-
const depsFp = checkDepsFingerprint(stable, callOpts);
|
|
253
|
-
// 截断指纹不可信:fail-open,整文件与 L0 都不 memo
|
|
254
|
-
const allowMemo = useMemo && !depsFp.truncated;
|
|
255
|
-
if (allowMemo) {
|
|
256
|
-
memoKey = checkMemoKey(filePath, stable, opts, depsFp);
|
|
257
|
-
const hit = checkMemoGet(memoKey);
|
|
258
|
-
if (hit) return cloneCheckReport(hit);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const issues: CheckIssue[] = [];
|
|
262
|
-
const signatures: NudoSig[] = [];
|
|
263
|
-
// 单次 parse:listTopFunctions / generalize / analyzeFn / scan 共用
|
|
264
|
-
const file = parse(source);
|
|
265
|
-
const names = listTopFunctions(source, file);
|
|
266
|
-
|
|
267
|
-
// 递归截断:与 TypeValue 的 nudo:recursion-truncated 对齐
|
|
268
|
-
const truncated = new Set<string>();
|
|
269
|
-
resetAbsCallBudget();
|
|
270
|
-
setAbsTruncationCollector((label) => truncated.add(label));
|
|
271
|
-
try {
|
|
272
|
-
const report = checkSourceInner(
|
|
273
|
-
filePath,
|
|
274
|
-
source,
|
|
275
|
-
file,
|
|
276
|
-
phi,
|
|
277
|
-
callOpts,
|
|
278
|
-
issues,
|
|
279
|
-
signatures,
|
|
280
|
-
names,
|
|
281
|
-
truncated,
|
|
282
|
-
opts,
|
|
283
|
-
depsFp,
|
|
284
|
-
);
|
|
285
|
-
if (memoKey) checkMemoSet(memoKey, report, depsFp.paths);
|
|
286
|
-
return cloneCheckReport(report);
|
|
287
|
-
} finally {
|
|
288
|
-
setAbsTruncationCollector(null);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function checkSourceInner(
|
|
293
|
-
filePath: string,
|
|
294
|
-
source: string,
|
|
295
|
-
file: ReturnType<typeof parse>,
|
|
296
|
-
phi: Phi,
|
|
297
|
-
opts: CheckOptions,
|
|
298
|
-
issues: CheckIssue[],
|
|
299
|
-
signatures: NudoSig[],
|
|
300
|
-
names: string[],
|
|
301
|
-
truncated: Set<string>,
|
|
302
|
-
identityOpts: CheckOptions = opts,
|
|
303
|
-
depsFp?: LoadDepsFingerprint,
|
|
304
|
-
): CheckReport {
|
|
305
|
-
// 整文件一次判定,避免 per-function includes 全文扫
|
|
306
|
-
const hasRefineDirective = source.includes("@nudo:refine");
|
|
307
|
-
// generalize L0 用调用方原始 loadModule 身份;opts 可能是 per-call I/O wrapper
|
|
308
|
-
const refineLoad = identityOpts.loadModule ?? opts.loadModule;
|
|
309
|
-
const refineFrom = identityOpts.fromFile ?? opts.fromFile ?? filePath;
|
|
310
|
-
for (const name of names) {
|
|
311
|
-
const g = generalizeFromAst(name, source, {
|
|
312
|
-
file,
|
|
313
|
-
refine: {
|
|
314
|
-
loadModule: refineLoad,
|
|
315
|
-
fromFile: refineFrom,
|
|
316
|
-
},
|
|
317
|
-
depsFp,
|
|
318
|
-
});
|
|
319
|
-
if (!g) {
|
|
320
|
-
issues.push({
|
|
321
|
-
severity: "warning",
|
|
322
|
-
code: "nudo:no-signature",
|
|
323
|
-
message: `${name}: 无法归纳符号 Abs`,
|
|
324
|
-
suggestion: "补 @nudo:case 或让函数体可代数求值",
|
|
325
|
-
fn: name,
|
|
326
|
-
});
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
// L0 命中时 symbolic 是稳定对象:格式化结果可 WeakMap 复用
|
|
330
|
-
const fmt = formatSigCached(g.symbolic, name);
|
|
331
|
-
signatures.push({
|
|
332
|
-
name,
|
|
333
|
-
params: g.params,
|
|
334
|
-
abs: g.symbolic,
|
|
335
|
-
display: fmt.display,
|
|
336
|
-
detail: fmt.detail,
|
|
337
|
-
conf: g.symbolic.conf,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// 后置:@nudo:refine return <constraint> —— 推断返回值 ⊭ 契约
|
|
341
|
-
if (hasRefineDirective) {
|
|
342
|
-
const ret = extractRefineReturnFromSource(source, name, {
|
|
343
|
-
loadModule: refineLoad,
|
|
344
|
-
fromFile: refineFrom,
|
|
345
|
-
});
|
|
346
|
-
if (ret) {
|
|
347
|
-
issues.push(
|
|
348
|
-
...checkReturnConstraint(name, ret.name, ret.constraint, g.symbolic),
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// generalize 已用同一入口实参求过 body;conf 确信时跳过重复 analyzeFn
|
|
354
|
-
// (after-edit 下 L0 命中 → 这里是 O(1),否则 400 函数会白跑 400 次)
|
|
355
|
-
if (g.symbolic.conf === "opaque" || g.symbolic.conf === "partial") {
|
|
356
|
-
const entryArgs = g.typeParams.map((t) => t.value);
|
|
357
|
-
try {
|
|
358
|
-
const r = analyzeFn(source, name, entryArgs, phi, undefined, file);
|
|
359
|
-
if (r.conf === "opaque" && !truncated.has(name)) {
|
|
360
|
-
issues.push({
|
|
361
|
-
severity: "info",
|
|
362
|
-
code: "nudo:opaque-result",
|
|
363
|
-
message: `${name}(...): conf=opaque(路径未覆盖或 native)`,
|
|
364
|
-
suggestion: "补 @nudo:case 或调用点",
|
|
365
|
-
fn: name,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
} catch (e) {
|
|
369
|
-
issues.push({
|
|
370
|
-
severity: "error",
|
|
371
|
-
code: "nudo:eval-error",
|
|
372
|
-
message: `${name}: 求值失败 — ${(e as Error).message}`,
|
|
373
|
-
fn: name,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
for (const label of truncated) {
|
|
380
|
-
issues.push({
|
|
381
|
-
severity: "warning",
|
|
382
|
-
code: "nudo:recursion-truncated",
|
|
383
|
-
message: `Recursive evaluation of '${label}' was truncated (depth/size budget); result widened to unknown`,
|
|
384
|
-
suggestion: "收窄递归基例或改用显式 @nudo:refine return 契约",
|
|
385
|
-
fn: label,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// 一次 evalProgramAbs:结构赋值记录 + 顶层绑定表(scanLiteralCalls 实参解析用)
|
|
390
|
-
const records: AbsAssignRecord[] = [];
|
|
391
|
-
const varAbs = new Map<string, Abs>();
|
|
392
|
-
setAbsAssignCollector((r) => records.push(r));
|
|
393
|
-
try {
|
|
394
|
-
const { env } = evalProgramAbs(source, { file });
|
|
395
|
-
for (const [k, v] of env.vars) varAbs.set(k, v);
|
|
396
|
-
} catch {
|
|
397
|
-
/* 求值失败:无赋值记录、无绑定表 */
|
|
398
|
-
} finally {
|
|
399
|
-
setAbsAssignCollector(null);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const callIssues = canSkipLiteralCallScan(source, file)
|
|
403
|
-
? []
|
|
404
|
-
: scanLiteralCalls(source, names, phi, {
|
|
405
|
-
loadModule: opts.loadModule,
|
|
406
|
-
fromFile: filePath,
|
|
407
|
-
file,
|
|
408
|
-
varAbs,
|
|
409
|
-
});
|
|
410
|
-
issues.push(...callIssues);
|
|
411
|
-
|
|
412
|
-
// case 是见证:@nudo:case 实参 ⊄ refine → inconsistency
|
|
413
|
-
issues.push(
|
|
414
|
-
...scanCaseInconsistency(source, names, {
|
|
415
|
-
loadModule: opts.loadModule,
|
|
416
|
-
fromFile: filePath,
|
|
417
|
-
file,
|
|
418
|
-
}),
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
// 结构可赋值:赋值语句 prev ⊇ next(Abs leq)
|
|
422
|
-
issues.push(...structuralAssignIssues(records));
|
|
423
|
-
|
|
424
|
-
const errors = issues.filter((i) => i.severity === "error").length;
|
|
425
|
-
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
426
|
-
const infos = issues.filter((i) => i.severity === "info").length;
|
|
427
|
-
|
|
428
|
-
return {
|
|
429
|
-
file: filePath,
|
|
430
|
-
issues,
|
|
431
|
-
ok: errors === 0,
|
|
432
|
-
signatures,
|
|
433
|
-
summary: { errors, warnings, infos, functions: signatures.length },
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function absUnknown(): Abs {
|
|
438
|
-
return { shape: { k: "unknown" }, conf: "partial" };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/** L0 命中的 symbolic 对象稳定:display/detail 按 Abs 身份缓存 */
|
|
442
|
-
const sigFormatCache = new WeakMap<Abs, { display: string; detail: string }>();
|
|
443
|
-
|
|
444
|
-
function formatSigCached(absVal: Abs, name: string): { display: string; detail: string } {
|
|
445
|
-
const hit = sigFormatCache.get(absVal);
|
|
446
|
-
if (hit) return hit;
|
|
447
|
-
const out = {
|
|
448
|
-
display: formatAbs(absVal),
|
|
449
|
-
detail: formatAbsMultiline(absVal, name),
|
|
450
|
-
};
|
|
451
|
-
sigFormatCache.set(absVal, out);
|
|
452
|
-
return out;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* 后置契约:推断返回 Abs ⊭ @nudo:refine return 声明。
|
|
457
|
-
* 只在有确定信息时报(字面量界 / prim 类型 / shape 缺字段)。
|
|
458
|
-
*/
|
|
459
|
-
function checkReturnConstraint(
|
|
460
|
-
fnName: string,
|
|
461
|
-
cName: string,
|
|
462
|
-
constraint: NudoConstraint,
|
|
463
|
-
ret: Abs,
|
|
464
|
-
): CheckIssue[] {
|
|
465
|
-
const out: CheckIssue[] = [];
|
|
466
|
-
// 无信息不猜
|
|
467
|
-
if (ret.shape.k === "unknown" && !ret.term) return out;
|
|
468
|
-
if (ret.shape.k === "any") return out;
|
|
469
|
-
|
|
470
|
-
const push = (actual: string, expected: string, suggestion: string): void => {
|
|
471
|
-
out.push({
|
|
472
|
-
severity: "error",
|
|
473
|
-
code: "nudo:constraint-violated",
|
|
474
|
-
message: `${fnName}: 返回值 ⊭ @nudo:refine return ${cName}`,
|
|
475
|
-
actual,
|
|
476
|
-
expected,
|
|
477
|
-
suggestion,
|
|
478
|
-
fn: fnName,
|
|
479
|
-
});
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
// shape 后置
|
|
483
|
-
if (constraint.fields) {
|
|
484
|
-
const slots =
|
|
485
|
-
ret.shape.k === "obj"
|
|
486
|
-
? (ret.shape as { slots: Record<string, { value: Abs; optional?: boolean }> }).slots
|
|
487
|
-
: undefined;
|
|
488
|
-
if (!slots) {
|
|
489
|
-
if (ret.shape.k !== "never") {
|
|
490
|
-
push(formatAbs(ret), `object shape (${cName})`, `返回满足 ${cName} 形状的 object`);
|
|
491
|
-
}
|
|
492
|
-
return out;
|
|
493
|
-
}
|
|
494
|
-
for (const [key, field] of Object.entries(constraint.fields) as Array<
|
|
495
|
-
[string, NudoField]
|
|
496
|
-
>) {
|
|
497
|
-
const slot = slots[key];
|
|
498
|
-
if (!slot) {
|
|
499
|
-
if (!field.optional && !field.constraint.isOptional) {
|
|
500
|
-
push(formatAbs(ret), `missing field ${key}`, `返回值补全字段 ${key}`);
|
|
501
|
-
}
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
// 字段 prim
|
|
505
|
-
if (field.constraint.prim && slot.value.shape.k === "prim") {
|
|
506
|
-
const actualPrim = (slot.value.shape as { type: string }).type;
|
|
507
|
-
if (actualPrim !== field.constraint.prim) {
|
|
508
|
-
push(
|
|
509
|
-
formatAbs(slot.value),
|
|
510
|
-
`typeof ${key} = "${field.constraint.prim}"`,
|
|
511
|
-
`把返回值的 ${key} 改成 ${field.constraint.prim}`,
|
|
512
|
-
);
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
// 字段数值界
|
|
517
|
-
const lv = litValue(slot.value);
|
|
518
|
-
if (lv !== undefined && typeof lv === "number") {
|
|
519
|
-
for (const p of field.constraint.preds) {
|
|
520
|
-
const flat = p.op === "and" ? p.args : [p];
|
|
521
|
-
for (const atom of flat) {
|
|
522
|
-
if (
|
|
523
|
-
(atom.op === "gt" || atom.op === "ge" || atom.op === "lt" || atom.op === "le") &&
|
|
524
|
-
atom.b.op === "lit" &&
|
|
525
|
-
typeof atom.b.value === "number"
|
|
526
|
-
) {
|
|
527
|
-
const n = atom.b.value;
|
|
528
|
-
let ok = true;
|
|
529
|
-
if (atom.op === "gt") ok = lv > n;
|
|
530
|
-
if (atom.op === "ge") ok = lv >= n;
|
|
531
|
-
if (atom.op === "lt") ok = lv < n;
|
|
532
|
-
if (atom.op === "le") ok = lv <= n;
|
|
533
|
-
if (!ok) {
|
|
534
|
-
const opSym = { gt: ">", ge: "≥", lt: "<", le: "≤" }[atom.op];
|
|
535
|
-
push(
|
|
536
|
-
formatAbs(slot.value),
|
|
537
|
-
`${key} ${opSym} ${n}`,
|
|
538
|
-
`返回值的 ${key} 应满足 ${key} ${opSym} ${n}`,
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
return out;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// 标量后置
|
|
550
|
-
if (constraint.prim && ret.shape.k === "prim") {
|
|
551
|
-
const actualPrim = (ret.shape as { type: string }).type;
|
|
552
|
-
if (actualPrim !== constraint.prim) {
|
|
553
|
-
push(formatAbs(ret), `typeof return = "${constraint.prim}"`, `返回 ${constraint.prim}`);
|
|
554
|
-
return out;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
const lv = litValue(ret);
|
|
558
|
-
if (lv !== undefined && typeof lv === "number") {
|
|
559
|
-
for (const p of constraint.preds) {
|
|
560
|
-
const flat = p.op === "and" ? p.args : [p];
|
|
561
|
-
for (const atom of flat) {
|
|
562
|
-
if (
|
|
563
|
-
(atom.op === "gt" || atom.op === "ge" || atom.op === "lt" || atom.op === "le") &&
|
|
564
|
-
atom.b.op === "lit" &&
|
|
565
|
-
typeof atom.b.value === "number"
|
|
566
|
-
) {
|
|
567
|
-
const n = atom.b.value;
|
|
568
|
-
let ok = true;
|
|
569
|
-
if (atom.op === "gt") ok = lv > n;
|
|
570
|
-
if (atom.op === "ge") ok = lv >= n;
|
|
571
|
-
if (atom.op === "lt") ok = lv < n;
|
|
572
|
-
if (atom.op === "le") ok = lv <= n;
|
|
573
|
-
if (!ok) {
|
|
574
|
-
const opSym = { gt: ">", ge: "≥", lt: "<", le: "≤" }[atom.op];
|
|
575
|
-
push(formatAbs(ret), `return ${opSym} ${n}`, `返回满足 ${opSym} ${n} 的值`);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return out;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* 结构可赋值:`let a = {x:1}; a = {y:2}` 应报 missing slot x。
|
|
586
|
-
* 输入为 evalProgramAbs 收集的赋值记录(与 scanLiteralCalls 共享一次求值)。
|
|
587
|
-
*/
|
|
588
|
-
function structuralAssignIssues(records: AbsAssignRecord[]): CheckIssue[] {
|
|
589
|
-
const out: CheckIssue[] = [];
|
|
590
|
-
for (const r of records) {
|
|
591
|
-
if (!r.prev) continue;
|
|
592
|
-
// 跳过 unknown / never 源(无信息)
|
|
593
|
-
if (r.next.shape.k === "unknown" && !r.next.term) continue;
|
|
594
|
-
if (r.prev.shape.k === "unknown" && !r.prev.term) continue;
|
|
595
|
-
const leq = leqAbs(r.next, r.prev);
|
|
596
|
-
if (!leq.ok) {
|
|
597
|
-
out.push({
|
|
598
|
-
severity: "error",
|
|
599
|
-
code: "nudo:assign-mismatch",
|
|
600
|
-
message: `${r.name}: 赋值 ⊭ 原有形状`,
|
|
601
|
-
actual: formatAbs(r.next),
|
|
602
|
-
expected: formatAbs(r.prev),
|
|
603
|
-
suggestion: leq.reason ?? "改用兼容的值,或放宽绑定类型",
|
|
604
|
-
fn: r.name,
|
|
605
|
-
line: r.line,
|
|
606
|
-
column: r.column,
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
return out;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* case 是契约的见证:`@nudo:case` 实参 ⊄ refine → nudo:case-inconsistency。
|
|
615
|
-
* 只检查字面量实参(数字/字符串/布尔/null);非字面量跳过,不猜。
|
|
616
|
-
*/
|
|
617
|
-
function scanCaseInconsistency(
|
|
618
|
-
source: string,
|
|
619
|
-
knownFns: string[],
|
|
620
|
-
opts: {
|
|
621
|
-
loadModule?: (spec: string, fromFile: string) => string | undefined;
|
|
622
|
-
fromFile?: string;
|
|
623
|
-
file?: ReturnType<typeof parse>;
|
|
624
|
-
},
|
|
625
|
-
): CheckIssue[] {
|
|
626
|
-
const out: CheckIssue[] = [];
|
|
627
|
-
// 快路径:无 case 指令则免整树 walk;无 refine 时 case 不可能 ⊄ 契约
|
|
628
|
-
if (!source.includes("@nudo:case")) return out;
|
|
629
|
-
if (!source.includes("@nudo:refine")) return out;
|
|
630
|
-
const file = opts.file ?? parse(source);
|
|
631
|
-
|
|
632
|
-
/** 解析 case 实参列表里的简单字面量 */
|
|
633
|
-
const parseLitArg = (s: string): Abs | undefined => {
|
|
634
|
-
const t = s.trim();
|
|
635
|
-
if (t === "") return undefined;
|
|
636
|
-
if (t === "true") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: true }, conf: "exact" };
|
|
637
|
-
if (t === "false") return { shape: { k: "prim", type: "boolean" }, term: { op: "lit", value: false }, conf: "exact" };
|
|
638
|
-
if (t === "null") return { shape: { k: "unknown" }, term: { op: "lit", value: null }, conf: "exact" };
|
|
639
|
-
if (t === "undefined") return { shape: { k: "unknown" }, term: { op: "lit", value: undefined }, conf: "exact" };
|
|
640
|
-
if (/^-?\d+(\.\d+)?$/.test(t)) return numLit(Number(t));
|
|
641
|
-
const str = t.match(/^(['"])([\s\S]*)\1$/);
|
|
642
|
-
if (str) {
|
|
643
|
-
return {
|
|
644
|
-
shape: { k: "prim", type: "string" },
|
|
645
|
-
term: { op: "lit", value: str[2]! },
|
|
646
|
-
conf: "exact",
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
return undefined;
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
/** 从 `@nudo:case "name" (a, b)` 抽实参原文 */
|
|
653
|
-
const parseCaseArgs = (raw: string): string[] | undefined => {
|
|
654
|
-
const m = raw.match(/@nudo:case\s+"[^"]+"\s*\(([\s\S]*)\)/);
|
|
655
|
-
if (!m) return undefined;
|
|
656
|
-
const inner = m[1]!.trim();
|
|
657
|
-
if (inner === "") return [];
|
|
658
|
-
// 顶层逗号切分(不处理嵌套对象/数组——那些不是字面量见证)
|
|
659
|
-
const parts: string[] = [];
|
|
660
|
-
let depth = 0;
|
|
661
|
-
let cur = "";
|
|
662
|
-
let quote: string | null = null;
|
|
663
|
-
for (let i = 0; i < inner.length; i++) {
|
|
664
|
-
const ch = inner[i]!;
|
|
665
|
-
if (quote) {
|
|
666
|
-
cur += ch;
|
|
667
|
-
if (ch === quote && inner[i - 1] !== "\\") quote = null;
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
if (ch === '"' || ch === "'") {
|
|
671
|
-
quote = ch;
|
|
672
|
-
cur += ch;
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
676
|
-
if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
677
|
-
if (ch === "," && depth === 0) {
|
|
678
|
-
parts.push(cur);
|
|
679
|
-
cur = "";
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
cur += ch;
|
|
683
|
-
}
|
|
684
|
-
if (cur.trim()) parts.push(cur);
|
|
685
|
-
return parts;
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
const checkCaseAgainstReqs = (
|
|
689
|
-
fnName: string,
|
|
690
|
-
caseName: string,
|
|
691
|
-
args: string[],
|
|
692
|
-
line: number | undefined,
|
|
693
|
-
): void => {
|
|
694
|
-
const g = generalizeFromAst(fnName, source, file ? { file } : {});
|
|
695
|
-
if (!g) return;
|
|
696
|
-
const paramNames = g.params;
|
|
697
|
-
const reqs = refineToIndexedFull(source, fnName, paramNames, {
|
|
698
|
-
loadModule: opts.loadModule,
|
|
699
|
-
fromFile: opts.fromFile ?? "",
|
|
700
|
-
});
|
|
701
|
-
if (reqs.length === 0) return;
|
|
702
|
-
|
|
703
|
-
const absArgs = args.map((a) => parseLitArg(a) ?? absUnknown());
|
|
704
|
-
// 标量界
|
|
705
|
-
for (const [idx, entry] of reqs) {
|
|
706
|
-
if (entry.constraint.fields) continue;
|
|
707
|
-
const arg = absArgs[idx];
|
|
708
|
-
if (!arg) continue;
|
|
709
|
-
const lv = litValue(arg);
|
|
710
|
-
if (lv === undefined || typeof lv !== "number") continue;
|
|
711
|
-
const flatten = (p: Pred): Pred[] => (p.op === "and" ? p.args.flatMap(flatten) : p.op === "true" ? [] : [p]);
|
|
712
|
-
for (const p of flatten(entry.pred)) {
|
|
713
|
-
if (
|
|
714
|
-
(p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") &&
|
|
715
|
-
p.b.op === "lit" &&
|
|
716
|
-
typeof p.b.value === "number"
|
|
717
|
-
) {
|
|
718
|
-
const n = p.b.value;
|
|
719
|
-
let ok = true;
|
|
720
|
-
if (p.op === "gt") ok = lv > n;
|
|
721
|
-
if (p.op === "ge") ok = lv >= n;
|
|
722
|
-
if (p.op === "lt") ok = lv < n;
|
|
723
|
-
if (p.op === "le") ok = lv <= n;
|
|
724
|
-
if (!ok) {
|
|
725
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
726
|
-
out.push({
|
|
727
|
-
severity: "error",
|
|
728
|
-
code: "nudo:case-inconsistency",
|
|
729
|
-
message: `${fnName} case "${caseName}": 见证 ⊭ 契约`,
|
|
730
|
-
actual: formatAbs(arg),
|
|
731
|
-
expected: predToString(p),
|
|
732
|
-
suggestion: `改 case 实参,或放宽 ${paramName} 的 refine`,
|
|
733
|
-
fn: fnName,
|
|
734
|
-
line,
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
const visit = (n: unknown): void => {
|
|
743
|
-
if (!n || typeof n !== "object") return;
|
|
744
|
-
const obj = n as Record<string, unknown> & {
|
|
745
|
-
type?: string;
|
|
746
|
-
leadingComments?: Array<{ value: string; loc?: { start: { line: number } } }>;
|
|
747
|
-
loc?: { start: { line: number } };
|
|
748
|
-
};
|
|
749
|
-
// 顶层函数声明上的 leading comments
|
|
750
|
-
let decl: Record<string, unknown> | undefined = obj;
|
|
751
|
-
if (obj.type === "ExportNamedDeclaration" || obj.type === "ExportDefaultDeclaration") {
|
|
752
|
-
decl = obj.declaration as Record<string, unknown> | undefined;
|
|
753
|
-
}
|
|
754
|
-
if (
|
|
755
|
-
decl &&
|
|
756
|
-
(decl.type === "FunctionDeclaration" ||
|
|
757
|
-
(decl.type === "VariableDeclaration" &&
|
|
758
|
-
((decl as { declarations?: Array<Record<string, unknown>> }).declarations ?? [])[0]?.init &&
|
|
759
|
-
["ArrowFunctionExpression", "FunctionExpression"].includes(
|
|
760
|
-
String(
|
|
761
|
-
((decl as { declarations: Array<Record<string, unknown>> }).declarations[0]!.init as { type?: string })
|
|
762
|
-
.type,
|
|
763
|
-
),
|
|
764
|
-
)))
|
|
765
|
-
) {
|
|
766
|
-
const id =
|
|
767
|
-
decl.type === "FunctionDeclaration"
|
|
768
|
-
? (decl.id as { name?: string } | undefined)?.name
|
|
769
|
-
: ((decl as { declarations: Array<{ id?: { name?: string } }> }).declarations[0]?.id as
|
|
770
|
-
| { name?: string }
|
|
771
|
-
| undefined)?.name;
|
|
772
|
-
if (id && knownFns.includes(id)) {
|
|
773
|
-
for (const c of obj.leadingComments ?? []) {
|
|
774
|
-
const caseArgs = parseCaseArgs(c.value);
|
|
775
|
-
if (!caseArgs) continue;
|
|
776
|
-
const caseName = /@nudo:case\s+"([^"]+)"/.exec(c.value)?.[1] ?? "?";
|
|
777
|
-
checkCaseAgainstReqs(id, caseName, caseArgs, c.loc?.start.line);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
for (const key of Object.keys(obj)) {
|
|
782
|
-
if (key === "loc" || key === "start" || key === "end" || key === "leadingComments") continue;
|
|
783
|
-
const val = obj[key];
|
|
784
|
-
if (Array.isArray(val)) val.forEach(visit);
|
|
785
|
-
else if (val && typeof val === "object") visit(val);
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
visit(file);
|
|
789
|
-
return out;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/** 从函数体抽参数必填 slot:`function f(p){ return p.x + p.y }` → {p: {x,y}} */
|
|
793
|
-
function collectParamStructReqs(
|
|
794
|
-
source: string,
|
|
795
|
-
fnName: string,
|
|
796
|
-
fileAst?: ReturnType<typeof parse>,
|
|
797
|
-
): Map<string, Set<string>> {
|
|
798
|
-
const reqs = new Map<string, Set<string>>();
|
|
799
|
-
const g = generalizeFromAst(fnName, source, fileAst ? { file: fileAst } : {});
|
|
800
|
-
if (!g) return reqs;
|
|
801
|
-
const params = new Set(g.params);
|
|
802
|
-
const file = fileAst ?? parse(source);
|
|
803
|
-
|
|
804
|
-
const visit = (n: unknown): void => {
|
|
805
|
-
if (!n || typeof n !== "object") return;
|
|
806
|
-
const obj = n as Record<string, unknown> & { type?: string };
|
|
807
|
-
if (obj.type === "MemberExpression" && !obj.computed) {
|
|
808
|
-
const o = obj.object as { type?: string; name?: string } | undefined;
|
|
809
|
-
const p = obj.property as { type?: string; name?: string } | undefined;
|
|
810
|
-
if (o?.type === "Identifier" && o.name && params.has(o.name) && p?.type === "Identifier" && p.name) {
|
|
811
|
-
let set = reqs.get(o.name);
|
|
812
|
-
if (!set) {
|
|
813
|
-
set = new Set();
|
|
814
|
-
reqs.set(o.name, set);
|
|
815
|
-
}
|
|
816
|
-
set.add(p.name);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
for (const key of Object.keys(obj)) {
|
|
820
|
-
if (key === "loc" || key === "start" || key === "end") continue;
|
|
821
|
-
const val = obj[key];
|
|
822
|
-
if (Array.isArray(val)) val.forEach(visit);
|
|
823
|
-
else if (val && typeof val === "object") visit(val);
|
|
824
|
-
}
|
|
825
|
-
};
|
|
826
|
-
visit(file);
|
|
827
|
-
return reqs;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
/** 静态求值实参节点 → Abs(标识符走绑定表) */
|
|
831
|
-
function evalArgAbs(
|
|
832
|
-
node: Record<string, unknown>,
|
|
833
|
-
lookupVar?: (name: string) => Abs | undefined,
|
|
834
|
-
): Abs | undefined {
|
|
835
|
-
if (node.type === "Identifier" && typeof node.name === "string" && lookupVar) {
|
|
836
|
-
return lookupVar(node.name);
|
|
837
|
-
}
|
|
838
|
-
try {
|
|
839
|
-
const env = emptyEnv();
|
|
840
|
-
return evalNode(node as unknown as Node, env, pTrue, defaultLeakBudget).value;
|
|
841
|
-
} catch {
|
|
842
|
-
return undefined;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
/** 扫描前收集:别名 / 对象属性 / require 导入 */
|
|
847
|
-
type CallResolve = {
|
|
848
|
-
/** 本地名 → 真实函数名(同文件) */
|
|
849
|
-
aliasToFn: Map<string, string>;
|
|
850
|
-
/** 对象名.属性名 → 真实函数名(同文件) */
|
|
851
|
-
memberToFn: Map<string, string>;
|
|
852
|
-
/** 本地名 → 外部模块函数(源码 + 导出名) */
|
|
853
|
-
externalFn: Map<string, { source: string; fnName: string }>;
|
|
854
|
-
/** 对象名.属性名 → 外部模块函数 */
|
|
855
|
-
externalMember: Map<string, { source: string; fnName: string }>;
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
function requireSpecOf(init: Record<string, unknown>): string | undefined {
|
|
859
|
-
// require('./x')
|
|
860
|
-
if (init.type === "CallExpression") {
|
|
861
|
-
const callee = init.callee as { type?: string; name?: string } | undefined;
|
|
862
|
-
const args = init.arguments as Array<{ type?: string; value?: unknown }> | undefined;
|
|
863
|
-
if (
|
|
864
|
-
callee?.type === "Identifier" &&
|
|
865
|
-
callee.name === "require" &&
|
|
866
|
-
args?.[0]?.type === "StringLiteral"
|
|
867
|
-
) {
|
|
868
|
-
return String(args[0].value);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
// require('./x').fn
|
|
872
|
-
if (init.type === "MemberExpression") {
|
|
873
|
-
const obj = init.object as Record<string, unknown> | undefined;
|
|
874
|
-
if (obj) return requireSpecOf(obj);
|
|
875
|
-
}
|
|
876
|
-
return undefined;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function requireExportName(init: Record<string, unknown>): string | undefined {
|
|
880
|
-
// require('./x').fn
|
|
881
|
-
if (init.type === "MemberExpression" && !init.computed) {
|
|
882
|
-
const prop = init.property as { type?: string; name?: string } | undefined;
|
|
883
|
-
if (prop?.type === "Identifier") return prop.name;
|
|
884
|
-
}
|
|
885
|
-
return undefined;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/** `await import('./m')` / `import('./m')` → spec */
|
|
889
|
-
function dynamicImportSpec(node: Record<string, unknown>): string | undefined {
|
|
890
|
-
let n: Record<string, unknown> | undefined = node;
|
|
891
|
-
if (n?.type === "AwaitExpression") {
|
|
892
|
-
n = n.argument as Record<string, unknown> | undefined;
|
|
893
|
-
}
|
|
894
|
-
if (n?.type !== "CallExpression") return undefined;
|
|
895
|
-
const callee = n.callee as { type?: string } | undefined;
|
|
896
|
-
// Babel: dynamic import callee.type === "Import"
|
|
897
|
-
if (callee?.type !== "Import") return undefined;
|
|
898
|
-
const args = n.arguments as Array<{ type?: string; value?: unknown }> | undefined;
|
|
899
|
-
if (args?.[0]?.type === "StringLiteral") return String(args[0].value);
|
|
900
|
-
return undefined;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* 模块源码里找不到 fnName 时,沿 `export { fn } from './other'` / `export * from` 一跳跟进。
|
|
905
|
-
* 返回定义了该函数的源码。
|
|
906
|
-
*/
|
|
907
|
-
function resolveExportSource(
|
|
908
|
-
modSrc: string,
|
|
909
|
-
fnName: string,
|
|
910
|
-
loadSpec: (spec: string) => string | undefined,
|
|
911
|
-
depth = 0,
|
|
912
|
-
): string {
|
|
913
|
-
if (depth > 3) return modSrc;
|
|
914
|
-
try {
|
|
915
|
-
if (listTopFunctions(modSrc).includes(fnName)) return modSrc;
|
|
916
|
-
} catch {
|
|
917
|
-
return modSrc;
|
|
918
|
-
}
|
|
919
|
-
const file = parse(modSrc);
|
|
920
|
-
let nextSpec: string | undefined;
|
|
921
|
-
for (const stmt of file.program.body) {
|
|
922
|
-
if (stmt.type !== "ExportNamedDeclaration" && stmt.type !== "ExportAllDeclaration") continue;
|
|
923
|
-
const src = (stmt as { source?: { type?: string; value?: unknown } }).source;
|
|
924
|
-
if (src?.type !== "StringLiteral" || typeof src.value !== "string") continue;
|
|
925
|
-
if (stmt.type === "ExportAllDeclaration") {
|
|
926
|
-
nextSpec = String(src.value);
|
|
927
|
-
break;
|
|
928
|
-
}
|
|
929
|
-
const clause = ((stmt as { specifiers?: unknown[] }).specifiers ?? []) as Array<Record<string, unknown>>;
|
|
930
|
-
for (const sp of clause) {
|
|
931
|
-
if (sp.type !== "ExportSpecifier") continue;
|
|
932
|
-
const local = sp.local as { type?: string; name?: string } | undefined;
|
|
933
|
-
if (local?.type === "Identifier" && local.name === fnName) {
|
|
934
|
-
nextSpec = String(src.value);
|
|
935
|
-
break;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
if (nextSpec) break;
|
|
939
|
-
}
|
|
940
|
-
if (!nextSpec) return modSrc;
|
|
941
|
-
const next = loadSpec(nextSpec);
|
|
942
|
-
if (!next) return modSrc;
|
|
943
|
-
return resolveExportSource(next, fnName, loadSpec, depth + 1);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
/** 只递归可能含 Import/VariableDeclaration 的语句容器(resolvers/forwarders 用) */
|
|
947
|
-
const STMT_CONTAINER = new Set([
|
|
948
|
-
"File",
|
|
949
|
-
"Program",
|
|
950
|
-
"BlockStatement",
|
|
951
|
-
"FunctionDeclaration",
|
|
952
|
-
"FunctionExpression",
|
|
953
|
-
"ArrowFunctionExpression",
|
|
954
|
-
"IfStatement",
|
|
955
|
-
"ForStatement",
|
|
956
|
-
"ForInStatement",
|
|
957
|
-
"ForOfStatement",
|
|
958
|
-
"WhileStatement",
|
|
959
|
-
"DoWhileStatement",
|
|
960
|
-
"SwitchStatement",
|
|
961
|
-
"SwitchCase",
|
|
962
|
-
"TryStatement",
|
|
963
|
-
"CatchClause",
|
|
964
|
-
"LabeledStatement",
|
|
965
|
-
"ExportNamedDeclaration",
|
|
966
|
-
"ExportDefaultDeclaration",
|
|
967
|
-
"ExportAllDeclaration",
|
|
968
|
-
"ClassDeclaration",
|
|
969
|
-
"ClassBody",
|
|
970
|
-
"ClassMethod",
|
|
971
|
-
"StaticBlock",
|
|
972
|
-
"ObjectMethod",
|
|
973
|
-
]);
|
|
974
|
-
|
|
975
|
-
function walkStatements(n: unknown, onStmt: (obj: Record<string, unknown> & { type?: string }) => void): void {
|
|
976
|
-
if (!n || typeof n !== "object") return;
|
|
977
|
-
const obj = n as Record<string, unknown> & { type?: string };
|
|
978
|
-
if (typeof obj.type === "string") onStmt(obj);
|
|
979
|
-
if (!STMT_CONTAINER.has(String(obj.type))) return;
|
|
980
|
-
for (const key of Object.keys(obj)) {
|
|
981
|
-
if (key === "loc" || key === "start" || key === "end" || key === "leadingComments") continue;
|
|
982
|
-
const val = obj[key];
|
|
983
|
-
if (Array.isArray(val)) val.forEach((x) => walkStatements(x, onStmt));
|
|
984
|
-
else if (val && typeof val === "object") walkStatements(val, onStmt);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function collectCallResolvers(
|
|
989
|
-
source: string,
|
|
990
|
-
knownFns: string[],
|
|
991
|
-
opts?: {
|
|
992
|
-
loadModule?: (spec: string, fromFile: string) => string | undefined;
|
|
993
|
-
fromFile?: string;
|
|
994
|
-
file?: ReturnType<typeof parse>;
|
|
995
|
-
},
|
|
996
|
-
): CallResolve {
|
|
997
|
-
const knownSet = new Set(knownFns);
|
|
998
|
-
const aliasToFn = new Map<string, string>();
|
|
999
|
-
const memberToFn = new Map<string, string>();
|
|
1000
|
-
const externalFn = new Map<string, { source: string; fnName: string }>();
|
|
1001
|
-
const externalMember = new Map<string, { source: string; fnName: string }>();
|
|
1002
|
-
const file = opts?.file ?? parse(source);
|
|
1003
|
-
const load = opts?.loadModule;
|
|
1004
|
-
const fromFile = opts?.fromFile ?? "";
|
|
1005
|
-
const modCache = new Map<string, string | undefined>();
|
|
1006
|
-
const loadSpec = (spec: string): string | undefined => {
|
|
1007
|
-
if (!load) return undefined;
|
|
1008
|
-
if (!modCache.has(spec)) modCache.set(spec, load(spec, fromFile));
|
|
1009
|
-
return modCache.get(spec);
|
|
1010
|
-
};
|
|
1011
|
-
const bindExternal = (local: string, modSrc: string, fnName: string): void => {
|
|
1012
|
-
externalFn.set(local, {
|
|
1013
|
-
source: resolveExportSource(modSrc, fnName, loadSpec),
|
|
1014
|
-
fnName,
|
|
1015
|
-
});
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
const visit = (n: unknown): void => {
|
|
1019
|
-
if (!n || typeof n !== "object") return;
|
|
1020
|
-
const obj = n as Record<string, unknown> & { type?: string };
|
|
1021
|
-
|
|
1022
|
-
// ESM:import { fn } / import { fn as x } / import * as ns from '...'
|
|
1023
|
-
if (obj.type === "ImportDeclaration") {
|
|
1024
|
-
const spec = obj.source as { type?: string; value?: unknown } | undefined;
|
|
1025
|
-
if (spec?.type === "StringLiteral" && typeof spec.value === "string") {
|
|
1026
|
-
const modSrc = loadSpec(String(spec.value));
|
|
1027
|
-
if (modSrc) {
|
|
1028
|
-
for (const sp of (obj.specifiers as Array<Record<string, unknown>> | undefined) ?? []) {
|
|
1029
|
-
if (sp.type === "ImportSpecifier") {
|
|
1030
|
-
const imported = sp.imported as { type?: string; name?: string; value?: unknown } | undefined;
|
|
1031
|
-
const local = sp.local as { type?: string; name?: string } | undefined;
|
|
1032
|
-
const exportName =
|
|
1033
|
-
imported?.type === "Identifier"
|
|
1034
|
-
? imported.name
|
|
1035
|
-
: imported?.type === "StringLiteral"
|
|
1036
|
-
? String(imported.value)
|
|
1037
|
-
: undefined;
|
|
1038
|
-
if (exportName && local?.name) {
|
|
1039
|
-
bindExternal(local.name, modSrc, exportName);
|
|
1040
|
-
}
|
|
1041
|
-
} else if (sp.type === "ImportNamespaceSpecifier") {
|
|
1042
|
-
const local = sp.local as { type?: string; name?: string } | undefined;
|
|
1043
|
-
if (local?.name) {
|
|
1044
|
-
externalMember.set(`${local.name}.__module__`, { source: modSrc, fnName: "" });
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
// ImportDefaultSpecifier:默认导出名不定,暂不绑定
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (obj.type === "VariableDeclaration") {
|
|
1054
|
-
for (const d of (obj.declarations as Array<Record<string, unknown>> | undefined) ?? []) {
|
|
1055
|
-
const id = d.id as { type?: string; name?: string; properties?: Array<Record<string, unknown>> };
|
|
1056
|
-
const init = d.init as Record<string, unknown> | null | undefined;
|
|
1057
|
-
if (!id || !init) continue;
|
|
1058
|
-
|
|
1059
|
-
// 动态 import:const m = await import('./x') / const { fn } = await import('./x')
|
|
1060
|
-
const dynSpec = dynamicImportSpec(init);
|
|
1061
|
-
if (dynSpec) {
|
|
1062
|
-
const modSrc = loadSpec(dynSpec);
|
|
1063
|
-
if (modSrc && id.type === "ObjectPattern") {
|
|
1064
|
-
for (const p of id.properties ?? []) {
|
|
1065
|
-
const key = p.key as { type?: string; name?: string; value?: unknown } | undefined;
|
|
1066
|
-
const val = p.value as { type?: string; name?: string } | undefined;
|
|
1067
|
-
const exported =
|
|
1068
|
-
key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? String(key.value) : undefined;
|
|
1069
|
-
const local = val?.type === "Identifier" ? val.name : exported;
|
|
1070
|
-
if (exported && local) bindExternal(local, modSrc, exported);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
if (modSrc && id.type === "Identifier" && id.name) {
|
|
1074
|
-
externalMember.set(`${id.name}.__module__`, { source: modSrc, fnName: "" });
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// require 导入
|
|
1079
|
-
const spec = requireSpecOf(init);
|
|
1080
|
-
if (spec) {
|
|
1081
|
-
const modSrc = loadSpec(spec);
|
|
1082
|
-
// const { fn } = require(...)
|
|
1083
|
-
if (id.type === "ObjectPattern" && modSrc) {
|
|
1084
|
-
for (const p of id.properties ?? []) {
|
|
1085
|
-
const key = p.key as { type?: string; name?: string; value?: unknown } | undefined;
|
|
1086
|
-
const val = p.value as { type?: string; name?: string } | undefined;
|
|
1087
|
-
const exported =
|
|
1088
|
-
key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? String(key.value) : undefined;
|
|
1089
|
-
const local = val?.type === "Identifier" ? val.name : exported;
|
|
1090
|
-
if (exported && local) {
|
|
1091
|
-
bindExternal(local, modSrc, exported);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
// const m = require(...) → m.fn
|
|
1096
|
-
if (id.type === "Identifier" && id.name && modSrc) {
|
|
1097
|
-
const exportName = requireExportName(init);
|
|
1098
|
-
if (exportName) {
|
|
1099
|
-
bindExternal(id.name, modSrc, exportName);
|
|
1100
|
-
} else {
|
|
1101
|
-
externalMember.set(`${id.name}.__module__`, { source: modSrc, fnName: "" });
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
if (id.type !== "Identifier" || !id.name) continue;
|
|
1107
|
-
// const f = needsPositive
|
|
1108
|
-
if (init.type === "Identifier" && typeof init.name === "string" && knownSet.has(init.name)) {
|
|
1109
|
-
aliasToFn.set(id.name, init.name);
|
|
1110
|
-
}
|
|
1111
|
-
// const api = { needsPositive }
|
|
1112
|
-
if (init.type === "ObjectExpression") {
|
|
1113
|
-
for (const p of (init.properties as Array<Record<string, unknown>> | undefined) ?? []) {
|
|
1114
|
-
if (p.type !== "ObjectProperty") continue;
|
|
1115
|
-
const key = p.key as { type?: string; name?: string; value?: unknown };
|
|
1116
|
-
const value = p.value as { type?: string; name?: string } | undefined;
|
|
1117
|
-
if (value?.type === "Identifier" && value.name && knownSet.has(value.name)) {
|
|
1118
|
-
const propKey =
|
|
1119
|
-
key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? String(key.value) : undefined;
|
|
1120
|
-
if (propKey) memberToFn.set(`${id.name}.${propKey}`, value.name);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
};
|
|
1127
|
-
// 语句容器 walk:不进表达式树,O(语句) 而非 O(全部节点)
|
|
1128
|
-
walkStatements(file, visit);
|
|
1129
|
-
return { aliasToFn, memberToFn, externalFn, externalMember };
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
/** 解析 callee → 同文件名或外部模块描述 */
|
|
1133
|
-
function resolveCalleeFn(
|
|
1134
|
-
callee: Record<string, unknown>,
|
|
1135
|
-
resolve: CallResolve,
|
|
1136
|
-
knownFns: Set<string>,
|
|
1137
|
-
): string | { external: { source: string; fnName: string } } | undefined {
|
|
1138
|
-
if (callee.type === "Identifier" && typeof callee.name === "string") {
|
|
1139
|
-
const ext = resolve.externalFn.get(callee.name);
|
|
1140
|
-
if (ext) return { external: ext };
|
|
1141
|
-
const aliased = resolve.aliasToFn.get(callee.name);
|
|
1142
|
-
if (aliased) return aliased;
|
|
1143
|
-
if (knownFns.has(callee.name)) return callee.name;
|
|
1144
|
-
return undefined;
|
|
1145
|
-
}
|
|
1146
|
-
if (callee.type === "MemberExpression") {
|
|
1147
|
-
const obj = callee.object as { type?: string; name?: string } | undefined;
|
|
1148
|
-
const prop = callee.property as { type?: string; name?: string } | undefined;
|
|
1149
|
-
if (
|
|
1150
|
-
!callee.computed &&
|
|
1151
|
-
obj?.type === "Identifier" &&
|
|
1152
|
-
obj.name &&
|
|
1153
|
-
prop?.type === "Identifier" &&
|
|
1154
|
-
prop.name
|
|
1155
|
-
) {
|
|
1156
|
-
const key = `${obj.name}.${prop.name}`;
|
|
1157
|
-
const extM = resolve.externalMember.get(key);
|
|
1158
|
-
if (extM) return { external: extM };
|
|
1159
|
-
// m = require(...);调用 m.fn
|
|
1160
|
-
const mod = resolve.externalMember.get(`${obj.name}.__module__`);
|
|
1161
|
-
if (mod?.source) {
|
|
1162
|
-
return { external: { source: mod.source, fnName: prop.name } };
|
|
1163
|
-
}
|
|
1164
|
-
const local = resolve.memberToFn.get(key);
|
|
1165
|
-
if (local) return local;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
return undefined;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/**
|
|
1172
|
-
* 无条件转发:`function w(a,b){ return target(a,b); }`
|
|
1173
|
-
* 或箭头 `const w = (a) => target(a)`。
|
|
1174
|
-
* map[i] = wrapper 第 i 参 → target 的第几参。
|
|
1175
|
-
*/
|
|
1176
|
-
type Forward = { target: string; map: number[] };
|
|
1177
|
-
|
|
1178
|
-
function collectForwarders(
|
|
1179
|
-
source: string,
|
|
1180
|
-
knownFns: Set<string>,
|
|
1181
|
-
resolve: CallResolve,
|
|
1182
|
-
fileAst?: ReturnType<typeof parse>,
|
|
1183
|
-
): Map<string, Forward> {
|
|
1184
|
-
const forwards = new Map<string, Forward>();
|
|
1185
|
-
const file = fileAst ?? parse(source);
|
|
1186
|
-
|
|
1187
|
-
const tryFn = (
|
|
1188
|
-
name: string,
|
|
1189
|
-
params: Array<{ type?: string; name?: string }>,
|
|
1190
|
-
body: Record<string, unknown> | undefined,
|
|
1191
|
-
): void => {
|
|
1192
|
-
if (!name || !params.length) return;
|
|
1193
|
-
const paramNames = params.map((p) => (p?.type === "Identifier" ? p.name : undefined));
|
|
1194
|
-
if (paramNames.some((n) => !n)) return;
|
|
1195
|
-
|
|
1196
|
-
// body: BlockStatement 仅一条 ReturnStatement(CallExpression)
|
|
1197
|
-
// 或箭头表达式体 CallExpression
|
|
1198
|
-
let call: Record<string, unknown> | undefined;
|
|
1199
|
-
if (body?.type === "BlockStatement") {
|
|
1200
|
-
const stmts = (body.body as Array<Record<string, unknown>> | undefined) ?? [];
|
|
1201
|
-
if (stmts.length !== 1 || stmts[0]!.type !== "ReturnStatement") return;
|
|
1202
|
-
const arg = stmts[0]!.argument as Record<string, unknown> | undefined;
|
|
1203
|
-
if (arg?.type !== "CallExpression") return;
|
|
1204
|
-
call = arg;
|
|
1205
|
-
} else if (body?.type === "CallExpression") {
|
|
1206
|
-
call = body;
|
|
1207
|
-
}
|
|
1208
|
-
if (!call) return;
|
|
1209
|
-
|
|
1210
|
-
const resolved = resolveCalleeFn(call.callee as Record<string, unknown>, resolve, knownFns);
|
|
1211
|
-
if (!resolved || typeof resolved !== "string") return;
|
|
1212
|
-
if (!knownFns.has(resolved) || resolved === name) return;
|
|
1213
|
-
|
|
1214
|
-
const args = (call.arguments as Array<Record<string, unknown>> | undefined) ?? [];
|
|
1215
|
-
if (args.length === 0) return;
|
|
1216
|
-
// 每个实参必须是 wrapper 自己的参数标识符
|
|
1217
|
-
const map: number[] = [];
|
|
1218
|
-
for (const a of args) {
|
|
1219
|
-
if (a.type !== "Identifier" || typeof a.name !== "string") return;
|
|
1220
|
-
const idx = paramNames.indexOf(a.name);
|
|
1221
|
-
if (idx < 0) return;
|
|
1222
|
-
map.push(idx);
|
|
1223
|
-
}
|
|
1224
|
-
forwards.set(name, { target: resolved, map });
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
walkStatements(file, (obj) => {
|
|
1228
|
-
if (obj.type === "FunctionDeclaration") {
|
|
1229
|
-
const id = obj.id as { name?: string } | undefined;
|
|
1230
|
-
tryFn(
|
|
1231
|
-
id?.name ?? "",
|
|
1232
|
-
(obj.params as Array<{ type?: string; name?: string }>) ?? [],
|
|
1233
|
-
obj.body as Record<string, unknown>,
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
if (obj.type === "VariableDeclaration") {
|
|
1237
|
-
for (const d of (obj.declarations as Array<Record<string, unknown>> | undefined) ?? []) {
|
|
1238
|
-
const id = d.id as { type?: string; name?: string };
|
|
1239
|
-
const init = d.init as Record<string, unknown> | null | undefined;
|
|
1240
|
-
if (
|
|
1241
|
-
id?.type === "Identifier" &&
|
|
1242
|
-
id.name &&
|
|
1243
|
-
init &&
|
|
1244
|
-
(init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
|
|
1245
|
-
) {
|
|
1246
|
-
tryFn(
|
|
1247
|
-
id.name,
|
|
1248
|
-
(init.params as Array<{ type?: string; name?: string }>) ?? [],
|
|
1249
|
-
init.body as Record<string, unknown>,
|
|
1250
|
-
);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
});
|
|
1255
|
-
return forwards;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
/** 找 `name(literalArgs)` / `alias(lit)` / `obj.fn(lit)` / require 导入,检查约束 */
|
|
1259
|
-
function scanLiteralCalls(
|
|
1260
|
-
source: string,
|
|
1261
|
-
knownFns: string[],
|
|
1262
|
-
phi: Phi,
|
|
1263
|
-
opts?: {
|
|
1264
|
-
loadModule?: (spec: string, fromFile: string) => string | undefined;
|
|
1265
|
-
fromFile?: string;
|
|
1266
|
-
file?: ReturnType<typeof parse>;
|
|
1267
|
-
/** 顶层绑定表(与结构赋值共享的 evalProgramAbs 结果) */
|
|
1268
|
-
varAbs?: Map<string, Abs>;
|
|
1269
|
-
},
|
|
1270
|
-
): CheckIssue[] {
|
|
1271
|
-
const out: CheckIssue[] = [];
|
|
1272
|
-
const file = opts?.file ?? parse(source);
|
|
1273
|
-
const knownSet = new Set(knownFns);
|
|
1274
|
-
const resolve = collectCallResolvers(source, knownFns, opts);
|
|
1275
|
-
const forwards = collectForwarders(source, knownSet, resolve, file);
|
|
1276
|
-
const varAbs = opts?.varAbs ?? new Map<string, Abs>();
|
|
1277
|
-
|
|
1278
|
-
const flattenPred = (p: Pred): Pred[] =>
|
|
1279
|
-
p.op === "and" ? p.args.flatMap(flattenPred) : p.op === "true" ? [] : [p];
|
|
1280
|
-
|
|
1281
|
-
const checkReqs = (
|
|
1282
|
-
displayName: string,
|
|
1283
|
-
reqs: Array<[number, import("./pred.ts").Pred]>,
|
|
1284
|
-
paramNames: string[],
|
|
1285
|
-
absArgs: Abs[],
|
|
1286
|
-
/** target 参下标 → 实参下标;缺省恒等 */
|
|
1287
|
-
argIndexOf: (reqIdx: number) => number | undefined,
|
|
1288
|
-
loc?: { start: { line: number; column: number } },
|
|
1289
|
-
): void => {
|
|
1290
|
-
for (const [idx, pred] of reqs) {
|
|
1291
|
-
const argIdx = argIndexOf(idx);
|
|
1292
|
-
if (argIdx === undefined) continue;
|
|
1293
|
-
const arg = absArgs[argIdx];
|
|
1294
|
-
if (!arg) continue;
|
|
1295
|
-
const lv = litValue(arg);
|
|
1296
|
-
if (lv === undefined) continue;
|
|
1297
|
-
const isStr = arg.shape.k === "prim" && (arg.shape as { type: string }).type === "string";
|
|
1298
|
-
const strLen = typeof lv === "string" ? lv.length : undefined;
|
|
1299
|
-
for (const p of flattenPred(pred)) {
|
|
1300
|
-
if (
|
|
1301
|
-
(p.op === "gt" || p.op === "ge" || p.op === "lt" || p.op === "le") &&
|
|
1302
|
-
p.b.op === "lit" &&
|
|
1303
|
-
typeof p.b.value === "number"
|
|
1304
|
-
) {
|
|
1305
|
-
const n = p.b.value;
|
|
1306
|
-
const opSym = { gt: ">", ge: "≥", lt: "<", le: "≤" }[p.op];
|
|
1307
|
-
// length(t) 形式(string().min/max)
|
|
1308
|
-
if (p.a.op === "app" && p.a.fn === "length" && strLen !== undefined) {
|
|
1309
|
-
let ok = true;
|
|
1310
|
-
if (p.op === "gt") ok = strLen > n;
|
|
1311
|
-
if (p.op === "ge") ok = strLen >= n;
|
|
1312
|
-
if (p.op === "lt") ok = strLen < n;
|
|
1313
|
-
if (p.op === "le") ok = strLen <= n;
|
|
1314
|
-
if (!ok) {
|
|
1315
|
-
const paramName = paramNames[idx] ?? `arg${idx}`;
|
|
1316
|
-
out.push({
|
|
1317
|
-
severity: "error",
|
|
1318
|
-
code: "nudo:constraint-violated",
|
|
1319
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1320
|
-
actual: formatAbs(arg),
|
|
1321
|
-
expected: `length(${paramName}) ${opSym} ${n}`,
|
|
1322
|
-
suggestion: `改用满足长度 ${opSym} ${n} 的值,或放宽 ${paramName} 的前置`,
|
|
1323
|
-
fn: displayName,
|
|
1324
|
-
line: loc?.start.line,
|
|
1325
|
-
column: loc?.start.column,
|
|
1326
|
-
});
|
|
1327
|
-
}
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
// 普通数值界
|
|
1331
|
-
if (isStr || typeof lv !== "number") continue;
|
|
1332
|
-
let ok = true;
|
|
1333
|
-
if (p.op === "gt") ok = (lv as number) > n;
|
|
1334
|
-
if (p.op === "ge") ok = (lv as number) >= n;
|
|
1335
|
-
if (p.op === "lt") ok = (lv as number) < n;
|
|
1336
|
-
if (p.op === "le") ok = (lv as number) <= n;
|
|
1337
|
-
if (!ok) {
|
|
1338
|
-
const paramName = paramNames[idx] ?? `arg${idx}`;
|
|
1339
|
-
out.push({
|
|
1340
|
-
severity: "error",
|
|
1341
|
-
code: "nudo:constraint-violated",
|
|
1342
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1343
|
-
actual: formatAbs(arg),
|
|
1344
|
-
expected: predToString(p),
|
|
1345
|
-
suggestion: `改用满足 ${predToString(p)} 的值,或放宽 ${paramName} 的前置`,
|
|
1346
|
-
fn: displayName,
|
|
1347
|
-
line: loc?.start.line,
|
|
1348
|
-
column: loc?.start.column,
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
};
|
|
1355
|
-
|
|
1356
|
-
/**
|
|
1357
|
-
* shape 约束:实参 object Abs 的每个字段 ⊭ 嵌套约束。
|
|
1358
|
-
* 缺字段 / 类型不符 / 数值界违例 → nudo:constraint-violated。
|
|
1359
|
-
*/
|
|
1360
|
-
const checkShapeAgainstAbs = (
|
|
1361
|
-
displayName: string,
|
|
1362
|
-
paramName: string,
|
|
1363
|
-
constraint: NudoConstraint,
|
|
1364
|
-
absArg: Abs,
|
|
1365
|
-
path: string,
|
|
1366
|
-
loc?: { start: { line: number; column: number } },
|
|
1367
|
-
): void => {
|
|
1368
|
-
if (!constraint.fields) return;
|
|
1369
|
-
// unknown 实参:无信息,不猜
|
|
1370
|
-
if (absArg.shape.k === "unknown" && !absArg.term) return;
|
|
1371
|
-
|
|
1372
|
-
const slots =
|
|
1373
|
-
absArg.shape.k === "obj"
|
|
1374
|
-
? (absArg.shape as { slots: Record<string, { value: Abs; optional?: boolean }> }).slots
|
|
1375
|
-
: undefined;
|
|
1376
|
-
|
|
1377
|
-
if (!slots) {
|
|
1378
|
-
// 有形状信息但不是 object
|
|
1379
|
-
if (absArg.shape.k !== "unknown" && absArg.shape.k !== "never") {
|
|
1380
|
-
out.push({
|
|
1381
|
-
severity: "error",
|
|
1382
|
-
code: "nudo:constraint-violated",
|
|
1383
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1384
|
-
actual: formatAbs(absArg),
|
|
1385
|
-
expected: `object shape at ${path}`,
|
|
1386
|
-
suggestion: `改用满足 ${path} 形状约束的 object`,
|
|
1387
|
-
fn: displayName,
|
|
1388
|
-
line: loc?.start.line,
|
|
1389
|
-
column: loc?.start.column,
|
|
1390
|
-
});
|
|
1391
|
-
}
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
for (const [key, field] of Object.entries(constraint.fields) as Array<
|
|
1396
|
-
[string, NudoField]
|
|
1397
|
-
>) {
|
|
1398
|
-
const slot = slots[key];
|
|
1399
|
-
const fieldPath = path === paramName ? `${paramName}.${key}` : `${path}.${key}`;
|
|
1400
|
-
if (!slot) {
|
|
1401
|
-
if (!field.optional && !field.constraint.isOptional) {
|
|
1402
|
-
out.push({
|
|
1403
|
-
severity: "error",
|
|
1404
|
-
code: "nudo:constraint-violated",
|
|
1405
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1406
|
-
actual: formatAbs(absArg),
|
|
1407
|
-
expected: `missing field ${fieldPath}`,
|
|
1408
|
-
suggestion: `补全字段 ${fieldPath}`,
|
|
1409
|
-
fn: displayName,
|
|
1410
|
-
line: loc?.start.line,
|
|
1411
|
-
column: loc?.start.column,
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
continue;
|
|
1415
|
-
}
|
|
1416
|
-
checkFieldConstraint(displayName, paramName, field.constraint, slot.value, fieldPath, loc);
|
|
1417
|
-
}
|
|
1418
|
-
};
|
|
1419
|
-
|
|
1420
|
-
/** 单字段:prim 类型 + 数值界 + 嵌套 shape + array 元素 + int + 长度 */
|
|
1421
|
-
const checkFieldConstraint = (
|
|
1422
|
-
displayName: string,
|
|
1423
|
-
paramName: string,
|
|
1424
|
-
constraint: NudoConstraint,
|
|
1425
|
-
fieldAbs: Abs,
|
|
1426
|
-
fieldPath: string,
|
|
1427
|
-
loc?: { start: { line: number; column: number } },
|
|
1428
|
-
): void => {
|
|
1429
|
-
if (fieldAbs.shape.k === "unknown" && !fieldAbs.term) return;
|
|
1430
|
-
|
|
1431
|
-
// prim 类型
|
|
1432
|
-
if (constraint.prim && fieldAbs.shape.k === "prim") {
|
|
1433
|
-
const actualPrim = (fieldAbs.shape as { type: string }).type;
|
|
1434
|
-
if (actualPrim !== constraint.prim) {
|
|
1435
|
-
out.push({
|
|
1436
|
-
severity: "error",
|
|
1437
|
-
code: "nudo:constraint-violated",
|
|
1438
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1439
|
-
actual: formatAbs(fieldAbs),
|
|
1440
|
-
expected: `typeof ${fieldPath} = "${constraint.prim}"`,
|
|
1441
|
-
suggestion: `把 ${fieldPath} 改成 ${constraint.prim}`,
|
|
1442
|
-
fn: displayName,
|
|
1443
|
-
line: loc?.start.line,
|
|
1444
|
-
column: loc?.start.column,
|
|
1445
|
-
});
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
// int:字面量必须是整数
|
|
1451
|
-
if (constraint.int) {
|
|
1452
|
-
const iv = litValue(fieldAbs);
|
|
1453
|
-
if (typeof iv === "number" && !Number.isInteger(iv)) {
|
|
1454
|
-
out.push({
|
|
1455
|
-
severity: "error",
|
|
1456
|
-
code: "nudo:constraint-violated",
|
|
1457
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1458
|
-
actual: formatAbs(fieldAbs),
|
|
1459
|
-
expected: `${fieldPath} is int`,
|
|
1460
|
-
suggestion: `把 ${fieldPath} 改成整数`,
|
|
1461
|
-
fn: displayName,
|
|
1462
|
-
line: loc?.start.line,
|
|
1463
|
-
column: loc?.start.column,
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// 嵌套 shape
|
|
1469
|
-
if (constraint.fields) {
|
|
1470
|
-
checkShapeAgainstAbs(displayName, paramName, constraint, fieldAbs, fieldPath, loc);
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
// array 元素:逐元素检查
|
|
1474
|
-
if (constraint.element) {
|
|
1475
|
-
if (fieldAbs.shape.k === "arr") {
|
|
1476
|
-
checkFieldConstraint(
|
|
1477
|
-
displayName,
|
|
1478
|
-
paramName,
|
|
1479
|
-
constraint.element,
|
|
1480
|
-
(fieldAbs.shape as { element: Abs }).element,
|
|
1481
|
-
`${fieldPath}[]`,
|
|
1482
|
-
loc,
|
|
1483
|
-
);
|
|
1484
|
-
} else if (fieldAbs.shape.k === "tuple") {
|
|
1485
|
-
const els = (fieldAbs.shape as { elements: Abs[] }).elements;
|
|
1486
|
-
els.forEach((el, i) => {
|
|
1487
|
-
checkFieldConstraint(
|
|
1488
|
-
displayName,
|
|
1489
|
-
paramName,
|
|
1490
|
-
constraint.element!,
|
|
1491
|
-
el,
|
|
1492
|
-
`${fieldPath}[${i}]`,
|
|
1493
|
-
loc,
|
|
1494
|
-
);
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// 数值界 + 长度界(preds 里可能含 length(t) 比较)
|
|
1500
|
-
const lv = litValue(fieldAbs);
|
|
1501
|
-
const sv = typeof lv === "string" ? lv.length : undefined;
|
|
1502
|
-
const isStr = fieldAbs.shape.k === "prim" && (fieldAbs.shape as { type: string }).type === "string";
|
|
1503
|
-
for (const p of constraint.preds) {
|
|
1504
|
-
const flat = p.op === "and" ? p.args : [p];
|
|
1505
|
-
for (const atom of flat) {
|
|
1506
|
-
if (
|
|
1507
|
-
(atom.op === "gt" || atom.op === "ge" || atom.op === "lt" || atom.op === "le") &&
|
|
1508
|
-
atom.b.op === "lit" &&
|
|
1509
|
-
typeof atom.b.value === "number"
|
|
1510
|
-
) {
|
|
1511
|
-
const n = atom.b.value;
|
|
1512
|
-
const opSym = { gt: ">", ge: "≥", lt: "<", le: "≤" }[atom.op];
|
|
1513
|
-
// length(t) 形式
|
|
1514
|
-
if (atom.a.op === "app" && atom.a.fn === "length") {
|
|
1515
|
-
if (sv === undefined) continue;
|
|
1516
|
-
let ok = true;
|
|
1517
|
-
if (atom.op === "gt") ok = sv > n;
|
|
1518
|
-
if (atom.op === "ge") ok = sv >= n;
|
|
1519
|
-
if (atom.op === "lt") ok = sv < n;
|
|
1520
|
-
if (atom.op === "le") ok = sv <= n;
|
|
1521
|
-
if (!ok) {
|
|
1522
|
-
out.push({
|
|
1523
|
-
severity: "error",
|
|
1524
|
-
code: "nudo:constraint-violated",
|
|
1525
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1526
|
-
actual: formatAbs(fieldAbs),
|
|
1527
|
-
expected: `length(${fieldPath}) ${opSym} ${n}`,
|
|
1528
|
-
suggestion: `改用满足长度 ${opSym} ${n} 的 ${fieldPath}`,
|
|
1529
|
-
fn: displayName,
|
|
1530
|
-
line: loc?.start.line,
|
|
1531
|
-
column: loc?.start.column,
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
continue;
|
|
1535
|
-
}
|
|
1536
|
-
// 普通数值界
|
|
1537
|
-
if (lv === undefined || typeof lv !== "number" || isStr) continue;
|
|
1538
|
-
let ok = true;
|
|
1539
|
-
if (atom.op === "gt") ok = lv > n;
|
|
1540
|
-
if (atom.op === "ge") ok = lv >= n;
|
|
1541
|
-
if (atom.op === "lt") ok = lv < n;
|
|
1542
|
-
if (atom.op === "le") ok = lv <= n;
|
|
1543
|
-
if (!ok) {
|
|
1544
|
-
out.push({
|
|
1545
|
-
severity: "error",
|
|
1546
|
-
code: "nudo:constraint-violated",
|
|
1547
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1548
|
-
actual: formatAbs(fieldAbs),
|
|
1549
|
-
expected: `${fieldPath} ${opSym} ${n}`,
|
|
1550
|
-
suggestion: `改用满足 ${fieldPath} ${opSym} ${n} 的值,或放宽 ${paramName} 的前置`,
|
|
1551
|
-
fn: displayName,
|
|
1552
|
-
line: loc?.start.line,
|
|
1553
|
-
column: loc?.start.column,
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
|
|
1561
|
-
/** 对带 shape / array / int 的 refine 做结构检查 */
|
|
1562
|
-
const checkShapeReqs = (
|
|
1563
|
-
displayName: string,
|
|
1564
|
-
reqs: Array<[number, RefineEntry]>,
|
|
1565
|
-
paramNames: string[],
|
|
1566
|
-
absArgs: Abs[],
|
|
1567
|
-
argIndexOf: (reqIdx: number) => number | undefined,
|
|
1568
|
-
loc?: { start: { line: number; column: number } },
|
|
1569
|
-
): void => {
|
|
1570
|
-
for (const [idx, entry] of reqs) {
|
|
1571
|
-
const c = entry.constraint;
|
|
1572
|
-
if (!c.fields && !c.element && !c.int) continue;
|
|
1573
|
-
const argIdx = argIndexOf(idx);
|
|
1574
|
-
if (argIdx === undefined) continue;
|
|
1575
|
-
const arg = absArgs[argIdx];
|
|
1576
|
-
if (!arg) continue;
|
|
1577
|
-
const paramName = entry.param || paramNames[idx] || `arg${idx}`;
|
|
1578
|
-
// shape 字段
|
|
1579
|
-
if (c.fields) {
|
|
1580
|
-
checkShapeAgainstAbs(displayName, paramName, c, arg, paramName, loc);
|
|
1581
|
-
}
|
|
1582
|
-
// 顶层 array 元素
|
|
1583
|
-
if (c.element) {
|
|
1584
|
-
if (arg.shape.k === "arr") {
|
|
1585
|
-
checkFieldConstraint(
|
|
1586
|
-
displayName,
|
|
1587
|
-
paramName,
|
|
1588
|
-
c.element,
|
|
1589
|
-
(arg.shape as { element: Abs }).element,
|
|
1590
|
-
`${paramName}[]`,
|
|
1591
|
-
loc,
|
|
1592
|
-
);
|
|
1593
|
-
} else if (arg.shape.k === "tuple") {
|
|
1594
|
-
const els = (arg.shape as { elements: Abs[] }).elements;
|
|
1595
|
-
els.forEach((el, i) => {
|
|
1596
|
-
checkFieldConstraint(
|
|
1597
|
-
displayName,
|
|
1598
|
-
paramName,
|
|
1599
|
-
c.element!,
|
|
1600
|
-
el,
|
|
1601
|
-
`${paramName}[${i}]`,
|
|
1602
|
-
loc,
|
|
1603
|
-
);
|
|
1604
|
-
});
|
|
1605
|
-
} else if (arg.shape.k !== "unknown" && arg.shape.k !== "any") {
|
|
1606
|
-
out.push({
|
|
1607
|
-
severity: "error",
|
|
1608
|
-
code: "nudo:constraint-violated",
|
|
1609
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1610
|
-
actual: formatAbs(arg),
|
|
1611
|
-
expected: `array at ${paramName}`,
|
|
1612
|
-
suggestion: `改用满足 array 约束的值`,
|
|
1613
|
-
fn: displayName,
|
|
1614
|
-
line: loc?.start.line,
|
|
1615
|
-
column: loc?.start.column,
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
// 顶层 int
|
|
1620
|
-
if (c.int) {
|
|
1621
|
-
const iv = litValue(arg);
|
|
1622
|
-
if (typeof iv === "number" && !Number.isInteger(iv)) {
|
|
1623
|
-
out.push({
|
|
1624
|
-
severity: "error",
|
|
1625
|
-
code: "nudo:constraint-violated",
|
|
1626
|
-
message: `${displayName}[${paramName}]: 实参 ⊭ 前置`,
|
|
1627
|
-
actual: formatAbs(arg),
|
|
1628
|
-
expected: `${paramName} is int`,
|
|
1629
|
-
suggestion: `改用整数`,
|
|
1630
|
-
fn: displayName,
|
|
1631
|
-
line: loc?.start.line,
|
|
1632
|
-
column: loc?.start.column,
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
|
|
1639
|
-
const parseCallArgs = (
|
|
1640
|
-
args: Array<Record<string, unknown>>,
|
|
1641
|
-
): { absArgs: Abs[]; hasInfo: boolean } => {
|
|
1642
|
-
const absArgs: Abs[] = [];
|
|
1643
|
-
let hasInfo = false;
|
|
1644
|
-
for (const a of args ?? []) {
|
|
1645
|
-
if (a.type === "NumericLiteral" && typeof a.value === "number") {
|
|
1646
|
-
absArgs.push(numLit(a.value));
|
|
1647
|
-
hasInfo = true;
|
|
1648
|
-
} else if (
|
|
1649
|
-
a.type === "UnaryExpression" &&
|
|
1650
|
-
(a as { operator?: string }).operator === "-" &&
|
|
1651
|
-
(a as { argument?: Record<string, unknown> }).argument?.type === "NumericLiteral"
|
|
1652
|
-
) {
|
|
1653
|
-
const num = (a as { argument: { value: number } }).argument;
|
|
1654
|
-
absArgs.push(numLit(-num.value));
|
|
1655
|
-
hasInfo = true;
|
|
1656
|
-
} else if (a.type === "ObjectExpression") {
|
|
1657
|
-
const abs = evalArgAbs(a, (n) => varAbs.get(n));
|
|
1658
|
-
absArgs.push(abs ?? absUnknown());
|
|
1659
|
-
if (abs && abs.shape.k === "obj") hasInfo = true;
|
|
1660
|
-
} else if (a.type === "ArrayExpression") {
|
|
1661
|
-
const abs = evalArgAbs(a, (n) => varAbs.get(n));
|
|
1662
|
-
absArgs.push(abs ?? absUnknown());
|
|
1663
|
-
if (abs && (abs.shape.k === "arr" || abs.shape.k === "tuple")) hasInfo = true;
|
|
1664
|
-
} else if (a.type === "StringLiteral" && typeof a.value === "string") {
|
|
1665
|
-
absArgs.push({
|
|
1666
|
-
shape: { k: "prim", type: "string" },
|
|
1667
|
-
term: { op: "lit", value: a.value },
|
|
1668
|
-
conf: "exact",
|
|
1669
|
-
});
|
|
1670
|
-
hasInfo = true;
|
|
1671
|
-
} else if (a.type === "Identifier" && typeof a.name === "string") {
|
|
1672
|
-
const abs = varAbs.get(a.name);
|
|
1673
|
-
absArgs.push(abs ?? absUnknown());
|
|
1674
|
-
if (abs && abs.shape.k !== "unknown") hasInfo = true;
|
|
1675
|
-
} else {
|
|
1676
|
-
absArgs.push(absUnknown());
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
return { absArgs, hasInfo };
|
|
1680
|
-
};
|
|
1681
|
-
|
|
1682
|
-
const checkOneCall = (
|
|
1683
|
-
fnName: string,
|
|
1684
|
-
args: Array<Record<string, unknown>>,
|
|
1685
|
-
loc?: { start: { line: number; column: number } },
|
|
1686
|
-
): void => {
|
|
1687
|
-
if (!knownFns.includes(fnName)) return;
|
|
1688
|
-
|
|
1689
|
-
// 传参结构:实参字面量 Abs ≤ 形参必填 slot
|
|
1690
|
-
checkArgStructures(fnName, source, args, loc);
|
|
1691
|
-
|
|
1692
|
-
const { absArgs, hasInfo } = parseCallArgs(args);
|
|
1693
|
-
if (!hasInfo || absArgs.length === 0) return;
|
|
1694
|
-
|
|
1695
|
-
const g = generalizeFromAst(fnName, source, {
|
|
1696
|
-
file,
|
|
1697
|
-
refine: {
|
|
1698
|
-
loadModule: opts?.loadModule,
|
|
1699
|
-
fromFile: opts?.fromFile ?? "",
|
|
1700
|
-
},
|
|
1701
|
-
});
|
|
1702
|
-
const paramNames = g?.params ?? [];
|
|
1703
|
-
const optsR = {
|
|
1704
|
-
loadModule: opts?.loadModule,
|
|
1705
|
-
fromFile: opts?.fromFile ?? "",
|
|
1706
|
-
};
|
|
1707
|
-
const ownFull = refineToIndexedFull(source, fnName, paramNames, optsR);
|
|
1708
|
-
checkShapeReqs(fnName, ownFull, paramNames, absArgs, (i) => i, loc);
|
|
1709
|
-
checkReqs(
|
|
1710
|
-
fnName,
|
|
1711
|
-
ownFull.map(([i, e]) => [i, e.pred] as [number, Pred]),
|
|
1712
|
-
paramNames,
|
|
1713
|
-
absArgs,
|
|
1714
|
-
(i) => i,
|
|
1715
|
-
loc,
|
|
1716
|
-
);
|
|
1717
|
-
|
|
1718
|
-
const fwd = forwards.get(fnName);
|
|
1719
|
-
if (fwd) {
|
|
1720
|
-
const tg = generalizeFromAst(fwd.target, source, file ? { file } : {});
|
|
1721
|
-
const tParams = tg?.params ?? [];
|
|
1722
|
-
const tFull = refineToIndexedFull(source, fwd.target, tParams, optsR);
|
|
1723
|
-
if (tFull.length > 0) {
|
|
1724
|
-
const wrapperArgOfTarget = new Map<number, number>();
|
|
1725
|
-
fwd.map.forEach((wrapperIdx, targetIdx) => {
|
|
1726
|
-
wrapperArgOfTarget.set(targetIdx, wrapperIdx);
|
|
1727
|
-
});
|
|
1728
|
-
const mapArg = (targetIdx: number) => wrapperArgOfTarget.get(targetIdx);
|
|
1729
|
-
checkShapeReqs(`${fnName}→${fwd.target}`, tFull, tParams, absArgs, mapArg, loc);
|
|
1730
|
-
checkReqs(
|
|
1731
|
-
`${fnName}→${fwd.target}`,
|
|
1732
|
-
tFull.map(([i, e]) => [i, e.pred] as [number, Pred]),
|
|
1733
|
-
tParams,
|
|
1734
|
-
absArgs,
|
|
1735
|
-
mapArg,
|
|
1736
|
-
loc,
|
|
1737
|
-
);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
};
|
|
1741
|
-
|
|
1742
|
-
const checkExternalCall = (
|
|
1743
|
-
ext: { source: string; fnName: string },
|
|
1744
|
-
args: Array<Record<string, unknown>>,
|
|
1745
|
-
displayName: string,
|
|
1746
|
-
loc?: { start: { line: number; column: number } },
|
|
1747
|
-
): void => {
|
|
1748
|
-
checkArgStructures(ext.fnName, ext.source, args, loc, displayName);
|
|
1749
|
-
const { absArgs, hasInfo } = parseCallArgs(args);
|
|
1750
|
-
if (!hasInfo || absArgs.length === 0) return;
|
|
1751
|
-
let full: Array<[number, RefineEntry]> = [];
|
|
1752
|
-
let paramNames: string[] = [];
|
|
1753
|
-
try {
|
|
1754
|
-
const g = generalizeFromAst(ext.fnName, ext.source);
|
|
1755
|
-
paramNames = g?.params ?? [];
|
|
1756
|
-
full = refineToIndexedFull(ext.source, ext.fnName, paramNames, {
|
|
1757
|
-
loadModule: opts?.loadModule,
|
|
1758
|
-
fromFile: opts?.fromFile ?? "",
|
|
1759
|
-
});
|
|
1760
|
-
} catch {
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
checkShapeReqs(displayName, full, paramNames, absArgs, (i) => i, loc);
|
|
1764
|
-
checkReqs(
|
|
1765
|
-
displayName,
|
|
1766
|
-
full.map(([i, e]) => [i, e.pred] as [number, Pred]),
|
|
1767
|
-
paramNames,
|
|
1768
|
-
absArgs,
|
|
1769
|
-
(i) => i,
|
|
1770
|
-
loc,
|
|
1771
|
-
);
|
|
1772
|
-
};
|
|
1773
|
-
|
|
1774
|
-
/**
|
|
1775
|
-
* 实参结构 ≤ 形参必填 slot(从 `p.foo` 访问推出)。
|
|
1776
|
-
* 字面量节点静态求 Abs;标识符用文件绑定表。
|
|
1777
|
-
*/
|
|
1778
|
-
const checkArgStructures = (
|
|
1779
|
-
fnName: string,
|
|
1780
|
-
fnSource: string,
|
|
1781
|
-
args: Array<Record<string, unknown>>,
|
|
1782
|
-
loc?: { start: { line: number; column: number } },
|
|
1783
|
-
displayName?: string,
|
|
1784
|
-
): void => {
|
|
1785
|
-
let structReqs: Map<string, Set<string>>;
|
|
1786
|
-
let paramNames: string[];
|
|
1787
|
-
try {
|
|
1788
|
-
// 同文件调用复用预解析 AST;跨文件源码各自 parse
|
|
1789
|
-
const sameFile = fnSource === source;
|
|
1790
|
-
structReqs = collectParamStructReqs(fnSource, fnName, sameFile ? file : undefined);
|
|
1791
|
-
const g = generalizeFromAst(
|
|
1792
|
-
fnName,
|
|
1793
|
-
fnSource,
|
|
1794
|
-
sameFile && file ? { file } : {},
|
|
1795
|
-
);
|
|
1796
|
-
paramNames = g?.params ?? [];
|
|
1797
|
-
} catch {
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
if (structReqs.size === 0) return;
|
|
1801
|
-
for (let i = 0; i < args.length; i++) {
|
|
1802
|
-
const pname = paramNames[i];
|
|
1803
|
-
if (!pname) continue;
|
|
1804
|
-
const keys = structReqs.get(pname);
|
|
1805
|
-
if (!keys || keys.size === 0) continue;
|
|
1806
|
-
const argNode = args[i];
|
|
1807
|
-
if (!argNode) continue;
|
|
1808
|
-
const absArg = evalArgAbs(argNode, (n) => varAbs.get(n));
|
|
1809
|
-
if (!absArg || absArg.shape.k === "unknown") continue;
|
|
1810
|
-
// 数组/元组天然有 length;string 也有 length
|
|
1811
|
-
const isLeny =
|
|
1812
|
-
absArg.shape.k === "arr" ||
|
|
1813
|
-
absArg.shape.k === "tuple" ||
|
|
1814
|
-
(absArg.shape.k === "prim" &&
|
|
1815
|
-
(absArg.shape as { type: string }).type === "string");
|
|
1816
|
-
const needKeys = isLeny
|
|
1817
|
-
? [...keys].filter((k) => k !== "length")
|
|
1818
|
-
: [...keys];
|
|
1819
|
-
if (needKeys.length === 0) continue;
|
|
1820
|
-
const slots: Record<string, { value: Abs }> = {};
|
|
1821
|
-
for (const k of needKeys) slots[k] = { value: absUnknown() };
|
|
1822
|
-
const target = makeAbs({ k: "obj", slots }, undefined, undefined, "exact");
|
|
1823
|
-
const leq = leqAbs(absArg, target);
|
|
1824
|
-
if (!leq.ok) {
|
|
1825
|
-
const name = displayName ?? fnName;
|
|
1826
|
-
out.push({
|
|
1827
|
-
severity: "error",
|
|
1828
|
-
code: "nudo:arg-structure",
|
|
1829
|
-
message: `${name}[${pname}]: 实参结构 ⊭ 形参`,
|
|
1830
|
-
actual: formatAbs(absArg),
|
|
1831
|
-
expected: formatAbs(target),
|
|
1832
|
-
suggestion: leq.reason ?? `补全 ${pname} 上被访问的字段`,
|
|
1833
|
-
fn: name,
|
|
1834
|
-
line: loc?.start.line,
|
|
1835
|
-
column: loc?.start.column,
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
};
|
|
1840
|
-
|
|
1841
|
-
const visit = (n: unknown): void => {
|
|
1842
|
-
if (!n || typeof n !== "object") return;
|
|
1843
|
-
const obj = n as Record<string, unknown> & {
|
|
1844
|
-
type?: string;
|
|
1845
|
-
loc?: { start: { line: number; column: number } };
|
|
1846
|
-
};
|
|
1847
|
-
if (obj.type === "CallExpression") {
|
|
1848
|
-
const callee = obj.callee as Record<string, unknown>;
|
|
1849
|
-
const args = (obj.arguments as Array<Record<string, unknown>>) ?? [];
|
|
1850
|
-
const resolved = resolveCalleeFn(callee, resolve, knownSet);
|
|
1851
|
-
if (typeof resolved === "string") {
|
|
1852
|
-
checkOneCall(resolved, args, obj.loc);
|
|
1853
|
-
} else if (resolved?.external) {
|
|
1854
|
-
const name = resolved.external.fnName || "require()";
|
|
1855
|
-
checkExternalCall(resolved.external, args, name, obj.loc);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
for (const key of Object.keys(obj)) {
|
|
1859
|
-
if (key === "loc" || key === "start" || key === "end") continue;
|
|
1860
|
-
const val = obj[key];
|
|
1861
|
-
if (Array.isArray(val)) val.forEach(visit);
|
|
1862
|
-
else if (val && typeof val === "object") visit(val);
|
|
1863
|
-
}
|
|
1864
|
-
};
|
|
1865
|
-
visit(file);
|
|
1866
|
-
return out;
|
|
1867
|
-
}
|