@domainlang/language 0.6.0 → 0.8.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 +1 -1
- package/out/domain-lang-module.d.ts +2 -0
- package/out/domain-lang-module.js +23 -2
- package/out/domain-lang-module.js.map +1 -1
- package/out/lsp/domain-lang-completion.d.ts +142 -1
- package/out/lsp/domain-lang-completion.js +620 -22
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
- package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
- package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
- package/out/lsp/domain-lang-index-manager.d.ts +98 -1
- package/out/lsp/domain-lang-index-manager.js +214 -7
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
- package/out/lsp/domain-lang-node-kind-provider.js +87 -0
- package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
- package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
- package/out/lsp/domain-lang-scope-provider.js +170 -0
- package/out/lsp/domain-lang-scope-provider.js.map +1 -0
- package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
- package/out/lsp/domain-lang-workspace-manager.js +148 -4
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
- package/out/lsp/hover/domain-lang-hover.js +160 -134
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/hover-builders.d.ts +57 -0
- package/out/lsp/hover/hover-builders.js +171 -0
- package/out/lsp/hover/hover-builders.js.map +1 -0
- package/out/main.js +116 -20
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +2 -1
- package/out/sdk/index.js +1 -1
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/loader-node.js +1 -1
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/loader.d.ts +55 -2
- package/out/sdk/loader.js +87 -28
- package/out/sdk/loader.js.map +1 -1
- package/out/sdk/query.js +14 -11
- package/out/sdk/query.js.map +1 -1
- package/out/services/import-resolver.d.ts +29 -6
- package/out/services/import-resolver.js +48 -9
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/package-boundary-detector.d.ts +101 -0
- package/out/services/package-boundary-detector.js +211 -0
- package/out/services/package-boundary-detector.js.map +1 -0
- package/out/services/performance-optimizer.js +6 -2
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/types.d.ts +24 -0
- package/out/services/types.js.map +1 -1
- package/out/services/workspace-manager.d.ts +73 -6
- package/out/services/workspace-manager.js +210 -57
- package/out/services/workspace-manager.js.map +1 -1
- package/out/utils/import-utils.d.ts +9 -6
- package/out/utils/import-utils.js +26 -15
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +20 -0
- package/out/validation/constants.js +39 -3
- package/out/validation/constants.js.map +1 -1
- package/out/validation/import.d.ts +22 -1
- package/out/validation/import.js +104 -16
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +101 -3
- package/out/validation/maps.js.map +1 -1
- package/package.json +5 -5
- package/src/domain-lang-module.ts +26 -3
- package/src/lsp/domain-lang-completion.ts +736 -27
- package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
- package/src/lsp/domain-lang-index-manager.ts +250 -7
- package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
- package/src/lsp/domain-lang-scope-provider.ts +250 -0
- package/src/lsp/domain-lang-workspace-manager.ts +187 -4
- package/src/lsp/hover/domain-lang-hover.ts +189 -131
- package/src/lsp/hover/hover-builders.ts +208 -0
- package/src/main.ts +156 -23
- package/src/sdk/index.ts +2 -1
- package/src/sdk/loader-node.ts +2 -1
- package/src/sdk/loader.ts +125 -34
- package/src/sdk/query.ts +15 -11
- package/src/services/import-resolver.ts +60 -9
- package/src/services/package-boundary-detector.ts +238 -0
- package/src/services/performance-optimizer.ts +6 -2
- package/src/services/types.ts +25 -0
- package/src/services/workspace-manager.ts +259 -62
- package/src/utils/import-utils.ts +27 -15
- package/src/validation/constants.ts +47 -6
- package/src/validation/import.ts +124 -16
- package/src/validation/maps.ts +118 -4
package/src/validation/import.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Cancellation } from 'langium';
|
|
|
5
5
|
import type { DomainLangAstType, ImportStatement } from '../generated/ast.js';
|
|
6
6
|
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
7
7
|
import type { WorkspaceManager } from '../services/workspace-manager.js';
|
|
8
|
+
import type { ImportResolver } from '../services/import-resolver.js';
|
|
8
9
|
import type { ExtendedDependencySpec, ModelManifest, LockFile } from '../services/types.js';
|
|
9
10
|
import { ValidationMessages, buildCodeDescription, IssueCodes } from './constants.js';
|
|
10
11
|
|
|
@@ -15,15 +16,18 @@ import { ValidationMessages, buildCodeDescription, IssueCodes } from './constant
|
|
|
15
16
|
* the shared WorkspaceManager service with its cached manifest/lock file reading.
|
|
16
17
|
*
|
|
17
18
|
* Checks:
|
|
19
|
+
* - All import URIs resolve to existing files
|
|
18
20
|
* - External imports require manifest + alias
|
|
19
21
|
* - Local path dependencies stay inside workspace
|
|
20
22
|
* - Lock file exists for external dependencies
|
|
21
23
|
*/
|
|
22
24
|
export class ImportValidator {
|
|
23
25
|
private readonly workspaceManager: WorkspaceManager;
|
|
26
|
+
private readonly importResolver: ImportResolver;
|
|
24
27
|
|
|
25
28
|
constructor(services: DomainLangServices) {
|
|
26
29
|
this.workspaceManager = services.imports.WorkspaceManager;
|
|
30
|
+
this.importResolver = services.imports.ImportResolver;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/**
|
|
@@ -48,6 +52,13 @@ export class ImportValidator {
|
|
|
48
52
|
return;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
// First, verify the import resolves to a valid file
|
|
56
|
+
// This catches renamed/moved/deleted files immediately
|
|
57
|
+
const resolveError = await this.validateImportResolves(imp, document, accept);
|
|
58
|
+
if (resolveError) {
|
|
59
|
+
return; // Don't continue with other validations if can't resolve
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
if (!this.isExternalImport(imp.uri)) {
|
|
52
63
|
return;
|
|
53
64
|
}
|
|
@@ -67,35 +78,36 @@ export class ImportValidator {
|
|
|
67
78
|
return;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
const
|
|
81
|
+
// Find the matching dependency by key (owner/package format)
|
|
82
|
+
const match = this.findDependency(manifest, imp.uri);
|
|
72
83
|
|
|
73
|
-
if (!
|
|
74
|
-
accept('error', ValidationMessages.IMPORT_NOT_IN_MANIFEST(
|
|
84
|
+
if (!match) {
|
|
85
|
+
accept('error', ValidationMessages.IMPORT_NOT_IN_MANIFEST(imp.uri), {
|
|
75
86
|
node: imp,
|
|
76
87
|
property: 'uri',
|
|
77
88
|
codeDescription: buildCodeDescription('language.md', 'imports'),
|
|
78
|
-
data: { code: IssueCodes.ImportNotInManifest, alias }
|
|
89
|
+
data: { code: IssueCodes.ImportNotInManifest, alias: imp.uri }
|
|
79
90
|
});
|
|
80
91
|
return;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
|
|
94
|
+
const { key, dependency } = match;
|
|
95
|
+
this.validateDependencyConfig(dependency, key, accept, imp);
|
|
84
96
|
|
|
85
97
|
// External source dependencies require lock file and cached packages
|
|
86
98
|
if (dependency.source) {
|
|
87
99
|
const lockFile = await this.workspaceManager.getLockFile();
|
|
88
100
|
if (!lockFile) {
|
|
89
|
-
accept('error', ValidationMessages.IMPORT_NOT_INSTALLED(
|
|
101
|
+
accept('error', ValidationMessages.IMPORT_NOT_INSTALLED(key), {
|
|
90
102
|
node: imp,
|
|
91
103
|
property: 'uri',
|
|
92
104
|
codeDescription: buildCodeDescription('language.md', 'imports'),
|
|
93
|
-
data: { code: IssueCodes.ImportNotInstalled, alias }
|
|
105
|
+
data: { code: IssueCodes.ImportNotInstalled, alias: key }
|
|
94
106
|
});
|
|
95
107
|
return;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
await this.validateCachedPackage(dependency,
|
|
110
|
+
await this.validateCachedPackage(dependency, key, lockFile, accept, imp);
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
@@ -118,20 +130,111 @@ export class ImportValidator {
|
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
/**
|
|
121
|
-
*
|
|
133
|
+
* Validates that an import URI resolves to an existing file.
|
|
134
|
+
* Returns true if there was an error (import doesn't resolve).
|
|
135
|
+
*/
|
|
136
|
+
private async validateImportResolves(
|
|
137
|
+
imp: ImportStatement,
|
|
138
|
+
document: LangiumDocument,
|
|
139
|
+
accept: ValidationAcceptor
|
|
140
|
+
): Promise<boolean> {
|
|
141
|
+
if (!imp.uri) {
|
|
142
|
+
return true; // Error already reported
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const docDir = path.dirname(document.uri.fsPath);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const resolvedUri = await this.importResolver.resolveFrom(docDir, imp.uri);
|
|
149
|
+
|
|
150
|
+
// Check if the resolved file actually exists
|
|
151
|
+
const filePath = resolvedUri.fsPath;
|
|
152
|
+
const exists = await this.fileExists(filePath);
|
|
153
|
+
|
|
154
|
+
if (!exists) {
|
|
155
|
+
accept('error', ValidationMessages.IMPORT_UNRESOLVED(imp.uri), {
|
|
156
|
+
node: imp,
|
|
157
|
+
property: 'uri',
|
|
158
|
+
codeDescription: buildCodeDescription('language.md', 'imports'),
|
|
159
|
+
data: { code: IssueCodes.ImportUnresolved, uri: imp.uri }
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return false;
|
|
165
|
+
} catch {
|
|
166
|
+
// Resolution failed - report as unresolved import
|
|
167
|
+
accept('error', ValidationMessages.IMPORT_UNRESOLVED(imp.uri), {
|
|
168
|
+
node: imp,
|
|
169
|
+
property: 'uri',
|
|
170
|
+
codeDescription: buildCodeDescription('language.md', 'imports'),
|
|
171
|
+
data: { code: IssueCodes.ImportUnresolved, uri: imp.uri }
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Checks if a file exists (async).
|
|
179
|
+
*/
|
|
180
|
+
private async fileExists(filePath: string): Promise<boolean> {
|
|
181
|
+
try {
|
|
182
|
+
const stat = await fs.stat(filePath);
|
|
183
|
+
return stat.isFile();
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Finds the dependency configuration that matches the import specifier.
|
|
191
|
+
*
|
|
192
|
+
* Dependencies can be keyed as:
|
|
193
|
+
* - owner/package (recommended, matches "owner/package" or "owner/package/subpath")
|
|
194
|
+
* - short-alias (matches "short-alias" or "short-alias/subpath")
|
|
195
|
+
*
|
|
196
|
+
* @returns The matching key and normalized dependency, or undefined if not found
|
|
197
|
+
*/
|
|
198
|
+
private findDependency(
|
|
199
|
+
manifest: ModelManifest,
|
|
200
|
+
specifier: string
|
|
201
|
+
): { key: string; dependency: ExtendedDependencySpec } | undefined {
|
|
202
|
+
const dependencies = manifest.dependencies;
|
|
203
|
+
if (!dependencies) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sort keys by length descending to match most specific first
|
|
208
|
+
const sortedKeys = Object.keys(dependencies).sort((a, b) => b.length - a.length);
|
|
209
|
+
|
|
210
|
+
for (const key of sortedKeys) {
|
|
211
|
+
// Exact match or prefix match (key followed by /)
|
|
212
|
+
if (specifier === key || specifier.startsWith(`${key}/`)) {
|
|
213
|
+
const dependency = this.getDependency(manifest, key);
|
|
214
|
+
if (dependency) {
|
|
215
|
+
return { key, dependency };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Gets the normalized dependency configuration for a key.
|
|
122
225
|
*/
|
|
123
|
-
private getDependency(manifest: ModelManifest,
|
|
124
|
-
const dep = manifest.dependencies?.[
|
|
226
|
+
private getDependency(manifest: ModelManifest, key: string): ExtendedDependencySpec | undefined {
|
|
227
|
+
const dep = manifest.dependencies?.[key];
|
|
125
228
|
if (!dep) {
|
|
126
229
|
return undefined;
|
|
127
230
|
}
|
|
128
231
|
|
|
129
232
|
if (typeof dep === 'string') {
|
|
130
|
-
return { source:
|
|
233
|
+
return { source: key, ref: dep };
|
|
131
234
|
}
|
|
132
235
|
|
|
133
236
|
if (!dep.source && !dep.path) {
|
|
134
|
-
return { ...dep, source:
|
|
237
|
+
return { ...dep, source: key };
|
|
135
238
|
}
|
|
136
239
|
|
|
137
240
|
return dep;
|
|
@@ -296,8 +399,13 @@ export function createImportChecks(services: DomainLangServices): ValidationChec
|
|
|
296
399
|
return {
|
|
297
400
|
// Langium 4.x supports async validators via MaybePromise<void>
|
|
298
401
|
ImportStatement: async (imp, accept, cancelToken) => {
|
|
299
|
-
|
|
300
|
-
|
|
402
|
+
// Get document from root (Model), not from ImportStatement
|
|
403
|
+
// Langium sets $document only on the root AST node
|
|
404
|
+
const root = imp.$container;
|
|
405
|
+
const document = root?.$document;
|
|
406
|
+
if (!document) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
301
409
|
|
|
302
410
|
await validator.checkImportPath(imp, accept, document, cancelToken);
|
|
303
411
|
}
|
package/src/validation/maps.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ValidationAcceptor } from 'langium';
|
|
2
|
-
import type { ContextMap, DomainMap } from '../generated/ast.js';
|
|
3
|
-
import {
|
|
2
|
+
import type { ContextMap, DomainMap, Relationship, BoundedContextRef } from '../generated/ast.js';
|
|
3
|
+
import { isThisRef } from '../generated/ast.js';
|
|
4
|
+
import { ValidationMessages, buildCodeDescription, IssueCodes } from './constants.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Validates that a context map contains at least one bounded context.
|
|
@@ -22,6 +23,36 @@ function validateContextMapHasContexts(
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Validates that MultiReference items in a context map resolve.
|
|
28
|
+
* Langium doesn't report errors for unresolved MultiReference items by default,
|
|
29
|
+
* so we need custom validation to catch these cases.
|
|
30
|
+
*
|
|
31
|
+
* @param map - The context map to validate
|
|
32
|
+
* @param accept - The validation acceptor for reporting issues
|
|
33
|
+
*/
|
|
34
|
+
function validateContextMapReferences(
|
|
35
|
+
map: ContextMap,
|
|
36
|
+
accept: ValidationAcceptor
|
|
37
|
+
): void {
|
|
38
|
+
if (!map.boundedContexts) return;
|
|
39
|
+
|
|
40
|
+
for (const multiRef of map.boundedContexts) {
|
|
41
|
+
// A MultiReference has a $refText (the source text) and items (resolved refs)
|
|
42
|
+
// If $refText exists but items is empty, the reference didn't resolve
|
|
43
|
+
const refText = multiRef.$refText;
|
|
44
|
+
if (refText && multiRef.items.length === 0) {
|
|
45
|
+
accept('error', ValidationMessages.UNRESOLVED_REFERENCE('BoundedContext', refText), {
|
|
46
|
+
node: map,
|
|
47
|
+
// Find the CST node for this specific reference
|
|
48
|
+
property: 'boundedContexts',
|
|
49
|
+
index: map.boundedContexts.indexOf(multiRef),
|
|
50
|
+
code: IssueCodes.UnresolvedReference
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
25
56
|
/**
|
|
26
57
|
* Validates that a context map has at least one relationship if it contains multiple contexts.
|
|
27
58
|
* Multiple unrelated contexts should have documented relationships.
|
|
@@ -66,11 +97,94 @@ function validateDomainMapHasDomains(
|
|
|
66
97
|
}
|
|
67
98
|
}
|
|
68
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Validates that MultiReference items in a domain map resolve.
|
|
102
|
+
*
|
|
103
|
+
* @param map - The domain map to validate
|
|
104
|
+
* @param accept - The validation acceptor for reporting issues
|
|
105
|
+
*/
|
|
106
|
+
function validateDomainMapReferences(
|
|
107
|
+
map: DomainMap,
|
|
108
|
+
accept: ValidationAcceptor
|
|
109
|
+
): void {
|
|
110
|
+
if (!map.domains) return;
|
|
111
|
+
|
|
112
|
+
for (const multiRef of map.domains) {
|
|
113
|
+
const refText = multiRef.$refText;
|
|
114
|
+
if (refText && multiRef.items.length === 0) {
|
|
115
|
+
accept('error', ValidationMessages.UNRESOLVED_REFERENCE('Domain', refText), {
|
|
116
|
+
node: map,
|
|
117
|
+
property: 'domains',
|
|
118
|
+
index: map.domains.indexOf(multiRef),
|
|
119
|
+
code: IssueCodes.UnresolvedReference
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Gets a canonical name for a BoundedContextRef for comparison purposes.
|
|
127
|
+
*/
|
|
128
|
+
function getRefKey(ref: BoundedContextRef): string {
|
|
129
|
+
if (isThisRef(ref)) {
|
|
130
|
+
return 'this';
|
|
131
|
+
}
|
|
132
|
+
return ref.link?.$refText ?? '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Builds a canonical key for a relationship for duplicate detection.
|
|
137
|
+
* The key captures both endpoints, arrow direction, and integration patterns.
|
|
138
|
+
*/
|
|
139
|
+
function buildRelationshipKey(rel: Relationship): string {
|
|
140
|
+
const left = getRefKey(rel.left);
|
|
141
|
+
const right = getRefKey(rel.right);
|
|
142
|
+
const leftPatterns = (rel.leftPatterns ?? []).slice().sort((a, b) => a.localeCompare(b)).join(',');
|
|
143
|
+
const rightPatterns = (rel.rightPatterns ?? []).slice().sort((a, b) => a.localeCompare(b)).join(',');
|
|
144
|
+
return `[${leftPatterns}]${left}${rel.arrow}[${rightPatterns}]${right}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validates that a context map does not contain duplicate relationships.
|
|
149
|
+
* Two relationships are considered duplicate if they have the same endpoints,
|
|
150
|
+
* direction, and integration patterns.
|
|
151
|
+
*
|
|
152
|
+
* @param map - The context map to validate
|
|
153
|
+
* @param accept - The validation acceptor for reporting issues
|
|
154
|
+
*/
|
|
155
|
+
function validateNoDuplicateRelationships(
|
|
156
|
+
map: ContextMap,
|
|
157
|
+
accept: ValidationAcceptor
|
|
158
|
+
): void {
|
|
159
|
+
if (!map.relationships || map.relationships.length < 2) return;
|
|
160
|
+
|
|
161
|
+
const seen = new Map<string, number>();
|
|
162
|
+
for (let i = 0; i < map.relationships.length; i++) {
|
|
163
|
+
const rel = map.relationships[i];
|
|
164
|
+
const key = buildRelationshipKey(rel);
|
|
165
|
+
|
|
166
|
+
if (seen.has(key)) {
|
|
167
|
+
accept('warning', ValidationMessages.CONTEXT_MAP_DUPLICATE_RELATIONSHIP(
|
|
168
|
+
getRefKey(rel.left), getRefKey(rel.right)
|
|
169
|
+
), {
|
|
170
|
+
node: rel,
|
|
171
|
+
property: 'arrow',
|
|
172
|
+
codeDescription: buildCodeDescription('language.md', 'context-maps')
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
seen.set(key, i);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
69
180
|
export const contextMapChecks = [
|
|
70
181
|
validateContextMapHasContexts,
|
|
71
|
-
|
|
182
|
+
validateContextMapReferences,
|
|
183
|
+
validateContextMapHasRelationships,
|
|
184
|
+
validateNoDuplicateRelationships
|
|
72
185
|
];
|
|
73
186
|
|
|
74
187
|
export const domainMapChecks = [
|
|
75
|
-
validateDomainMapHasDomains
|
|
188
|
+
validateDomainMapHasDomains,
|
|
189
|
+
validateDomainMapReferences
|
|
76
190
|
];
|