@words-lang/parser 0.1.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 (41) hide show
  1. package/dist/analyser/analyser.d.ts +106 -0
  2. package/dist/analyser/analyser.d.ts.map +1 -0
  3. package/dist/analyser/analyser.js +291 -0
  4. package/dist/analyser/analyser.js.map +1 -0
  5. package/dist/analyser/diagnostics.d.ts +166 -0
  6. package/dist/analyser/diagnostics.d.ts.map +1 -0
  7. package/dist/analyser/diagnostics.js +139 -0
  8. package/dist/analyser/diagnostics.js.map +1 -0
  9. package/dist/analyser/workspace.d.ts +198 -0
  10. package/dist/analyser/workspace.d.ts.map +1 -0
  11. package/dist/analyser/workspace.js +403 -0
  12. package/dist/analyser/workspace.js.map +1 -0
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +31 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lexer/lexer.d.ts +120 -0
  18. package/dist/lexer/lexer.d.ts.map +1 -0
  19. package/dist/lexer/lexer.js +365 -0
  20. package/dist/lexer/lexer.js.map +1 -0
  21. package/dist/lexer/token.d.ts +247 -0
  22. package/dist/lexer/token.d.ts.map +1 -0
  23. package/dist/lexer/token.js +250 -0
  24. package/dist/lexer/token.js.map +1 -0
  25. package/dist/parser/ast.d.ts +685 -0
  26. package/dist/parser/ast.d.ts.map +1 -0
  27. package/dist/parser/ast.js +3 -0
  28. package/dist/parser/ast.js.map +1 -0
  29. package/dist/parser/parser.d.ts +411 -0
  30. package/dist/parser/parser.d.ts.map +1 -0
  31. package/dist/parser/parser.js +1600 -0
  32. package/dist/parser/parser.js.map +1 -0
  33. package/package.json +23 -0
  34. package/src/analyser/analyser.ts +403 -0
  35. package/src/analyser/diagnostics.ts +232 -0
  36. package/src/analyser/workspace.ts +457 -0
  37. package/src/index.ts +7 -0
  38. package/src/lexer/lexer.ts +379 -0
  39. package/src/lexer/token.ts +331 -0
  40. package/src/parser/ast.ts +798 -0
  41. package/src/parser/parser.ts +1815 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * diagnostics.ts
3
+ *
4
+ * Defines the Diagnostic type returned by the parser and analyser.
5
+ * Diagnostics are never thrown — they are collected and returned alongside
6
+ * the AST so the caller always receives the fullest possible picture of
7
+ * what the source contains, even when it is malformed.
8
+ *
9
+ * The LSP server maps these directly to VS Code diagnostics, using the
10
+ * `range` to underline the offending token in the editor.
11
+ */
12
+
13
+ // ── Severity ──────────────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * The severity of a diagnostic.
17
+ *
18
+ * `error` — the source is invalid and cannot produce a correct implementation.
19
+ * e.g. a `when` rule referencing a state that has no definition.
20
+ * `warning` — the source is valid but likely wrong or incomplete.
21
+ * e.g. a process narrative omitted where one is strongly recommended.
22
+ * `hint` — a stylistic or structural suggestion.
23
+ * e.g. a module with no description.
24
+ */
25
+ export type DiagnosticSeverity = 'error' | 'warning' | 'hint'
26
+
27
+ // ── Source position ───────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * A zero-based line and character position in the source.
31
+ * Matches the LSP `Position` type so diagnostics can be forwarded
32
+ * to VS Code without translation.
33
+ */
34
+ export interface Position {
35
+ /** Zero-based line number. */
36
+ line: number
37
+ /** Zero-based character offset within the line. */
38
+ character: number
39
+ }
40
+
41
+ /**
42
+ * A start-inclusive, end-exclusive range in the source.
43
+ * Matches the LSP `Range` type.
44
+ */
45
+ export interface Range {
46
+ start: Position
47
+ end: Position
48
+ }
49
+
50
+ // ── Diagnostic codes ──────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Every diagnostic code the parser and analyser can produce.
54
+ * Codes are namespaced by layer:
55
+ * P_* — parse errors (structural / syntactic problems)
56
+ * A_* — analyser errors (semantic problems)
57
+ * W_* — warnings
58
+ * H_* — hints
59
+ */
60
+ export enum DiagnosticCode {
61
+
62
+ // ── Parse errors ────────────────────────────────────────────────────────────
63
+
64
+ /** An unexpected token was encountered where a specific token was required. */
65
+ P_UNEXPECTED_TOKEN = 'P001',
66
+
67
+ /** The source ended before the construct was complete. */
68
+ P_UNEXPECTED_EOF = 'P002',
69
+
70
+ /** An opening `(` was not matched by a closing `)`. */
71
+ P_UNCLOSED_PAREN = 'P003',
72
+
73
+ /** A string literal was opened with `"` but never closed. */
74
+ P_UNCLOSED_STRING = 'P004',
75
+
76
+ /** A construct keyword was found in a position where it is not valid. */
77
+ P_INVALID_CONSTRUCT_POSITION = 'P005',
78
+
79
+ /** A `when` rule is missing its `enter` clause. */
80
+ P_MISSING_ENTER = 'P006',
81
+
82
+ /** A `when` rule is missing its `returns` clause. */
83
+ P_MISSING_RETURNS = 'P007',
84
+
85
+ /** A type annotation `(Type)` was expected but not found. */
86
+ P_MISSING_TYPE = 'P008',
87
+
88
+ /** An identifier (PascalCase or camelCase) was expected but not found. */
89
+ P_MISSING_IDENTIFIER = 'P009',
90
+
91
+ /** An `is` keyword was expected in an argument assignment but not found. */
92
+ P_MISSING_IS = 'P010',
93
+
94
+ // ── Analyser errors ─────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * A state referenced in a `when` rule or `start` declaration has no
98
+ * corresponding state definition in the module's directory.
99
+ */
100
+ A_UNDEFINED_STATE = 'A001',
101
+
102
+ /**
103
+ * A context referenced in a `when` rule, `receives`, or `returns` clause
104
+ * has no corresponding context definition in the module's directory.
105
+ */
106
+ A_UNDEFINED_CONTEXT = 'A002',
107
+
108
+ /**
109
+ * A context listed in a state's `returns` block has no corresponding
110
+ * `when` rule in any of the module's processes.
111
+ */
112
+ A_UNHANDLED_RETURN = 'A003',
113
+
114
+ /**
115
+ * A state is defined but never referenced in any process `when` rule
116
+ * and is not the module's `start` state.
117
+ */
118
+ A_UNREACHABLE_STATE = 'A004',
119
+
120
+ /**
121
+ * A module listed in the system's `modules` block has no corresponding
122
+ * module definition file.
123
+ */
124
+ A_UNDEFINED_MODULE = 'A005',
125
+
126
+ /**
127
+ * A component referenced by qualified name (e.g. `UIModule.LoginForm`)
128
+ * cannot be resolved — either the module does not exist or the component
129
+ * is not defined within it.
130
+ */
131
+ A_UNDEFINED_COMPONENT = 'A006',
132
+
133
+ /**
134
+ * The owning module declared at the top of a component file (e.g.
135
+ * `module AuthModule`) does not match the module the construct names itself.
136
+ */
137
+ A_MODULE_MISMATCH = 'A007',
138
+
139
+ /**
140
+ * A `state.return(contextName)` call references a context name that does
141
+ * not appear in the enclosing state's `returns` clause.
142
+ */
143
+ A_INVALID_STATE_RETURN = 'A008',
144
+
145
+ /**
146
+ * An `implements` block references a handler interface that is not declared
147
+ * in the named module.
148
+ */
149
+ A_UNDEFINED_INTERFACE = 'A009',
150
+
151
+ // ── Warnings ────────────────────────────────────────────────────────────────
152
+
153
+ /**
154
+ * A `when` rule has no transition narrative.
155
+ * Narratives are optional but strongly recommended for readability.
156
+ */
157
+ W_MISSING_NARRATIVE = 'W001',
158
+
159
+ /**
160
+ * A construct has no description string.
161
+ * Descriptions are optional but recommended for documentation.
162
+ */
163
+ W_MISSING_DESCRIPTION = 'W002',
164
+
165
+ // ── Hints ────────────────────────────────────────────────────────────────────
166
+
167
+ /**
168
+ * A state has no `uses` block — it is a transient state that holds a
169
+ * position in the process map while something external resolves.
170
+ * This is valid but worth flagging so the designer is aware.
171
+ */
172
+ H_EMPTY_USES = 'H001',
173
+ }
174
+
175
+ // ── Diagnostic ────────────────────────────────────────────────────────────────
176
+
177
+ /**
178
+ * A single diagnostic produced by the parser or analyser.
179
+ *
180
+ * `code` — the diagnostic code, used by the LSP to deduplicate and
181
+ * provide quick-fix suggestions.
182
+ * `message` — a human-readable explanation of the problem.
183
+ * `severity` — how serious the problem is.
184
+ * `range` — the source range to underline in the editor. Derived from
185
+ * the offending token's line, column, and value length.
186
+ * `source` — identifies which layer produced the diagnostic:
187
+ * `'parser'` for structural problems, `'analyser'` for semantic ones.
188
+ */
189
+ export interface Diagnostic {
190
+ code: DiagnosticCode
191
+ message: string
192
+ severity: DiagnosticSeverity
193
+ range: Range
194
+ source: 'parser' | 'analyser'
195
+ }
196
+
197
+ // ── Convenience constructors ──────────────────────────────────────────────────
198
+
199
+ /**
200
+ * Creates a Range from a 1-based line and column and the length of the
201
+ * offending text. Converts to the zero-based LSP convention internally.
202
+ */
203
+ export function rangeFromToken(line: number, column: number, length: number): Range {
204
+ return {
205
+ start: { line: line - 1, character: column - 1 },
206
+ end: { line: line - 1, character: column - 1 + length },
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Creates a parser-layer Diagnostic.
212
+ */
213
+ export function parseDiagnostic(
214
+ code: DiagnosticCode,
215
+ message: string,
216
+ severity: DiagnosticSeverity,
217
+ range: Range
218
+ ): Diagnostic {
219
+ return { code, message, severity, range, source: 'parser' }
220
+ }
221
+
222
+ /**
223
+ * Creates an analyser-layer Diagnostic.
224
+ */
225
+ export function analyserDiagnostic(
226
+ code: DiagnosticCode,
227
+ message: string,
228
+ severity: DiagnosticSeverity,
229
+ range: Range
230
+ ): Diagnostic {
231
+ return { code, message, severity, range, source: 'analyser' }
232
+ }
@@ -0,0 +1,457 @@
1
+ /**
2
+ * workspace.ts
3
+ *
4
+ * The Workspace scans a WORDS project directory, parses every `.wds` file it
5
+ * finds, and builds a cross-file index of all constructs organised by module.
6
+ *
7
+ * The index is the shared data structure consumed by the Analyser and later
8
+ * by the LSP server for go-to-definition and find-references. Neither the
9
+ * Analyser nor the LSP reads files directly — they always go through the
10
+ * Workspace.
11
+ *
12
+ * Design principles:
13
+ *
14
+ * - One parse per file. The Workspace caches parse results so the Analyser
15
+ * can query the index repeatedly without re-parsing.
16
+ *
17
+ * - Flat index structure. Constructs are indexed by module name and construct
18
+ * name so lookups are O(1) rather than requiring a tree walk.
19
+ *
20
+ * - Parse errors are preserved. Files that fail to parse partially are still
21
+ * indexed — whatever nodes were recovered are included. Parse diagnostics
22
+ * are stored alongside the index and returned with analyser diagnostics.
23
+ *
24
+ * - The Workspace is synchronous. File I/O uses Node's `fs` module directly.
25
+ * The LSP layer wraps this in async when needed.
26
+ */
27
+
28
+ import * as fs from 'fs'
29
+ import * as path from 'path'
30
+ import { Lexer } from '../lexer/lexer'
31
+ import { Parser, ParseResult } from '../parser/parser'
32
+ import {
33
+ SystemNode,
34
+ ModuleNode,
35
+ StateNode,
36
+ ContextNode,
37
+ ScreenNode,
38
+ ViewNode,
39
+ ProviderNode,
40
+ AdapterNode,
41
+ InterfaceNode,
42
+ TopLevelNode,
43
+ DocumentNode,
44
+ } from '../parser/ast'
45
+ import { Diagnostic } from './diagnostics'
46
+
47
+ // ── Construct index maps ──────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * A map from construct name to node, scoped to one module.
51
+ * e.g. states.get('AuthModule')?.get('Unauthenticated') → StateNode
52
+ */
53
+ type ConstructMap<T> = Map<string, T>
54
+
55
+ /**
56
+ * A map from module name to a per-module construct map.
57
+ */
58
+ type ModuleIndex<T> = Map<string, ConstructMap<T>>
59
+
60
+ // ── File record ───────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Everything the Workspace knows about a single `.wds` file.
64
+ *
65
+ * `filePath` — absolute path to the file on disk.
66
+ * `source` — raw source text read from disk.
67
+ * `parseResult` — the result of parsing the source, including the document
68
+ * AST and any parse-layer diagnostics.
69
+ */
70
+ export interface FileRecord {
71
+ filePath: string
72
+ source: string
73
+ parseResult: ParseResult
74
+ }
75
+
76
+ // ── Workspace ─────────────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * The cross-file index of a WORDS project.
80
+ *
81
+ * Built by calling `Workspace.load(projectRoot)`. Consumers query the index
82
+ * directly via the public maps — there are no query methods, just data.
83
+ */
84
+ export class Workspace {
85
+ /** Absolute path to the project root directory. */
86
+ readonly projectRoot: string
87
+
88
+ /**
89
+ * All parsed files, keyed by absolute file path.
90
+ * Includes both successfully parsed files and files with parse errors.
91
+ */
92
+ readonly files: Map<string, FileRecord> = new Map()
93
+
94
+ /**
95
+ * All parse-layer diagnostics collected across all files.
96
+ * Indexed by absolute file path.
97
+ */
98
+ readonly parseDiagnostics: Map<string, Diagnostic[]> = new Map()
99
+
100
+ /**
101
+ * The single system declaration found in the project.
102
+ * Null if no system file was found or if it failed to parse.
103
+ */
104
+ system: SystemNode | null = null
105
+
106
+ /**
107
+ * The file path of the system declaration, for diagnostic reporting.
108
+ */
109
+ systemFilePath: string | null = null
110
+
111
+ /**
112
+ * All module definitions, keyed by module name.
113
+ * e.g. modules.get('AuthModule') → ModuleNode
114
+ */
115
+ readonly modules: Map<string, ModuleNode> = new Map()
116
+
117
+ /**
118
+ * File path of each module definition, for diagnostic reporting.
119
+ * e.g. modulePaths.get('AuthModule') → '/project/AuthModule/AuthModule.wds'
120
+ */
121
+ readonly modulePaths: Map<string, string> = new Map()
122
+
123
+ /**
124
+ * All state definitions, keyed by module name then state name.
125
+ * e.g. states.get('AuthModule')?.get('Unauthenticated') → StateNode
126
+ */
127
+ readonly states: ModuleIndex<StateNode> = new Map()
128
+
129
+ /**
130
+ * All context definitions, keyed by module name then context name.
131
+ */
132
+ readonly contexts: ModuleIndex<ContextNode> = new Map()
133
+
134
+ /**
135
+ * All screen definitions, keyed by module name then screen name.
136
+ */
137
+ readonly screens: ModuleIndex<ScreenNode> = new Map()
138
+
139
+ /**
140
+ * All view definitions, keyed by module name then view name.
141
+ */
142
+ readonly views: ModuleIndex<ViewNode> = new Map()
143
+
144
+ /**
145
+ * All provider definitions, keyed by module name then provider name.
146
+ */
147
+ readonly providers: ModuleIndex<ProviderNode> = new Map()
148
+
149
+ /**
150
+ * All adapter definitions, keyed by module name then adapter name.
151
+ */
152
+ readonly adapters: ModuleIndex<AdapterNode> = new Map()
153
+
154
+ /**
155
+ * All interface component definitions, keyed by module name then interface name.
156
+ */
157
+ readonly interfaces: ModuleIndex<InterfaceNode> = new Map()
158
+
159
+ /**
160
+ * File path of each construct, for go-to-definition.
161
+ * Key format: 'ModuleName/ConstructName' (e.g. 'AuthModule/Unauthenticated')
162
+ */
163
+ readonly constructPaths: Map<string, string> = new Map()
164
+
165
+ private constructor(projectRoot: string) {
166
+ this.projectRoot = projectRoot
167
+ }
168
+
169
+ // ── Factory ────────────────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Scans `projectRoot` recursively for `.wds` files, parses each one,
173
+ * and returns a fully populated Workspace.
174
+ *
175
+ * Never throws — files that cannot be read or parsed are recorded with
176
+ * their errors and skipped during indexing.
177
+ */
178
+ static load(projectRoot: string): Workspace {
179
+ const ws = new Workspace(path.resolve(projectRoot))
180
+ const wdsPaths = ws.findWdsFiles(ws.projectRoot)
181
+
182
+ for (const filePath of wdsPaths) {
183
+ ws.loadFile(filePath)
184
+ }
185
+
186
+ ws.buildIndex()
187
+ return ws
188
+ }
189
+
190
+ /**
191
+ * Reloads a single file and rebuilds the index.
192
+ * Used by the LSP server when a file changes on disk.
193
+ */
194
+ reload(filePath: string): void {
195
+ this.loadFile(path.resolve(filePath))
196
+ this.clearIndex()
197
+ this.buildIndex()
198
+ }
199
+
200
+ // ── Query helpers ──────────────────────────────────────────────────────────
201
+
202
+ /**
203
+ * Returns the StateNode for the given module and state name, or null.
204
+ */
205
+ getState(moduleName: string, stateName: string): StateNode | null {
206
+ return this.states.get(moduleName)?.get(stateName) ?? null
207
+ }
208
+
209
+ /**
210
+ * Returns the ContextNode for the given module and context name, or null.
211
+ */
212
+ getContext(moduleName: string, contextName: string): ContextNode | null {
213
+ return this.contexts.get(moduleName)?.get(contextName) ?? null
214
+ }
215
+
216
+ /**
217
+ * Returns the ScreenNode for the given module and screen name, or null.
218
+ */
219
+ getScreen(moduleName: string, screenName: string): ScreenNode | null {
220
+ return this.screens.get(moduleName)?.get(screenName) ?? null
221
+ }
222
+
223
+ /**
224
+ * Returns the ViewNode for the given module and view name, or null.
225
+ * Also searches other modules if moduleName is null.
226
+ */
227
+ getView(moduleName: string, viewName: string): ViewNode | null {
228
+ return this.views.get(moduleName)?.get(viewName) ?? null
229
+ }
230
+
231
+ /**
232
+ * Returns all parse diagnostics across all files as a flat array,
233
+ * each annotated with the file path that produced it.
234
+ */
235
+ allParseDiagnostics(): Array<{ filePath: string; diagnostic: Diagnostic }> {
236
+ const result: Array<{ filePath: string; diagnostic: Diagnostic }> = []
237
+ for (const [filePath, diags] of this.parseDiagnostics) {
238
+ for (const d of diags) {
239
+ result.push({ filePath, diagnostic: d })
240
+ }
241
+ }
242
+ return result
243
+ }
244
+
245
+ // ── Private — file loading ─────────────────────────────────────────────────
246
+
247
+ /**
248
+ * Reads and parses a single `.wds` file, storing the result in `this.files`.
249
+ * Silently records any read error as a parse diagnostic.
250
+ */
251
+ private loadFile(filePath: string): void {
252
+ let source: string
253
+ try {
254
+ source = fs.readFileSync(filePath, 'utf-8')
255
+ } catch (err) {
256
+ // File could not be read — record and skip
257
+ this.files.set(filePath, {
258
+ filePath,
259
+ source: '',
260
+ parseResult: {
261
+ document: { kind: 'Document', ownerModule: null, nodes: [] },
262
+ diagnostics: [],
263
+ },
264
+ })
265
+ return
266
+ }
267
+
268
+ const tokens = new Lexer(source).tokenize()
269
+ const parseResult = new Parser(tokens).parse()
270
+
271
+ this.files.set(filePath, { filePath, source, parseResult })
272
+
273
+ if (parseResult.diagnostics.length > 0) {
274
+ this.parseDiagnostics.set(filePath, parseResult.diagnostics)
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Recursively finds all `.wds` files under `dir`.
280
+ * Skips `node_modules` and hidden directories.
281
+ */
282
+ private findWdsFiles(dir: string): string[] {
283
+ const results: string[] = []
284
+ let entries: fs.Dirent[]
285
+
286
+ try {
287
+ entries = fs.readdirSync(dir, { withFileTypes: true })
288
+ } catch {
289
+ return results
290
+ }
291
+
292
+ for (const entry of entries) {
293
+ if (entry.name.startsWith('.')) continue
294
+ if (entry.name === 'node_modules') continue
295
+
296
+ const fullPath = path.join(dir, entry.name)
297
+
298
+ if (entry.isDirectory()) {
299
+ results.push(...this.findWdsFiles(fullPath))
300
+ } else if (entry.isFile() && entry.name.endsWith('.wds')) {
301
+ results.push(fullPath)
302
+ }
303
+ }
304
+
305
+ return results
306
+ }
307
+
308
+ // ── Private — index building ───────────────────────────────────────────────
309
+
310
+ /**
311
+ * Clears all index maps. Called before a rebuild.
312
+ */
313
+ private clearIndex(): void {
314
+ this.system = null
315
+ this.systemFilePath = null
316
+ this.modules.clear()
317
+ this.modulePaths.clear()
318
+ this.states.clear()
319
+ this.contexts.clear()
320
+ this.screens.clear()
321
+ this.views.clear()
322
+ this.providers.clear()
323
+ this.adapters.clear()
324
+ this.interfaces.clear()
325
+ this.constructPaths.clear()
326
+ }
327
+
328
+ /**
329
+ * Walks all parsed documents and populates the index maps.
330
+ * Called once after all files are loaded, and again after each reload.
331
+ */
332
+ private buildIndex(): void {
333
+ for (const [filePath, record] of this.files) {
334
+ this.indexDocument(filePath, record.parseResult.document)
335
+ }
336
+
337
+ // Second pass: fill in the module name on component nodes that were
338
+ // parsed from files with an ownerModule declaration. The parser leaves
339
+ // module: '' on component nodes — we fill it in here from ownerModule.
340
+ for (const [, record] of this.files) {
341
+ const doc = record.parseResult.document
342
+ if (doc.ownerModule) {
343
+ for (const node of doc.nodes) {
344
+ if ('module' in node && node.module === '') {
345
+ ; (node as any).module = doc.ownerModule
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Indexes all top-level nodes in a single document into the appropriate maps.
354
+ */
355
+ private indexDocument(filePath: string, document: DocumentNode): void {
356
+ const ownerModule = document.ownerModule
357
+
358
+ for (const node of document.nodes) {
359
+ this.indexNode(filePath, node, ownerModule)
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Indexes a single top-level node into the appropriate map.
365
+ * Uses `ownerModule` from the ownership declaration if the node's own
366
+ * `module` field is empty (component files).
367
+ */
368
+ private indexNode(
369
+ filePath: string,
370
+ node: TopLevelNode,
371
+ ownerModule: string | null
372
+ ): void {
373
+ switch (node.kind) {
374
+ case 'System':
375
+ this.system = node
376
+ this.systemFilePath = filePath
377
+ break
378
+
379
+ case 'Module':
380
+ this.modules.set(node.name, node)
381
+ this.modulePaths.set(node.name, filePath)
382
+ break
383
+
384
+ case 'State': {
385
+ const mod = node.module || ownerModule || ''
386
+ if (!mod) break
387
+ this.ensureModuleMap(this.states, mod)
388
+ this.states.get(mod)!.set(node.name, node)
389
+ this.constructPaths.set(`${mod}/${node.name}`, filePath)
390
+ break
391
+ }
392
+
393
+ case 'Context': {
394
+ const mod = node.module || ownerModule || ''
395
+ if (!mod) break
396
+ this.ensureModuleMap(this.contexts, mod)
397
+ this.contexts.get(mod)!.set(node.name, node)
398
+ this.constructPaths.set(`${mod}/${node.name}`, filePath)
399
+ break
400
+ }
401
+
402
+ case 'Screen': {
403
+ const mod = node.module || ownerModule || ''
404
+ if (!mod) break
405
+ this.ensureModuleMap(this.screens, mod)
406
+ this.screens.get(mod)!.set(node.name, node)
407
+ this.constructPaths.set(`${mod}/${node.name}`, filePath)
408
+ break
409
+ }
410
+
411
+ case 'View': {
412
+ const mod = node.module || ownerModule || ''
413
+ if (!mod) break
414
+ this.ensureModuleMap(this.views, mod)
415
+ this.views.get(mod)!.set(node.name, node)
416
+ this.constructPaths.set(`${mod}/${node.name}`, filePath)
417
+ break
418
+ }
419
+
420
+ case 'Provider': {
421
+ const mod = node.module || ownerModule || ''
422
+ if (!mod) break
423
+ this.ensureModuleMap(this.providers, mod)
424
+ this.providers.get(mod)!.set(node.name, node)
425
+ this.constructPaths.set(`${mod}/${node.name}`, filePath)
426
+ break
427
+ }
428
+
429
+ case 'Adapter': {
430
+ const mod = node.module || ownerModule || ''
431
+ if (!mod) break
432
+ this.ensureModuleMap(this.adapters, mod)
433
+ this.adapters.get(mod)!.set(node.name, node)
434
+ this.constructPaths.set(`${mod}/${node.name}`, filePath)
435
+ break
436
+ }
437
+
438
+ case 'Interface': {
439
+ const mod = node.module || ownerModule || ''
440
+ if (!mod) break
441
+ this.ensureModuleMap(this.interfaces, mod)
442
+ this.interfaces.get(mod)!.set(node.name, node)
443
+ this.constructPaths.set(`${mod}/${node.name}`, filePath)
444
+ break
445
+ }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Ensures a per-module map exists for the given module name.
451
+ */
452
+ private ensureModuleMap<T>(index: ModuleIndex<T>, moduleName: string): void {
453
+ if (!index.has(moduleName)) {
454
+ index.set(moduleName, new Map())
455
+ }
456
+ }
457
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { Lexer } from './lexer/lexer'
2
+ export { Token, TokenType, token } from './lexer/token'
3
+ export * from './parser/ast'
4
+ export { Parser, ParseResult } from './parser/parser'
5
+ export * from './analyser/diagnostics'
6
+ export { Workspace, FileRecord } from './analyser/workspace'
7
+ export { Analyser, AnalysisResult } from './analyser/analyser'