@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.
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +502 -4
- package/dist/index.js +3017 -212
- package/package.json +5 -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 +71 -3
- 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__/cjs-require.test.ts +54 -0
- package/src/__tests__/diagnostics.test.ts +0 -30
- package/src/__tests__/fn-cache-evict.test.ts +56 -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 +89 -0
- package/src/__tests__/m3-intension.test.ts +28 -0
- package/src/__tests__/mock-seed-fingerprint.test.ts +64 -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 +1416 -124
- package/src/bpath-diagnostics.ts +283 -0
- package/src/bpath-run.ts +409 -0
- package/src/case-emitter.ts +2 -0
- package/src/dts-generator.ts +3 -3
- package/src/env-to-abs.ts +57 -0
- package/src/fn-analysis-cache.ts +119 -0
- package/src/guard-generator.ts +8 -1
- 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 +104 -1
- package/src/infer-json.ts +137 -0
- package/src/load-module.ts +34 -0
- package/src/mock-abs.ts +275 -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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
evictFnAnalysisCacheForFiles,
|
|
4
|
+
clearFnAnalysisCache,
|
|
5
|
+
fnAnalysisCacheSet,
|
|
6
|
+
fnAnalysisCacheGet,
|
|
7
|
+
type CachedFnAnalysis,
|
|
8
|
+
} from "../fn-analysis-cache.ts";
|
|
9
|
+
|
|
10
|
+
function entry(name: string): CachedFnAnalysis {
|
|
11
|
+
return {
|
|
12
|
+
analysis: {
|
|
13
|
+
name,
|
|
14
|
+
loc: {
|
|
15
|
+
start: { line: 1, column: 0 },
|
|
16
|
+
end: { line: 1, column: 10 },
|
|
17
|
+
},
|
|
18
|
+
paramNames: [],
|
|
19
|
+
cases: [],
|
|
20
|
+
},
|
|
21
|
+
diagnostics: [],
|
|
22
|
+
caseHints: [],
|
|
23
|
+
callRecords: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SEP = "\0";
|
|
28
|
+
|
|
29
|
+
describe("evictFnAnalysisCacheForFiles", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
clearFnAnalysisCache();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("drops every fn entry for the given entry file", () => {
|
|
35
|
+
const keyA1 = `/t/a.js${SEP}own1${SEP}deps1${SEP}0`;
|
|
36
|
+
const keyA2 = `/t/a.js${SEP}own2${SEP}deps2${SEP}0`;
|
|
37
|
+
const keyB = `/t/b.js${SEP}own1${SEP}deps1${SEP}0`;
|
|
38
|
+
fnAnalysisCacheSet(keyA1, entry("a"));
|
|
39
|
+
fnAnalysisCacheSet(keyA2, entry("b"));
|
|
40
|
+
fnAnalysisCacheSet(keyB, entry("c"));
|
|
41
|
+
const n = evictFnAnalysisCacheForFiles(["/t/a.js"]);
|
|
42
|
+
expect(n).toBe(2);
|
|
43
|
+
expect(fnAnalysisCacheGet(keyA1)).toBeUndefined();
|
|
44
|
+
expect(fnAnalysisCacheGet(keyB)).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not drop a different file with a shared path prefix", () => {
|
|
48
|
+
const keyA = `/t/a.js${SEP}x${SEP}y${SEP}0`;
|
|
49
|
+
const keyBak = `/t/a.js.bak${SEP}x${SEP}y${SEP}0`;
|
|
50
|
+
fnAnalysisCacheSet(keyA, entry("a"));
|
|
51
|
+
fnAnalysisCacheSet(keyBak, entry("bak"));
|
|
52
|
+
evictFnAnalysisCacheForFiles(["/t/a.js"]);
|
|
53
|
+
expect(fnAnalysisCacheGet(keyA)).toBeUndefined();
|
|
54
|
+
expect(fnAnalysisCacheGet(keyBak)).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { barePackageName, collectBarePackages } from "../harvest-auto.ts";
|
|
3
|
+
|
|
4
|
+
describe("barePackageName", () => {
|
|
5
|
+
it("keeps simple package names", () => {
|
|
6
|
+
expect(barePackageName("ms")).toBe("ms");
|
|
7
|
+
expect(barePackageName("lodash/fp")).toBe("lodash");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("keeps scoped packages", () => {
|
|
11
|
+
expect(barePackageName("@types/node")).toBe("@types/node");
|
|
12
|
+
expect(barePackageName("@babel/parser")).toBe("@babel/parser");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("rejects relative / absolute / node builtins", () => {
|
|
16
|
+
expect(barePackageName("./a.js")).toBeUndefined();
|
|
17
|
+
expect(barePackageName("../b")).toBeUndefined();
|
|
18
|
+
expect(barePackageName("/abs/path")).toBeUndefined();
|
|
19
|
+
expect(barePackageName("node:fs")).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("collectBarePackages", () => {
|
|
24
|
+
it("extracts import and require specs", () => {
|
|
25
|
+
const src = `
|
|
26
|
+
import ms from "ms";
|
|
27
|
+
import { join } from "path";
|
|
28
|
+
const x = require("lodash/fp");
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import local from "./local.js";
|
|
31
|
+
`;
|
|
32
|
+
const pkgs = collectBarePackages(src);
|
|
33
|
+
expect(pkgs).toContain("ms");
|
|
34
|
+
expect(pkgs).toContain("path");
|
|
35
|
+
expect(pkgs).toContain("lodash");
|
|
36
|
+
expect(pkgs).not.toContain("node:fs");
|
|
37
|
+
expect(pkgs).not.toContain("./local.js");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns empty for unparsable source", () => {
|
|
41
|
+
expect(collectBarePackages("function (")).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { harvestNodeTypes, summarizeNodeEnv } from "../harvest-node.ts";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
describe("harvest @types/node", () => {
|
|
6
|
+
it("harvests @types/node when installed", () => {
|
|
7
|
+
const root = resolve(process.cwd(), "../..");
|
|
8
|
+
const r = harvestNodeTypes(root, 6);
|
|
9
|
+
if (!r.ok) {
|
|
10
|
+
// env may not have @types/node in some CI — soft skip
|
|
11
|
+
expect(r.error).toBeTruthy();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
expect(r.files).toBeGreaterThan(0);
|
|
15
|
+
expect(r.env.stats.symbols).toBeGreaterThan(0);
|
|
16
|
+
const s = summarizeNodeEnv(r.env);
|
|
17
|
+
expect(s.symbolCount).toBeGreaterThan(0);
|
|
18
|
+
// Node env 通常含 process / Buffer 等全局
|
|
19
|
+
const hasUseful =
|
|
20
|
+
s.globals.some((g) => /process|Buffer|console/i.test(g)) ||
|
|
21
|
+
s.modules.length > 0;
|
|
22
|
+
expect(hasUseful).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { harvestPackage, formatHarvestSummary, resolvePackageRoot } from "../harvest-package.ts";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
describe("harvest npm package", () => {
|
|
7
|
+
const root = resolve(process.cwd(), "../..");
|
|
8
|
+
// monorepo root may vary; try a few
|
|
9
|
+
const cwdHasTs = existsSync(resolve(root, "node_modules/typescript/package.json"));
|
|
10
|
+
|
|
11
|
+
it("resolvePackageRoot finds workspace packages", () => {
|
|
12
|
+
// typescript is a root devDep of the monorepo
|
|
13
|
+
const r = resolvePackageRoot("typescript", root);
|
|
14
|
+
if (r) {
|
|
15
|
+
expect(existsSync(r)).toBe(true);
|
|
16
|
+
} else {
|
|
17
|
+
// environment without typescript installed — skip soft
|
|
18
|
+
expect(r).toBeUndefined();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("harvestPackage on typescript if available", () => {
|
|
23
|
+
const r = resolvePackageRoot("typescript", root);
|
|
24
|
+
if (!r) {
|
|
25
|
+
expect(true).toBe(true);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const h = harvestPackage("typescript", root);
|
|
29
|
+
if ("error" in h) {
|
|
30
|
+
// no d.ts collected — still ok in constrained env
|
|
31
|
+
expect(h.error).toBeTruthy();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
expect(h.dtsFiles.length).toBeGreaterThan(0);
|
|
35
|
+
expect(h.env.stats.symbols).toBeGreaterThan(0);
|
|
36
|
+
expect(formatHarvestSummary(h)).toContain("typescript");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import { writeFileSync, rmSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { harvestedValueToAbs, bareSpecToAbsModules, evalAbsModuleGraph } from "@nudojs/service";
|
|
5
|
+
import { T, analyzeFn, numLit, absToString, getFnImpl } from "@nudojs/core";
|
|
6
|
+
|
|
7
|
+
const dirs: string[] = [];
|
|
8
|
+
afterAll(() => {
|
|
9
|
+
for (const d of dirs) rmSync(d, { recursive: true, force: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("harvest → Abs", () => {
|
|
13
|
+
it("fnSig becomes callable absFunction with declared return", () => {
|
|
14
|
+
const tv = T.fnSig([T.number], T.string);
|
|
15
|
+
const a = harvestedValueToAbs(tv);
|
|
16
|
+
expect(a.shape.k).toBe("fn");
|
|
17
|
+
expect(a.conf).toBe("mock");
|
|
18
|
+
expect(getFnImpl(a)?.apply).toBeTypeOf("function");
|
|
19
|
+
const r = getFnImpl(a)!.apply!([numLit(1)]);
|
|
20
|
+
expect(absToString(r)).toContain("string");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("bareSpecToAbsModules finds lodash from monorepo root", () => {
|
|
24
|
+
const file = join(process.cwd(), "packages/service/src/__probe.js");
|
|
25
|
+
const mods = bareSpecToAbsModules("lodash", file);
|
|
26
|
+
expect(mods).toBeDefined();
|
|
27
|
+
expect(Object.keys(mods!.named).length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("module graph injects bare import into analyzeFn", () => {
|
|
31
|
+
// 放在 monorepo 内,harvest 才能找到 node_modules/@types/lodash
|
|
32
|
+
const dir = join(process.cwd(), "packages/service/src/__tmp-bare-mod");
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
dirs.push(dir);
|
|
35
|
+
const main = `import { chunk } from "lodash";
|
|
36
|
+
export function go(x) { return chunk(x, 2); }
|
|
37
|
+
`;
|
|
38
|
+
const mainPath = join(dir, "main.js");
|
|
39
|
+
writeFileSync(mainPath, main, "utf-8");
|
|
40
|
+
const { modules } = evalAbsModuleGraph(main, mainPath);
|
|
41
|
+
expect(modules["lodash"]).toBeDefined();
|
|
42
|
+
const r = analyzeFn(main, "go", [numLit(4)], undefined, undefined, undefined, modules);
|
|
43
|
+
expect(r).toBeDefined();
|
|
44
|
+
expect(["mock", "partial", "path", "exact", "widened", "opaque"]).toContain(r.conf);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getHoverAtPosition } from "../analyzer.ts";
|
|
3
|
+
|
|
4
|
+
describe("getHoverAtPosition lossless Abs", () => {
|
|
5
|
+
it("hover on function name shows Abs + intension", () => {
|
|
6
|
+
const source = `function scale(x) { return x + 1; }\nscale(2);\n`;
|
|
7
|
+
const hover = getHoverAtPosition("/t/hover.js", source, 1, 9);
|
|
8
|
+
expect(hover).not.toBeNull();
|
|
9
|
+
expect(hover!.intension ?? hover!.typeText).toContain("scale");
|
|
10
|
+
expect(hover!.intension ?? "").toContain("+");
|
|
11
|
+
// 无损 Abs:含 conf 标记
|
|
12
|
+
expect(hover!.abs).toBeDefined();
|
|
13
|
+
expect(hover!.abs).toContain("#");
|
|
14
|
+
expect(hover!.absMultiline).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("hover on binding shows Abs from evalProgramAbs", () => {
|
|
18
|
+
const source = `const x = 1 + 2;\n`;
|
|
19
|
+
const hover = getHoverAtPosition("/t/h3.js", source, 1, 7);
|
|
20
|
+
if (hover?.abs) {
|
|
21
|
+
expect(hover.abs).toContain("3");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("hover on expression uses Abs node table (lossless)", () => {
|
|
26
|
+
const source = `const n = 1 + 2;\n`;
|
|
27
|
+
// column of `2` is 14 (0-based)
|
|
28
|
+
const hover = getHoverAtPosition("/t/h4.js", source, 1, 14);
|
|
29
|
+
expect(hover).not.toBeNull();
|
|
30
|
+
expect(hover!.abs).toBeDefined();
|
|
31
|
+
expect(hover!.abs).toContain("2");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("hover on non-function still returns type", () => {
|
|
35
|
+
const source = `const n = 1;\n`;
|
|
36
|
+
const hover = getHoverAtPosition("/t/h2.js", source, 1, 7);
|
|
37
|
+
if (hover) expect(typeof hover.typeText).toBe("string");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { analyzeFile, serializeInferJson } from "../index.ts";
|
|
3
|
+
|
|
4
|
+
describe("InferJson contract v1", () => {
|
|
5
|
+
it("self-contained function has abs in intension", () => {
|
|
6
|
+
const src = `function scale(x) { return x + 1; }\n`;
|
|
7
|
+
const result = analyzeFile("/t/scale.js", src);
|
|
8
|
+
const json = serializeInferJson(result, "/t/scale.js");
|
|
9
|
+
expect(json.version).toBe(1);
|
|
10
|
+
expect(json.file).toBe("/t/scale.js");
|
|
11
|
+
expect(json.summary.functions).toBeGreaterThan(0);
|
|
12
|
+
const fn = json.functions.find((f) => f.name === "scale");
|
|
13
|
+
expect(fn).toBeDefined();
|
|
14
|
+
expect(fn!.entryOnly).toBe(true);
|
|
15
|
+
const entry = fn!.cases.find((c) => c.name.startsWith("entry@"));
|
|
16
|
+
expect(entry).toBeDefined();
|
|
17
|
+
expect(entry!.intension?.abs).toContain("#");
|
|
18
|
+
expect(entry!.intension?.display).toContain("scale");
|
|
19
|
+
// 外延仍有 TypeValue 字符串
|
|
20
|
+
expect(typeof entry!.result).toBe("string");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("call site cases include args/result strings", () => {
|
|
24
|
+
const src = `
|
|
25
|
+
function add(a, b) { return a + b; }
|
|
26
|
+
const r = add(2, 3);
|
|
27
|
+
`;
|
|
28
|
+
const result = analyzeFile("/t/add.js", src);
|
|
29
|
+
const json = serializeInferJson(result, "/t/add.js");
|
|
30
|
+
const add = json.functions.find((f) => f.name === "add");
|
|
31
|
+
expect(add!.cases.length).toBeGreaterThan(0);
|
|
32
|
+
expect(add!.cases[0]!.args.length).toBe(2);
|
|
33
|
+
expect(add!.cases[0]!.result).toBe("5");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -330,3 +330,92 @@ function alwaysFails(x) {
|
|
|
330
330
|
expect(throwDiags[0].message).toContain("alwaysFails");
|
|
331
331
|
});
|
|
332
332
|
});
|
|
333
|
+
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// .ts 输入支持:analyzeFile/analyzeFileAsync 走统一剥除后的 parse(),
|
|
336
|
+
// 推断结果与等价 .js 一致,且 TS 语法(interface/import type/enum/断言)
|
|
337
|
+
// 不产生 unknown-global 等假诊断。
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
import { isNudoTargetPath } from "../target-path.ts";
|
|
341
|
+
|
|
342
|
+
describe("integration: .ts source through the full pipeline", () => {
|
|
343
|
+
const tsSource = `
|
|
344
|
+
interface Point { x: number; y: number }
|
|
345
|
+
import type { Gone } from "./no-such-module";
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @nudo:case "nums" (1, 2)
|
|
349
|
+
* @nudo:case "syms" (T.number, T.number)
|
|
350
|
+
*/
|
|
351
|
+
function add(a: number, b: number): number {
|
|
352
|
+
return a + b;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** @nudo:case "cast" (T.string) */
|
|
356
|
+
function cast(s: string) {
|
|
357
|
+
const v = s as unknown as { n: number };
|
|
358
|
+
const w = v!;
|
|
359
|
+
return s.length + 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export { add, cast };
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
it("infers .ts functions identically to equivalent .js", () => {
|
|
366
|
+
const jsSource = `
|
|
367
|
+
/**
|
|
368
|
+
* @nudo:case "nums" (1, 2)
|
|
369
|
+
* @nudo:case "syms" (T.number, T.number)
|
|
370
|
+
*/
|
|
371
|
+
function add(a, b) {
|
|
372
|
+
return a + b;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** @nudo:case "cast" (T.string) */
|
|
376
|
+
function cast(s) {
|
|
377
|
+
const v = s;
|
|
378
|
+
const w = v;
|
|
379
|
+
return s.length + 1;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export { add, cast };
|
|
383
|
+
`;
|
|
384
|
+
const ts = analyzeFile("/test.ts", tsSource);
|
|
385
|
+
const js = analyzeFile("/test.js", jsSource);
|
|
386
|
+
expect(ts.functions.map((f) => f.name)).toEqual(js.functions.map((f) => f.name));
|
|
387
|
+
expect(ts.functions).toHaveLength(2);
|
|
388
|
+
const tsAdd = ts.functions[0];
|
|
389
|
+
const jsAdd = js.functions[0];
|
|
390
|
+
expect(tsAdd.cases).toHaveLength(jsAdd.cases.length);
|
|
391
|
+
for (let i = 0; i < tsAdd.cases.length; i++) {
|
|
392
|
+
expect(typeValueToString(tsAdd.cases[i].result)).toBe(typeValueToString(jsAdd.cases[i].result));
|
|
393
|
+
}
|
|
394
|
+
expect(typeValueToString(tsAdd.cases[0].result)).toBe("3");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("TS-only syntax produces no false diagnostics", () => {
|
|
398
|
+
const result = analyzeFile("/test.ts", tsSource);
|
|
399
|
+
const falseDiagnostics = result.diagnostics.filter(
|
|
400
|
+
(d) => d.code === "nudo:unknown-global" || d.code === "nudo:builtin-unknown",
|
|
401
|
+
);
|
|
402
|
+
expect(falseDiagnostics).toHaveLength(0);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("case-emitter round-trip works on stripped .ts source (call@ directives reinsertable)", () => {
|
|
406
|
+
const source = `function scale(n) { return n * 2; }\nconst out = scale(21);\n`;
|
|
407
|
+
const result = analyzeFile("/test.ts", source);
|
|
408
|
+
// 全程序推断:无指令函数从调用点合成 case
|
|
409
|
+
const scale = result.functions.find((f) => f.name === "scale");
|
|
410
|
+
expect(scale).toBeDefined();
|
|
411
|
+
expect(scale!.cases.length).toBeGreaterThan(0);
|
|
412
|
+
expect(scale!.cases[0].source).toBe("callsite");
|
|
413
|
+
expect(typeValueToString(scale!.cases[0].result)).toBe("42");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("isNudoTargetPath is exported from the service surface", () => {
|
|
417
|
+
expect(isNudoTargetPath("a.ts")).toBe(true);
|
|
418
|
+
expect(isNudoTargetPath("a.d.ts")).toBe(false);
|
|
419
|
+
expect(isNudoTargetPath("a.tsx")).toBe(false);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { analyzeFile } from "../index.ts";
|
|
3
|
+
|
|
4
|
+
describe("analyzer intension (always on)", () => {
|
|
5
|
+
it("entry@ gets intension display", () => {
|
|
6
|
+
const source = `function scale(x) { return x + 1; }\n`;
|
|
7
|
+
const result = analyzeFile("f.js", source);
|
|
8
|
+
const fn = result.functions.find((f) => f.name === "scale");
|
|
9
|
+
expect(fn).toBeDefined();
|
|
10
|
+
const entry = fn!.cases.find((c) => c.name.startsWith("entry@"));
|
|
11
|
+
expect(entry).toBeDefined();
|
|
12
|
+
expect(entry!.intension).toBeDefined();
|
|
13
|
+
expect(entry!.intension!.display).toContain("scale");
|
|
14
|
+
expect(entry!.intension!.display).toContain("+");
|
|
15
|
+
// 无损 Abs(不经 bridge)
|
|
16
|
+
expect(entry!.intension!.abs).toBeDefined();
|
|
17
|
+
expect(entry!.intension!.abs).toContain("#");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("add generalizes to A1+A2", () => {
|
|
21
|
+
const source = `function add(a, b) { return a + b; }\n`;
|
|
22
|
+
const result = analyzeFile("f.js", source);
|
|
23
|
+
const fn = result.functions.find((f) => f.name === "add");
|
|
24
|
+
const entry = fn!.cases.find((c) => c.name.startsWith("entry@"));
|
|
25
|
+
expect(entry!.intension!.display).toContain("A1");
|
|
26
|
+
expect(entry!.intension!.display).toContain("+");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { numLit, unknown } from "@nudojs/core";
|
|
3
|
+
import { parseTypeValueExpr } from "@nudojs/parser";
|
|
4
|
+
import { mockSeedFingerprint } from "../bpath-run.ts";
|
|
5
|
+
import { mockDirectivesToAbsSeeds } from "../mock-abs.ts";
|
|
6
|
+
|
|
7
|
+
describe("mockSeedFingerprint", () => {
|
|
8
|
+
it("misses when mock values change under the same names", () => {
|
|
9
|
+
const a = mockSeedFingerprint({ fetch: numLit(1) });
|
|
10
|
+
const b = mockSeedFingerprint({ fetch: numLit(2) });
|
|
11
|
+
expect(a).not.toBe(b);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("hits for identical name+value sets regardless of key order", () => {
|
|
15
|
+
const a = mockSeedFingerprint({ x: numLit(1), y: unknown });
|
|
16
|
+
const b = mockSeedFingerprint({ y: unknown, x: numLit(1) });
|
|
17
|
+
expect(a).toBe(b);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("empty and missing mocks share the dash key", () => {
|
|
21
|
+
expect(mockSeedFingerprint(undefined)).toBe("-");
|
|
22
|
+
expect(mockSeedFingerprint({})).toBe("-");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("misses when real sinon.stub().returns value changes", () => {
|
|
26
|
+
const seeds1 = mockDirectivesToAbsSeeds([
|
|
27
|
+
{
|
|
28
|
+
directives: [
|
|
29
|
+
{
|
|
30
|
+
kind: "mock",
|
|
31
|
+
name: "fetch",
|
|
32
|
+
sinonExpr: { type: "sinon.stub", returnValue: parseTypeValueExpr("1") },
|
|
33
|
+
} as never,
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
const seeds2 = mockDirectivesToAbsSeeds([
|
|
38
|
+
{
|
|
39
|
+
directives: [
|
|
40
|
+
{
|
|
41
|
+
kind: "mock",
|
|
42
|
+
name: "fetch",
|
|
43
|
+
sinonExpr: { type: "sinon.stub", returnValue: parseTypeValueExpr("2") },
|
|
44
|
+
} as never,
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
const a = mockSeedFingerprint(seeds1.seedVars, seeds1.seedFns);
|
|
49
|
+
const b = mockSeedFingerprint(seeds2.seedVars, seeds2.seedFns);
|
|
50
|
+
expect(a).not.toBe(b);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("includes seedFns in the fingerprint", () => {
|
|
54
|
+
const a = mockSeedFingerprint(
|
|
55
|
+
{},
|
|
56
|
+
{ load: { params: ["x"], body: { type: "Identifier", name: "x" } as never } },
|
|
57
|
+
);
|
|
58
|
+
const b = mockSeedFingerprint(
|
|
59
|
+
{},
|
|
60
|
+
{ load: { params: ["y"], body: { type: "Identifier", name: "y" } as never } },
|
|
61
|
+
);
|
|
62
|
+
expect(a).not.toBe(b);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主契约回归:入口 source 未变、依赖模块内容变了时,
|
|
3
|
+
* 必须 evictAnalysisCachesForFiles(CLI watch / vite-plugin),
|
|
4
|
+
* 否则 AnalysisResult / B-path / fn-cache 会命中陈旧结果。
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
7
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
analyzeFile,
|
|
12
|
+
clearAnalysisSessionCaches,
|
|
13
|
+
evictAnalysisCachesForFiles,
|
|
14
|
+
} from "@nudojs/service";
|
|
15
|
+
import { typeValueToString } from "@nudojs/core";
|
|
16
|
+
|
|
17
|
+
const dirs: string[] = [];
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
clearAnalysisSessionCaches();
|
|
20
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function caseResultOf(result: ReturnType<typeof analyzeFile>, fnName: string, caseName: string): string {
|
|
24
|
+
const fn = result.functions.find((f) => f.name === fnName);
|
|
25
|
+
const c = fn?.cases.find((x) => x.name === caseName);
|
|
26
|
+
if (!c) throw new Error(`missing case ${fnName}/${caseName}`);
|
|
27
|
+
return typeValueToString(c.result);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("dep-change host eviction contract", () => {
|
|
31
|
+
const MAIN = `
|
|
32
|
+
const util = require("./util.js");
|
|
33
|
+
/**
|
|
34
|
+
* @nudo:case "t" (1)
|
|
35
|
+
*/
|
|
36
|
+
function go(n) {
|
|
37
|
+
return util.inc(n);
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
it("without eviction, parent analysis hits stale result after dep change", () => {
|
|
42
|
+
clearAnalysisSessionCaches();
|
|
43
|
+
const dir = mkdtempSync(join(tmpdir(), "nudo-stale-"));
|
|
44
|
+
dirs.push(dir);
|
|
45
|
+
const depPath = join(dir, "util.js");
|
|
46
|
+
const mainPath = join(dir, "main.js");
|
|
47
|
+
writeFileSync(depPath, `export function inc(x) { return x + 1; }\n`);
|
|
48
|
+
writeFileSync(mainPath, MAIN);
|
|
49
|
+
|
|
50
|
+
const r1 = analyzeFile(mainPath, MAIN);
|
|
51
|
+
expect(caseResultOf(r1, "go", "t")).toBe("2");
|
|
52
|
+
|
|
53
|
+
// dep 变了,parent source 不变;不逐出 → 整文件缓存命中
|
|
54
|
+
writeFileSync(depPath, `export function inc(x) { return x + 99; }\n`);
|
|
55
|
+
const r2 = analyzeFile(mainPath, MAIN);
|
|
56
|
+
expect(caseResultOf(r2, "go", "t")).toBe("2"); // 陈旧——正是契约要防的
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("evictAnalysisCachesForFiles makes parent see dep change", () => {
|
|
60
|
+
clearAnalysisSessionCaches();
|
|
61
|
+
const dir = mkdtempSync(join(tmpdir(), "nudo-evict-"));
|
|
62
|
+
dirs.push(dir);
|
|
63
|
+
const depPath = join(dir, "util.js");
|
|
64
|
+
const mainPath = join(dir, "main.js");
|
|
65
|
+
writeFileSync(depPath, `export function inc(x) { return x + 1; }\n`);
|
|
66
|
+
writeFileSync(mainPath, MAIN);
|
|
67
|
+
|
|
68
|
+
expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("2");
|
|
69
|
+
|
|
70
|
+
writeFileSync(depPath, `export function inc(x) { return x + 99; }\n`);
|
|
71
|
+
// 模拟 CLI watch:脏集入口文件定向逐出
|
|
72
|
+
evictAnalysisCachesForFiles([mainPath]);
|
|
73
|
+
expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("100");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("clearAnalysisSessionCaches is a full reset", () => {
|
|
77
|
+
clearAnalysisSessionCaches();
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), "nudo-clear-"));
|
|
79
|
+
dirs.push(dir);
|
|
80
|
+
const depPath = join(dir, "util.js");
|
|
81
|
+
const mainPath = join(dir, "main.js");
|
|
82
|
+
writeFileSync(depPath, `export function inc(x) { return x + 1; }\n`);
|
|
83
|
+
writeFileSync(mainPath, MAIN);
|
|
84
|
+
|
|
85
|
+
expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("2");
|
|
86
|
+
writeFileSync(depPath, `export function inc(x) { return x + 5; }\n`);
|
|
87
|
+
clearAnalysisSessionCaches();
|
|
88
|
+
expect(caseResultOf(analyzeFile(mainPath, MAIN), "go", "t")).toBe("6");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { collectStaticImports } from "../static-imports.ts";
|
|
6
|
+
|
|
7
|
+
describe("host static imports (not a bundler)", () => {
|
|
8
|
+
it("collects relative imports for type facts", () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "nudo-imp-"));
|
|
10
|
+
try {
|
|
11
|
+
writeFileSync(
|
|
12
|
+
join(dir, "math.js"),
|
|
13
|
+
`export function add(a, b) { return a + b; }\nexport function scale(x) { return add(x, 1); }\n`,
|
|
14
|
+
);
|
|
15
|
+
writeFileSync(
|
|
16
|
+
join(dir, "index.js"),
|
|
17
|
+
`import { add, scale } from "./math.js";\nexport function double(x) { return scale(x) + x; }\n`,
|
|
18
|
+
);
|
|
19
|
+
const graph = collectStaticImports(join(dir, "index.js"));
|
|
20
|
+
expect(graph.size).toBe(2);
|
|
21
|
+
const index = graph.get(join(dir, "index.js"))!;
|
|
22
|
+
expect(index.named.has("double")).toBe(true);
|
|
23
|
+
expect(index.poly.get("double")?.display).toContain("double");
|
|
24
|
+
const math = graph.get(join(dir, "math.js"))!;
|
|
25
|
+
expect(math.poly.get("add")?.display).toContain("A1");
|
|
26
|
+
} finally {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("skips bare specifiers (npm packages)", () => {
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "nudo-bare-"));
|
|
33
|
+
try {
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(dir, "app.js"),
|
|
36
|
+
`import lodash from "lodash";\nexport function f(x) { return x + 1; }\n`,
|
|
37
|
+
);
|
|
38
|
+
const graph = collectStaticImports(join(dir, "app.js"));
|
|
39
|
+
expect(graph.size).toBe(1);
|
|
40
|
+
} finally {
|
|
41
|
+
rmSync(dir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|