@nudojs/service 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +27 -0
- package/dist/index.d.ts +638 -5
- package/dist/index.js +4439 -208
- package/package.json +17 -4
- package/src/__tests__/abs-bindings.test.ts +53 -0
- package/src/__tests__/abs-call-records.test.ts +33 -0
- package/src/__tests__/abs-calls.test.ts +59 -0
- package/src/__tests__/abs-module-cache.test.ts +121 -0
- package/src/__tests__/abs-modules-graph.test.ts +100 -0
- package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
- package/src/__tests__/analyzer.test.ts +745 -2
- package/src/__tests__/as-replace.test.ts +165 -0
- package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
- package/src/__tests__/bpath-as.test.ts +54 -0
- package/src/__tests__/bpath-cache-key.test.ts +33 -0
- package/src/__tests__/bpath-calls.test.ts +55 -0
- package/src/__tests__/bpath-diagnostics.test.ts +78 -0
- package/src/__tests__/bpath-env-modules.test.ts +30 -0
- package/src/__tests__/bpath-env.test.ts +80 -0
- package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
- package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
- package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
- package/src/__tests__/bpath-method-diag.test.ts +45 -0
- package/src/__tests__/bpath-module-diags.test.ts +101 -0
- package/src/__tests__/bpath-path-env.test.ts +46 -0
- package/src/__tests__/bpath-reexport.test.ts +131 -0
- package/src/__tests__/bpath-replace.test.ts +52 -0
- package/src/__tests__/bpath-require.test.ts +66 -0
- package/src/__tests__/bpath-run.test.ts +109 -0
- package/src/__tests__/case-emitter.test.ts +636 -0
- package/src/__tests__/cjs-require.test.ts +54 -0
- package/src/__tests__/diagnostics.test.ts +60 -0
- package/src/__tests__/dts-generator.test.ts +290 -6
- package/src/__tests__/fn-cache-evict.test.ts +56 -0
- package/src/__tests__/fn-sig-impl.test.ts +247 -0
- package/src/__tests__/guard-generator.test.ts +29 -0
- package/src/__tests__/harvest-auto.test.ts +43 -0
- package/src/__tests__/harvest-node.test.ts +24 -0
- package/src/__tests__/harvest-package.test.ts +38 -0
- package/src/__tests__/harvest-to-abs.test.ts +46 -0
- package/src/__tests__/hover-intension.test.ts +39 -0
- package/src/__tests__/infer-json.test.ts +35 -0
- package/src/__tests__/integration.test.ts +421 -0
- package/src/__tests__/m3-intension.test.ts +28 -0
- package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
- package/src/__tests__/schema-generator.test.ts +38 -0
- package/src/__tests__/session-cache-evict.test.ts +90 -0
- package/src/__tests__/static-imports.test.ts +44 -0
- package/src/abs-modules-graph.ts +387 -0
- package/src/analysis-file-cache.ts +51 -0
- package/src/analyzer.ts +2569 -94
- package/src/bpath-diagnostics.ts +283 -0
- package/src/bpath-run.ts +409 -0
- package/src/case-emitter.ts +477 -0
- package/src/dts-generator.ts +220 -24
- package/src/env-to-abs.ts +57 -0
- package/src/fn-analysis-cache.ts +119 -0
- package/src/guard-generator.ts +57 -0
- package/src/harvest-auto.ts +86 -0
- package/src/harvest-node.ts +48 -0
- package/src/harvest-package.ts +204 -0
- package/src/harvest-to-abs.ts +129 -0
- package/src/index.ts +128 -0
- package/src/infer-json.ts +137 -0
- package/src/load-module.ts +34 -0
- package/src/mock-abs.ts +275 -0
- package/src/schema-generator.ts +43 -0
- package/src/semantic-tokens.ts +216 -0
- package/src/session-cache.ts +50 -0
- package/src/static-imports.ts +241 -0
- package/src/target-path.ts +17 -0
package/src/mock-abs.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TypeValue } from "@nudojs/core";
|
|
2
|
+
|
|
3
|
+
export function typeValueToZodSchema(tv: TypeValue): string {
|
|
4
|
+
switch (tv.kind) {
|
|
5
|
+
case "literal": {
|
|
6
|
+
if (tv.value === null) return "z.null()";
|
|
7
|
+
if (tv.value === undefined) return "z.undefined()";
|
|
8
|
+
if (typeof tv.value === "string") return `z.literal(${JSON.stringify(tv.value)})`;
|
|
9
|
+
if (typeof tv.value === "boolean") return `z.literal(${tv.value})`;
|
|
10
|
+
return `z.literal(${tv.value})`;
|
|
11
|
+
}
|
|
12
|
+
case "primitive":
|
|
13
|
+
return `z.${tv.type}()`;
|
|
14
|
+
case "refined":
|
|
15
|
+
return typeValueToZodSchema(tv.base);
|
|
16
|
+
case "object": {
|
|
17
|
+
const entries = Object.entries(tv.properties)
|
|
18
|
+
.map(([k, v]) => `${k}: ${typeValueToZodSchema(v)}`)
|
|
19
|
+
.join(", ");
|
|
20
|
+
return `z.object({ ${entries} })`;
|
|
21
|
+
}
|
|
22
|
+
case "array":
|
|
23
|
+
return `z.array(${typeValueToZodSchema(tv.element)})`;
|
|
24
|
+
case "tuple": {
|
|
25
|
+
const inner = tv.elements.map(typeValueToZodSchema).join(", ");
|
|
26
|
+
return `z.tuple([${inner}])`;
|
|
27
|
+
}
|
|
28
|
+
case "function":
|
|
29
|
+
return "z.function()";
|
|
30
|
+
case "promise":
|
|
31
|
+
return `z.promise(${typeValueToZodSchema(tv.value)})`;
|
|
32
|
+
case "instance":
|
|
33
|
+
return `z.instanceof(${tv.className})`;
|
|
34
|
+
case "union": {
|
|
35
|
+
const members = tv.members.map(typeValueToZodSchema).join(", ");
|
|
36
|
+
return `z.union([${members}])`;
|
|
37
|
+
}
|
|
38
|
+
case "never":
|
|
39
|
+
return "z.never()";
|
|
40
|
+
case "unknown":
|
|
41
|
+
return "z.unknown()";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import type { Node } from "@babel/types";
|
|
3
|
+
import { createEnvironment, type Environment } from "@nudojs/core";
|
|
4
|
+
import { parse } from "@nudojs/parser";
|
|
5
|
+
import {
|
|
6
|
+
evaluateProgram,
|
|
7
|
+
setModuleResolver,
|
|
8
|
+
setCurrentFileDir,
|
|
9
|
+
resetMemo,
|
|
10
|
+
} from "@nudojs/cli/evaluator";
|
|
11
|
+
import { resolveModule } from "./analyzer.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Semantic tokens 图例(tokenTypes 下标即 LSP 编码里的 tokenType 值)。
|
|
15
|
+
* 与 LSP server capabilities 里声明的 legend 必须逐字对齐——server.ts 直接
|
|
16
|
+
* 导入本常量注册,保证「提取端索引」与「客户端图例」单一来源。
|
|
17
|
+
* 顺序沿用 lsp 包原 legend(function/variable/parameter/property 在前),
|
|
18
|
+
* 末尾追加 method(对象字面量方法键),只追加不重排,客户端索引稳定。
|
|
19
|
+
*/
|
|
20
|
+
export const SEMANTIC_TOKEN_TYPES = [
|
|
21
|
+
"function",
|
|
22
|
+
"variable",
|
|
23
|
+
"parameter",
|
|
24
|
+
"property",
|
|
25
|
+
"type",
|
|
26
|
+
"keyword",
|
|
27
|
+
"string",
|
|
28
|
+
"number",
|
|
29
|
+
"comment",
|
|
30
|
+
"decorator",
|
|
31
|
+
"method",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export const SEMANTIC_TOKEN_MODIFIERS = [
|
|
35
|
+
"declaration",
|
|
36
|
+
"readonly",
|
|
37
|
+
"deprecated",
|
|
38
|
+
"unreachable",
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export type SemanticToken = {
|
|
42
|
+
line: number;
|
|
43
|
+
char: number;
|
|
44
|
+
length: number;
|
|
45
|
+
typeIndex: number;
|
|
46
|
+
modifierBitmask: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** LSP 标准相对五元组编码:deltaLine/deltaStartChar/length/tokenType/tokenModifiers。 */
|
|
50
|
+
export function encodeSemanticTokens(tokens: SemanticToken[]): number[] {
|
|
51
|
+
const result: number[] = [];
|
|
52
|
+
let prevLine = 0;
|
|
53
|
+
let prevChar = 0;
|
|
54
|
+
|
|
55
|
+
for (const token of tokens) {
|
|
56
|
+
const deltaLine = token.line - prevLine;
|
|
57
|
+
const deltaChar = deltaLine === 0 ? token.char - prevChar : token.char;
|
|
58
|
+
|
|
59
|
+
result.push(deltaLine, deltaChar, token.length, token.typeIndex, token.modifierBitmask);
|
|
60
|
+
|
|
61
|
+
prevLine = token.line;
|
|
62
|
+
prevChar = token.char;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const TYPE_FUNCTION = SEMANTIC_TOKEN_TYPES.indexOf("function");
|
|
69
|
+
const TYPE_VARIABLE = SEMANTIC_TOKEN_TYPES.indexOf("variable");
|
|
70
|
+
const TYPE_PARAMETER = SEMANTIC_TOKEN_TYPES.indexOf("parameter");
|
|
71
|
+
const TYPE_PROPERTY = SEMANTIC_TOKEN_TYPES.indexOf("property");
|
|
72
|
+
const TYPE_METHOD = SEMANTIC_TOKEN_TYPES.indexOf("method");
|
|
73
|
+
const MOD_DECLARATION = 1 << SEMANTIC_TOKEN_MODIFIERS.indexOf("declaration");
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 从源码提取 semantic tokens 并按 LSP 相对编码返回扁平 number[]。
|
|
77
|
+
*
|
|
78
|
+
* 上色范围(声明位优先,未解析的 token 一律不上色):
|
|
79
|
+
* - 顶层 const/let/var 声明的绑定名:推断为函数绑定 → function,否则 variable
|
|
80
|
+
* (函数体内部的声明不做 env 反查——外层同名绑定会串味,统一 variable);
|
|
81
|
+
* - 函数声明/函数表达式的名字 → function;所有函数的参数 → parameter;
|
|
82
|
+
* - 对象字面量的键:值为函数 → method,否则 property。
|
|
83
|
+
*
|
|
84
|
+
* 推断复用 evaluateProgram 的绑定分析(与补全/诊断同一求值链),不另建
|
|
85
|
+
* 符号体系;解析失败返回 [],求值中断则按已绑定的部分结果继续上色。
|
|
86
|
+
*/
|
|
87
|
+
export function buildSemanticTokens(filePath: string, source: string): number[] {
|
|
88
|
+
let ast: Node;
|
|
89
|
+
try {
|
|
90
|
+
ast = parse(source);
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const env = createEnvironment();
|
|
96
|
+
resetMemo();
|
|
97
|
+
setModuleResolver(resolveModule);
|
|
98
|
+
setCurrentFileDir(dirname(filePath));
|
|
99
|
+
try {
|
|
100
|
+
evaluateProgram(ast, env);
|
|
101
|
+
} catch {
|
|
102
|
+
// 求值中断(如未支持语法):尽力而为,用已落进 env 的绑定继续上色
|
|
103
|
+
} finally {
|
|
104
|
+
setModuleResolver(null);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const program = (ast as { program?: Node }).program ?? ast;
|
|
108
|
+
const ownBindings = env.getOwnBindings();
|
|
109
|
+
// 顶层声明器节点集合:仅这些绑定名做 env 反查区分 function/variable
|
|
110
|
+
const topLevelDeclarators = new Set<unknown>();
|
|
111
|
+
for (const stmt of ((program as { body?: Node[] }).body ?? []) as Node[]) {
|
|
112
|
+
const decl =
|
|
113
|
+
stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration"
|
|
114
|
+
? ((stmt as { declaration?: Node }).declaration ?? null)
|
|
115
|
+
: stmt;
|
|
116
|
+
if (decl && decl.type === "VariableDeclaration") {
|
|
117
|
+
for (const d of (decl as unknown as { declarations: Node[] }).declarations) {
|
|
118
|
+
topLevelDeclarators.add(d);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const tokens: SemanticToken[] = [];
|
|
124
|
+
const pushIdentifier = (id: Node, typeIndex: number): void => {
|
|
125
|
+
const loc = (id as { loc?: { start: { line: number; column: number } } }).loc;
|
|
126
|
+
const name = (id as { name?: string }).name;
|
|
127
|
+
if (!loc || typeof name !== "string") return;
|
|
128
|
+
tokens.push({
|
|
129
|
+
line: loc.start.line - 1,
|
|
130
|
+
char: loc.start.column,
|
|
131
|
+
length: name.length,
|
|
132
|
+
typeIndex,
|
|
133
|
+
modifierBitmask: MOD_DECLARATION,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const collectParams = (params: Node[]): void => {
|
|
138
|
+
for (const p of params) {
|
|
139
|
+
if (!p) continue;
|
|
140
|
+
if (p.type === "Identifier") pushIdentifier(p, TYPE_PARAMETER);
|
|
141
|
+
else if (p.type === "AssignmentPattern" && (p as { left?: Node }).left?.type === "Identifier") {
|
|
142
|
+
pushIdentifier((p as { left: Node }).left, TYPE_PARAMETER);
|
|
143
|
+
} else if (p.type === "RestElement" && (p as { argument?: Node }).argument?.type === "Identifier") {
|
|
144
|
+
pushIdentifier((p as { argument: Node }).argument, TYPE_PARAMETER);
|
|
145
|
+
}
|
|
146
|
+
// 解构模式(ObjectPattern/ArrayPattern)内部不上色:子绑定的
|
|
147
|
+
// function/variable 区分没有可靠推断依据,宁缺毋滥
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const isFunctionValue = (name: string): boolean =>
|
|
152
|
+
ownBindings[name]?.kind === "function";
|
|
153
|
+
|
|
154
|
+
const visit = (node: unknown): void => {
|
|
155
|
+
if (!node || typeof node !== "object") return;
|
|
156
|
+
const n = node as Record<string, unknown>;
|
|
157
|
+
|
|
158
|
+
switch (n.type) {
|
|
159
|
+
case "VariableDeclarator": {
|
|
160
|
+
const id = n.id as Node | undefined;
|
|
161
|
+
if (id?.type === "Identifier") {
|
|
162
|
+
const isTopLevel = topLevelDeclarators.has(node);
|
|
163
|
+
const typeIndex =
|
|
164
|
+
isTopLevel && isFunctionValue((id as { name: string }).name) ? TYPE_FUNCTION : TYPE_VARIABLE;
|
|
165
|
+
pushIdentifier(id, typeIndex);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "FunctionDeclaration":
|
|
170
|
+
case "FunctionExpression": {
|
|
171
|
+
const id = n.id as Node | undefined;
|
|
172
|
+
if (id?.type === "Identifier") pushIdentifier(id, TYPE_FUNCTION);
|
|
173
|
+
collectParams((n.params as Node[] | undefined) ?? []);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case "ArrowFunctionExpression":
|
|
177
|
+
collectParams((n.params as Node[] | undefined) ?? []);
|
|
178
|
+
break;
|
|
179
|
+
case "ObjectMethod": {
|
|
180
|
+
const key = n.key as Node | undefined;
|
|
181
|
+
if (key?.type === "Identifier" && !n.computed) pushIdentifier(key, TYPE_METHOD);
|
|
182
|
+
collectParams((n.params as Node[] | undefined) ?? []);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "ObjectProperty": {
|
|
186
|
+
const key = n.key as Node | undefined;
|
|
187
|
+
const value = n.value as Node | undefined;
|
|
188
|
+
if (key?.type === "Identifier" && !n.computed) {
|
|
189
|
+
const isFn =
|
|
190
|
+
value?.type === "FunctionExpression" || value?.type === "ArrowFunctionExpression";
|
|
191
|
+
pushIdentifier(key, isFn ? TYPE_METHOD : TYPE_PROPERTY);
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const key of Object.keys(n)) {
|
|
198
|
+
if (key === "loc" || key === "start" || key === "end" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
|
|
199
|
+
const child = n[key];
|
|
200
|
+
if (Array.isArray(child)) {
|
|
201
|
+
for (const item of child) {
|
|
202
|
+
if (item && typeof item === "object" && typeof (item as { type?: unknown }).type === "string") {
|
|
203
|
+
visit(item);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} else if (child && typeof child === "object" && typeof (child as { type?: unknown }).type === "string") {
|
|
207
|
+
visit(child);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
visit(program);
|
|
213
|
+
|
|
214
|
+
tokens.sort((a, b) => a.line - b.line || a.char - b.char);
|
|
215
|
+
return encodeSemanticTokens(tokens);
|
|
216
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话缓存失效入口(宿主契约的唯一接线点)。
|
|
3
|
+
*
|
|
4
|
+
* analyzeFile / tryRunBPath / generalize L0 的键都不含「依赖模块内容」——
|
|
5
|
+
* 入口 source 未变但 dep 变了时,必须由宿主主动逐出,否则会命中陈旧结果。
|
|
6
|
+
* LSP 走定向逐出;CLI watch / vite-plugin 走这里。
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
resetGeneralizeMemo,
|
|
10
|
+
resetCheckSourceMemo,
|
|
11
|
+
resetNudoModuleExecCache,
|
|
12
|
+
resetParseSourceCache,
|
|
13
|
+
} from "@nudojs/core";
|
|
14
|
+
import { clearAbsModuleCache, evictAbsModuleCacheFiles } from "./abs-modules-graph.ts";
|
|
15
|
+
import { clearBPathCache, evictBPathCacheForFiles } from "./bpath-run.ts";
|
|
16
|
+
import { clearAnalysisFileCache, evictAnalysisFileCacheForFiles } from "./analysis-file-cache.ts";
|
|
17
|
+
import { clearFnAnalysisCache, evictFnAnalysisCacheForFiles } from "./fn-analysis-cache.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 依赖内容变更后:按入口文件定向逐出 service 层缓存。
|
|
21
|
+
* 调用方应传「以这些文件为入口」的路径(脏集里的 dependents),
|
|
22
|
+
* 而不是变更的 dep 文件本身——dep 自己 source 变了会自然 miss。
|
|
23
|
+
*/
|
|
24
|
+
export function evictAnalysisCachesForFiles(files: string[]): void {
|
|
25
|
+
if (files.length === 0) return;
|
|
26
|
+
evictBPathCacheForFiles(files);
|
|
27
|
+
evictAnalysisFileCacheForFiles(files);
|
|
28
|
+
evictFnAnalysisCacheForFiles(files);
|
|
29
|
+
evictAbsModuleCacheFiles(files);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 清空全部会话级分析缓存(service + core)。
|
|
34
|
+
* 适用于:CLI watch 增量批前、vite buildStart / watchChange、测试隔离。
|
|
35
|
+
* 比定向逐出重,但保证无陈旧命中;单次 analyze 内部的 per-fn / generalize
|
|
36
|
+
* memo 不受影响(它们在同一轮里先写后读)。
|
|
37
|
+
*/
|
|
38
|
+
export function clearAnalysisSessionCaches(): void {
|
|
39
|
+
clearBPathCache(); // cascades analysis-file + fn-analysis
|
|
40
|
+
clearAbsModuleCache();
|
|
41
|
+
resetGeneralizeMemo();
|
|
42
|
+
resetCheckSourceMemo();
|
|
43
|
+
resetNudoModuleExecCache();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 比 clearAnalysisSessionCaches 更彻底:再丢 AST LRU(测试 / 进程复用场景) */
|
|
47
|
+
export function resetAllAnalysisCaches(): void {
|
|
48
|
+
clearAnalysisSessionCaches();
|
|
49
|
+
resetParseSourceCache();
|
|
50
|
+
}
|