@nudojs/service 0.2.1 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.d.ts +502 -4
  3. package/dist/index.js +3017 -212
  4. package/package.json +5 -4
  5. package/src/__tests__/abs-bindings.test.ts +53 -0
  6. package/src/__tests__/abs-call-records.test.ts +33 -0
  7. package/src/__tests__/abs-calls.test.ts +59 -0
  8. package/src/__tests__/abs-module-cache.test.ts +121 -0
  9. package/src/__tests__/abs-modules-graph.test.ts +100 -0
  10. package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
  11. package/src/__tests__/analyzer.test.ts +71 -3
  12. package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
  13. package/src/__tests__/bpath-as.test.ts +54 -0
  14. package/src/__tests__/bpath-cache-key.test.ts +33 -0
  15. package/src/__tests__/bpath-calls.test.ts +55 -0
  16. package/src/__tests__/bpath-diagnostics.test.ts +78 -0
  17. package/src/__tests__/bpath-env-modules.test.ts +30 -0
  18. package/src/__tests__/bpath-env.test.ts +80 -0
  19. package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
  20. package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
  21. package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
  22. package/src/__tests__/bpath-method-diag.test.ts +45 -0
  23. package/src/__tests__/bpath-module-diags.test.ts +101 -0
  24. package/src/__tests__/bpath-path-env.test.ts +46 -0
  25. package/src/__tests__/bpath-reexport.test.ts +131 -0
  26. package/src/__tests__/bpath-replace.test.ts +52 -0
  27. package/src/__tests__/bpath-require.test.ts +66 -0
  28. package/src/__tests__/bpath-run.test.ts +109 -0
  29. package/src/__tests__/cjs-require.test.ts +54 -0
  30. package/src/__tests__/diagnostics.test.ts +0 -30
  31. package/src/__tests__/fn-cache-evict.test.ts +56 -0
  32. package/src/__tests__/harvest-auto.test.ts +43 -0
  33. package/src/__tests__/harvest-node.test.ts +24 -0
  34. package/src/__tests__/harvest-package.test.ts +38 -0
  35. package/src/__tests__/harvest-to-abs.test.ts +46 -0
  36. package/src/__tests__/hover-intension.test.ts +39 -0
  37. package/src/__tests__/infer-json.test.ts +35 -0
  38. package/src/__tests__/integration.test.ts +89 -0
  39. package/src/__tests__/m3-intension.test.ts +28 -0
  40. package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
  41. package/src/__tests__/session-cache-evict.test.ts +90 -0
  42. package/src/__tests__/static-imports.test.ts +44 -0
  43. package/src/abs-modules-graph.ts +387 -0
  44. package/src/analysis-file-cache.ts +51 -0
  45. package/src/analyzer.ts +1416 -124
  46. package/src/bpath-diagnostics.ts +283 -0
  47. package/src/bpath-run.ts +409 -0
  48. package/src/case-emitter.ts +2 -0
  49. package/src/dts-generator.ts +3 -3
  50. package/src/env-to-abs.ts +57 -0
  51. package/src/fn-analysis-cache.ts +119 -0
  52. package/src/guard-generator.ts +8 -1
  53. package/src/harvest-auto.ts +86 -0
  54. package/src/harvest-node.ts +48 -0
  55. package/src/harvest-package.ts +204 -0
  56. package/src/harvest-to-abs.ts +129 -0
  57. package/src/index.ts +104 -1
  58. package/src/infer-json.ts +137 -0
  59. package/src/load-module.ts +34 -0
  60. package/src/mock-abs.ts +275 -0
  61. package/src/semantic-tokens.ts +216 -0
  62. package/src/session-cache.ts +50 -0
  63. package/src/static-imports.ts +241 -0
  64. package/src/target-path.ts +17 -0
package/src/index.ts CHANGED
@@ -19,6 +19,8 @@ export {
19
19
  type CallRecord,
20
20
  getTypeAtPosition,
21
21
  getTypeAtPositionAsync,
22
+ getHoverAtPosition,
23
+ type HoverInfo,
22
24
  getCompletionsAtPosition,
23
25
  getCasesForFile,
24
26
  buildModuleGraph,
@@ -27,6 +29,105 @@ export {
27
29
  topoSortDirty,
28
30
  } from "./analyzer.ts";
29
31
 
32
+ export { collectAbsInlays, type AbsInlay } from "@nudojs/core";
33
+
34
+ export {
35
+ serializeInferJson,
36
+ type InferJson,
37
+ type InferJsonCase,
38
+ type InferJsonFunction,
39
+ } from "./infer-json.ts";
40
+
41
+ export { isNudoTargetPath } from "./target-path.ts";
42
+ export { defaultLoadModule, type LoadModule } from "./load-module.ts";
43
+ export {
44
+ collectStaticImports,
45
+ analyzeExportsFromSource,
46
+ collectDependencySpecs,
47
+ type ModuleExports,
48
+ } from "./static-imports.ts";
49
+ export {
50
+ harvestPackage,
51
+ collectDtsFromEntry,
52
+ formatHarvestSummary,
53
+ lookupHarvested,
54
+ resolvePackageRoot,
55
+ type PackageHarvest,
56
+ } from "./harvest-package.ts";
57
+ export {
58
+ barePackageName,
59
+ collectBarePackages,
60
+ autoHarvestModules,
61
+ harvestPackageCached,
62
+ clearHarvestCache,
63
+ } from "./harvest-auto.ts";
64
+ export {
65
+ evalAbsModuleGraph,
66
+ evalProgramAbsWithModules,
67
+ collectAbsBindingsFromGraph,
68
+ defaultAbsLoadModule,
69
+ clearAbsModuleCache,
70
+ evictAbsModuleCacheFiles,
71
+ type AbsModuleGraphResult,
72
+ type AbsGraphOptions,
73
+ type AbsModuleLoadIssue,
74
+ type AbsModuleCacheEntry,
75
+ } from "./abs-modules-graph.ts";
76
+ export {
77
+ harvestToAbsModules,
78
+ packageHarvestToAbsModules,
79
+ bareSpecToAbsModules,
80
+ harvestedValueToAbs,
81
+ } from "./harvest-to-abs.ts";
82
+ export {
83
+ isBPathCapable,
84
+ tryRunBPath,
85
+ tryBPathCall,
86
+ tryBPathCallFull,
87
+ clearBPathCache,
88
+ evictBPathCacheForFiles,
89
+ collectBPathReplacements,
90
+ collectEnvGlobals,
91
+ collectEnvModules,
92
+ type BPathRunResult,
93
+ } from "./bpath-run.ts";
94
+ export {
95
+ clearAnalysisFileCache,
96
+ getAnalysisFileCacheSize,
97
+ evictAnalysisFileCacheForFiles,
98
+ } from "./analysis-file-cache.ts";
99
+ export {
100
+ clearFnAnalysisCache,
101
+ evictFnAnalysisCacheForFiles,
102
+ } from "./fn-analysis-cache.ts";
103
+ export {
104
+ evictAnalysisCachesForFiles,
105
+ clearAnalysisSessionCaches,
106
+ resetAllAnalysisCaches,
107
+ } from "./session-cache.ts";
108
+ export { envValueToAbs } from "./env-to-abs.ts";
109
+ export type { BMemberDiag } from "@nudojs/core";
110
+
111
+ export {
112
+ collectBPathDiagnostics,
113
+ type BPathDiagnostics,
114
+ type BPathUnreachable,
115
+ type BPathBuiltinUnknown,
116
+ } from "./bpath-diagnostics.ts";
117
+ export {
118
+ harvestNodeTypes,
119
+ summarizeNodeEnv,
120
+ type NodeEnvResult,
121
+ } from "./harvest-node.ts";
122
+
123
+ export {
124
+ buildSemanticTokens,
125
+ encodeSemanticTokens,
126
+ SEMANTIC_TOKEN_TYPES,
127
+ SEMANTIC_TOKEN_MODIFIERS,
128
+ type SemanticToken,
129
+ } from "./semantic-tokens.ts";
130
+
30
131
  export {
31
132
  typeValueToTSType,
32
133
  generateDts,
@@ -34,7 +135,7 @@ export {
34
135
  } from "./dts-generator.ts";
35
136
 
36
137
  export { typeValueToZodSchema } from "./schema-generator.ts";
37
- export { generateGuardFunction } from "./guard-generator.ts";
138
+ export { generateGuardFunction, generateGuardFunctionFromAbs } from "./guard-generator.ts";
38
139
 
39
140
  export {
40
141
  serializeCaseArg,
@@ -45,3 +146,5 @@ export {
45
146
  type EmitSkipReason,
46
147
  type EmitResult,
47
148
  } from "./case-emitter.ts";
149
+
150
+ export { mockDirectivesToAbsSeeds, type AbsMockSeeds } from "./mock-abs.ts";
@@ -0,0 +1,137 @@
1
+ /**
2
+ * `nudo infer --json` 稳定契约 v1(CI / Agent)。
3
+ *
4
+ * 字段只增不改语义:
5
+ * - ext_*:TypeValue 投影字符串(有损,兼容)
6
+ * - intension.abs*:无损 Abs 展示(主线)
7
+ */
8
+
9
+ import { typeValueToString, type TypeValue } from "@nudojs/core";
10
+ import type { AnalysisResult, CaseResult, FunctionAnalysis, SourceLocation } from "./analyzer.ts";
11
+
12
+ export type InferJsonCase = {
13
+ name: string;
14
+ /** TypeValue 投影(有损外延) */
15
+ args: string[];
16
+ result: string;
17
+ throws: string | null;
18
+ source: string | null;
19
+ aggregatedFrom?: number;
20
+ /** 无损内涵(Abs);无则省略 */
21
+ intension?: {
22
+ display?: string;
23
+ abs?: string;
24
+ absMultiline?: string;
25
+ term?: string;
26
+ pred?: string;
27
+ conf?: string;
28
+ };
29
+ };
30
+
31
+ export type InferJsonFunction = {
32
+ name: string;
33
+ loc: SourceLocation;
34
+ entryOnly: boolean;
35
+ noDeclaration?: boolean;
36
+ cases: InferJsonCase[];
37
+ combined?: string;
38
+ };
39
+
40
+ export type InferJson = {
41
+ version: 1;
42
+ file: string;
43
+ summary: {
44
+ functions: number;
45
+ externalFunctions: number;
46
+ cases: number;
47
+ diagnostics: number;
48
+ };
49
+ functions: InferJsonFunction[];
50
+ externalFunctions?: Array<{
51
+ name: string;
52
+ fromModule?: string;
53
+ cases: InferJsonCase[];
54
+ }>;
55
+ diagnostics: Array<{
56
+ range: SourceLocation;
57
+ severity: string;
58
+ message: string;
59
+ code?: string;
60
+ suggestions?: string[];
61
+ tags?: string[];
62
+ origin?: { line: number; column: number };
63
+ }>;
64
+ };
65
+
66
+ function mapCase(c: CaseResult): InferJsonCase {
67
+ const out: InferJsonCase = {
68
+ name: c.name,
69
+ args: c.args.map((a: TypeValue) => typeValueToString(a)),
70
+ result: typeValueToString(c.result),
71
+ throws: c.throws.kind !== "never" ? typeValueToString(c.throws) : null,
72
+ source: c.source ?? null,
73
+ };
74
+ if (c.aggregatedFrom !== undefined) out.aggregatedFrom = c.aggregatedFrom;
75
+ if (c.intension) {
76
+ const i = c.intension;
77
+ out.intension = {
78
+ ...(i.display !== undefined ? { display: i.display } : {}),
79
+ ...(i.abs !== undefined ? { abs: i.abs } : {}),
80
+ ...(i.absMultiline !== undefined ? { absMultiline: i.absMultiline } : {}),
81
+ ...(i.term !== undefined ? { term: i.term } : {}),
82
+ ...(i.pred !== undefined ? { pred: i.pred } : {}),
83
+ ...(i.conf !== undefined ? { conf: i.conf } : {}),
84
+ };
85
+ }
86
+ return out;
87
+ }
88
+
89
+ function mapFunction(f: FunctionAnalysis): InferJsonFunction {
90
+ const out: InferJsonFunction = {
91
+ name: f.name,
92
+ loc: f.loc,
93
+ entryOnly: f.entryOnly ?? false,
94
+ cases: f.cases.map(mapCase),
95
+ };
96
+ if (f.noDeclaration) out.noDeclaration = true;
97
+ if (f.combined) out.combined = typeValueToString(f.combined);
98
+ return out;
99
+ }
100
+
101
+ export function serializeInferJson(
102
+ result: AnalysisResult,
103
+ file: string,
104
+ ): InferJson {
105
+ const functions = result.functions.map(mapFunction);
106
+ const external = result.externalFunctions?.map((f) => ({
107
+ name: f.name,
108
+ fromModule: f.fromModule,
109
+ cases: f.cases.map(mapCase),
110
+ }));
111
+ const caseCount =
112
+ functions.reduce((n, f) => n + f.cases.length, 0) +
113
+ (external ?? []).reduce((n, f) => n + f.cases.length, 0);
114
+
115
+ const json: InferJson = {
116
+ version: 1,
117
+ file,
118
+ summary: {
119
+ functions: functions.length,
120
+ externalFunctions: external?.length ?? 0,
121
+ cases: caseCount,
122
+ diagnostics: result.diagnostics.length,
123
+ },
124
+ functions,
125
+ diagnostics: result.diagnostics.map((d) => ({
126
+ range: d.range,
127
+ severity: d.severity,
128
+ message: d.message,
129
+ ...(d.code !== undefined ? { code: d.code } : {}),
130
+ ...(d.suggestions ? { suggestions: d.suggestions } : {}),
131
+ ...(d.tags ? { tags: d.tags } : {}),
132
+ ...(d.origin ? { origin: d.origin } : {}),
133
+ })),
134
+ };
135
+ if (external) json.externalFunctions = external;
136
+ return json;
137
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 相对/绝对 specifier → 模块源码。
3
+ * CLI / LSP / vite / check 共用同一扩展名表,避免门禁结果分叉。
4
+ */
5
+
6
+ import { existsSync, statSync, readFileSync } from "node:fs";
7
+ import { dirname, resolve, join } from "node:path";
8
+
9
+ export type LoadModule = (spec: string, fromFile: string) => string | undefined;
10
+
11
+ /** 默认 loadModule:支持 .js/.mjs/.ts 与 index 入口 */
12
+ export function defaultLoadModule(spec: string, fromFile: string): string | undefined {
13
+ if (!spec.startsWith(".") && !spec.startsWith("/")) return undefined;
14
+ try {
15
+ const base = dirname(resolve(fromFile));
16
+ const p = resolve(base, spec);
17
+ for (const cand of [
18
+ p,
19
+ `${p}.js`,
20
+ `${p}.mjs`,
21
+ `${p}.ts`,
22
+ join(p, "index.js"),
23
+ join(p, "index.mjs"),
24
+ join(p, "index.ts"),
25
+ ]) {
26
+ if (existsSync(cand) && !statSync(cand).isDirectory()) {
27
+ return readFileSync(cand, "utf-8");
28
+ }
29
+ }
30
+ return undefined;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * @nudo:mock / sinon 指令 → Abs seed(供 evalProgramAbs)。
3
+ * host 层:依赖 parser 指令形态;core 只吃 seedVars/seedFns。
4
+ */
5
+
6
+ import type { Node } from "@babel/types";
7
+ import type { FunctionWithDirectives } from "@nudojs/parser";
8
+ import type { MockHelper, TypeValue } from "@nudojs/core";
9
+ import {
10
+ type Abs,
11
+ absFunction,
12
+ emptyEnv,
13
+ typeValueToAbs,
14
+ abs as makeAbs,
15
+ confJoin,
16
+ unknown as absUnknown,
17
+ litValue,
18
+ formatAbs,
19
+ getFnImpl,
20
+ typeValueToString,
21
+ } from "@nudojs/core";
22
+
23
+ /**
24
+ * mock 依赖结果 conf 不得高于 mock。
25
+ * 必须保留同一对象身份:absFunction 的 impl 挂在 WeakMap 上,
26
+ * makeAbs 新建对象会丢掉 body/apply。
27
+ */
28
+ function markMockConf(a: Abs): Abs {
29
+ a.conf = confJoin(a.conf, "mock");
30
+ return a;
31
+ }
32
+
33
+ /** Structural content key for mock body AST (callsFake etc.). */
34
+ function astContentKey(n: unknown, depth = 0): string {
35
+ if (n == null || depth > 24) return "";
36
+ if (typeof n !== "object") return String(n);
37
+ if (Array.isArray(n)) return n.map((x) => astContentKey(x, depth + 1)).join(",");
38
+ const obj = n as Record<string, unknown>;
39
+ const type = typeof obj.type === "string" ? obj.type : "";
40
+ const parts: string[] = [type];
41
+ for (const key of ["name", "value", "raw", "operator", "computed"]) {
42
+ const v = obj[key];
43
+ if (v !== undefined && (typeof v !== "object" || v === null)) parts.push(`${key}=${String(v)}`);
44
+ }
45
+ for (const key of Object.keys(obj)) {
46
+ if (
47
+ key === "loc" ||
48
+ key === "start" ||
49
+ key === "end" ||
50
+ key === "leadingComments" ||
51
+ key === "trailingComments" ||
52
+ key === "innerComments" ||
53
+ key === "type" ||
54
+ key === "name" ||
55
+ key === "value" ||
56
+ key === "raw" ||
57
+ key === "operator" ||
58
+ key === "computed"
59
+ ) {
60
+ continue;
61
+ }
62
+ const v = obj[key];
63
+ if (v && typeof v === "object") parts.push(`${key}:{${astContentKey(v, depth + 1)}}`);
64
+ }
65
+ return parts.join("|");
66
+ }
67
+
68
+ function stampFingerprint(a: Abs, fp: string): Abs {
69
+ const impl = getFnImpl(a);
70
+ if (impl) impl.fingerprint = fp;
71
+ return a;
72
+ }
73
+
74
+ /** 用常量 Abs 造 mock 函数:调用即返回该值(结果 conf 标 mock) */
75
+ function constantMockFn(result: Abs): Abs {
76
+ const marked = markMockConf(result);
77
+ const mockEnv = emptyEnv();
78
+ const retName = "__nudo_mock_ret";
79
+ mockEnv.vars.set(retName, marked);
80
+ const body = {
81
+ type: "BlockStatement",
82
+ body: [
83
+ {
84
+ type: "ReturnStatement",
85
+ argument: { type: "Identifier", name: retName },
86
+ },
87
+ ],
88
+ } as unknown as Node;
89
+ let retKey: string;
90
+ try {
91
+ retKey = formatAbs(marked);
92
+ } catch {
93
+ retKey = "?";
94
+ }
95
+ return stampFingerprint(
96
+ markMockConf(absFunction(["...args"], { body, env: mockEnv })),
97
+ `ret=${retKey}`,
98
+ );
99
+ }
100
+
101
+ /**
102
+ * withArgs 实参匹配(与 mock-helpers.mockArgMatches 同语义):
103
+ * - 字面量声明要求实参同值字面量
104
+ * - primitive 声明接受同源字面量
105
+ */
106
+ function absArgMatches(declared: TypeValue, actual: Abs | undefined): boolean {
107
+ if (!actual) return false;
108
+ const declaredAbs = typeValueToAbs(declared);
109
+ // 同 shape + 同字面量
110
+ const av = litValue(actual);
111
+ const dv = litValue(declaredAbs);
112
+ if (dv !== undefined) {
113
+ return av !== undefined && Object.is(av, dv);
114
+ }
115
+ // declared 是 primitive(如 T.number):接受同源字面量或同 prim
116
+ if (declared.kind === "primitive") {
117
+ if (actual.shape.k === "prim") return actual.shape.type === declared.type;
118
+ if (av !== undefined) {
119
+ const t = typeof av;
120
+ return (
121
+ (declared.type === "number" && t === "number") ||
122
+ (declared.type === "string" && t === "string") ||
123
+ (declared.type === "boolean" && t === "boolean") ||
124
+ (declared.type === "bigint" && t === "bigint")
125
+ );
126
+ }
127
+ }
128
+ // unknown 声明不视为可证明匹配
129
+ return false;
130
+ }
131
+
132
+ function dispatchMockFn(defaultReturn: Abs, cases?: { args: TypeValue[]; returnValue: TypeValue }[]): Abs {
133
+ if (!cases?.length) return constantMockFn(defaultReturn);
134
+ const caseAbs = cases.map((c) => ({
135
+ declared: c.args,
136
+ result: markMockConf(typeValueToAbs(c.returnValue)),
137
+ }));
138
+ const dummyBody = {
139
+ type: "BlockStatement",
140
+ body: [{ type: "ReturnStatement", argument: null }],
141
+ } as unknown as Node;
142
+ const markedDefault = markMockConf(defaultReturn);
143
+ let defaultKey: string;
144
+ try {
145
+ defaultKey = formatAbs(markedDefault);
146
+ } catch {
147
+ defaultKey = "?";
148
+ }
149
+ const caseKey = cases
150
+ .map(
151
+ (c) =>
152
+ `${c.args.map((a) => typeValueToString(a)).join(",")}->${typeValueToString(c.returnValue)}`,
153
+ )
154
+ .join("|");
155
+ return stampFingerprint(
156
+ markMockConf(
157
+ absFunction(["...args"], {
158
+ body: dummyBody,
159
+ env: emptyEnv(),
160
+ apply: (args: Abs[]): Abs => {
161
+ for (const c of caseAbs) {
162
+ if (c.declared.every((d, i) => absArgMatches(d, args[i]))) {
163
+ return c.result;
164
+ }
165
+ }
166
+ return markedDefault;
167
+ },
168
+ }),
169
+ ),
170
+ `dispatch=default=${defaultKey};cases=${caseKey}`,
171
+ );
172
+ }
173
+
174
+ function absFromMockHelper(h: MockHelper): Abs {
175
+ if (h.callsFakeImpl && h.callsFakeImpl.kind === "function") {
176
+ const fn = h.callsFakeImpl;
177
+ const body = fn.body as Node;
178
+ return stampFingerprint(
179
+ absFunction(fn.params, { body, async: false }),
180
+ `fake=${fn.params.join(",")}:${astContentKey(body)}`,
181
+ );
182
+ }
183
+
184
+ let defaultReturn: Abs;
185
+ if (h.resolvedValue) {
186
+ defaultReturn = makeAbs(
187
+ { k: "eff", eff: "promise", inner: typeValueToAbs(h.resolvedValue) },
188
+ undefined,
189
+ undefined,
190
+ "path",
191
+ );
192
+ } else if (h.rejectedValue) {
193
+ defaultReturn = makeAbs({ k: "never" }, undefined, undefined, "exact");
194
+ } else if (h.returnValue) {
195
+ defaultReturn = typeValueToAbs(h.returnValue);
196
+ } else if (h.onFirstCallValue) {
197
+ // 与 TypeValue 路径一致:无 returnValue 时 onFirstCall 作默认返回
198
+ defaultReturn = typeValueToAbs(h.onFirstCallValue);
199
+ } else {
200
+ defaultReturn = absUnknown;
201
+ }
202
+
203
+ return dispatchMockFn(defaultReturn, h.withArgsCases);
204
+ }
205
+
206
+ function absFromSinon(sinonExpr: {
207
+ type: string;
208
+ returnValue?: TypeValue;
209
+ resolvedValue?: TypeValue;
210
+ rejectedValue?: TypeValue;
211
+ }): Abs {
212
+ if (sinonExpr.resolvedValue) {
213
+ return constantMockFn(
214
+ makeAbs(
215
+ { k: "eff", eff: "promise", inner: typeValueToAbs(sinonExpr.resolvedValue) },
216
+ undefined,
217
+ undefined,
218
+ "path",
219
+ ),
220
+ );
221
+ }
222
+ if (sinonExpr.rejectedValue) {
223
+ return constantMockFn(makeAbs({ k: "never" }, undefined, undefined, "exact"));
224
+ }
225
+ if (sinonExpr.returnValue) {
226
+ return constantMockFn(typeValueToAbs(sinonExpr.returnValue));
227
+ }
228
+ return constantMockFn(absUnknown);
229
+ }
230
+
231
+ export type AbsMockSeeds = {
232
+ seedVars: Record<string, Abs>;
233
+ seedFns: Record<string, { params: string[]; body: Node; async?: boolean; fingerprint?: string }>;
234
+ };
235
+
236
+ /**
237
+ * B 路径注入用:seedVars + seedFns 统一为 Abs 函数绑定。
238
+ * arrowFn mock 落在 seedFns(AST body,供 ast-eval),B 路径的
239
+ * envGlobals 注入只吃 Abs——不合并会把 mock 丢掉,函数体内的调用
240
+ * 会落到真实原生函数(如 fetch 拿 Abs 当 URL,直接崩)。
241
+ */
242
+ export function mockSeedsToAbsMocks(seeds: AbsMockSeeds): Record<string, Abs> {
243
+ const out: Record<string, Abs> = { ...seeds.seedVars };
244
+ for (const [name, fn] of Object.entries(seeds.seedFns)) {
245
+ out[name] = absFunction(fn.params, { body: fn.body, async: fn.async ?? false });
246
+ }
247
+ return out;
248
+ }
249
+
250
+ /** 从函数上的 @nudo:mock 指令收集 Abs seed */
251
+ export function mockDirectivesToAbsSeeds(
252
+ functions: Array<{ directives: FunctionWithDirectives["directives"] }>,
253
+ ): AbsMockSeeds {
254
+ const seedVars: Record<string, Abs> = {};
255
+ const seedFns: AbsMockSeeds["seedFns"] = {};
256
+ for (const fn of functions) {
257
+ for (const d of fn.directives) {
258
+ if (d.kind !== "mock") continue;
259
+ if (d.arrowFn) {
260
+ const body = d.arrowFn.body as Node;
261
+ seedFns[d.name] = {
262
+ params: d.arrowFn.params,
263
+ body,
264
+ async: false,
265
+ fingerprint: `fake=${d.arrowFn.params.join(",")}:${astContentKey(body)}`,
266
+ };
267
+ } else if (d.nudoMock) {
268
+ seedVars[d.name] = markMockConf(absFromMockHelper(d.nudoMock));
269
+ } else if (d.sinonExpr) {
270
+ seedVars[d.name] = markMockConf(absFromSinon(d.sinonExpr));
271
+ }
272
+ }
273
+ }
274
+ return { seedVars, seedFns };
275
+ }