harmonyc 0.18.0 → 0.18.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/cli/cli.d.ts +2 -0
- package/cli/cli.js +32 -0
- package/cli/run.d.ts +2 -0
- package/cli/run.js +21 -0
- package/cli/watch.d.ts +1 -0
- package/cli/watch.js +35 -0
- package/code_generator/VitestGenerator.d.ts +32 -0
- package/code_generator/VitestGenerator.js +221 -0
- package/code_generator/outFile.d.ts +20 -0
- package/code_generator/outFile.js +60 -0
- package/code_generator/test_phrases.d.ts +11 -0
- package/code_generator/test_phrases.js +27 -0
- package/compiler/compile.d.ts +9 -0
- package/compiler/compile.js +32 -0
- package/compiler/compiler.d.ts +10 -0
- package/compiler/compiler.js +53 -0
- package/filenames/filenames.d.ts +3 -0
- package/filenames/filenames.js +17 -0
- package/model/Router.d.ts +22 -0
- package/model/Router.js +54 -0
- package/model/model.d.ts +217 -0
- package/model/model.js +526 -0
- package/optimizations/autoLabel/autoLabel.d.ts +2 -0
- package/optimizations/autoLabel/autoLabel.js +19 -0
- package/package.json +1 -4
- package/parser/lexer.d.ts +21 -0
- package/parser/lexer.js +123 -0
- package/parser/lexer_rules.d.ts +33 -0
- package/parser/lexer_rules.js +74 -0
- package/parser/parser.d.ts +18 -0
- package/parser/parser.js +76 -0
- package/util/indent.d.ts +1 -0
- package/util/indent.js +5 -0
- package/util/iterators.d.ts +1 -0
- package/util/iterators.js +5 -0
- package/util/xmur3.d.ts +1 -0
- package/util/xmur3.js +12 -0
- package/vitest/index.d.ts +9 -0
- package/vitest/index.js +84 -0
package/model/model.js
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { Routers } from "./Router.js";
|
|
2
|
+
export class Feature {
|
|
3
|
+
constructor(name) {
|
|
4
|
+
this.name = name;
|
|
5
|
+
this.root = new Section();
|
|
6
|
+
this.prelude = '';
|
|
7
|
+
}
|
|
8
|
+
get tests() {
|
|
9
|
+
return makeTests(this.root);
|
|
10
|
+
}
|
|
11
|
+
get testGroups() {
|
|
12
|
+
return makeGroups(this.tests);
|
|
13
|
+
}
|
|
14
|
+
toCode(cg) {
|
|
15
|
+
cg.feature(this);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class Node {
|
|
19
|
+
at([startToken, endToken]) {
|
|
20
|
+
while (startToken && startToken.kind === 'newline') {
|
|
21
|
+
startToken = startToken.next;
|
|
22
|
+
}
|
|
23
|
+
if (startToken) {
|
|
24
|
+
this.start = {
|
|
25
|
+
line: startToken.pos.rowBegin,
|
|
26
|
+
column: startToken.pos.columnBegin - 1,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (startToken && endToken) {
|
|
30
|
+
let t = startToken;
|
|
31
|
+
while (t.next && t.next !== endToken) {
|
|
32
|
+
t = t.next;
|
|
33
|
+
}
|
|
34
|
+
this.end = {
|
|
35
|
+
line: t.pos.rowEnd,
|
|
36
|
+
column: t.pos.columnEnd - 1,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
atSameAs(other) {
|
|
42
|
+
this.start = other.start;
|
|
43
|
+
this.end = other.end;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class Branch extends Node {
|
|
48
|
+
constructor(children = []) {
|
|
49
|
+
super();
|
|
50
|
+
this.isFork = false;
|
|
51
|
+
this.isEnd = false;
|
|
52
|
+
this.children = children;
|
|
53
|
+
children.forEach((child) => (child.parent = this));
|
|
54
|
+
}
|
|
55
|
+
setFork(isFork) {
|
|
56
|
+
this.isFork = isFork;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
setFeature(feature) {
|
|
60
|
+
for (const child of this.children)
|
|
61
|
+
child.setFeature(feature);
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
addChild(child, index = this.children.length) {
|
|
65
|
+
this.children.splice(index, 0, child);
|
|
66
|
+
child.parent = this;
|
|
67
|
+
return child;
|
|
68
|
+
}
|
|
69
|
+
get isLeaf() {
|
|
70
|
+
return this.children.length === 0;
|
|
71
|
+
}
|
|
72
|
+
get successors() {
|
|
73
|
+
if (!this.isLeaf)
|
|
74
|
+
return this.children.filter((c, i) => i === 0 || c.isFork);
|
|
75
|
+
else {
|
|
76
|
+
if (this.isEnd)
|
|
77
|
+
return [];
|
|
78
|
+
const next = this.nextNonForkAncestorSibling;
|
|
79
|
+
if (next)
|
|
80
|
+
return [next];
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get nextNonForkAncestorSibling() {
|
|
85
|
+
if (!this.parent)
|
|
86
|
+
return undefined;
|
|
87
|
+
const { nextSibling } = this;
|
|
88
|
+
if (nextSibling && !nextSibling.isFork)
|
|
89
|
+
return nextSibling;
|
|
90
|
+
return this.parent.nextNonForkAncestorSibling;
|
|
91
|
+
}
|
|
92
|
+
get nextSibling() {
|
|
93
|
+
if (!this.parent)
|
|
94
|
+
return undefined;
|
|
95
|
+
return this.parent.children[this.siblingIndex + 1];
|
|
96
|
+
}
|
|
97
|
+
get siblingIndex() {
|
|
98
|
+
var _a, _b;
|
|
99
|
+
return (_b = (_a = this.parent) === null || _a === void 0 ? void 0 : _a.children.indexOf(this)) !== null && _b !== void 0 ? _b : -1;
|
|
100
|
+
}
|
|
101
|
+
toString() {
|
|
102
|
+
return this.children
|
|
103
|
+
.map((c) => (c.isFork ? '+ ' : '- ') + c.toString())
|
|
104
|
+
.join('\n');
|
|
105
|
+
}
|
|
106
|
+
replaceWith(newBranch) {
|
|
107
|
+
if (!this.parent)
|
|
108
|
+
throw new Error('cannot replace root');
|
|
109
|
+
this.parent.children.splice(this.siblingIndex, 1, newBranch);
|
|
110
|
+
newBranch.parent = this.parent;
|
|
111
|
+
this.parent = undefined;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
switch(_i) {
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export class Step extends Branch {
|
|
119
|
+
constructor(action, responses = [], children, isFork = false) {
|
|
120
|
+
super(children);
|
|
121
|
+
this.action = action;
|
|
122
|
+
this.responses = responses;
|
|
123
|
+
this.isFork = isFork;
|
|
124
|
+
}
|
|
125
|
+
get phrases() {
|
|
126
|
+
return [this.action, ...this.responses];
|
|
127
|
+
}
|
|
128
|
+
toCode(cg) {
|
|
129
|
+
if (this.responses[0] instanceof ErrorResponse) {
|
|
130
|
+
cg.errorStep(this.action, this.responses[0]);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
cg.step(this.action, this.responses);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
setFeature(feature) {
|
|
137
|
+
this.action.setFeature(feature);
|
|
138
|
+
for (const response of this.responses)
|
|
139
|
+
response.setFeature(feature);
|
|
140
|
+
return super.setFeature(feature);
|
|
141
|
+
}
|
|
142
|
+
headToString() {
|
|
143
|
+
return this.phrases.join(' ');
|
|
144
|
+
}
|
|
145
|
+
toString() {
|
|
146
|
+
return this.headToString() + indent(super.toString());
|
|
147
|
+
}
|
|
148
|
+
get isEmpty() {
|
|
149
|
+
return this.phrases.every((phrase) => phrase.isEmpty);
|
|
150
|
+
}
|
|
151
|
+
switch(i) {
|
|
152
|
+
return new Step(this.action.switch(i), this.responses.map((r) => r.switch(i)));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export class State {
|
|
156
|
+
constructor(text = '') {
|
|
157
|
+
this.text = text;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export class Label extends Node {
|
|
161
|
+
constructor(text = '') {
|
|
162
|
+
super();
|
|
163
|
+
this.text = text;
|
|
164
|
+
}
|
|
165
|
+
get isEmpty() {
|
|
166
|
+
return this.text === '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export class Section extends Branch {
|
|
170
|
+
constructor(label, children, isFork = false) {
|
|
171
|
+
super(children);
|
|
172
|
+
this.label = label !== null && label !== void 0 ? label : new Label();
|
|
173
|
+
this.isFork = isFork;
|
|
174
|
+
}
|
|
175
|
+
toString() {
|
|
176
|
+
if (this.label.text === '')
|
|
177
|
+
return super.toString();
|
|
178
|
+
return this.label.text + ':' + indent(super.toString());
|
|
179
|
+
}
|
|
180
|
+
get isEmpty() {
|
|
181
|
+
return this.label.isEmpty;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export class Part {
|
|
185
|
+
toSingleLineString() {
|
|
186
|
+
return this.toString();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export class DummyKeyword extends Part {
|
|
190
|
+
constructor(text = '') {
|
|
191
|
+
super();
|
|
192
|
+
this.text = text;
|
|
193
|
+
}
|
|
194
|
+
toString() {
|
|
195
|
+
return this.text;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export class Word extends Part {
|
|
199
|
+
constructor(text = '') {
|
|
200
|
+
super();
|
|
201
|
+
this.text = text;
|
|
202
|
+
}
|
|
203
|
+
toString() {
|
|
204
|
+
return this.text;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export class Repeater extends Part {
|
|
208
|
+
constructor(choices) {
|
|
209
|
+
super();
|
|
210
|
+
this.choices = choices;
|
|
211
|
+
}
|
|
212
|
+
toString() {
|
|
213
|
+
return `{${this.choices.map((ps) => ps.join(' ')).join(' & ')}}`;
|
|
214
|
+
}
|
|
215
|
+
toSingleLineString() {
|
|
216
|
+
return `{${this.choices
|
|
217
|
+
.map((ps) => ps.map((p) => p.toSingleLineString()).join(' '))
|
|
218
|
+
.join(' & ')}}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export class Switch extends Part {
|
|
222
|
+
constructor(choices) {
|
|
223
|
+
super();
|
|
224
|
+
this.choices = choices;
|
|
225
|
+
}
|
|
226
|
+
toString() {
|
|
227
|
+
return `{ ${this.choices.join(' / ')} }`;
|
|
228
|
+
}
|
|
229
|
+
toSingleLineString() {
|
|
230
|
+
return `{ ${this.choices.map((c) => c.toSingleLineString()).join(' / ')} }`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export class Router extends Part {
|
|
234
|
+
constructor(choices) {
|
|
235
|
+
super();
|
|
236
|
+
this.choices = choices;
|
|
237
|
+
}
|
|
238
|
+
toString() {
|
|
239
|
+
return `{ ${this.choices.join(' ; ')} }`;
|
|
240
|
+
}
|
|
241
|
+
toSingleLineString() {
|
|
242
|
+
return `{ ${this.choices.map((c) => c.toSingleLineString()).join(' ; ')} }`;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export class Arg extends Part {
|
|
246
|
+
}
|
|
247
|
+
export class StringLiteral extends Arg {
|
|
248
|
+
constructor(text = '') {
|
|
249
|
+
super();
|
|
250
|
+
this.text = text;
|
|
251
|
+
}
|
|
252
|
+
toString() {
|
|
253
|
+
return JSON.stringify(this.text);
|
|
254
|
+
}
|
|
255
|
+
toSingleLineString() {
|
|
256
|
+
return this.toString();
|
|
257
|
+
}
|
|
258
|
+
toCode(cg) {
|
|
259
|
+
return cg.stringLiteral(this.text, { withVariables: true });
|
|
260
|
+
}
|
|
261
|
+
toDeclaration(cg, index) {
|
|
262
|
+
return cg.stringParamDeclaration(index);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export class Docstring extends StringLiteral {
|
|
266
|
+
toCode(cg) {
|
|
267
|
+
return cg.stringLiteral(this.text, { withVariables: false });
|
|
268
|
+
}
|
|
269
|
+
toString() {
|
|
270
|
+
return this.text
|
|
271
|
+
.split('\n')
|
|
272
|
+
.map((l) => '| ' + l)
|
|
273
|
+
.join('\n');
|
|
274
|
+
}
|
|
275
|
+
toSingleLineString() {
|
|
276
|
+
return super.toString();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
export class CodeLiteral extends Arg {
|
|
280
|
+
constructor(src = '') {
|
|
281
|
+
super();
|
|
282
|
+
this.src = src;
|
|
283
|
+
}
|
|
284
|
+
toString() {
|
|
285
|
+
return '`' + this.src + '`';
|
|
286
|
+
}
|
|
287
|
+
toCode(cg) {
|
|
288
|
+
return cg.codeLiteral(this.src);
|
|
289
|
+
}
|
|
290
|
+
toDeclaration(cg, index) {
|
|
291
|
+
return cg.variantParamDeclaration(index);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export class Phrase extends Node {
|
|
295
|
+
constructor(parts) {
|
|
296
|
+
super();
|
|
297
|
+
this.parts = parts;
|
|
298
|
+
}
|
|
299
|
+
setFeature(feature) {
|
|
300
|
+
this.feature = feature;
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
get keyword() {
|
|
304
|
+
return this.kind === 'action' ? 'When' : 'Then';
|
|
305
|
+
}
|
|
306
|
+
get args() {
|
|
307
|
+
return this.parts.filter((c) => c instanceof Arg);
|
|
308
|
+
}
|
|
309
|
+
get isEmpty() {
|
|
310
|
+
return this.parts.length === 0;
|
|
311
|
+
}
|
|
312
|
+
toString() {
|
|
313
|
+
const parts = this.parts.map((p) => p.toString());
|
|
314
|
+
const isMultiline = parts.map((p) => p.includes('\n'));
|
|
315
|
+
return parts
|
|
316
|
+
.map((p, i) => i === 0 ? p : isMultiline[i - 1] || isMultiline[i] ? '\n' + p : ' ' + p)
|
|
317
|
+
.join('');
|
|
318
|
+
}
|
|
319
|
+
toSingleLineString() {
|
|
320
|
+
return this.parts.map((p) => p.toSingleLineString()).join(' ');
|
|
321
|
+
}
|
|
322
|
+
switch(i) {
|
|
323
|
+
return new this.constructor(this.parts.map((p) => (p instanceof Switch ? p.choices[i] : p))).setFeature(this.feature);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export class Action extends Phrase {
|
|
327
|
+
constructor() {
|
|
328
|
+
super(...arguments);
|
|
329
|
+
this.kind = 'action';
|
|
330
|
+
}
|
|
331
|
+
toCode(cg) {
|
|
332
|
+
if (this.isEmpty)
|
|
333
|
+
return;
|
|
334
|
+
cg.phrase(this);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
export class Response extends Phrase {
|
|
338
|
+
constructor(parts, saveToVariable) {
|
|
339
|
+
super([...parts, ...(saveToVariable ? [saveToVariable] : [])]);
|
|
340
|
+
this.saveToVariable = saveToVariable;
|
|
341
|
+
this.kind = 'response';
|
|
342
|
+
}
|
|
343
|
+
get isEmpty() {
|
|
344
|
+
return this.parts.length === 0 && !this.saveToVariable;
|
|
345
|
+
}
|
|
346
|
+
toString() {
|
|
347
|
+
return `=> ${super.toString()}`;
|
|
348
|
+
}
|
|
349
|
+
toSingleLineString() {
|
|
350
|
+
return `=> ${super.toSingleLineString()}`;
|
|
351
|
+
}
|
|
352
|
+
toCode(cg) {
|
|
353
|
+
if (this.isEmpty)
|
|
354
|
+
return;
|
|
355
|
+
cg.phrase(this);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
export class ErrorResponse extends Response {
|
|
359
|
+
constructor(message) {
|
|
360
|
+
super(message ? [new DummyKeyword('!!'), message] : [new DummyKeyword('!!')]);
|
|
361
|
+
this.message = message;
|
|
362
|
+
}
|
|
363
|
+
toCode(cg) {
|
|
364
|
+
cg.errorStep;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
export class SetVariable extends Action {
|
|
368
|
+
constructor(variableName, value) {
|
|
369
|
+
super([new DummyKeyword(`\${${variableName}}`), value]);
|
|
370
|
+
this.variableName = variableName;
|
|
371
|
+
this.value = value;
|
|
372
|
+
}
|
|
373
|
+
toCode(cg) {
|
|
374
|
+
cg.setVariable(this);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
export class SaveToVariable extends Part {
|
|
378
|
+
constructor(variableName) {
|
|
379
|
+
super();
|
|
380
|
+
this.variableName = variableName;
|
|
381
|
+
}
|
|
382
|
+
toCode(cg) {
|
|
383
|
+
cg.saveToVariable(this);
|
|
384
|
+
}
|
|
385
|
+
toString() {
|
|
386
|
+
return `\${${this.variableName}}`;
|
|
387
|
+
}
|
|
388
|
+
get words() {
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
export class Precondition extends Branch {
|
|
393
|
+
constructor(state = '') {
|
|
394
|
+
super();
|
|
395
|
+
this.state = new State();
|
|
396
|
+
this.state.text = state;
|
|
397
|
+
}
|
|
398
|
+
get isEmpty() {
|
|
399
|
+
return this.state.text === '';
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
export function makeTests(root) {
|
|
403
|
+
const routers = new Routers(root);
|
|
404
|
+
let tests = [];
|
|
405
|
+
let ic = routers.getIncompleteCount();
|
|
406
|
+
let newIc;
|
|
407
|
+
do {
|
|
408
|
+
const newTest = new Test(routers.nextWalk());
|
|
409
|
+
newIc = routers.getIncompleteCount();
|
|
410
|
+
if (newIc < ic)
|
|
411
|
+
tests.push(newTest);
|
|
412
|
+
ic = newIc;
|
|
413
|
+
} while (ic > 0);
|
|
414
|
+
// sort by order of appearance of the last branch
|
|
415
|
+
const branchIndex = new Map();
|
|
416
|
+
let i = 0;
|
|
417
|
+
function walk(branch) {
|
|
418
|
+
branchIndex.set(branch, i++);
|
|
419
|
+
for (const child of branch.children)
|
|
420
|
+
walk(child);
|
|
421
|
+
}
|
|
422
|
+
walk(root);
|
|
423
|
+
tests = tests.filter((t) => t.steps.length > 0);
|
|
424
|
+
tests.sort((a, b) => branchIndex.get(a.last) - branchIndex.get(b.last));
|
|
425
|
+
resolveSwitches(tests);
|
|
426
|
+
tests.forEach((test, i) => (test.testNumber = `T${i + 1}`));
|
|
427
|
+
return tests;
|
|
428
|
+
}
|
|
429
|
+
function resolveSwitches(tests) {
|
|
430
|
+
for (let i = 0; i < tests.length; ++i) {
|
|
431
|
+
const test = tests[i];
|
|
432
|
+
const phrases = test.steps.flatMap((s) => s.phrases);
|
|
433
|
+
const switches = phrases.flatMap((p) => p.parts.filter((p) => p instanceof Switch));
|
|
434
|
+
if (switches.length === 0)
|
|
435
|
+
continue;
|
|
436
|
+
const count = switches[0].choices.length;
|
|
437
|
+
if (switches.some((s) => s.choices.length !== count)) {
|
|
438
|
+
throw new Error(`all switches in a test case must have the same number of choices: ${test.name} has ${switches.map((s) => s.choices.length)} choices`);
|
|
439
|
+
}
|
|
440
|
+
const newTests = switches[0].choices.map((_, j) => test.switch(j));
|
|
441
|
+
tests.splice(i, 1, ...newTests);
|
|
442
|
+
i += count - 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
export class Test {
|
|
446
|
+
constructor(branches) {
|
|
447
|
+
this.branches = branches;
|
|
448
|
+
this.branches = this.branches.filter((b) => !b.isEmpty);
|
|
449
|
+
this.labels = this.branches
|
|
450
|
+
.filter((b) => b instanceof Section)
|
|
451
|
+
.filter((s) => !s.isEmpty)
|
|
452
|
+
.map((s) => s.label);
|
|
453
|
+
}
|
|
454
|
+
get steps() {
|
|
455
|
+
return this.branches.filter((b) => b instanceof Step);
|
|
456
|
+
}
|
|
457
|
+
get last() {
|
|
458
|
+
return this.steps.at(-1);
|
|
459
|
+
}
|
|
460
|
+
get lastStrain() {
|
|
461
|
+
// Find the last branch that has no forks after it
|
|
462
|
+
const lastForking = this.branches.length -
|
|
463
|
+
1 -
|
|
464
|
+
this.branches
|
|
465
|
+
.slice()
|
|
466
|
+
.reverse()
|
|
467
|
+
.findIndex((b) => b.successors.length > 1);
|
|
468
|
+
if (lastForking === this.branches.length)
|
|
469
|
+
return this.branches.at(0);
|
|
470
|
+
return this.branches.at(lastForking + 1);
|
|
471
|
+
}
|
|
472
|
+
get name() {
|
|
473
|
+
return `${[this.testNumber, ...this.labels.map((x) => x.text)].join(' - ')}`;
|
|
474
|
+
}
|
|
475
|
+
toCode(cg) {
|
|
476
|
+
cg.test(this);
|
|
477
|
+
}
|
|
478
|
+
toString() {
|
|
479
|
+
return `+ ${this.name}:\n${this.steps
|
|
480
|
+
.map((s) => ` - ${s.headToString()}`)
|
|
481
|
+
.join('\n')}`;
|
|
482
|
+
}
|
|
483
|
+
switch(j) {
|
|
484
|
+
return new Test(this.branches.map((b) => b.switch(j)));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
export function makeGroups(tests) {
|
|
488
|
+
if (tests.length === 0)
|
|
489
|
+
return [];
|
|
490
|
+
if (tests[0].labels.length === 0)
|
|
491
|
+
return [tests[0], ...makeGroups(tests.slice(1))];
|
|
492
|
+
const label = tests[0].labels[0];
|
|
493
|
+
let count = tests.findIndex((t) =>
|
|
494
|
+
// using identity instead of text equality, which means identically named labels will not be grouped together
|
|
495
|
+
t.labels[0] !== label);
|
|
496
|
+
if (count === -1)
|
|
497
|
+
count = tests.length;
|
|
498
|
+
if (count === 1)
|
|
499
|
+
return [tests[0], ...makeGroups(tests.slice(1))];
|
|
500
|
+
tests.slice(0, count).forEach((test) => test.labels.shift());
|
|
501
|
+
return [
|
|
502
|
+
new TestGroup(label, makeGroups(tests.slice(0, count))),
|
|
503
|
+
...makeGroups(tests.slice(count)),
|
|
504
|
+
];
|
|
505
|
+
}
|
|
506
|
+
export class TestGroup {
|
|
507
|
+
constructor(label, items) {
|
|
508
|
+
this.label = label;
|
|
509
|
+
this.items = items;
|
|
510
|
+
}
|
|
511
|
+
toString() {
|
|
512
|
+
return `+ ${this.label.text}:` + indent(this.items.join('\n'));
|
|
513
|
+
}
|
|
514
|
+
toCode(cg) {
|
|
515
|
+
cg.testGroup(this);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function indent(s) {
|
|
519
|
+
if (!s)
|
|
520
|
+
return '';
|
|
521
|
+
return ('\n' +
|
|
522
|
+
s
|
|
523
|
+
.split('\n')
|
|
524
|
+
.map((l) => ' ' + l)
|
|
525
|
+
.join('\n'));
|
|
526
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Label, Section, Step } from "../../model/model.js";
|
|
2
|
+
export function autoLabel(b) {
|
|
3
|
+
const forks = b.children.filter((c, i) => c.isFork || i === 0);
|
|
4
|
+
if (forks.length > 1) {
|
|
5
|
+
forks
|
|
6
|
+
.filter((child) => child instanceof Step)
|
|
7
|
+
.forEach((step) => {
|
|
8
|
+
const label = step.action.toSingleLineString();
|
|
9
|
+
const autoLabel = new Label(label);
|
|
10
|
+
autoLabel.atSameAs(step.action);
|
|
11
|
+
const autoSection = new Section(autoLabel, [], step.isFork);
|
|
12
|
+
// todo this is some redundancy with both section and label storing the position
|
|
13
|
+
autoSection.atSameAs(step);
|
|
14
|
+
step.replaceWith(autoSection);
|
|
15
|
+
autoSection.addChild(step);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
b.children.forEach((c) => autoLabel(c));
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harmonyc",
|
|
3
3
|
"description": "Harmony Code - model-driven BDD for Vitest",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.1",
|
|
5
5
|
"author": "Bernát Kalló",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"harmonyc": "./cli.js"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
11
|
-
"."
|
|
12
|
-
],
|
|
13
10
|
"exports": {
|
|
14
11
|
"./vitest": {
|
|
15
12
|
"types": "./vitest/index.d.ts",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Token, Lexer, TokenPosition } from 'typescript-parsec';
|
|
2
|
+
export { T } from './lexer_rules.js';
|
|
3
|
+
declare class TokenImpl<T> implements Token<T> {
|
|
4
|
+
private readonly lexer;
|
|
5
|
+
private readonly input;
|
|
6
|
+
kind: T;
|
|
7
|
+
text: string;
|
|
8
|
+
pos: TokenPosition;
|
|
9
|
+
keep: boolean;
|
|
10
|
+
private nextToken;
|
|
11
|
+
constructor(lexer: LexerImpl<T>, input: string, kind: T, text: string, pos: TokenPosition, keep: boolean);
|
|
12
|
+
get next(): Token<T> | undefined;
|
|
13
|
+
}
|
|
14
|
+
declare class LexerImpl<T> implements Lexer<T> {
|
|
15
|
+
rules: [boolean, RegExp, T][];
|
|
16
|
+
constructor(rules: [boolean, RegExp, T][]);
|
|
17
|
+
parse(input: string): TokenImpl<T> | undefined;
|
|
18
|
+
parseNext(input: string, indexStart: number, rowBegin: number, columnBegin: number): TokenImpl<T> | undefined;
|
|
19
|
+
parseNextAvailable(input: string, index: number, rowBegin: number, columnBegin: number): TokenImpl<T> | undefined;
|
|
20
|
+
}
|
|
21
|
+
export declare const lexer: LexerImpl<import("./lexer_rules.js").T>;
|
package/parser/lexer.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { TokenError } from 'typescript-parsec';
|
|
2
|
+
import rules from './lexer_rules.js';
|
|
3
|
+
export { T } from './lexer_rules.js';
|
|
4
|
+
// based on https://github.com/microsoft/ts-parsec/blob/3350fcb/packages/ts-parsec/src/Lexer.ts
|
|
5
|
+
/*
|
|
6
|
+
MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) Microsoft Corporation.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE
|
|
27
|
+
*/
|
|
28
|
+
class TokenImpl {
|
|
29
|
+
constructor(lexer, input, kind, text, pos, keep) {
|
|
30
|
+
this.lexer = lexer;
|
|
31
|
+
this.input = input;
|
|
32
|
+
this.kind = kind;
|
|
33
|
+
this.text = text;
|
|
34
|
+
this.pos = pos;
|
|
35
|
+
this.keep = keep;
|
|
36
|
+
}
|
|
37
|
+
get next() {
|
|
38
|
+
if (this.nextToken === undefined) {
|
|
39
|
+
this.nextToken = this.lexer.parseNextAvailable(this.input, this.pos.index + this.text.length, this.pos.rowEnd, this.pos.columnEnd);
|
|
40
|
+
if (this.nextToken === undefined) {
|
|
41
|
+
this.nextToken = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return this.nextToken === null ? undefined : this.nextToken;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
class LexerImpl {
|
|
48
|
+
constructor(rules) {
|
|
49
|
+
this.rules = rules;
|
|
50
|
+
for (const rule of this.rules) {
|
|
51
|
+
if (!rule[1].sticky) {
|
|
52
|
+
throw new Error(`Regular expression patterns for a tokenizer should be sticky: ${rule[1].source}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
parse(input) {
|
|
57
|
+
return this.parseNextAvailable(input, 0, 1, 1);
|
|
58
|
+
}
|
|
59
|
+
parseNext(input, indexStart, rowBegin, columnBegin) {
|
|
60
|
+
if (indexStart === input.length) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
// changed here: instead of slicing the input string, we use a running index
|
|
64
|
+
const lastIndex = indexStart;
|
|
65
|
+
// let result: TokenImpl<T> | undefined
|
|
66
|
+
for (const [keep, regexp, kind] of this.rules) {
|
|
67
|
+
// changed here: instead of slicing the input string, we use a running index
|
|
68
|
+
regexp.lastIndex = lastIndex;
|
|
69
|
+
if (regexp.test(input)) {
|
|
70
|
+
// changed here: instead of slicing the input string, we use a running index
|
|
71
|
+
const text = input.slice(lastIndex, regexp.lastIndex);
|
|
72
|
+
let rowEnd = rowBegin;
|
|
73
|
+
let columnEnd = columnBegin;
|
|
74
|
+
for (const c of text) {
|
|
75
|
+
switch (c) {
|
|
76
|
+
case '\r':
|
|
77
|
+
break;
|
|
78
|
+
case '\n':
|
|
79
|
+
rowEnd++;
|
|
80
|
+
columnEnd = 1;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
columnEnd++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const newResult = new TokenImpl(this, input, kind, text, { index: indexStart, rowBegin, columnBegin, rowEnd, columnEnd }, keep);
|
|
87
|
+
// changed here: instead of keeping the longest token, we keep the first one
|
|
88
|
+
return newResult;
|
|
89
|
+
// if (
|
|
90
|
+
// result === undefined ||
|
|
91
|
+
// result.text.length < newResult.text.length
|
|
92
|
+
// ) {
|
|
93
|
+
// result = newResult
|
|
94
|
+
// }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// changed here: instead of keeping the longest token, we keep the first one
|
|
98
|
+
// if (result === undefined) {
|
|
99
|
+
throw new TokenError({
|
|
100
|
+
index: indexStart,
|
|
101
|
+
rowBegin,
|
|
102
|
+
columnBegin,
|
|
103
|
+
rowEnd: rowBegin,
|
|
104
|
+
columnEnd: columnBegin,
|
|
105
|
+
}, `Unable to tokenize the rest of the input: ${input.substr(indexStart)}`);
|
|
106
|
+
// } else {
|
|
107
|
+
// return result
|
|
108
|
+
// }
|
|
109
|
+
}
|
|
110
|
+
parseNextAvailable(input, index, rowBegin, columnBegin) {
|
|
111
|
+
let token;
|
|
112
|
+
while (true) {
|
|
113
|
+
token = this.parseNext(input, token === undefined ? index : token.pos.index + token.text.length, token === undefined ? rowBegin : token.pos.rowEnd, token === undefined ? columnBegin : token.pos.columnEnd);
|
|
114
|
+
if (token === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
else if (token.keep) {
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export const lexer = new LexerImpl(rules);
|