@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.
- package/dist/analyser/analyser.d.ts +106 -0
- package/dist/analyser/analyser.d.ts.map +1 -0
- package/dist/analyser/analyser.js +291 -0
- package/dist/analyser/analyser.js.map +1 -0
- package/dist/analyser/diagnostics.d.ts +166 -0
- package/dist/analyser/diagnostics.d.ts.map +1 -0
- package/dist/analyser/diagnostics.js +139 -0
- package/dist/analyser/diagnostics.js.map +1 -0
- package/dist/analyser/workspace.d.ts +198 -0
- package/dist/analyser/workspace.d.ts.map +1 -0
- package/dist/analyser/workspace.js +403 -0
- package/dist/analyser/workspace.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/lexer.d.ts +120 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/lexer.js +365 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lexer/token.d.ts +247 -0
- package/dist/lexer/token.d.ts.map +1 -0
- package/dist/lexer/token.js +250 -0
- package/dist/lexer/token.js.map +1 -0
- package/dist/parser/ast.d.ts +685 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +3 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/parser.d.ts +411 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +1600 -0
- package/dist/parser/parser.js.map +1 -0
- package/package.json +23 -0
- package/src/analyser/analyser.ts +403 -0
- package/src/analyser/diagnostics.ts +232 -0
- package/src/analyser/workspace.ts +457 -0
- package/src/index.ts +7 -0
- package/src/lexer/lexer.ts +379 -0
- package/src/lexer/token.ts +331 -0
- package/src/parser/ast.ts +798 -0
- 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'
|