spex-parser 0.1.4 → 0.6.1
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 +312 -1
- package/dist/ast.d.ts +70 -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 +19 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +172 -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 +103 -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 +181 -8
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +93 -0
- package/src/constants.ts +40 -0
- package/src/index.ts +20 -3
- package/src/lexer.ts +164 -6
- package/src/parser.ts +128 -7
- package/src/visitor.ts +195 -11
- package/tests/constants.test.ts +102 -0
- package/tests/e2e.test.ts +15 -3
- package/tests/lexer.test.ts +291 -9
- package/tests/parser.test.ts +489 -0
- 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 +100 -60
- package/tests/props/typescript_cli_env.spex +16 -0
- package/tests/props/typescript_todo_cli.spex +70 -0
- package/tests/visitor.test.ts +892 -0
package/src/visitor.ts
CHANGED
|
@@ -7,14 +7,21 @@ import type {
|
|
|
7
7
|
ExportDeclaration,
|
|
8
8
|
GenerateDeclaration,
|
|
9
9
|
PackageDeclaration,
|
|
10
|
+
RealizeDeclaration,
|
|
11
|
+
IncludeDeclaration,
|
|
12
|
+
EnumObject,
|
|
13
|
+
LiteralObject,
|
|
14
|
+
SetObject,
|
|
15
|
+
CoproductObject,
|
|
16
|
+
PatternLiteralObject,
|
|
17
|
+
ExponentialPattern,
|
|
18
|
+
PatternBlock,
|
|
10
19
|
ObjectExpression,
|
|
11
20
|
NamedObject,
|
|
12
|
-
ProductObject,
|
|
13
21
|
SubObject,
|
|
14
22
|
ArrayObject,
|
|
15
23
|
Constraint,
|
|
16
24
|
ConstraintPart,
|
|
17
|
-
ConstraintReference,
|
|
18
25
|
} from './ast.js'
|
|
19
26
|
import { SpexLexer } from './lexer.js'
|
|
20
27
|
import { SpexParser } from './parser.js'
|
|
@@ -24,6 +31,15 @@ const BaseSpexVisitor = parserInstance.getBaseCstVisitorConstructor()
|
|
|
24
31
|
|
|
25
32
|
export const REFERENCE_PATTERN = /@([a-zA-Z_][\w]*(?:\.[a-zA-Z_][\w]*)*)/g
|
|
26
33
|
|
|
34
|
+
function unescapeConstraint(text: string): string {
|
|
35
|
+
return text.replace(/\\([\\{}])/g, '$1')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stringLiteralValue(image: string): string {
|
|
39
|
+
const inner = image.slice(1, -1)
|
|
40
|
+
return inner.replace(/\\([\\'"])/g, '$1')
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export function parseConstraint(raw: string): Constraint {
|
|
28
44
|
const parts: ConstraintPart[] = []
|
|
29
45
|
let lastIndex = 0
|
|
@@ -56,6 +72,9 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
declaration(ctx: any): Declaration {
|
|
75
|
+
if (ctx.realizeDeclaration) {
|
|
76
|
+
return this.visit(ctx.realizeDeclaration)
|
|
77
|
+
}
|
|
59
78
|
if (ctx.packageDeclaration) {
|
|
60
79
|
return this.visit(ctx.packageDeclaration)
|
|
61
80
|
}
|
|
@@ -68,17 +87,65 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
68
87
|
if (ctx.exportDeclaration) {
|
|
69
88
|
return this.visit(ctx.exportDeclaration)
|
|
70
89
|
}
|
|
90
|
+
if (ctx.includeDeclaration) {
|
|
91
|
+
return this.visit(ctx.includeDeclaration)
|
|
92
|
+
}
|
|
71
93
|
return this.visit(ctx.generateDeclaration)
|
|
72
94
|
}
|
|
73
95
|
|
|
96
|
+
enumObject(ctx: any): EnumObject {
|
|
97
|
+
return {
|
|
98
|
+
kind: 'EnumObject',
|
|
99
|
+
values: ctx.StringLiteral.map((s: any) => stringLiteralValue(s.image)),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
literalObject(ctx: any): LiteralObject {
|
|
104
|
+
if (ctx.StringLiteral) {
|
|
105
|
+
return {
|
|
106
|
+
kind: 'StringLiteralObject',
|
|
107
|
+
value: stringLiteralValue(ctx.StringLiteral[0].image),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (ctx.NumberLiteral) {
|
|
111
|
+
return { kind: 'NumberLiteralObject', value: ctx.NumberLiteral[0].image }
|
|
112
|
+
}
|
|
113
|
+
return { kind: 'BoolLiteralObject', value: ctx.TrueTok ? true : false }
|
|
114
|
+
}
|
|
115
|
+
|
|
74
116
|
objectDeclaration(ctx: any): ObjectDeclaration {
|
|
75
117
|
return {
|
|
76
118
|
kind: 'ObjectDeclaration',
|
|
77
119
|
name: ctx.Identifier[0].image,
|
|
78
|
-
object: this.visit(ctx.
|
|
120
|
+
object: this.visit(ctx.setObject),
|
|
79
121
|
}
|
|
80
122
|
}
|
|
81
123
|
|
|
124
|
+
setObject(ctx: any): SetObject {
|
|
125
|
+
const operands = ctx.coproductObject.map((expr: any) => this.visit(expr))
|
|
126
|
+
let result: ObjectExpression = operands[0]
|
|
127
|
+
for (let i = 1; i < operands.length; i++) {
|
|
128
|
+
const op = ctx.op[i - 1].tokenType.name
|
|
129
|
+
if (op === 'UnionTok') {
|
|
130
|
+
result = { kind: 'SetUnionObject', left: result, right: operands[i] }
|
|
131
|
+
} else if (op === 'IntersectTok') {
|
|
132
|
+
result = { kind: 'SetIntersectionObject', left: result, right: operands[i] }
|
|
133
|
+
} else {
|
|
134
|
+
result = { kind: 'SetDifferenceObject', left: result, right: operands[i] }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result as SetObject
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
coproductObject(ctx: any): CoproductObject {
|
|
141
|
+
const operands = ctx.objectExpression.map((expr: any) => this.visit(expr))
|
|
142
|
+
let result: ObjectExpression = operands[0]
|
|
143
|
+
for (let i = 1; i < operands.length; i++) {
|
|
144
|
+
result = { kind: 'CoproductObject', left: result, right: operands[i] }
|
|
145
|
+
}
|
|
146
|
+
return result as CoproductObject
|
|
147
|
+
}
|
|
148
|
+
|
|
82
149
|
objectExpression(ctx: any): ObjectExpression {
|
|
83
150
|
if (ctx.base) {
|
|
84
151
|
const exponent = this.visit(ctx.base)
|
|
@@ -98,8 +165,18 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
98
165
|
let expr: ObjectExpression
|
|
99
166
|
if (ctx.subObject) {
|
|
100
167
|
expr = this.visit(ctx.subObject)
|
|
168
|
+
} else if (ctx.parenthesizedObject) {
|
|
169
|
+
expr = this.visit(ctx.parenthesizedObject)
|
|
101
170
|
} else if (ctx.productObject) {
|
|
102
171
|
expr = this.visit(ctx.productObject)
|
|
172
|
+
} else if (ctx.enumObject) {
|
|
173
|
+
expr = this.visit(ctx.enumObject)
|
|
174
|
+
} else if (ctx.patternObject) {
|
|
175
|
+
expr = this.visit(ctx.patternObject)
|
|
176
|
+
} else if (ctx.lambdaObject) {
|
|
177
|
+
expr = this.visit(ctx.lambdaObject)
|
|
178
|
+
} else if (ctx.literalObject) {
|
|
179
|
+
expr = this.visit(ctx.literalObject)
|
|
103
180
|
} else {
|
|
104
181
|
expr = this.visit(ctx.namedObject)
|
|
105
182
|
}
|
|
@@ -111,6 +188,20 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
111
188
|
return expr
|
|
112
189
|
}
|
|
113
190
|
|
|
191
|
+
parenthesizedObject(ctx: any): ObjectExpression {
|
|
192
|
+
return this.visit(ctx.setObject)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
patternObject(ctx: any): PatternLiteralObject {
|
|
196
|
+
const image: string = ctx.PatternLiteral[0].image
|
|
197
|
+
const lastSlash = image.lastIndexOf('/')
|
|
198
|
+
return {
|
|
199
|
+
kind: 'PatternLiteralObject',
|
|
200
|
+
source: image.slice(1, lastSlash),
|
|
201
|
+
flags: image.slice(lastSlash + 1),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
114
205
|
namedObject(ctx: any): NamedObject {
|
|
115
206
|
let parts: string[]
|
|
116
207
|
if (ctx.StringTok) {
|
|
@@ -121,6 +212,10 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
121
212
|
parts = [ctx.BoolTok[0].image, ...(ctx.Identifier ?? []).map((id: any) => id.image)]
|
|
122
213
|
} else if (ctx.UnitTok) {
|
|
123
214
|
parts = [ctx.UnitTok[0].image, ...(ctx.Identifier ?? []).map((id: any) => id.image)]
|
|
215
|
+
} else if (ctx.ConceptTok) {
|
|
216
|
+
parts = [ctx.ConceptTok[0].image, ...(ctx.Identifier ?? []).map((id: any) => id.image)]
|
|
217
|
+
} else if (ctx.EnvironmentTok) {
|
|
218
|
+
parts = [ctx.EnvironmentTok[0].image, ...(ctx.Identifier ?? []).map((id: any) => id.image)]
|
|
124
219
|
} else {
|
|
125
220
|
parts = ctx.Identifier.map((id: any) => id.image)
|
|
126
221
|
}
|
|
@@ -130,18 +225,26 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
130
225
|
}
|
|
131
226
|
}
|
|
132
227
|
|
|
133
|
-
productObject(ctx: any):
|
|
228
|
+
productObject(ctx: any): ObjectExpression {
|
|
229
|
+
const names = ctx.Identifier ?? []
|
|
134
230
|
const fields: Record<string, ObjectExpression> = {}
|
|
135
|
-
for (let i = 0; i <
|
|
136
|
-
const name =
|
|
137
|
-
|
|
231
|
+
for (let i = 0; i < names.length; i++) {
|
|
232
|
+
const name = names[i].image
|
|
233
|
+
const value = this.visit(ctx.setObject[i])
|
|
234
|
+
if (value.kind === 'NamedObject' && value.name === 'unit') {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
fields[name] = value
|
|
238
|
+
}
|
|
239
|
+
if (Object.keys(fields).length === 0) {
|
|
240
|
+
return { kind: 'NamedObject', name: 'unit' }
|
|
138
241
|
}
|
|
139
242
|
return { kind: 'ProductObject', fields }
|
|
140
243
|
}
|
|
141
244
|
|
|
142
245
|
subObject(ctx: any): SubObject {
|
|
143
246
|
const rawText: string = ctx.SelectBlock[0].image
|
|
144
|
-
const rawConstraint = rawText.slice(1, -1).trim()
|
|
247
|
+
const rawConstraint = unescapeConstraint(rawText.slice(1, -1).trim())
|
|
145
248
|
return {
|
|
146
249
|
kind: 'SubObject',
|
|
147
250
|
base: this.visit(ctx.base),
|
|
@@ -158,7 +261,7 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
158
261
|
|
|
159
262
|
namedImport(ctx: any): ImportDeclaration {
|
|
160
263
|
const name = ctx.Identifier[0].image
|
|
161
|
-
const source = ctx.
|
|
264
|
+
const source = stringLiteralValue(ctx.StringLiteral[0].image)
|
|
162
265
|
const alias = ctx.Identifier[1] ? ctx.Identifier[1].image : null
|
|
163
266
|
return {
|
|
164
267
|
kind: 'ImportDeclaration',
|
|
@@ -169,7 +272,7 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
169
272
|
}
|
|
170
273
|
|
|
171
274
|
moduleImport(ctx: any): ImportDeclaration {
|
|
172
|
-
const source = ctx.
|
|
275
|
+
const source = stringLiteralValue(ctx.StringLiteral[0].image)
|
|
173
276
|
const alias = ctx.Identifier[0].image
|
|
174
277
|
return {
|
|
175
278
|
kind: 'ImportDeclaration',
|
|
@@ -192,7 +295,7 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
192
295
|
kind: 'PackageDeclaration',
|
|
193
296
|
packageType,
|
|
194
297
|
name: ctx.Identifier[0].image,
|
|
195
|
-
objectName: this.visit(ctx.
|
|
298
|
+
objectName: this.visit(ctx.setObject),
|
|
196
299
|
}
|
|
197
300
|
}
|
|
198
301
|
|
|
@@ -202,10 +305,91 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
202
305
|
name: ctx.Identifier[0].image,
|
|
203
306
|
}
|
|
204
307
|
}
|
|
308
|
+
|
|
309
|
+
realizeDeclaration(ctx: any): RealizeDeclaration {
|
|
310
|
+
return {
|
|
311
|
+
kind: 'RealizeDeclaration',
|
|
312
|
+
object: this.visit(ctx.object),
|
|
313
|
+
target: this.visit(ctx.target),
|
|
314
|
+
environment: ctx.environment
|
|
315
|
+
? this.visit(ctx.environment)
|
|
316
|
+
: { kind: 'NamedObject', name: 'environment' },
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
includeDeclaration(ctx: any): IncludeDeclaration {
|
|
321
|
+
const address = stringLiteralValue(ctx.StringLiteral[0].image)
|
|
322
|
+
const name = ctx.Identifier[0].image
|
|
323
|
+
return {
|
|
324
|
+
kind: 'IncludeDeclaration',
|
|
325
|
+
name,
|
|
326
|
+
address,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
lambdaObject(ctx: any): ExponentialPattern {
|
|
331
|
+
const image: string = ctx.CodeBlock[0].image
|
|
332
|
+
const firstNewline = image.indexOf('\n')
|
|
333
|
+
const langEnd = image.indexOf('\n')
|
|
334
|
+
const language = image.slice(3, langEnd)
|
|
335
|
+
const body = image.slice(firstNewline + 1, -3).trimEnd()
|
|
336
|
+
|
|
337
|
+
const patterns: PatternBlock[] = []
|
|
338
|
+
let i = 0
|
|
339
|
+
while (i < body.length) {
|
|
340
|
+
if (body[i] === '@' && body[i + 1] === '{') {
|
|
341
|
+
const start = i
|
|
342
|
+
let depth = 1
|
|
343
|
+
i += 2
|
|
344
|
+
while (i < body.length && depth > 0) {
|
|
345
|
+
if (body[i] === '\\') {
|
|
346
|
+
i += 2
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if (body[i] === '{') depth++
|
|
350
|
+
if (body[i] === '}') depth--
|
|
351
|
+
i++
|
|
352
|
+
}
|
|
353
|
+
const end = i
|
|
354
|
+
const raw = body.slice(start + 2, end - 1).trim()
|
|
355
|
+
patterns.push({
|
|
356
|
+
raw,
|
|
357
|
+
parts: parseConstraint(raw).parts,
|
|
358
|
+
start,
|
|
359
|
+
end,
|
|
360
|
+
})
|
|
361
|
+
} else {
|
|
362
|
+
i++
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
kind: 'ExponentialPattern',
|
|
368
|
+
base: this.visit(ctx.exponent),
|
|
369
|
+
exponent: this.visit(ctx.base),
|
|
370
|
+
language,
|
|
371
|
+
body,
|
|
372
|
+
patterns,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
205
375
|
}
|
|
206
376
|
|
|
207
377
|
export function parseToAst(text: string): SpexFile {
|
|
208
378
|
const lexingResult = SpexLexer.tokenize(text)
|
|
379
|
+
|
|
380
|
+
if (lexingResult.errors.length > 0) {
|
|
381
|
+
const details = lexingResult.errors
|
|
382
|
+
.map((e) => {
|
|
383
|
+
const where =
|
|
384
|
+
e.line !== undefined && e.column !== undefined
|
|
385
|
+
? ` (line ${e.line}, column ${e.column})`
|
|
386
|
+
: ''
|
|
387
|
+
return e.message + where
|
|
388
|
+
})
|
|
389
|
+
.join('; ')
|
|
390
|
+
throw new Error(`Lexing errors: ${details}`)
|
|
391
|
+
}
|
|
392
|
+
|
|
209
393
|
parserInstance.input = lexingResult.tokens
|
|
210
394
|
const cst = parserInstance.spexFile()
|
|
211
395
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parseToAst } from '../src/visitor.js'
|
|
3
|
+
import { constantOf } from '../src/constants.js'
|
|
4
|
+
import type { ObjectDeclaration } from '../src/ast.js'
|
|
5
|
+
|
|
6
|
+
function firstObject(text: string) {
|
|
7
|
+
const ast = parseToAst(text)
|
|
8
|
+
return (ast.declarations[0] as ObjectDeclaration).object
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('constantOf', () => {
|
|
12
|
+
it('should identify string literals as constants', () => {
|
|
13
|
+
expect(constantOf(firstObject('create K as "SpexFile";'))).toEqual({
|
|
14
|
+
kind: 'StringConstant',
|
|
15
|
+
value: 'SpexFile',
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should identify number literals as constants', () => {
|
|
20
|
+
expect(constantOf(firstObject('create K as 42;'))).toEqual({
|
|
21
|
+
kind: 'NumberConstant',
|
|
22
|
+
value: '42',
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should identify bool literals as constants', () => {
|
|
27
|
+
expect(constantOf(firstObject('create K as true;'))).toEqual({
|
|
28
|
+
kind: 'BoolConstant',
|
|
29
|
+
value: true,
|
|
30
|
+
})
|
|
31
|
+
expect(constantOf(firstObject('create K as false;'))).toEqual({
|
|
32
|
+
kind: 'BoolConstant',
|
|
33
|
+
value: false,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should identify products of constants as constants', () => {
|
|
38
|
+
expect(constantOf(firstObject('create K as (name: "John", age: 42);'))).toEqual({
|
|
39
|
+
kind: 'ProductConstant',
|
|
40
|
+
fields: {
|
|
41
|
+
name: { kind: 'StringConstant', value: 'John' },
|
|
42
|
+
age: { kind: 'NumberConstant', value: '42' },
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should reject products with non-constant fields', () => {
|
|
48
|
+
expect(constantOf(firstObject('create K as (name: "John", age: number);'))).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should identify exponentials of constants as constants', () => {
|
|
52
|
+
expect(constantOf(firstObject('create K as "a" -> "b";'))).toEqual({
|
|
53
|
+
kind: 'ExponentialConstant',
|
|
54
|
+
base: { kind: 'StringConstant', value: 'b' },
|
|
55
|
+
exponent: { kind: 'StringConstant', value: 'a' },
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should reject exponentials with non-constant sides', () => {
|
|
60
|
+
expect(constantOf(firstObject('create K as number -> "b";'))).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should reject nested non-constants', () => {
|
|
64
|
+
expect(
|
|
65
|
+
constantOf(firstObject('create K as (a: (b: "x", c: string));'))
|
|
66
|
+
).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should reject named types', () => {
|
|
70
|
+
expect(constantOf(firstObject('create K as string;'))).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should reject subobjects', () => {
|
|
74
|
+
expect(constantOf(firstObject('create K as from string select { are positive };'))).toBeNull()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should reject enums', () => {
|
|
78
|
+
expect(constantOf(firstObject("create K as enum ('a', 'b');"))).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should reject arrays', () => {
|
|
82
|
+
expect(constantOf(firstObject('create K as string[];'))).toBeNull()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should reject set objects', () => {
|
|
86
|
+
expect(constantOf(firstObject('create K as "a" UNION "b";'))).toBeNull()
|
|
87
|
+
expect(constantOf(firstObject('create K as string EXCEPT "root";'))).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should treat the empty product the same as unit', () => {
|
|
91
|
+
expect(constantOf(firstObject('create K as ();'))).toBeNull()
|
|
92
|
+
expect(constantOf(firstObject('create K as unit;'))).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should reject patterns', () => {
|
|
96
|
+
expect(constantOf(firstObject('create K as /\\d+/;'))).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should reject coproducts', () => {
|
|
100
|
+
expect(constantOf(firstObject('create K as "a" | "b";'))).toBeNull()
|
|
101
|
+
})
|
|
102
|
+
})
|
package/tests/e2e.test.ts
CHANGED
|
@@ -7,11 +7,23 @@ import { parseToAst } from '../src/visitor.js'
|
|
|
7
7
|
const __filename = fileURLToPath(import.meta.url)
|
|
8
8
|
const __dirname = dirname(__filename)
|
|
9
9
|
|
|
10
|
+
const fixtures: [string, number][] = [
|
|
11
|
+
['todo.spex', 32],
|
|
12
|
+
['python_cli_env.spex', 4],
|
|
13
|
+
['typescript_cli_env.spex', 4],
|
|
14
|
+
['flask_web_env.spex', 4],
|
|
15
|
+
['express_web_env.spex', 4],
|
|
16
|
+
['python_todo_cli.spex', 16],
|
|
17
|
+
['typescript_todo_cli.spex', 16],
|
|
18
|
+
['flask_todo_web.spex', 22],
|
|
19
|
+
['express_todo_web.spex', 22],
|
|
20
|
+
]
|
|
21
|
+
|
|
10
22
|
describe('end-to-end', () => {
|
|
11
|
-
it('should parse
|
|
12
|
-
const code = readFileSync(join(__dirname, 'props
|
|
23
|
+
it.each(fixtures)('should parse %s', (file, expectedDeclarations) => {
|
|
24
|
+
const code = readFileSync(join(__dirname, 'props', file), 'utf-8')
|
|
13
25
|
const ast = parseToAst(code)
|
|
14
26
|
expect(ast.kind).toBe('SpexFile')
|
|
15
|
-
expect(ast.declarations.length).toBe(
|
|
27
|
+
expect(ast.declarations.length).toBe(expectedDeclarations)
|
|
16
28
|
})
|
|
17
29
|
})
|