@domainlang/language 0.10.0 → 0.12.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/README.md +44 -102
- package/out/diagram/context-map-diagram-generator.d.ts +65 -0
- package/out/diagram/context-map-diagram-generator.js +356 -0
- package/out/diagram/context-map-diagram-generator.js.map +1 -0
- package/out/diagram/context-map-layout-configurator.d.ts +15 -0
- package/out/diagram/context-map-layout-configurator.js +39 -0
- package/out/diagram/context-map-layout-configurator.js.map +1 -0
- package/out/diagram/elk-layout-factory.d.ts +43 -0
- package/out/diagram/elk-layout-factory.js +64 -0
- package/out/diagram/elk-layout-factory.js.map +1 -0
- package/out/domain-lang-module.d.ts +9 -2
- package/out/domain-lang-module.js +13 -4
- package/out/domain-lang-module.js.map +1 -1
- package/out/index.d.ts +6 -0
- package/out/index.js +7 -0
- package/out/index.js.map +1 -1
- package/out/lsp/domain-lang-code-lens-provider.d.ts +8 -0
- package/out/lsp/domain-lang-code-lens-provider.js +48 -0
- package/out/lsp/domain-lang-code-lens-provider.js.map +1 -0
- package/out/lsp/domain-lang-completion.js +1 -1
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-document-symbol-provider.js +5 -5
- package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -1
- package/out/lsp/domain-lang-index-manager.d.ts +149 -5
- package/out/lsp/domain-lang-index-manager.js +388 -52
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-refresh.d.ts +35 -0
- package/out/lsp/domain-lang-refresh.js +129 -0
- package/out/lsp/domain-lang-refresh.js.map +1 -0
- package/out/lsp/domain-lang-workspace-manager.d.ts +10 -0
- package/out/lsp/domain-lang-workspace-manager.js +35 -0
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/tool-handlers.js +63 -57
- package/out/lsp/tool-handlers.js.map +1 -1
- package/out/main.js +30 -190
- package/out/main.js.map +1 -1
- package/out/sdk/loader-node.js +1 -1
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/validator.js +17 -14
- package/out/sdk/validator.js.map +1 -1
- package/out/services/import-resolver.d.ts +67 -17
- package/out/services/import-resolver.js +146 -65
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/lsp-logger.d.ts +42 -0
- package/out/services/lsp-logger.js +50 -0
- package/out/services/lsp-logger.js.map +1 -0
- package/out/services/lsp-runtime-settings.d.ts +20 -0
- package/out/services/lsp-runtime-settings.js +20 -0
- package/out/services/lsp-runtime-settings.js.map +1 -0
- package/out/services/performance-optimizer.d.ts +9 -9
- package/out/services/performance-optimizer.js +17 -41
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/workspace-manager.d.ts +22 -1
- package/out/services/workspace-manager.js +57 -9
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/import-utils.js +6 -6
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +6 -0
- package/out/validation/constants.js +7 -0
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +13 -3
- package/out/validation/import.js +54 -10
- package/out/validation/import.js.map +1 -1
- package/package.json +5 -2
- package/src/diagram/context-map-diagram-generator.ts +451 -0
- package/src/diagram/context-map-layout-configurator.ts +43 -0
- package/src/diagram/elk-layout-factory.ts +83 -0
- package/src/domain-lang-module.ts +22 -5
- package/src/index.ts +8 -0
- package/src/lsp/domain-lang-code-lens-provider.ts +54 -0
- package/src/lsp/domain-lang-completion.ts +3 -3
- package/src/lsp/domain-lang-document-symbol-provider.ts +5 -5
- package/src/lsp/domain-lang-index-manager.ts +438 -56
- package/src/lsp/domain-lang-refresh.ts +205 -0
- package/src/lsp/domain-lang-workspace-manager.ts +45 -0
- package/src/lsp/tool-handlers.ts +61 -47
- package/src/main.ts +36 -244
- package/src/sdk/loader-node.ts +1 -1
- package/src/sdk/validator.ts +17 -13
- package/src/services/import-resolver.ts +196 -89
- package/src/services/lsp-logger.ts +89 -0
- package/src/services/lsp-runtime-settings.ts +34 -0
- package/src/services/performance-optimizer.ts +18 -57
- package/src/services/workspace-manager.ts +62 -10
- package/src/utils/import-utils.ts +6 -6
- package/src/validation/constants.ts +9 -0
- package/src/validation/import.ts +67 -12
|
@@ -6,11 +6,17 @@ import type { Model } from '../generated/ast.js';
|
|
|
6
6
|
import type { ImportResolver } from '../services/import-resolver.js';
|
|
7
7
|
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
8
8
|
import type { ImportInfo } from '../services/types.js';
|
|
9
|
+
import { createLogger } from '../services/lsp-logger.js';
|
|
10
|
+
|
|
11
|
+
const log = createLogger('IndexManager');
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Custom IndexManager that extends Langium's default to:
|
|
12
15
|
* 1. Automatically load imported documents during indexing
|
|
13
16
|
* 2. Track import dependencies for cross-file revalidation
|
|
17
|
+
* 3. Export-signature diffing to prevent unnecessary cascading (PRS-017 R2)
|
|
18
|
+
* 4. Import cycle detection with diagnostics (PRS-017 R3)
|
|
19
|
+
* 5. Targeted ImportResolver cache invalidation (PRS-017 R1)
|
|
14
20
|
*
|
|
15
21
|
* **Why this exists:**
|
|
16
22
|
* Langium's `DefaultIndexManager.isAffected()` only checks cross-references
|
|
@@ -52,6 +58,35 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
52
58
|
* Cleared on workspace config changes.
|
|
53
59
|
*/
|
|
54
60
|
private readonly importsLoaded = new Set<string>();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Per-cycle cache for the transitive affected set computation.
|
|
64
|
+
* Uses `changedUris` Set identity as cache key — Langium creates a fresh Set
|
|
65
|
+
* for each `DocumentBuilder.update()` cycle, so reference equality naturally
|
|
66
|
+
* invalidates the cache between cycles.
|
|
67
|
+
*/
|
|
68
|
+
private transitiveAffectedCache: { key: Set<string>; result: Set<string> } | undefined;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Export snapshot cache (PRS-017 R2): maps document URI to its exported symbol
|
|
72
|
+
* signatures. Used to detect whether a document's public interface actually
|
|
73
|
+
* changed, preventing cascading revalidation for implementation-only changes.
|
|
74
|
+
* Signature = "nodeType:qualifiedName" for each exported symbol.
|
|
75
|
+
*/
|
|
76
|
+
private readonly exportSnapshots = new Map<string, Set<string>>();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tracks which URIs had their exports actually change during the current
|
|
80
|
+
* update cycle. Reset before each updateContent() call. Used by isAffected()
|
|
81
|
+
* to skip transitive invalidation when exports are unchanged.
|
|
82
|
+
*/
|
|
83
|
+
private readonly changedExports = new Set<string>();
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detected import cycles (PRS-017 R3): maps document URI to the cycle path.
|
|
87
|
+
* Populated during trackImportDependencies(). Consumed by ImportValidator.
|
|
88
|
+
*/
|
|
89
|
+
private readonly detectedCycles = new Map<string, string[]>();
|
|
55
90
|
|
|
56
91
|
/**
|
|
57
92
|
* Reference to shared services for accessing LangiumDocuments.
|
|
@@ -95,19 +130,48 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
95
130
|
|
|
96
131
|
/**
|
|
97
132
|
* Extends the default content update to:
|
|
98
|
-
* 1.
|
|
99
|
-
* 2.
|
|
133
|
+
* 1. Capture export snapshot before update (PRS-017 R2)
|
|
134
|
+
* 2. Ensure all imported documents are loaded
|
|
135
|
+
* 3. Track import dependencies for change propagation
|
|
136
|
+
* 4. Compare export snapshot to detect interface changes (PRS-017 R2)
|
|
137
|
+
* 5. Detect import cycles (PRS-017 R3)
|
|
138
|
+
* 6. Trigger targeted ImportResolver cache invalidation (PRS-017 R1)
|
|
100
139
|
*
|
|
101
140
|
* Called by Langium during the IndexedContent build phase.
|
|
102
141
|
* This is BEFORE linking/validation, so imports are available for resolution.
|
|
103
142
|
*/
|
|
104
143
|
override async updateContent(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
|
|
105
|
-
|
|
144
|
+
const uri = document.uri.toString();
|
|
145
|
+
|
|
146
|
+
// R2: Capture export snapshot BEFORE re-indexing
|
|
147
|
+
const oldExports = this.exportSnapshots.get(uri);
|
|
148
|
+
|
|
149
|
+
// Standard content indexing
|
|
106
150
|
await super.updateContent(document, cancelToken);
|
|
107
151
|
|
|
108
|
-
//
|
|
152
|
+
// R2: Capture new export snapshot and compare
|
|
153
|
+
const newExports = this.captureExportSnapshot(uri);
|
|
154
|
+
this.exportSnapshots.set(uri, newExports);
|
|
155
|
+
const exportsChanged = !oldExports || !this.setsEqual(oldExports, newExports);
|
|
156
|
+
if (exportsChanged) {
|
|
157
|
+
this.changedExports.add(uri);
|
|
158
|
+
log.info('exports changed', { uri });
|
|
159
|
+
} else {
|
|
160
|
+
// R2: Remove from changedExports when exports stabilize.
|
|
161
|
+
// Without this, the set accumulates indefinitely and the
|
|
162
|
+
// anyExportsChanged() gate stays permanently open.
|
|
163
|
+
this.changedExports.delete(uri);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Ensure imports are loaded and track dependencies
|
|
109
167
|
await this.ensureImportsLoaded(document);
|
|
110
168
|
await this.trackImportDependencies(document);
|
|
169
|
+
|
|
170
|
+
// R3: Detect import cycles after tracking dependencies
|
|
171
|
+
this.detectAndStoreCycles(uri);
|
|
172
|
+
|
|
173
|
+
// R1: Targeted ImportResolver cache invalidation
|
|
174
|
+
this.invalidateImportResolverCache(uri);
|
|
111
175
|
}
|
|
112
176
|
|
|
113
177
|
/**
|
|
@@ -131,11 +195,19 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
131
195
|
}
|
|
132
196
|
|
|
133
197
|
/**
|
|
134
|
-
* Extends `isAffected` to
|
|
198
|
+
* Extends `isAffected` to check import dependencies — direct, transitive,
|
|
199
|
+
* and specifier-sensitive.
|
|
135
200
|
*
|
|
136
201
|
* A document is affected if:
|
|
137
202
|
* 1. It has cross-references to any changed document (default Langium behavior)
|
|
138
|
-
* 2. It imports any
|
|
203
|
+
* 2. It directly or transitively imports any changed document whose exports
|
|
204
|
+
* actually changed (PRS-017 R2 — export-signature diffing)
|
|
205
|
+
* 3. Its import specifiers match changed file paths (handles renames/moves)
|
|
206
|
+
*
|
|
207
|
+
* The transitive affected set is computed once per `update()` cycle and cached
|
|
208
|
+
* using `changedUris` Set identity (Langium creates a fresh Set per cycle).
|
|
209
|
+
* This avoids redundant BFS walks when `isAffected()` is called for every
|
|
210
|
+
* loaded document in the workspace.
|
|
139
211
|
*/
|
|
140
212
|
override isAffected(document: LangiumDocument, changedUris: Set<string>): boolean {
|
|
141
213
|
// First check Langium's default: cross-references
|
|
@@ -143,16 +215,84 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
143
215
|
return true;
|
|
144
216
|
}
|
|
145
217
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
218
|
+
// R2: If no changed URIs had their exports change, skip transitive check.
|
|
219
|
+
// This prevents cascading revalidation for implementation-only changes
|
|
220
|
+
// (e.g., editing a domain's vision string).
|
|
221
|
+
const hasExportChanges = this.anyExportsChanged(changedUris);
|
|
222
|
+
if (!hasExportChanges) {
|
|
223
|
+
// Still check specifier matches for file renames/moves
|
|
224
|
+
const changedPaths = this.extractPathSegments(changedUris);
|
|
225
|
+
for (const changedPath of changedPaths) {
|
|
226
|
+
const infos = this.documentImportInfo.get(document.uri.toString());
|
|
227
|
+
if (infos && this.hasMatchingSpecifierOrResolvedUri(infos, new Set([changedPath]))) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
152
230
|
}
|
|
231
|
+
return false;
|
|
153
232
|
}
|
|
154
233
|
|
|
155
|
-
|
|
234
|
+
// Then check our import dependency graph (direct + transitive + specifier)
|
|
235
|
+
const affectedSet = this.computeAffectedSet(changedUris);
|
|
236
|
+
return affectedSet.has(document.uri.toString());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Computes the full set of document URIs affected by changes.
|
|
241
|
+
* Cached per `changedUris` identity to avoid recomputation across multiple
|
|
242
|
+
* `isAffected()` calls within the same `DocumentBuilder.update()` cycle.
|
|
243
|
+
*
|
|
244
|
+
* Combines two dependency strategies:
|
|
245
|
+
* 1. **Reverse graph walk** — direct and transitive importers via `importDependencies`
|
|
246
|
+
* 2. **Specifier matching** — documents whose import specifiers match changed file
|
|
247
|
+
* paths (handles file renames/moves that change how imports resolve)
|
|
248
|
+
*/
|
|
249
|
+
private computeAffectedSet(changedUris: Set<string>): Set<string> {
|
|
250
|
+
// Cache hit: same changedUris Set reference means same update() cycle
|
|
251
|
+
if (this.transitiveAffectedCache?.key === changedUris) {
|
|
252
|
+
return this.transitiveAffectedCache.result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const affected = new Set<string>();
|
|
256
|
+
this.addTransitiveDependents(changedUris, affected);
|
|
257
|
+
this.addSpecifierMatches(changedUris, affected);
|
|
258
|
+
|
|
259
|
+
this.transitiveAffectedCache = { key: changedUris, result: affected };
|
|
260
|
+
return affected;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* BFS through the reverse dependency graph to find all transitive importers.
|
|
265
|
+
* If C changes and B imports C and A imports B, both A and B are added.
|
|
266
|
+
*/
|
|
267
|
+
private addTransitiveDependents(changedUris: Set<string>, affected: Set<string>): void {
|
|
268
|
+
const toProcess = [...changedUris];
|
|
269
|
+
let uri: string | undefined;
|
|
270
|
+
while ((uri = toProcess.pop()) !== undefined) {
|
|
271
|
+
const dependents = this.importDependencies.get(uri);
|
|
272
|
+
if (!dependents) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
for (const dep of dependents) {
|
|
276
|
+
if (!affected.has(dep) && !changedUris.has(dep)) {
|
|
277
|
+
affected.add(dep);
|
|
278
|
+
toProcess.push(dep);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Finds documents whose import specifiers fuzzy-match changed file paths.
|
|
286
|
+
* Handles file renames/moves where the resolved URI hasn't been updated yet.
|
|
287
|
+
*/
|
|
288
|
+
private addSpecifierMatches(changedUris: Set<string>, affected: Set<string>): void {
|
|
289
|
+
const changedPaths = this.extractPathSegments(changedUris);
|
|
290
|
+
for (const [docUri, importInfoList] of this.documentImportInfo) {
|
|
291
|
+
if (!affected.has(docUri) && !changedUris.has(docUri)
|
|
292
|
+
&& this.hasMatchingSpecifierOrResolvedUri(importInfoList, changedPaths)) {
|
|
293
|
+
affected.add(docUri);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
156
296
|
}
|
|
157
297
|
|
|
158
298
|
/**
|
|
@@ -183,33 +323,8 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
183
323
|
|
|
184
324
|
for (const imp of model.imports) {
|
|
185
325
|
if (!imp.uri) continue;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const resolvedUri = await this.resolveImport(document, imp.uri);
|
|
189
|
-
const importedUri = resolvedUri.toString();
|
|
190
|
-
|
|
191
|
-
// Track the full import info (specifier, alias, resolved URI)
|
|
192
|
-
importInfoList.push({
|
|
193
|
-
specifier: imp.uri,
|
|
194
|
-
alias: imp.alias,
|
|
195
|
-
resolvedUri: importedUri
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Add to reverse dependency graph: importedUri → importingUri
|
|
199
|
-
let dependents = this.importDependencies.get(importedUri);
|
|
200
|
-
if (!dependents) {
|
|
201
|
-
dependents = new Set();
|
|
202
|
-
this.importDependencies.set(importedUri, dependents);
|
|
203
|
-
}
|
|
204
|
-
dependents.add(importingUri);
|
|
205
|
-
} catch {
|
|
206
|
-
// Import resolution failed - still track the specifier with empty resolution
|
|
207
|
-
importInfoList.push({
|
|
208
|
-
specifier: imp.uri,
|
|
209
|
-
alias: imp.alias,
|
|
210
|
-
resolvedUri: ''
|
|
211
|
-
});
|
|
212
|
-
}
|
|
326
|
+
const info = await this.resolveAndTrackImport(document, imp, importingUri);
|
|
327
|
+
importInfoList.push(info);
|
|
213
328
|
}
|
|
214
329
|
|
|
215
330
|
if (importInfoList.length > 0) {
|
|
@@ -217,6 +332,45 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
217
332
|
}
|
|
218
333
|
}
|
|
219
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Resolves a single import and registers it in the reverse dependency graph.
|
|
337
|
+
* Falls back to searching loaded documents when the filesystem resolver fails.
|
|
338
|
+
*/
|
|
339
|
+
private async resolveAndTrackImport(
|
|
340
|
+
document: LangiumDocument,
|
|
341
|
+
imp: { uri?: string; alias?: string },
|
|
342
|
+
importingUri: string
|
|
343
|
+
): Promise<ImportInfo> {
|
|
344
|
+
const specifier = imp.uri ?? '';
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const resolvedUri = await this.resolveImport(document, specifier);
|
|
348
|
+
const importedUri = resolvedUri.toString();
|
|
349
|
+
this.addToDependencyGraph(importedUri, importingUri);
|
|
350
|
+
return { specifier, alias: imp.alias, resolvedUri: importedUri };
|
|
351
|
+
} catch {
|
|
352
|
+
// Filesystem resolution failed (e.g., unsaved file, EmptyFileSystem).
|
|
353
|
+
// Try to find a loaded document whose URI path matches the specifier.
|
|
354
|
+
const matchedUri = this.findLoadedDocumentByPath(specifier, importingUri);
|
|
355
|
+
if (matchedUri) {
|
|
356
|
+
this.addToDependencyGraph(matchedUri, importingUri);
|
|
357
|
+
}
|
|
358
|
+
return { specifier, alias: imp.alias, resolvedUri: matchedUri };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Adds an edge to the reverse dependency graph: importedUri → importingUri.
|
|
364
|
+
*/
|
|
365
|
+
private addToDependencyGraph(importedUri: string, importingUri: string): void {
|
|
366
|
+
let dependents = this.importDependencies.get(importedUri);
|
|
367
|
+
if (!dependents) {
|
|
368
|
+
dependents = new Set();
|
|
369
|
+
this.importDependencies.set(importedUri, dependents);
|
|
370
|
+
}
|
|
371
|
+
dependents.add(importingUri);
|
|
372
|
+
}
|
|
373
|
+
|
|
220
374
|
/**
|
|
221
375
|
* Ensures all imported documents are loaded and available.
|
|
222
376
|
* This is called during indexing, BEFORE linking/validation,
|
|
@@ -285,11 +439,22 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
285
439
|
* Called when a document is deleted.
|
|
286
440
|
*/
|
|
287
441
|
private removeImportDependencies(uri: string): void {
|
|
288
|
-
// Remove as an imported document
|
|
442
|
+
// Remove as an imported document (reverse graph entry)
|
|
289
443
|
this.importDependencies.delete(uri);
|
|
290
444
|
|
|
291
|
-
// Remove
|
|
445
|
+
// Remove import info for this document (forward graph entry)
|
|
446
|
+
this.documentImportInfo.delete(uri);
|
|
447
|
+
|
|
448
|
+
// Remove from all dependency sets (as an importer of other files)
|
|
292
449
|
this.removeDocumentFromDependencies(uri);
|
|
450
|
+
|
|
451
|
+
// Clean up PRS-017 caches
|
|
452
|
+
this.exportSnapshots.delete(uri);
|
|
453
|
+
this.changedExports.delete(uri);
|
|
454
|
+
this.detectedCycles.delete(uri);
|
|
455
|
+
|
|
456
|
+
// Invalidate the per-cycle cache since the graph changed
|
|
457
|
+
this.transitiveAffectedCache = undefined;
|
|
293
458
|
}
|
|
294
459
|
|
|
295
460
|
/**
|
|
@@ -310,6 +475,29 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
310
475
|
this.importDependencies.clear();
|
|
311
476
|
this.documentImportInfo.clear();
|
|
312
477
|
this.importsLoaded.clear();
|
|
478
|
+
this.transitiveAffectedCache = undefined;
|
|
479
|
+
this.exportSnapshots.clear();
|
|
480
|
+
this.changedExports.clear();
|
|
481
|
+
this.detectedCycles.clear();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Fallback for import resolution: searches loaded documents for one whose
|
|
486
|
+
* URI path matches the import specifier. Used when the filesystem-based
|
|
487
|
+
* resolver fails (e.g., unsaved files, EmptyFileSystem in tests).
|
|
488
|
+
*/
|
|
489
|
+
private findLoadedDocumentByPath(specifier: string, excludeUri: string): string {
|
|
490
|
+
const langiumDocuments = this.sharedServices.workspace.LangiumDocuments;
|
|
491
|
+
for (const doc of langiumDocuments.all) {
|
|
492
|
+
const docUri = doc.uri.toString();
|
|
493
|
+
if (docUri === excludeUri) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (doc.uri.path === specifier || doc.uri.path.endsWith(`/${specifier}`)) {
|
|
497
|
+
return docUri;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return '';
|
|
313
501
|
}
|
|
314
502
|
|
|
315
503
|
/**
|
|
@@ -468,7 +656,11 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
468
656
|
}
|
|
469
657
|
|
|
470
658
|
/**
|
|
471
|
-
* Checks if any specifier OR its resolved URI matches the changed paths.
|
|
659
|
+
* Checks if any specifier OR its resolved URI matches the changed paths (PRS-017 R4).
|
|
660
|
+
*
|
|
661
|
+
* Uses exact filename matching instead of substring matching to prevent
|
|
662
|
+
* false positives (e.g., changing `sales.dlang` should NOT trigger
|
|
663
|
+
* revalidation of a file importing `pre-sales.dlang`).
|
|
472
664
|
*
|
|
473
665
|
* This handles both regular imports and path aliases:
|
|
474
666
|
* - Regular: `./domains/sales.dlang` matches path `sales.dlang`
|
|
@@ -479,21 +671,211 @@ export class DomainLangIndexManager extends DefaultIndexManager {
|
|
|
479
671
|
*/
|
|
480
672
|
private hasMatchingSpecifierOrResolvedUri(importInfoList: ImportInfo[], changedPaths: Set<string>): boolean {
|
|
481
673
|
for (const info of importInfoList) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
for (const changedPath of changedPaths) {
|
|
485
|
-
// Check the raw specifier (handles relative imports)
|
|
486
|
-
if (info.specifier.includes(changedPath) || changedPath.endsWith(normalizedSpecifier)) {
|
|
487
|
-
return true;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Check the resolved URI (handles path aliases like @domains/...)
|
|
491
|
-
// The resolved URI contains the full file path which matches moved files
|
|
492
|
-
if (info.resolvedUri?.includes(changedPath)) {
|
|
493
|
-
return true;
|
|
494
|
-
}
|
|
674
|
+
if (this.matchesAnyChangedPath(info, changedPaths)) {
|
|
675
|
+
return true;
|
|
495
676
|
}
|
|
496
677
|
}
|
|
497
678
|
return false;
|
|
498
679
|
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Checks if a single import info matches any of the changed paths.
|
|
683
|
+
* Extracted to reduce cognitive complexity of hasMatchingSpecifierOrResolvedUri.
|
|
684
|
+
*/
|
|
685
|
+
private matchesAnyChangedPath(info: ImportInfo, changedPaths: Set<string>): boolean {
|
|
686
|
+
for (const changedPath of changedPaths) {
|
|
687
|
+
if (this.matchesChangedPath(info, changedPath)) {
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Checks if a single import info matches a single changed path.
|
|
696
|
+
*/
|
|
697
|
+
private matchesChangedPath(info: ImportInfo, changedPath: string): boolean {
|
|
698
|
+
const changedFileName = this.extractFileName(changedPath);
|
|
699
|
+
if (!changedFileName) return false;
|
|
700
|
+
|
|
701
|
+
// Check the resolved URI first (most reliable — already normalized)
|
|
702
|
+
if (info.resolvedUri && this.matchesResolvedUri(info.resolvedUri, changedFileName, changedPath)) {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Check the specifier (handles relative imports)
|
|
707
|
+
return this.matchesSpecifier(info.specifier, changedFileName, changedPath);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Checks if a resolved URI matches a changed path by exact filename comparison.
|
|
712
|
+
*/
|
|
713
|
+
private matchesResolvedUri(resolvedUri: string, changedFileName: string, changedPath: string): boolean {
|
|
714
|
+
const resolvedFileName = this.extractFileName(resolvedUri);
|
|
715
|
+
if (resolvedFileName && changedFileName === resolvedFileName) {
|
|
716
|
+
return this.pathEndsWith(resolvedUri, changedPath);
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Checks if an import specifier matches a changed path by exact filename comparison.
|
|
723
|
+
*/
|
|
724
|
+
private matchesSpecifier(specifier: string, changedFileName: string, changedPath: string): boolean {
|
|
725
|
+
const specifierFileName = this.extractFileName(specifier);
|
|
726
|
+
if (specifierFileName && changedFileName === specifierFileName) {
|
|
727
|
+
const normalizedSpecifier = specifier.replace(/^[.@/]+/, '');
|
|
728
|
+
return this.pathEndsWith(changedPath, normalizedSpecifier) ||
|
|
729
|
+
this.pathEndsWith(normalizedSpecifier, changedPath);
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Extracts the filename (without extension) from a path or URI string.
|
|
736
|
+
*/
|
|
737
|
+
private extractFileName(pathOrUri: string): string | undefined {
|
|
738
|
+
// Handle URI paths and regular paths
|
|
739
|
+
const lastSlash = Math.max(pathOrUri.lastIndexOf('/'), pathOrUri.lastIndexOf('\\'));
|
|
740
|
+
const fileName = lastSlash >= 0 ? pathOrUri.slice(lastSlash + 1) : pathOrUri;
|
|
741
|
+
return fileName.replace(/\.dlang$/, '') || undefined;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Checks if longPath ends with shortPath, comparing path segments.
|
|
746
|
+
* Prevents substring false positives (e.g., "pre-sales" matching "sales").
|
|
747
|
+
*/
|
|
748
|
+
private pathEndsWith(longPath: string, shortPath: string): boolean {
|
|
749
|
+
const normalizedLong = longPath.replaceAll('\\', '/').replace(/\.dlang$/, '');
|
|
750
|
+
const normalizedShort = shortPath.replaceAll('\\', '/').replace(/\.dlang$/, '');
|
|
751
|
+
return normalizedLong === normalizedShort ||
|
|
752
|
+
normalizedLong.endsWith(`/${normalizedShort}`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// --- PRS-017 R2: Export-signature diffing ---
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Captures a snapshot of exported symbol signatures for a document.
|
|
759
|
+
* Signature = "nodeType:qualifiedName" for each exported symbol.
|
|
760
|
+
* Used to detect whether a document's public interface actually changed.
|
|
761
|
+
*/
|
|
762
|
+
private captureExportSnapshot(uri: string): Set<string> {
|
|
763
|
+
const descriptions = this.symbolIndex.get(uri) ?? [];
|
|
764
|
+
const signatures = new Set<string>();
|
|
765
|
+
for (const desc of descriptions) {
|
|
766
|
+
signatures.add(`${desc.type}:${desc.name}`);
|
|
767
|
+
}
|
|
768
|
+
return signatures;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Checks if two sets of strings are equal (same size and same elements).
|
|
773
|
+
*/
|
|
774
|
+
private setsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
775
|
+
if (a.size !== b.size) return false;
|
|
776
|
+
for (const item of a) {
|
|
777
|
+
if (!b.has(item)) return false;
|
|
778
|
+
}
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Returns true if any of the changed URIs had their exports actually change.
|
|
784
|
+
* Used by isAffected() to skip transitive invalidation when only
|
|
785
|
+
* implementation details changed (e.g., editing a vision string).
|
|
786
|
+
*/
|
|
787
|
+
private anyExportsChanged(changedUris: Set<string>): boolean {
|
|
788
|
+
for (const uri of changedUris) {
|
|
789
|
+
if (this.changedExports.has(uri)) {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// --- PRS-017 R3: Import cycle detection ---
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Detects import cycles starting from a given document URI.
|
|
800
|
+
* Uses DFS with a recursion stack to find back-edges in the import graph.
|
|
801
|
+
* Stores detected cycles for reporting by ImportValidator.
|
|
802
|
+
*/
|
|
803
|
+
private detectAndStoreCycles(startUri: string): void {
|
|
804
|
+
// Clear any previous cycle for this document
|
|
805
|
+
this.detectedCycles.delete(startUri);
|
|
806
|
+
|
|
807
|
+
const cycle = this.findCycle(startUri);
|
|
808
|
+
if (cycle) {
|
|
809
|
+
// Store the cycle for each participant (skip last element which is the
|
|
810
|
+
// duplicate that closes the cycle, e.g. [A, B, C, A] → store for A, B, C)
|
|
811
|
+
for (let i = 0; i < cycle.length - 1; i++) {
|
|
812
|
+
this.detectedCycles.set(cycle[i], cycle);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* DFS to find a cycle in the forward import graph starting from startUri.
|
|
819
|
+
* Returns the cycle path (e.g., [A, B, C, A]) if found, undefined otherwise.
|
|
820
|
+
*/
|
|
821
|
+
private findCycle(startUri: string): string[] | undefined {
|
|
822
|
+
const visited = new Set<string>();
|
|
823
|
+
const stack = new Set<string>();
|
|
824
|
+
const path: string[] = [];
|
|
825
|
+
|
|
826
|
+
const dfs = (uri: string): string[] | undefined => {
|
|
827
|
+
if (stack.has(uri)) {
|
|
828
|
+
// Found cycle — extract the cycle path from the stack
|
|
829
|
+
const cycleStart = path.indexOf(uri);
|
|
830
|
+
return [...path.slice(cycleStart), uri];
|
|
831
|
+
}
|
|
832
|
+
if (visited.has(uri)) return undefined;
|
|
833
|
+
|
|
834
|
+
visited.add(uri);
|
|
835
|
+
stack.add(uri);
|
|
836
|
+
path.push(uri);
|
|
837
|
+
|
|
838
|
+
const imports = this.documentImportInfo.get(uri);
|
|
839
|
+
if (imports) {
|
|
840
|
+
for (const imp of imports) {
|
|
841
|
+
if (imp.resolvedUri) {
|
|
842
|
+
const cycle = dfs(imp.resolvedUri);
|
|
843
|
+
if (cycle) return cycle;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
stack.delete(uri);
|
|
849
|
+
path.pop();
|
|
850
|
+
return undefined;
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
return dfs(startUri);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Gets the detected import cycle for a document, if any.
|
|
858
|
+
* Returns the cycle path as an array of URIs, or undefined if no cycle.
|
|
859
|
+
* Used by ImportValidator to report cycle diagnostics (PRS-017 R3).
|
|
860
|
+
*/
|
|
861
|
+
getCycleForDocument(uri: string): string[] | undefined {
|
|
862
|
+
return this.detectedCycles.get(uri);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// --- PRS-017 R1: Targeted ImportResolver cache invalidation ---
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Invalidates the ImportResolver cache for the changed document and its dependents.
|
|
869
|
+
* This provides surgical cache invalidation instead of clearing the entire cache.
|
|
870
|
+
*/
|
|
871
|
+
private invalidateImportResolverCache(changedUri: string): void {
|
|
872
|
+
if (!this.importResolver) return;
|
|
873
|
+
|
|
874
|
+
const affectedUris = [changedUri];
|
|
875
|
+
const dependents = this.importDependencies.get(changedUri);
|
|
876
|
+
if (dependents) {
|
|
877
|
+
affectedUris.push(...dependents);
|
|
878
|
+
}
|
|
879
|
+
this.importResolver.invalidateForDocuments(affectedUris);
|
|
880
|
+
}
|
|
499
881
|
}
|