@nudojs/lsp 0.3.0 → 0.4.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 +22 -0
- package/agent-skill/SKILL.md +13 -14
- package/dist/server.js +309 -50
- package/package.json +4 -4
- package/src/__tests__/agent-check.test.ts +47 -0
- package/src/__tests__/agent-hover.test.ts +28 -0
- package/src/__tests__/agent-infer.test.ts +38 -0
- package/src/__tests__/check-diagnostics.test.ts +46 -0
- package/src/__tests__/lsp-integration.test.ts +110 -11
- package/src/__tests__/nudo-dep-evict.test.ts +103 -0
- package/src/agent-tools.ts +164 -3
- package/src/semantic-tokens.ts +9 -45
- package/src/server.ts +99 -23
- package/src/validation.ts +154 -3
package/src/server.ts
CHANGED
|
@@ -19,8 +19,12 @@ import { TextDocument } from "vscode-languageserver-textdocument";
|
|
|
19
19
|
import { typeValueToString } from "@nudojs/core";
|
|
20
20
|
import {
|
|
21
21
|
getTypeAtPosition,
|
|
22
|
+
getHoverAtPosition,
|
|
22
23
|
getCompletionsAtPosition,
|
|
23
24
|
getCasesForFile,
|
|
25
|
+
buildSemanticTokens,
|
|
26
|
+
isNudoTargetPath,
|
|
27
|
+
collectAbsInlays,
|
|
24
28
|
} from "@nudojs/service";
|
|
25
29
|
import { parse } from "@nudojs/parser";
|
|
26
30
|
import { buildSymbolTable, findDefinition, findReferences, findIdentifierAtPosition } from "./symbols.ts";
|
|
@@ -30,7 +34,9 @@ import {
|
|
|
30
34
|
evictModuleGraphCacheEntries,
|
|
31
35
|
forgetValidatedFile,
|
|
32
36
|
getCachedOrAnalyze,
|
|
37
|
+
handleNudoDepFileChanged,
|
|
33
38
|
hasNudoDirectives,
|
|
39
|
+
lspLoadModule,
|
|
34
40
|
toLspDiagnostic,
|
|
35
41
|
uriToFilePath,
|
|
36
42
|
validateText,
|
|
@@ -41,6 +47,9 @@ import {
|
|
|
41
47
|
suggestCase,
|
|
42
48
|
trace,
|
|
43
49
|
whatIf,
|
|
50
|
+
checkTool,
|
|
51
|
+
hoverTool,
|
|
52
|
+
inferTool,
|
|
44
53
|
type AgentToolDeps,
|
|
45
54
|
type AgentToolResult,
|
|
46
55
|
} from "./agent-tools.ts";
|
|
@@ -49,6 +58,9 @@ const NUDO_COMMANDS = [
|
|
|
49
58
|
"nudo.whatIf",
|
|
50
59
|
"nudo.suggestCase",
|
|
51
60
|
"nudo.trace",
|
|
61
|
+
"nudo.check",
|
|
62
|
+
"nudo.hover",
|
|
63
|
+
"nudo.infer",
|
|
52
64
|
"nudo.selectCase",
|
|
53
65
|
"nudo.getActiveCases",
|
|
54
66
|
] as const;
|
|
@@ -159,16 +171,29 @@ export function registerWatchedFilesListener(listener: (uris: string[]) => void)
|
|
|
159
171
|
* 清空其已发布诊断,并把被清理的 uri 列表广播给监听器。
|
|
160
172
|
* 打开中的文件跳过——其内容由编辑流负责,外部删除会被编辑器以 didOpen/didChange 覆盖。
|
|
161
173
|
*/
|
|
174
|
+
function isNudoDepPath(filePath: string): boolean {
|
|
175
|
+
return /\.nudo\.(js|mjs|ts)$/.test(filePath);
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
function handleWatchedFilesChanges(changes: readonly FileEvent[], isOpen: (uri: string) => boolean): string[] {
|
|
163
179
|
const gone: string[] = [];
|
|
180
|
+
const nudoTouched: string[] = [];
|
|
164
181
|
for (const change of changes) {
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
182
|
+
const filePath = uriToFilePath(change.uri);
|
|
183
|
+
if (change.type === FileChangeType.Deleted) {
|
|
184
|
+
if (isOpen(change.uri)) continue;
|
|
185
|
+
gone.push(change.uri);
|
|
186
|
+
forgetValidatedFile(filePath);
|
|
187
|
+
activeCases.delete(change.uri);
|
|
188
|
+
nudoFileCache.delete(change.uri);
|
|
189
|
+
connection.sendDiagnostics({ uri: change.uri, diagnostics: [] });
|
|
190
|
+
if (isNudoDepPath(filePath)) nudoTouched.push(filePath);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Create/Change:契约模板变更 → 定向逐出 L0 + 重检打开中的父文件
|
|
194
|
+
if (isNudoDepPath(filePath)) {
|
|
195
|
+
nudoTouched.push(filePath);
|
|
196
|
+
}
|
|
172
197
|
}
|
|
173
198
|
if (gone.length > 0) {
|
|
174
199
|
// 拷贝后再遍历:监听器内注销自身不应影响本轮广播
|
|
@@ -180,6 +205,12 @@ function handleWatchedFilesChanges(changes: readonly FileEvent[], isOpen: (uri:
|
|
|
180
205
|
}
|
|
181
206
|
}
|
|
182
207
|
}
|
|
208
|
+
if (nudoTouched.length > 0) {
|
|
209
|
+
const deps = validationDeps();
|
|
210
|
+
for (const p of nudoTouched) {
|
|
211
|
+
void handleNudoDepFileChanged(p, deps).catch(() => {});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
183
214
|
return gone;
|
|
184
215
|
}
|
|
185
216
|
|
|
@@ -224,13 +255,30 @@ connection.onHover((params) => {
|
|
|
224
255
|
const cases = getActiveCasesForUri(params.textDocument.uri);
|
|
225
256
|
|
|
226
257
|
try {
|
|
227
|
-
const
|
|
228
|
-
if (!
|
|
229
|
-
|
|
258
|
+
const hover = getHoverAtPosition(filePath, source, line, column, cases);
|
|
259
|
+
if (!hover) return null;
|
|
260
|
+
|
|
261
|
+
const lines: string[] = [];
|
|
262
|
+
// 无损 Abs 优先(类型即计算本体)
|
|
263
|
+
if (hover.absMultiline) {
|
|
264
|
+
lines.push("```nudo", hover.absMultiline, "```");
|
|
265
|
+
} else if (hover.abs) {
|
|
266
|
+
lines.push("```nudo", hover.abs, "```");
|
|
267
|
+
}
|
|
268
|
+
if (hover.intension && hover.intension !== hover.abs) {
|
|
269
|
+
lines.push("```nudo", hover.intension, "```");
|
|
270
|
+
}
|
|
271
|
+
// 外延 TypeValue 仅作对照,且与内涵不同时才显示
|
|
272
|
+
if (hover.typeText && hover.typeText !== hover.intension && hover.typeText !== hover.abs) {
|
|
273
|
+
lines.push("```nudo", `ext: ${hover.typeText}`, "```");
|
|
274
|
+
}
|
|
275
|
+
if (lines.length === 0) {
|
|
276
|
+
lines.push("```nudo", hover.typeText, "```");
|
|
277
|
+
}
|
|
230
278
|
return {
|
|
231
279
|
contents: {
|
|
232
280
|
kind: MarkupKind.Markdown,
|
|
233
|
-
value:
|
|
281
|
+
value: lines.join("\n"),
|
|
234
282
|
},
|
|
235
283
|
};
|
|
236
284
|
} catch {
|
|
@@ -331,6 +379,28 @@ connection.languages.inlayHint.on((params) => {
|
|
|
331
379
|
});
|
|
332
380
|
}
|
|
333
381
|
|
|
382
|
+
// Abs inlay:参数约束 + 返回 term/pred(类型即计算,无损)
|
|
383
|
+
try {
|
|
384
|
+
for (const abs of collectAbsInlays(source, {
|
|
385
|
+
loadModule: lspLoadModule,
|
|
386
|
+
fromFile: filePath,
|
|
387
|
+
})) {
|
|
388
|
+
const lineIdx = abs.line - 1;
|
|
389
|
+
if (lineIdx < 0 || lineIdx >= lines.length) continue;
|
|
390
|
+
hints.push({
|
|
391
|
+
position: { line: lineIdx, character: abs.character },
|
|
392
|
+
label: abs.label,
|
|
393
|
+
kind:
|
|
394
|
+
abs.kind === "parameter"
|
|
395
|
+
? InlayHintKind.Parameter
|
|
396
|
+
: InlayHintKind.Type,
|
|
397
|
+
paddingLeft: true,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
// Abs inlay 失败不影响 caseHints
|
|
402
|
+
}
|
|
403
|
+
|
|
334
404
|
return hints;
|
|
335
405
|
} catch {
|
|
336
406
|
return [];
|
|
@@ -456,15 +526,6 @@ connection.onCodeAction((params) => {
|
|
|
456
526
|
},
|
|
457
527
|
});
|
|
458
528
|
}
|
|
459
|
-
|
|
460
|
-
if (diag.code === "nudo-assertion-failed") {
|
|
461
|
-
actions.push({
|
|
462
|
-
title: "Update @nudo:returns to match inferred type",
|
|
463
|
-
kind: "quickfix",
|
|
464
|
-
diagnostics: [diag],
|
|
465
|
-
isPreferred: false,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
529
|
}
|
|
469
530
|
|
|
470
531
|
return actions;
|
|
@@ -557,8 +618,12 @@ connection.languages.semanticTokens.on((params) => {
|
|
|
557
618
|
if (!document) return { data: [] };
|
|
558
619
|
if (!isNudoFile(params.textDocument.uri)) return { data: [] };
|
|
559
620
|
|
|
560
|
-
|
|
561
|
-
|
|
621
|
+
try {
|
|
622
|
+
const filePath = uriToFilePath(document.uri);
|
|
623
|
+
return { data: buildSemanticTokens(filePath, document.getText()) };
|
|
624
|
+
} catch {
|
|
625
|
+
return { data: [] };
|
|
626
|
+
}
|
|
562
627
|
});
|
|
563
628
|
|
|
564
629
|
/** Resolve a `uri`- or `file`-identified target to the uri key used by activeCases/documents. */
|
|
@@ -612,6 +677,12 @@ function dispatchNudoCommand(command: string, arg: Record<string, unknown>) {
|
|
|
612
677
|
return suggestCase(arg as Parameters<typeof suggestCase>[0], agentToolDeps);
|
|
613
678
|
case "nudo.trace":
|
|
614
679
|
return trace(arg as Parameters<typeof trace>[0], agentToolDeps);
|
|
680
|
+
case "nudo.check":
|
|
681
|
+
return checkTool(arg as Parameters<typeof checkTool>[0], agentToolDeps);
|
|
682
|
+
case "nudo.hover":
|
|
683
|
+
return hoverTool(arg as Parameters<typeof hoverTool>[0], agentToolDeps);
|
|
684
|
+
case "nudo.infer":
|
|
685
|
+
return inferTool(arg as Parameters<typeof inferTool>[0], agentToolDeps);
|
|
615
686
|
case "nudo.selectCase":
|
|
616
687
|
return handleSelectCase(arg as Parameters<typeof handleSelectCase>[0]);
|
|
617
688
|
case "nudo.getActiveCases":
|
|
@@ -637,6 +708,9 @@ function dispatchAgentRequest(command: string, params: Record<string, unknown>):
|
|
|
637
708
|
connection.onRequest("nudo/whatIf", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.whatIf", params));
|
|
638
709
|
connection.onRequest("nudo/suggestCase", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.suggestCase", params));
|
|
639
710
|
connection.onRequest("nudo/trace", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.trace", params));
|
|
711
|
+
connection.onRequest("nudo/check", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.check", params));
|
|
712
|
+
connection.onRequest("nudo/hover", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.hover", params));
|
|
713
|
+
connection.onRequest("nudo/infer", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.infer", params));
|
|
640
714
|
|
|
641
715
|
connection.languages.diagnostics.on((params) => {
|
|
642
716
|
const document = documents.get(params.textDocument.uri);
|
|
@@ -658,7 +732,9 @@ connection.languages.diagnostics.on((params) => {
|
|
|
658
732
|
const nudoFileCache = new Map<string, boolean>();
|
|
659
733
|
|
|
660
734
|
function isNudoFile(uri: string): boolean {
|
|
661
|
-
|
|
735
|
+
// 推断目标判定收敛到 service 层(TaskA 提供):.js/.mjs/.ts 为目标,
|
|
736
|
+
// .d.ts(类型声明,harvester 输入)与 .tsx/.jsx 等一律排除
|
|
737
|
+
if (!isNudoTargetPath(uriToFilePath(uri))) return false;
|
|
662
738
|
const cached = nudoFileCache.get(uri);
|
|
663
739
|
if (cached !== undefined) return cached;
|
|
664
740
|
const doc = documents.get(uri);
|
package/src/validation.ts
CHANGED
|
@@ -4,16 +4,31 @@
|
|
|
4
4
|
* connection. server.ts wires these functions to `connection` / `documents`;
|
|
5
5
|
* tests wire them to fakes.
|
|
6
6
|
*/
|
|
7
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
8
|
+
import { dirname, resolve as resolvePath, join } from "node:path";
|
|
7
9
|
import {
|
|
8
10
|
analyzeFile,
|
|
9
11
|
analyzeFileAsync,
|
|
10
12
|
buildModuleGraph,
|
|
11
13
|
computeDirtySet,
|
|
14
|
+
defaultLoadModule,
|
|
15
|
+
clearAnalysisSessionCaches,
|
|
16
|
+
evictAbsModuleCacheFiles,
|
|
17
|
+
evictBPathCacheForFiles,
|
|
18
|
+
evictAnalysisFileCacheForFiles,
|
|
19
|
+
evictFnAnalysisCacheForFiles,
|
|
12
20
|
type AnalysisResult,
|
|
13
21
|
type Diagnostic as JsDiagnostic,
|
|
14
22
|
type DiagnosticSeverity as JsDiagSeverity,
|
|
15
23
|
type ModuleGraphCache,
|
|
16
24
|
} from "@nudojs/service";
|
|
25
|
+
import {
|
|
26
|
+
checkSource,
|
|
27
|
+
pTrue,
|
|
28
|
+
evictGeneralizeMemoForPaths,
|
|
29
|
+
evictCheckSourceMemoForPaths,
|
|
30
|
+
extractNudoImports,
|
|
31
|
+
} from "@nudojs/core";
|
|
17
32
|
import {
|
|
18
33
|
DiagnosticSeverity,
|
|
19
34
|
DiagnosticTag,
|
|
@@ -35,11 +50,76 @@ export const knownFiles = new Set<string>();
|
|
|
35
50
|
*/
|
|
36
51
|
export const moduleGraphCache: ModuleGraphCache = new Map();
|
|
37
52
|
|
|
53
|
+
/**
|
|
54
|
+
* `*.nudo.js` → 父分析文件(含 `@nudo:import` 的 .js/.ts)。
|
|
55
|
+
* 供 watched-files 变更时定向重检打开中的父缓冲。
|
|
56
|
+
*/
|
|
57
|
+
export const nudoDepParents = new Map<string, Set<string>>();
|
|
58
|
+
|
|
59
|
+
function normPath(p: string): string {
|
|
60
|
+
return p.replace(/\\/g, "/");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 每次 validate 后刷新:parent 的全部 @nudo:import 边 */
|
|
64
|
+
export function registerNudoImportDeps(filePath: string, source: string): void {
|
|
65
|
+
const parent = normPath(resolvePath(filePath));
|
|
66
|
+
for (const set of nudoDepParents.values()) {
|
|
67
|
+
set.delete(parent);
|
|
68
|
+
}
|
|
69
|
+
const imports = extractNudoImports(source);
|
|
70
|
+
for (const imp of imports) {
|
|
71
|
+
if (!imp.spec.startsWith(".") && !imp.spec.startsWith("/")) continue;
|
|
72
|
+
const dep = normPath(resolvePath(dirname(filePath), imp.spec));
|
|
73
|
+
let set = nudoDepParents.get(dep);
|
|
74
|
+
if (!set) {
|
|
75
|
+
set = new Set();
|
|
76
|
+
nudoDepParents.set(dep, set);
|
|
77
|
+
}
|
|
78
|
+
set.add(parent);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `*.nudo.js` 创建/变更:定向逐出 L0 memo 中依赖该文件的条目,
|
|
84
|
+
* 并重检打开中的父文件(propagate=false,不级联)。
|
|
85
|
+
*/
|
|
86
|
+
export async function handleNudoDepFileChanged(
|
|
87
|
+
nudoPath: string,
|
|
88
|
+
deps: ValidateTextDeps,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const p = normPath(resolvePath(nudoPath));
|
|
91
|
+
evictGeneralizeMemoForPaths([p]);
|
|
92
|
+
evictCheckSourceMemoForPaths([p]);
|
|
93
|
+
const parents = nudoDepParents.get(p);
|
|
94
|
+
if (!parents || parents.size === 0) return;
|
|
95
|
+
const parentList = [...parents];
|
|
96
|
+
// 父文件源码未变但依赖内容变了:整文件 check / B-path / AnalysisResult / fn-cache 都可能陈旧
|
|
97
|
+
evictBPathCacheForFiles(parentList);
|
|
98
|
+
evictAnalysisFileCacheForFiles(parentList);
|
|
99
|
+
evictFnAnalysisCacheForFiles(parentList);
|
|
100
|
+
for (const parent of parentList) {
|
|
101
|
+
const doc = deps.getOpenDocumentByPath?.(parent);
|
|
102
|
+
if (!doc) continue;
|
|
103
|
+
await validateText(
|
|
104
|
+
parent,
|
|
105
|
+
doc.uri,
|
|
106
|
+
doc.getText(),
|
|
107
|
+
doc.version,
|
|
108
|
+
deps,
|
|
109
|
+
false,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
38
114
|
/** Test hook — resets module-level session state. */
|
|
39
115
|
export function clearValidationState(): void {
|
|
40
116
|
analysisCache.clear();
|
|
41
117
|
knownFiles.clear();
|
|
42
118
|
moduleGraphCache.clear();
|
|
119
|
+
nudoDepParents.clear();
|
|
120
|
+
// service+core 会话 memo 全清(B-path / analysis-file / fn-analysis /
|
|
121
|
+
// abs-module / generalize L0 / check 整文件 / nudo-module exec)
|
|
122
|
+
clearAnalysisSessionCaches();
|
|
43
123
|
}
|
|
44
124
|
|
|
45
125
|
/**
|
|
@@ -48,8 +128,12 @@ export function clearValidationState(): void {
|
|
|
48
128
|
* 仅关闭(文件仍在磁盘上)不走这里,关闭文件仍可作为依赖图节点参与脏传播。
|
|
49
129
|
*/
|
|
50
130
|
export function forgetValidatedFile(filePath: string): void {
|
|
131
|
+
const parent = normPath(resolvePath(filePath));
|
|
51
132
|
knownFiles.delete(filePath);
|
|
52
133
|
analysisCache.delete(filePath);
|
|
134
|
+
for (const set of nudoDepParents.values()) {
|
|
135
|
+
set.delete(parent);
|
|
136
|
+
}
|
|
53
137
|
}
|
|
54
138
|
|
|
55
139
|
/**
|
|
@@ -57,13 +141,17 @@ export function forgetValidatedFile(filePath: string): void {
|
|
|
57
141
|
* 把「被删除且不在打开集」的 uri 列表广播到这里(uri→filePath 复用 uriToFilePath)。
|
|
58
142
|
* 已删除文件的条目只剩内存驻留价值——同名重建文件若 mtime/size 恰好撞上旧值,
|
|
59
143
|
* 会复用陈旧边集得出错误 dirty 集,因此删除时立即逐出。
|
|
144
|
+
* Abs 依赖模块导出缓存(abs-modules-graph)同口径逐出:虽然 stat 失效自愈,
|
|
145
|
+
* 但删除文件的内存驻留条目应随会话清理释放。
|
|
60
146
|
*/
|
|
61
147
|
export function evictModuleGraphCacheEntries(uris: string[]): void {
|
|
62
|
-
|
|
148
|
+
const paths = uris.map(uriToFilePath);
|
|
149
|
+
for (const p of paths) moduleGraphCache.delete(p);
|
|
150
|
+
evictAbsModuleCacheFiles(paths);
|
|
63
151
|
}
|
|
64
152
|
|
|
65
153
|
export function hasNudoDirectives(source: string): boolean {
|
|
66
|
-
return /@nudo:(case|mock|pure|skip|sample|
|
|
154
|
+
return /@nudo:(case|mock|pure|skip|sample|refine|import|env|mock-module|as|replace)\b/.test(source);
|
|
67
155
|
}
|
|
68
156
|
|
|
69
157
|
export function uriToFilePath(uri: string): string {
|
|
@@ -140,6 +228,60 @@ export function toLspDiagnostic(d: JsDiagnostic, uri: string): LspDiagnostic {
|
|
|
140
228
|
return diag;
|
|
141
229
|
}
|
|
142
230
|
|
|
231
|
+
/** CLI 与 LSP 共用的相对 require 解析(service defaultLoadModule) */
|
|
232
|
+
export function lspLoadModule(spec: string, fromFile: string): string | undefined {
|
|
233
|
+
return defaultLoadModule(spec, fromFile);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Abs check → LSP diagnostics(主通道:约束蕴含,非 TS assignability)。
|
|
238
|
+
*/
|
|
239
|
+
export function checkToLspDiagnostics(
|
|
240
|
+
filePath: string,
|
|
241
|
+
source: string,
|
|
242
|
+
loadModule?: (spec: string, fromFile: string) => string | undefined,
|
|
243
|
+
): LspDiagnostic[] {
|
|
244
|
+
try {
|
|
245
|
+
const report = checkSource(filePath, source, pTrue, {
|
|
246
|
+
loadModule: loadModule ?? lspLoadModule,
|
|
247
|
+
fromFile: filePath,
|
|
248
|
+
});
|
|
249
|
+
return report.issues
|
|
250
|
+
.filter((i) => i.severity === "error" || i.severity === "warning")
|
|
251
|
+
.map((i) => {
|
|
252
|
+
const line = (i.line ?? 1) - 1;
|
|
253
|
+
const col = i.column ?? 0;
|
|
254
|
+
const parts = [i.message];
|
|
255
|
+
if (i.actual) parts.push(`actual: ${i.actual}`);
|
|
256
|
+
if (i.expected) parts.push(`expected: ${i.expected}`);
|
|
257
|
+
if (i.suggestion) parts.push(i.suggestion);
|
|
258
|
+
return {
|
|
259
|
+
severity:
|
|
260
|
+
i.severity === "error"
|
|
261
|
+
? DiagnosticSeverity.Error
|
|
262
|
+
: i.severity === "warning"
|
|
263
|
+
? DiagnosticSeverity.Warning
|
|
264
|
+
: DiagnosticSeverity.Information,
|
|
265
|
+
range: {
|
|
266
|
+
start: { line, character: col },
|
|
267
|
+
end: { line, character: col + 1 },
|
|
268
|
+
},
|
|
269
|
+
message: parts.join(" · "),
|
|
270
|
+
source: "nudo-check",
|
|
271
|
+
code: i.code,
|
|
272
|
+
data: {
|
|
273
|
+
actual: i.actual,
|
|
274
|
+
expected: i.expected,
|
|
275
|
+
fn: i.fn,
|
|
276
|
+
suggestions: i.suggestion ? [i.suggestion] : [],
|
|
277
|
+
},
|
|
278
|
+
} satisfies LspDiagnostic;
|
|
279
|
+
});
|
|
280
|
+
} catch {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
143
285
|
/**
|
|
144
286
|
* Analyze one document (async so path-based `@nudo:env` files preload),
|
|
145
287
|
* publish diagnostics, refresh the analysis cache, and — when `propagate` —
|
|
@@ -177,7 +319,12 @@ export async function validateText(
|
|
|
177
319
|
|
|
178
320
|
analysisCache.set(filePath, { version, result });
|
|
179
321
|
knownFiles.add(filePath);
|
|
180
|
-
|
|
322
|
+
registerNudoImportDeps(filePath, text);
|
|
323
|
+
|
|
324
|
+
// Abs check 主通道 + evaluator 诊断
|
|
325
|
+
const checkDiags = checkToLspDiagnostics(filePath, text);
|
|
326
|
+
const evalDiags = result.diagnostics.map((d) => toLspDiagnostic(d, uri));
|
|
327
|
+
deps.sendDiagnostics({ uri, diagnostics: [...checkDiags, ...evalDiags] });
|
|
181
328
|
|
|
182
329
|
if (!propagate || !deps.getOpenDocumentByPath) return;
|
|
183
330
|
|
|
@@ -192,6 +339,10 @@ export async function validateText(
|
|
|
192
339
|
if (dirtyPath === filePath) continue;
|
|
193
340
|
const doc = deps.getOpenDocumentByPath(dirtyPath);
|
|
194
341
|
if (!doc) continue;
|
|
342
|
+
// 依赖内容变了但父文件源码未变:整文件 AnalysisResult / B-path / fn-cache 键不含 dep 指纹
|
|
343
|
+
evictBPathCacheForFiles([dirtyPath]);
|
|
344
|
+
evictAnalysisFileCacheForFiles([dirtyPath]);
|
|
345
|
+
evictFnAnalysisCacheForFiles([dirtyPath]);
|
|
195
346
|
await validateText(dirtyPath, doc.uri, doc.getText(), doc.version, deps, false);
|
|
196
347
|
}
|
|
197
348
|
}
|