spex-parser 0.1.5 → 0.6.2
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 +320 -9
- package/dist/ast.d.ts +71 -2
- package/dist/ast.d.ts.map +1 -1
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +32 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +17 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +159 -5
- package/dist/lexer.js.map +1 -1
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +105 -7
- package/dist/parser.js.map +1 -1
- package/dist/visitor.d.ts +11 -2
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +182 -8
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +94 -0
- package/src/constants.ts +40 -0
- package/src/index.ts +20 -3
- package/src/lexer.ts +150 -6
- package/src/parser.ts +130 -7
- package/src/visitor.ts +196 -11
- package/tests/constants.test.ts +102 -0
- package/tests/e2e.test.ts +15 -3
- package/tests/lexer.test.ts +230 -9
- package/tests/parser.test.ts +455 -10
- package/tests/props/express_todo_web.spex +108 -0
- package/tests/props/express_web_env.spex +16 -0
- package/tests/props/flask_todo_web.spex +111 -0
- package/tests/props/flask_web_env.spex +16 -0
- package/tests/props/python_cli_env.spex +16 -0
- package/tests/props/python_todo_cli.spex +70 -0
- package/tests/props/todo.spex +99 -68
- package/tests/props/typescript_cli_env.spex +16 -0
- package/tests/props/typescript_todo_cli.spex +70 -0
- package/tests/visitor.test.ts +905 -6
package/tests/lexer.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ describe('SpexLexer', () => {
|
|
|
5
5
|
describe('tokenization', () => {
|
|
6
6
|
it('should tokenize keywords', () => {
|
|
7
7
|
const result = SpexLexer.tokenize(
|
|
8
|
-
'create as from select generate import export package executable module'
|
|
8
|
+
'create as from select generate import export package executable module enum realize in'
|
|
9
9
|
)
|
|
10
10
|
expect(result.errors).toHaveLength(0)
|
|
11
11
|
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
@@ -19,12 +19,15 @@ describe('SpexLexer', () => {
|
|
|
19
19
|
'PackageTok',
|
|
20
20
|
'ExecutableTok',
|
|
21
21
|
'ModuleTok',
|
|
22
|
+
'EnumTok',
|
|
23
|
+
'RealizeTok',
|
|
24
|
+
'InTok',
|
|
22
25
|
])
|
|
23
26
|
})
|
|
24
27
|
|
|
25
28
|
it('should tokenize keywords case-insensitively', () => {
|
|
26
29
|
const result = SpexLexer.tokenize(
|
|
27
|
-
'CREATE AS FROM SELECT GENERATE IMPORT EXPORT PACKAGE EXECUTABLE MODULE'
|
|
30
|
+
'CREATE AS FROM SELECT GENERATE IMPORT EXPORT PACKAGE EXECUTABLE MODULE ENUM UNION INTERSECT EXCEPT REALIZE IN'
|
|
28
31
|
)
|
|
29
32
|
expect(result.errors).toHaveLength(0)
|
|
30
33
|
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
@@ -38,16 +41,37 @@ describe('SpexLexer', () => {
|
|
|
38
41
|
'PackageTok',
|
|
39
42
|
'ExecutableTok',
|
|
40
43
|
'ModuleTok',
|
|
44
|
+
'EnumTok',
|
|
45
|
+
'UnionTok',
|
|
46
|
+
'IntersectTok',
|
|
47
|
+
'ExceptTok',
|
|
48
|
+
'RealizeTok',
|
|
49
|
+
'InTok',
|
|
50
|
+
])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should tokenize enum object declarations', () => {
|
|
54
|
+
const result = SpexLexer.tokenize("create myEnum as enum ('v1', 'v2');")
|
|
55
|
+
expect(result.errors).toHaveLength(0)
|
|
56
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
57
|
+
'CreateTok',
|
|
58
|
+
'Identifier',
|
|
59
|
+
'AsTok',
|
|
60
|
+
'EnumTok',
|
|
61
|
+
'LParen',
|
|
62
|
+
'StringLiteral',
|
|
63
|
+
'Comma',
|
|
64
|
+
'StringLiteral',
|
|
65
|
+
'RParen',
|
|
66
|
+
'Semicolon',
|
|
41
67
|
])
|
|
42
68
|
})
|
|
43
69
|
|
|
44
70
|
it('should tokenize symbols', () => {
|
|
45
|
-
const result = SpexLexer.tokenize('->
|
|
71
|
+
const result = SpexLexer.tokenize('->[]():;,.|')
|
|
46
72
|
expect(result.errors).toHaveLength(0)
|
|
47
73
|
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
48
74
|
'ArrowTok',
|
|
49
|
-
'LCurly',
|
|
50
|
-
'RCurly',
|
|
51
75
|
'LBracket',
|
|
52
76
|
'RBracket',
|
|
53
77
|
'LParen',
|
|
@@ -56,9 +80,16 @@ describe('SpexLexer', () => {
|
|
|
56
80
|
'Semicolon',
|
|
57
81
|
'Comma',
|
|
58
82
|
'Dot',
|
|
83
|
+
'PipeTok',
|
|
59
84
|
])
|
|
60
85
|
})
|
|
61
86
|
|
|
87
|
+
it('should tokenize braces as separate symbols when not forming a block', () => {
|
|
88
|
+
const result = SpexLexer.tokenize('}{')
|
|
89
|
+
expect(result.errors).toHaveLength(0)
|
|
90
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['RCurly', 'LCurly'])
|
|
91
|
+
})
|
|
92
|
+
|
|
62
93
|
it('should tokenize identifiers', () => {
|
|
63
94
|
const result = SpexLexer.tokenize('foo bar _test _123 ABC')
|
|
64
95
|
expect(result.errors).toHaveLength(0)
|
|
@@ -89,10 +120,19 @@ describe('SpexLexer', () => {
|
|
|
89
120
|
|
|
90
121
|
it('should handle keywords with word boundary', () => {
|
|
91
122
|
const result = SpexLexer.tokenize(
|
|
92
|
-
'createfoo foocreate asfoo fooas fooselect selectfoo foofrom fromfoo generatefoo foogenerate importfoo fooimport exportfoo fooexport packagefoo fopackage executablefoo foexecutable modulefoo fomodule'
|
|
123
|
+
'createfoo foocreate asfoo fooas fooselect selectfoo foofrom fromfoo generatefoo foogenerate importfoo fooimport exportfoo fooexport packagefoo fopackage executablefoo foexecutable modulefoo fomodule conceptfoo fooconcept environmentfoo fooenvironment realizefoo foorealize info fooin'
|
|
93
124
|
)
|
|
94
125
|
expect(result.errors).toHaveLength(0)
|
|
95
|
-
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(Array(
|
|
126
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(Array(28).fill('Identifier'))
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should tokenize the concept and environment base types', () => {
|
|
130
|
+
const result = SpexLexer.tokenize('concept environment')
|
|
131
|
+
expect(result.errors).toHaveLength(0)
|
|
132
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
133
|
+
'ConceptTok',
|
|
134
|
+
'EnvironmentTok',
|
|
135
|
+
])
|
|
96
136
|
})
|
|
97
137
|
|
|
98
138
|
it('should tokenize the text between braces', () => {
|
|
@@ -103,14 +143,79 @@ describe('SpexLexer', () => {
|
|
|
103
143
|
expect(result.tokens[0]?.image).toBe('{hello\nworld}')
|
|
104
144
|
})
|
|
105
145
|
|
|
106
|
-
it('should tokenize
|
|
146
|
+
it('should tokenize double quoted string literals', () => {
|
|
107
147
|
const result = SpexLexer.tokenize('"types.spex"')
|
|
108
148
|
expect(result.errors).toHaveLength(0)
|
|
109
149
|
expect(result.tokens).toHaveLength(1)
|
|
110
|
-
expect(result.tokens[0]?.tokenType.name).toBe('
|
|
150
|
+
expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
|
|
111
151
|
expect(result.tokens[0]?.image).toBe('"types.spex"')
|
|
112
152
|
})
|
|
113
153
|
|
|
154
|
+
it('should tokenize single quoted string literals', () => {
|
|
155
|
+
const result = SpexLexer.tokenize("'types.spex'")
|
|
156
|
+
expect(result.errors).toHaveLength(0)
|
|
157
|
+
expect(result.tokens).toHaveLength(1)
|
|
158
|
+
expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
|
|
159
|
+
expect(result.tokens[0]?.image).toBe("'types.spex'")
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should tokenize string literals with escaped quotes', () => {
|
|
163
|
+
const result = SpexLexer.tokenize("'it\\'s a \"quote\"'")
|
|
164
|
+
expect(result.errors).toHaveLength(0)
|
|
165
|
+
expect(result.tokens).toHaveLength(1)
|
|
166
|
+
expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
|
|
167
|
+
expect(result.tokens[0]?.image).toBe("'it\\'s a \"quote\"'")
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('should tokenize string literals with escaped backslashes', () => {
|
|
171
|
+
const result = SpexLexer.tokenize('"a\\\\b"')
|
|
172
|
+
expect(result.errors).toHaveLength(0)
|
|
173
|
+
expect(result.tokens).toHaveLength(1)
|
|
174
|
+
expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
|
|
175
|
+
expect(result.tokens[0]?.image).toBe('"a\\\\b"')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should tokenize number literals', () => {
|
|
179
|
+
const result = SpexLexer.tokenize('42 3.14')
|
|
180
|
+
expect(result.errors).toHaveLength(0)
|
|
181
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
182
|
+
'NumberLiteral',
|
|
183
|
+
'NumberLiteral',
|
|
184
|
+
])
|
|
185
|
+
expect(result.tokens.map((t) => t.image)).toEqual(['42', '3.14'])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should tokenize bool literals', () => {
|
|
189
|
+
const result = SpexLexer.tokenize('true false TRUE FALSE')
|
|
190
|
+
expect(result.errors).toHaveLength(0)
|
|
191
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
192
|
+
'TrueTok',
|
|
193
|
+
'FalseTok',
|
|
194
|
+
'TrueTok',
|
|
195
|
+
'FalseTok',
|
|
196
|
+
])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should tokenize literal object declarations', () => {
|
|
200
|
+
const result = SpexLexer.tokenize("create Foo as (name: \"John\", age: 42);")
|
|
201
|
+
expect(result.errors).toHaveLength(0)
|
|
202
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
203
|
+
'CreateTok',
|
|
204
|
+
'Identifier',
|
|
205
|
+
'AsTok',
|
|
206
|
+
'LParen',
|
|
207
|
+
'Identifier',
|
|
208
|
+
'Colon',
|
|
209
|
+
'StringLiteral',
|
|
210
|
+
'Comma',
|
|
211
|
+
'Identifier',
|
|
212
|
+
'Colon',
|
|
213
|
+
'NumberLiteral',
|
|
214
|
+
'RParen',
|
|
215
|
+
'Semicolon',
|
|
216
|
+
])
|
|
217
|
+
})
|
|
218
|
+
|
|
114
219
|
it('should tokenize array brackets', () => {
|
|
115
220
|
const result = SpexLexer.tokenize('string[]')
|
|
116
221
|
expect(result.errors).toHaveLength(0)
|
|
@@ -123,6 +228,49 @@ describe('SpexLexer', () => {
|
|
|
123
228
|
})
|
|
124
229
|
})
|
|
125
230
|
|
|
231
|
+
describe('select block escaping', () => {
|
|
232
|
+
it('should tokenize an empty select block', () => {
|
|
233
|
+
const result = SpexLexer.tokenize('{}')
|
|
234
|
+
expect(result.errors).toHaveLength(0)
|
|
235
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
|
|
236
|
+
expect(result.tokens[0]?.image).toBe('{}')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should tokenize a select block with an escaped close brace', () => {
|
|
240
|
+
const result = SpexLexer.tokenize('{end with \\} }')
|
|
241
|
+
expect(result.errors).toHaveLength(0)
|
|
242
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
|
|
243
|
+
expect(result.tokens[0]?.image).toBe('{end with \\} }')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should tokenize a select block with escaped open and close braces', () => {
|
|
247
|
+
const result = SpexLexer.tokenize('{match \\{a\\} }')
|
|
248
|
+
expect(result.errors).toHaveLength(0)
|
|
249
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
|
|
250
|
+
expect(result.tokens[0]?.image).toBe('{match \\{a\\} }')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should tokenize a select block with escaped backslashes', () => {
|
|
254
|
+
const result = SpexLexer.tokenize('{a \\\\ b}')
|
|
255
|
+
expect(result.errors).toHaveLength(0)
|
|
256
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
|
|
257
|
+
expect(result.tokens[0]?.image).toBe('{a \\\\ b}')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should keep backslashes before ordinary characters', () => {
|
|
261
|
+
const result = SpexLexer.tokenize('{\\d matches}')
|
|
262
|
+
expect(result.errors).toHaveLength(0)
|
|
263
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
|
|
264
|
+
expect(result.tokens[0]?.image).toBe('{\\d matches}')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should fall back to individual symbols when a select block is unterminated', () => {
|
|
268
|
+
const result = SpexLexer.tokenize('{foo')
|
|
269
|
+
expect(result.errors).toHaveLength(0)
|
|
270
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['LCurly', 'Identifier'])
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
126
274
|
describe('comments', () => {
|
|
127
275
|
it('should skip single-line comments', () => {
|
|
128
276
|
const result = SpexLexer.tokenize('create Foo as string; -- a comment')
|
|
@@ -182,6 +330,79 @@ describe('SpexLexer', () => {
|
|
|
182
330
|
const result = SpexLexer.tokenize('create Foo as string; /* unclosed')
|
|
183
331
|
expect(result.errors).not.toHaveLength(0)
|
|
184
332
|
})
|
|
333
|
+
|
|
334
|
+
it('should not strip comment markers inside a select block', () => {
|
|
335
|
+
const result = SpexLexer.tokenize('select { are valid -- like emails };')
|
|
336
|
+
expect(result.errors).toHaveLength(0)
|
|
337
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
338
|
+
'SelectTok',
|
|
339
|
+
'SelectBlock',
|
|
340
|
+
'Semicolon',
|
|
341
|
+
])
|
|
342
|
+
expect(result.tokens[1]?.image).toBe('{ are valid -- like emails }')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('should preserve block comment markers inside a select block', () => {
|
|
346
|
+
const result = SpexLexer.tokenize('select { match /* strict */ pattern };')
|
|
347
|
+
expect(result.errors).toHaveLength(0)
|
|
348
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
349
|
+
'SelectTok',
|
|
350
|
+
'SelectBlock',
|
|
351
|
+
'Semicolon',
|
|
352
|
+
])
|
|
353
|
+
expect(result.tokens[1]?.image).toBe('{ match /* strict */ pattern }')
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('pattern literal', () => {
|
|
358
|
+
it('should tokenize a pattern literal with flags', () => {
|
|
359
|
+
const result = SpexLexer.tokenize('/create\\b/i')
|
|
360
|
+
expect(result.errors).toHaveLength(0)
|
|
361
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
|
|
362
|
+
expect(result.tokens[0]?.image).toBe('/create\\b/i')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should tokenize a pattern literal with an escaped slash', () => {
|
|
366
|
+
const result = SpexLexer.tokenize('/\\/\\*[\\s\\S]*?\\*\\//')
|
|
367
|
+
expect(result.errors).toHaveLength(0)
|
|
368
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
|
|
369
|
+
expect(result.tokens[0]?.image).toBe('/\\/\\*[\\s\\S]*?\\*\\//')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('should tokenize a pattern literal with quotes in char classes', () => {
|
|
373
|
+
const result = SpexLexer.tokenize("/'([^'\\\\]|\\\\.)*'|\"([^\"\\\\]|\\\\.)*\"/")
|
|
374
|
+
expect(result.errors).toHaveLength(0)
|
|
375
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
|
|
376
|
+
expect(result.tokens[0]?.image).toBe("/'([^'\\\\]|\\\\.)*'|\"([^\"\\\\]|\\\\.)*\"/")
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should tokenize an empty pattern', () => {
|
|
380
|
+
const result = SpexLexer.tokenize('//')
|
|
381
|
+
expect(result.errors).toHaveLength(0)
|
|
382
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
|
|
383
|
+
expect(result.tokens[0]?.image).toBe('//')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('should tokenize a pattern before following tokens', () => {
|
|
387
|
+
const result = SpexLexer.tokenize('/\\d+/ foo;')
|
|
388
|
+
expect(result.errors).toHaveLength(0)
|
|
389
|
+
expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
|
|
390
|
+
'PatternLiteral',
|
|
391
|
+
'Identifier',
|
|
392
|
+
'Semicolon',
|
|
393
|
+
])
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('should not tokenize an unterminated pattern', () => {
|
|
397
|
+
const result = SpexLexer.tokenize('/foo')
|
|
398
|
+
expect(result.errors).not.toHaveLength(0)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('should prefer a block comment over a pattern literal', () => {
|
|
402
|
+
const result = SpexLexer.tokenize('/* comment */')
|
|
403
|
+
expect(result.errors).toHaveLength(0)
|
|
404
|
+
expect(result.tokens).toHaveLength(0)
|
|
405
|
+
})
|
|
185
406
|
})
|
|
186
407
|
|
|
187
408
|
describe('error handling', () => {
|