@nudojs/lsp 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +525 -87
- package/package.json +15 -5
- package/CHANGELOG.md +0 -63
- package/agent-skill/SKILL.md +0 -82
- package/src/__tests__/agent-check.test.ts +0 -47
- package/src/__tests__/agent-hover.test.ts +0 -28
- package/src/__tests__/agent-infer.test.ts +0 -38
- package/src/__tests__/check-diagnostics.test.ts +0 -46
- package/src/__tests__/definition.test.ts +0 -32
- package/src/__tests__/lsp-integration.test.ts +0 -921
- package/src/__tests__/nudo-dep-evict.test.ts +0 -103
- package/src/agent-tools.ts +0 -486
- package/src/semantic-tokens.ts +0 -9
- package/src/server.ts +0 -748
- package/src/symbols.ts +0 -109
- package/src/validation.ts +0 -348
- package/tsconfig.json +0 -12
package/src/symbols.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type { Node } from "@babel/types";
|
|
2
|
-
import traverse from "@babel/traverse";
|
|
3
|
-
import type { SymbolInfo, ReferenceInfo, SymbolTable, SourceLocation } from "@nudojs/service";
|
|
4
|
-
|
|
5
|
-
function locFromNode(node: Node): SourceLocation {
|
|
6
|
-
return {
|
|
7
|
-
start: { line: node.loc?.start.line ?? 1, column: node.loc?.start.column ?? 0 },
|
|
8
|
-
end: { line: node.loc?.end.line ?? 1, column: node.loc?.end.column ?? 0 },
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function buildSymbolTable(ast: Node, uri: string): SymbolTable {
|
|
13
|
-
const definitions = new Map<string, SymbolInfo>();
|
|
14
|
-
const references: ReferenceInfo[] = [];
|
|
15
|
-
|
|
16
|
-
const traverseFn = (typeof traverse === "function" ? traverse : (traverse as any).default) as typeof traverse;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
traverseFn(ast, {
|
|
20
|
-
// Collect definitions
|
|
21
|
-
FunctionDeclaration(path) {
|
|
22
|
-
if (path.node.id) {
|
|
23
|
-
definitions.set(path.node.id.name, {
|
|
24
|
-
name: path.node.id.name,
|
|
25
|
-
kind: "function",
|
|
26
|
-
loc: locFromNode(path.node.id),
|
|
27
|
-
uri,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
VariableDeclarator(path) {
|
|
32
|
-
if (path.node.id.type === "Identifier") {
|
|
33
|
-
definitions.set(path.node.id.name, {
|
|
34
|
-
name: path.node.id.name,
|
|
35
|
-
kind: "variable",
|
|
36
|
-
loc: locFromNode(path.node.id),
|
|
37
|
-
uri,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
ClassDeclaration(path) {
|
|
42
|
-
if (path.node.id) {
|
|
43
|
-
definitions.set(path.node.id.name, {
|
|
44
|
-
name: path.node.id.name,
|
|
45
|
-
kind: "class",
|
|
46
|
-
loc: locFromNode(path.node.id),
|
|
47
|
-
uri,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
// Collect references
|
|
52
|
-
Identifier(path) {
|
|
53
|
-
// Skip definition sites
|
|
54
|
-
if (path.parentPath?.node.type === "FunctionDeclaration" && path.parentPath.node.id === path.node) return;
|
|
55
|
-
if (path.parentPath?.node.type === "VariableDeclarator" && path.parentPath.node.id === path.node) return;
|
|
56
|
-
if (path.parentPath?.node.type === "ClassDeclaration" && path.parentPath.node.id === path.node) return;
|
|
57
|
-
|
|
58
|
-
references.push({
|
|
59
|
-
name: path.node.name,
|
|
60
|
-
loc: locFromNode(path.node),
|
|
61
|
-
uri,
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
} catch {
|
|
66
|
-
// traverse may fail on partial ASTs
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return { definitions, references };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function findDefinition(
|
|
73
|
-
symbolTable: SymbolTable,
|
|
74
|
-
name: string,
|
|
75
|
-
): SymbolInfo | null {
|
|
76
|
-
return symbolTable.definitions.get(name) ?? null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function findReferences(
|
|
80
|
-
symbolTable: SymbolTable,
|
|
81
|
-
name: string,
|
|
82
|
-
): ReferenceInfo[] {
|
|
83
|
-
return symbolTable.references.filter((r) => r.name === name);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function findIdentifierAtPosition(ast: Node, line: number, column: number): string | null {
|
|
87
|
-
let found: string | null = null;
|
|
88
|
-
const traverseFn = (typeof traverse === "function" ? traverse : (traverse as any).default) as typeof traverse;
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
traverseFn(ast, {
|
|
92
|
-
Identifier(path) {
|
|
93
|
-
const loc = path.node.loc;
|
|
94
|
-
if (!loc) return;
|
|
95
|
-
if (
|
|
96
|
-
loc.start.line === line &&
|
|
97
|
-
loc.start.column <= column &&
|
|
98
|
-
loc.end.column >= column
|
|
99
|
-
) {
|
|
100
|
-
found = path.node.name;
|
|
101
|
-
path.stop();
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
} catch {
|
|
106
|
-
// ignore
|
|
107
|
-
}
|
|
108
|
-
return found;
|
|
109
|
-
}
|
package/src/validation.ts
DELETED
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core validation + analysis-cache logic for the LSP server, extracted from
|
|
3
|
-
* server.ts so it can be exercised directly in tests without a live LSP
|
|
4
|
-
* connection. server.ts wires these functions to `connection` / `documents`;
|
|
5
|
-
* tests wire them to fakes.
|
|
6
|
-
*/
|
|
7
|
-
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
8
|
-
import { dirname, resolve as resolvePath, join } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
analyzeFile,
|
|
11
|
-
analyzeFileAsync,
|
|
12
|
-
buildModuleGraph,
|
|
13
|
-
computeDirtySet,
|
|
14
|
-
defaultLoadModule,
|
|
15
|
-
clearAnalysisSessionCaches,
|
|
16
|
-
evictAbsModuleCacheFiles,
|
|
17
|
-
evictBPathCacheForFiles,
|
|
18
|
-
evictAnalysisFileCacheForFiles,
|
|
19
|
-
evictFnAnalysisCacheForFiles,
|
|
20
|
-
type AnalysisResult,
|
|
21
|
-
type Diagnostic as JsDiagnostic,
|
|
22
|
-
type DiagnosticSeverity as JsDiagSeverity,
|
|
23
|
-
type ModuleGraphCache,
|
|
24
|
-
} from "@nudojs/service";
|
|
25
|
-
import {
|
|
26
|
-
checkSource,
|
|
27
|
-
pTrue,
|
|
28
|
-
evictGeneralizeMemoForPaths,
|
|
29
|
-
evictCheckSourceMemoForPaths,
|
|
30
|
-
extractNudoImports,
|
|
31
|
-
} from "@nudojs/core";
|
|
32
|
-
import {
|
|
33
|
-
DiagnosticSeverity,
|
|
34
|
-
DiagnosticTag,
|
|
35
|
-
type Diagnostic as LspDiagnostic,
|
|
36
|
-
} from "vscode-languageserver/node";
|
|
37
|
-
|
|
38
|
-
/** Per-file analysis cache; version comes from TextDocument.version. */
|
|
39
|
-
export const analysisCache = new Map<string, { version: number; result: AnalysisResult }>();
|
|
40
|
-
|
|
41
|
-
/** Every file analyzed successfully in this session (import-graph nodes for dirty propagation). */
|
|
42
|
-
export const knownFiles = new Set<string>();
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 模块图边缓存(会话级常驻):key 为文件路径,value 为 mtimeMs+size+已抽取的 import 边。
|
|
46
|
-
* validateText 的脏传播把整个 knownFiles 喂给 buildModuleGraph——命中条目只做 stat
|
|
47
|
-
* 比对、跳过磁盘重读与重解析,使「编辑防抖→脏传播」不再每次全量重读会话摸过的
|
|
48
|
-
* 所有文件。条目仅是路径+边集字符串数组,内存随会话文件数线性有界;
|
|
49
|
-
* watched-files 删除事件经 evictModuleGraphCacheEntries 逐出对应条目。
|
|
50
|
-
*/
|
|
51
|
-
export const moduleGraphCache: ModuleGraphCache = new Map();
|
|
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
|
-
|
|
114
|
-
/** Test hook — resets module-level session state. */
|
|
115
|
-
export function clearValidationState(): void {
|
|
116
|
-
analysisCache.clear();
|
|
117
|
-
knownFiles.clear();
|
|
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();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* watched-files Deleted 清理:从会话登记中移除一个文件(knownFiles + analysisCache)。
|
|
127
|
-
* server.ts 的 DidChangeWatchedFiles handler 对「被删除且不在打开集」的文件逐个调用;
|
|
128
|
-
* 仅关闭(文件仍在磁盘上)不走这里,关闭文件仍可作为依赖图节点参与脏传播。
|
|
129
|
-
*/
|
|
130
|
-
export function forgetValidatedFile(filePath: string): void {
|
|
131
|
-
const parent = normPath(resolvePath(filePath));
|
|
132
|
-
knownFiles.delete(filePath);
|
|
133
|
-
analysisCache.delete(filePath);
|
|
134
|
-
for (const set of nudoDepParents.values()) {
|
|
135
|
-
set.delete(parent);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* watched-files 删除事件的模块图边缓存逐出:server.ts 通过 registerWatchedFilesListener
|
|
141
|
-
* 把「被删除且不在打开集」的 uri 列表广播到这里(uri→filePath 复用 uriToFilePath)。
|
|
142
|
-
* 已删除文件的条目只剩内存驻留价值——同名重建文件若 mtime/size 恰好撞上旧值,
|
|
143
|
-
* 会复用陈旧边集得出错误 dirty 集,因此删除时立即逐出。
|
|
144
|
-
* Abs 依赖模块导出缓存(abs-modules-graph)同口径逐出:虽然 stat 失效自愈,
|
|
145
|
-
* 但删除文件的内存驻留条目应随会话清理释放。
|
|
146
|
-
*/
|
|
147
|
-
export function evictModuleGraphCacheEntries(uris: string[]): void {
|
|
148
|
-
const paths = uris.map(uriToFilePath);
|
|
149
|
-
for (const p of paths) moduleGraphCache.delete(p);
|
|
150
|
-
evictAbsModuleCacheFiles(paths);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function hasNudoDirectives(source: string): boolean {
|
|
154
|
-
return /@nudo:(case|mock|pure|skip|sample|refine|import|env|mock-module|as|replace)\b/.test(source);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function uriToFilePath(uri: string): string {
|
|
158
|
-
return uri.startsWith("file://") ? decodeURIComponent(uri.slice(7)) : uri;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Cache-aware sync analysis for high-frequency handlers: returns the cached
|
|
163
|
-
* result when the document version matches, otherwise falls back to sync
|
|
164
|
-
* analyzeFile (path-based `@nudo:env` files degrade here — the async preload
|
|
165
|
-
* only runs on the validation path).
|
|
166
|
-
*/
|
|
167
|
-
export function getCachedOrAnalyze(
|
|
168
|
-
filePath: string,
|
|
169
|
-
source: string,
|
|
170
|
-
version: number,
|
|
171
|
-
activeCases?: Map<string, number>,
|
|
172
|
-
): AnalysisResult {
|
|
173
|
-
const cached = analysisCache.get(filePath);
|
|
174
|
-
if (cached && cached.version === version) return cached.result;
|
|
175
|
-
const result = analyzeFile(filePath, source, activeCases);
|
|
176
|
-
analysisCache.set(filePath, { version, result });
|
|
177
|
-
return result;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export type OpenDocumentLike = {
|
|
181
|
-
uri: string;
|
|
182
|
-
version: number;
|
|
183
|
-
getText(): string;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
export type ValidateTextDeps = {
|
|
187
|
-
sendDiagnostics: (params: { uri: string; diagnostics: LspDiagnostic[] }) => void;
|
|
188
|
-
/** Nudo-file gate; when omitted every uri is validated. */
|
|
189
|
-
isNudoUri?: (uri: string) => boolean;
|
|
190
|
-
getActiveCases?: (uri: string) => Map<string, number>;
|
|
191
|
-
/** Open-document lookup by file path — enables dirty propagation to dependents. */
|
|
192
|
-
getOpenDocumentByPath?: (filePath: string) => OpenDocumentLike | undefined;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const severityMap: Record<JsDiagSeverity, DiagnosticSeverity> = {
|
|
196
|
-
error: DiagnosticSeverity.Error,
|
|
197
|
-
warning: DiagnosticSeverity.Warning,
|
|
198
|
-
info: DiagnosticSeverity.Information,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
export function toLspDiagnostic(d: JsDiagnostic, uri: string): LspDiagnostic {
|
|
202
|
-
const diag: LspDiagnostic = {
|
|
203
|
-
severity: severityMap[d.severity],
|
|
204
|
-
range: {
|
|
205
|
-
start: { line: d.range.start.line - 1, character: d.range.start.column },
|
|
206
|
-
end: { line: d.range.end.line - 1, character: d.range.end.column },
|
|
207
|
-
},
|
|
208
|
-
message: d.message,
|
|
209
|
-
source: "nudo",
|
|
210
|
-
code: d.code,
|
|
211
|
-
data: { ...((d.data as object) ?? {}), suggestions: d.suggestions },
|
|
212
|
-
};
|
|
213
|
-
if (d.tags?.includes("unnecessary")) {
|
|
214
|
-
diag.tags = [DiagnosticTag.Unnecessary];
|
|
215
|
-
}
|
|
216
|
-
if (d.origin) {
|
|
217
|
-
diag.relatedInformation = [{
|
|
218
|
-
location: {
|
|
219
|
-
uri,
|
|
220
|
-
range: {
|
|
221
|
-
start: { line: d.origin.line - 1, character: d.origin.column },
|
|
222
|
-
end: { line: d.origin.line - 1, character: d.origin.column },
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
message: "value originates here",
|
|
226
|
-
}];
|
|
227
|
-
}
|
|
228
|
-
return diag;
|
|
229
|
-
}
|
|
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
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Analyze one document (async so path-based `@nudo:env` files preload),
|
|
287
|
-
* publish diagnostics, refresh the analysis cache, and — when `propagate` —
|
|
288
|
-
* revalidate open dependents of the changed file once. Propagation-triggered
|
|
289
|
-
* revalidations pass propagate=false, so dirt never cascades further.
|
|
290
|
-
*/
|
|
291
|
-
export async function validateText(
|
|
292
|
-
filePath: string,
|
|
293
|
-
uri: string,
|
|
294
|
-
text: string,
|
|
295
|
-
version: number,
|
|
296
|
-
deps: ValidateTextDeps,
|
|
297
|
-
propagate = false,
|
|
298
|
-
): Promise<void> {
|
|
299
|
-
if (deps.isNudoUri && !deps.isNudoUri(uri)) {
|
|
300
|
-
deps.sendDiagnostics({ uri, diagnostics: [] });
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
let result: AnalysisResult;
|
|
305
|
-
try {
|
|
306
|
-
result = await analyzeFileAsync(filePath, text, deps.getActiveCases?.(uri));
|
|
307
|
-
} catch (err) {
|
|
308
|
-
deps.sendDiagnostics({
|
|
309
|
-
uri,
|
|
310
|
-
diagnostics: [{
|
|
311
|
-
severity: DiagnosticSeverity.Error,
|
|
312
|
-
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
|
313
|
-
message: `Analysis error: ${(err as Error).message}`,
|
|
314
|
-
source: "nudo",
|
|
315
|
-
}],
|
|
316
|
-
});
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
analysisCache.set(filePath, { version, result });
|
|
321
|
-
knownFiles.add(filePath);
|
|
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] });
|
|
328
|
-
|
|
329
|
-
if (!propagate || !deps.getOpenDocumentByPath) return;
|
|
330
|
-
|
|
331
|
-
let dependents: Map<string, Set<string>>;
|
|
332
|
-
try {
|
|
333
|
-
// 传入会话级 moduleGraphCache:未变文件仅 stat 比对即复用边集,跳过重读重解析
|
|
334
|
-
({ dependents } = buildModuleGraph([...knownFiles], moduleGraphCache));
|
|
335
|
-
} catch {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
for (const dirtyPath of computeDirtySet(dependents, filePath)) {
|
|
339
|
-
if (dirtyPath === filePath) continue;
|
|
340
|
-
const doc = deps.getOpenDocumentByPath(dirtyPath);
|
|
341
|
-
if (!doc) continue;
|
|
342
|
-
// 依赖内容变了但父文件源码未变:整文件 AnalysisResult / B-path / fn-cache 键不含 dep 指纹
|
|
343
|
-
evictBPathCacheForFiles([dirtyPath]);
|
|
344
|
-
evictAnalysisFileCacheForFiles([dirtyPath]);
|
|
345
|
-
evictFnAnalysisCacheForFiles([dirtyPath]);
|
|
346
|
-
await validateText(dirtyPath, doc.uri, doc.getText(), doc.version, deps, false);
|
|
347
|
-
}
|
|
348
|
-
}
|