@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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/out/domain-lang-module.d.ts +2 -0
  3. package/out/domain-lang-module.js +23 -2
  4. package/out/domain-lang-module.js.map +1 -1
  5. package/out/lsp/domain-lang-completion.d.ts +142 -1
  6. package/out/lsp/domain-lang-completion.js +620 -22
  7. package/out/lsp/domain-lang-completion.js.map +1 -1
  8. package/out/lsp/domain-lang-document-symbol-provider.d.ts +79 -0
  9. package/out/lsp/domain-lang-document-symbol-provider.js +210 -0
  10. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -0
  11. package/out/lsp/domain-lang-index-manager.d.ts +98 -1
  12. package/out/lsp/domain-lang-index-manager.js +214 -7
  13. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  14. package/out/lsp/domain-lang-node-kind-provider.d.ts +27 -0
  15. package/out/lsp/domain-lang-node-kind-provider.js +87 -0
  16. package/out/lsp/domain-lang-node-kind-provider.js.map +1 -0
  17. package/out/lsp/domain-lang-scope-provider.d.ts +100 -0
  18. package/out/lsp/domain-lang-scope-provider.js +170 -0
  19. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  20. package/out/lsp/domain-lang-workspace-manager.d.ts +46 -0
  21. package/out/lsp/domain-lang-workspace-manager.js +148 -4
  22. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  23. package/out/lsp/hover/domain-lang-hover.d.ts +16 -6
  24. package/out/lsp/hover/domain-lang-hover.js +160 -134
  25. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  26. package/out/lsp/hover/hover-builders.d.ts +57 -0
  27. package/out/lsp/hover/hover-builders.js +171 -0
  28. package/out/lsp/hover/hover-builders.js.map +1 -0
  29. package/out/main.js +116 -20
  30. package/out/main.js.map +1 -1
  31. package/out/sdk/index.d.ts +2 -1
  32. package/out/sdk/index.js +1 -1
  33. package/out/sdk/index.js.map +1 -1
  34. package/out/sdk/loader-node.js +1 -1
  35. package/out/sdk/loader-node.js.map +1 -1
  36. package/out/sdk/loader.d.ts +55 -2
  37. package/out/sdk/loader.js +87 -28
  38. package/out/sdk/loader.js.map +1 -1
  39. package/out/sdk/query.js +14 -11
  40. package/out/sdk/query.js.map +1 -1
  41. package/out/services/import-resolver.d.ts +29 -6
  42. package/out/services/import-resolver.js +48 -9
  43. package/out/services/import-resolver.js.map +1 -1
  44. package/out/services/package-boundary-detector.d.ts +101 -0
  45. package/out/services/package-boundary-detector.js +211 -0
  46. package/out/services/package-boundary-detector.js.map +1 -0
  47. package/out/services/performance-optimizer.js +6 -2
  48. package/out/services/performance-optimizer.js.map +1 -1
  49. package/out/services/types.d.ts +24 -0
  50. package/out/services/types.js.map +1 -1
  51. package/out/services/workspace-manager.d.ts +73 -6
  52. package/out/services/workspace-manager.js +210 -57
  53. package/out/services/workspace-manager.js.map +1 -1
  54. package/out/utils/import-utils.d.ts +9 -6
  55. package/out/utils/import-utils.js +26 -15
  56. package/out/utils/import-utils.js.map +1 -1
  57. package/out/validation/constants.d.ts +20 -0
  58. package/out/validation/constants.js +39 -3
  59. package/out/validation/constants.js.map +1 -1
  60. package/out/validation/import.d.ts +22 -1
  61. package/out/validation/import.js +104 -16
  62. package/out/validation/import.js.map +1 -1
  63. package/out/validation/maps.js +101 -3
  64. package/out/validation/maps.js.map +1 -1
  65. package/package.json +5 -5
  66. package/src/domain-lang-module.ts +26 -3
  67. package/src/lsp/domain-lang-completion.ts +736 -27
  68. package/src/lsp/domain-lang-document-symbol-provider.ts +254 -0
  69. package/src/lsp/domain-lang-index-manager.ts +250 -7
  70. package/src/lsp/domain-lang-node-kind-provider.ts +119 -0
  71. package/src/lsp/domain-lang-scope-provider.ts +250 -0
  72. package/src/lsp/domain-lang-workspace-manager.ts +187 -4
  73. package/src/lsp/hover/domain-lang-hover.ts +189 -131
  74. package/src/lsp/hover/hover-builders.ts +208 -0
  75. package/src/main.ts +156 -23
  76. package/src/sdk/index.ts +2 -1
  77. package/src/sdk/loader-node.ts +2 -1
  78. package/src/sdk/loader.ts +125 -34
  79. package/src/sdk/query.ts +15 -11
  80. package/src/services/import-resolver.ts +60 -9
  81. package/src/services/package-boundary-detector.ts +238 -0
  82. package/src/services/performance-optimizer.ts +6 -2
  83. package/src/services/types.ts +25 -0
  84. package/src/services/workspace-manager.ts +259 -62
  85. package/src/utils/import-utils.ts +27 -15
  86. package/src/validation/constants.ts +47 -6
  87. package/src/validation/import.ts +124 -16
  88. package/src/validation/maps.ts +118 -4
@@ -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
- const alias = imp.uri.split('/')[0];
71
- const dependency = this.getDependency(manifest, alias);
81
+ // Find the matching dependency by key (owner/package format)
82
+ const match = this.findDependency(manifest, imp.uri);
72
83
 
73
- if (!dependency) {
74
- accept('error', ValidationMessages.IMPORT_NOT_IN_MANIFEST(alias), {
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
- this.validateDependencyConfig(dependency, alias, accept, imp);
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(alias), {
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, alias, lockFile, accept, imp);
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
- * Gets the normalized dependency configuration for an alias.
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, alias: string): ExtendedDependencySpec | undefined {
124
- const dep = manifest.dependencies?.[alias];
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: alias, ref: dep };
233
+ return { source: key, ref: dep };
131
234
  }
132
235
 
133
236
  if (!dep.source && !dep.path) {
134
- return { ...dep, source: alias };
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
- const document = imp.$document;
300
- if (!document) return;
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
  }
@@ -1,6 +1,7 @@
1
1
  import type { ValidationAcceptor } from 'langium';
2
- import type { ContextMap, DomainMap } from '../generated/ast.js';
3
- import { ValidationMessages, buildCodeDescription } from './constants.js';
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
- validateContextMapHasRelationships
182
+ validateContextMapReferences,
183
+ validateContextMapHasRelationships,
184
+ validateNoDuplicateRelationships
72
185
  ];
73
186
 
74
187
  export const domainMapChecks = [
75
- validateDomainMapHasDomains
188
+ validateDomainMapHasDomains,
189
+ validateDomainMapReferences
76
190
  ];