Skip to content

Commit 4a82fd2

Browse files
committed
test(parse): sanitise and use parser-utils.js
1 parent 3fe1ab2 commit 4a82fd2

File tree

3 files changed

+209
-40
lines changed

3 files changed

+209
-40
lines changed
Lines changed: 188 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,210 @@
1+
// @ts-check
2+
/* globals globalThis */
13
/// <reference path="../src/peg.d.ts"/>
24
import * as util from 'util';
3-
import tagToStringOnlyTag from '../src/tag-string.js';
45

5-
let curSrc = '';
6+
/**
7+
* @typedef {(val: any, message?: string) => asserts val} Assert
8+
*/
9+
10+
/**
11+
* @typedef {{
12+
* args: any[],
13+
* result: PromiseSettledResult<any>
14+
* }} TestCall
15+
*/
16+
17+
/**
18+
* @typedef {ReturnType<typeof makeAstBuilders>} TestTools
19+
*/
20+
21+
/**
22+
* @typedef {IParserTag<any, TestCall & { tools: TestTools }>} TestTag
23+
*/
24+
25+
/**
26+
* @type {Assert}
27+
*/
28+
const globalAssert = globalThis.assert;
29+
30+
/**
31+
* Map an array of nodes, also calling thunks for nodes.
32+
*
33+
* @param {any[]} args
34+
*/
35+
const arr = args =>
36+
args.map(a => {
37+
if (typeof a === 'function') {
38+
return a();
39+
}
40+
return a;
41+
});
42+
43+
const setPosition = (node, position) => {
44+
const mappedArgs = arr(node);
45+
return Object.assign(mappedArgs, {
46+
_pegPosition: position,
47+
});
48+
};
49+
50+
/** @param {string[]} tmpl */
51+
const makeAstBuilders = tmpl => {
52+
let nextHole = 0;
53+
54+
/**
55+
* Create an internal AST node.
56+
*
57+
* @param {number} pos
58+
* @param {...unknown} node
59+
*/
60+
const nest = (pos, ...node) => {
61+
const nh = nextHole;
62+
assert(
63+
nh < tmpl.length,
64+
`next hole ${nh} is more than template ${tmpl.length}`,
65+
);
66+
const ts = tmpl[nh];
67+
assert(
68+
pos < ts.length,
69+
`position ${pos} for templateString ${nh} is past ${ts.length}`,
70+
);
71+
return () => {
72+
const posNode = setPosition(
73+
node,
74+
`${JSON.stringify(tmpl[nextHole][pos])} #${nextHole}:${pos}`,
75+
);
76+
return posNode;
77+
};
78+
};
79+
80+
/**
81+
* Make an AST node that represents a hole to be filled in by the parser's
82+
* template arguments.
83+
*
84+
* @param {number} index
85+
* @param {...any} node
86+
*/
87+
const hole = (index, ...node) => {
88+
assert(
89+
index === nextHole,
90+
`index ${index} is not expected next hole ${nextHole}`,
91+
);
92+
const thisHole = nextHole;
93+
const nh = thisHole + 1;
94+
assert(
95+
nh < tmpl.length,
96+
`next hole ${nh} is more than template ${tmpl.length}`,
97+
);
98+
return () => {
99+
const posNode = setPosition(node, `hole #${index}`);
100+
nextHole = nh;
101+
return posNode;
102+
};
103+
};
104+
105+
/**
106+
* The root of an AST.
107+
*
108+
* @param {number | (() => any)} posOrThunk
109+
* @param {any[]} node
110+
*/
111+
const ast = (posOrThunk, ...node) => {
112+
nextHole = 0;
113+
if (typeof posOrThunk !== 'function') {
114+
posOrThunk = nest(posOrThunk, ...node);
115+
}
116+
return posOrThunk();
117+
};
118+
119+
return { ast, arr, nest, hole };
120+
};
6121

7122
/**
8-
* @param {IParserTag<any>} tag
123+
* @param {TestTag} testTag
124+
* @param {Assert} assert
9125
*/
10-
export function makeParser(tag) {
11-
/** @type {IParserTag<any>} */
12-
const stringTag = tagToStringOnlyTag(tag);
126+
const makeSerialParseWithTools = (testTag, assert) => {
13127
/**
128+
* @type {TestTools}
129+
*/
130+
let currentTestTools;
131+
132+
/**
133+
* @type {TestTools}
134+
*/
135+
const parseTestTools = {
136+
arr,
137+
ast: (...args) => currentTestTools.ast(...args),
138+
nest: (...args) => currentTestTools.nest(...args),
139+
hole: (...args) => currentTestTools.hole(...args),
140+
};
141+
142+
/**
143+
* This convenience `parse` updates the behaviour of `parseTestTools` with
144+
* every call That means you must finish using the test tools to process a
145+
* single `parse` at a time before running the next `parse`.
146+
*
14147
* @param {string} src
15148
* @param {boolean} [doDump]
16149
* @param {boolean} [doDebug]
17150
*/
18-
return (src, doDump = false, doDebug = false) => {
19-
curSrc = src;
20-
const dtag = doDebug ? stringTag.options({ debug: true }) : stringTag;
21-
const parsed = dtag`${src}`;
151+
const parse = (src, doDump = false, doDebug = false) => {
152+
const tmpl = Object.assign([src], { raw: [src] });
153+
const dtag = doDebug ? testTag.options({ debug: true }) : testTag;
154+
const { result, tools } = dtag(tmpl);
155+
assert(result.status === 'fulfilled', 'parse failed');
156+
const parsed = result.value;
157+
currentTestTools = tools;
22158
if (doDump) {
23159
// tslint:disable-next-line:no-console
24160
console.log('Dump:', util.inspect(parsed, { depth: Infinity }));
25161
doDump = false;
26162
}
27163
return parsed;
28164
};
29-
}
165+
166+
return { parse, ...parseTestTools };
167+
};
30168

31169
/**
32-
*
33-
* @param {number} pos
34-
* @param {...unknown} args
170+
* @param {IParserTag<any>} rawTag
171+
* @param {Assert} [testAssert]
35172
*/
36-
export function ast(pos, ...args) {
37-
return Object.assign(args, {
38-
_pegPosition: `${JSON.stringify(curSrc[pos])} #0:${pos}`,
39-
});
173+
export function makeParserUtils(rawTag, testAssert) {
174+
/** @type {Assert} */
175+
const assert = testAssert || globalAssert;
176+
177+
/**
178+
* Return a parser tag which returns testing tools.
179+
*
180+
* @param {IParserTag<any>} baseTag parser tag to wrap
181+
* @returns {TestTag}
182+
*/
183+
const wrapTag = baseTag => {
184+
const wrappedTag = (tmpl, ...holes) => {
185+
/** @type {PromiseSettledResult<any>} */
186+
let result;
187+
try {
188+
const value = baseTag(tmpl, ...holes);
189+
result = { status: 'fulfilled', value };
190+
} catch (e) {
191+
result = { status: 'rejected', reason: e };
192+
}
193+
return {
194+
args: [tmpl, ...holes],
195+
result,
196+
tools: makeAstBuilders(tmpl),
197+
};
198+
};
199+
return Object.assign(wrappedTag, {
200+
options: opts => wrapTag(baseTag.options(opts)),
201+
parserCreator: baseTag.parserCreator,
202+
// eslint-disable-next-line no-underscore-dangle
203+
_asExtending: baseTag._asExtending,
204+
});
205+
};
206+
207+
const testTag = wrapTag(rawTag);
208+
209+
return { testTag, ...makeSerialParseWithTools(testTag, assert) };
40210
}

packages/parse/test/test-json.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,24 @@
22
import { test } from './prepare-test-env-ava.js';
33

44
import { json } from '../src/main.js';
5-
import { ast, makeParser } from './parser-utils.js';
6-
7-
function defaultJsonParser() {
8-
return makeParser(json);
9-
}
5+
import { makeParserUtils } from './parser-utils.js';
106

117
test('data', t => {
12-
const parse = defaultJsonParser();
8+
const { parse, arr, ast } = makeParserUtils(json);
139
t.deepEqual(parse('{}'), ast(0, 'record', []));
1410
t.deepEqual(parse('[]'), ast(0, 'array', []));
1511
t.deepEqual(parse('123'), ast(0, 'data', 123));
1612
t.deepEqual(
1713
parse('{"abc": 123}'),
18-
ast(0, 'record', [
19-
ast(1, 'prop', ast(1, 'data', 'abc'), ast(8, 'data', 123)),
20-
]),
14+
ast(
15+
0,
16+
'record',
17+
arr([ast(1, 'prop', ast(1, 'data', 'abc'), ast(8, 'data', 123))]),
18+
),
2119
);
2220
t.deepEqual(
2321
parse('["abc", 123]'),
24-
ast(0, 'array', [ast(1, 'data', 'abc'), ast(8, 'data', 123)]),
22+
ast(0, 'array', arr([ast(1, 'data', 'abc'), ast(8, 'data', 123)])),
2523
);
2624
t.deepEqual(parse('"\\f\\r\\n\\t\\b"'), ast(0, 'data', '\f\r\n\t\b'));
2725
});

packages/parse/test/test-justin.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,26 @@
33
import { test } from './prepare-test-env-ava.js';
44

55
import { justin } from '../src/main.js';
6-
import { ast, makeParser } from './parser-utils.js';
7-
8-
function defaultJustinParser() {
9-
return makeParser(justin);
10-
}
6+
import { makeParserUtils } from './parser-utils.js';
117

128
test('data', t => {
13-
const parse = defaultJustinParser();
9+
const { parse, arr, ast } = makeParserUtils(justin, (val, message) =>
10+
t.assert(val, message),
11+
);
1412
t.deepEqual(parse(`12345`), ast(0, 'data', 12345));
1513
t.deepEqual(parse(`{}`), ast(0, 'record', []));
1614
t.deepEqual(parse(`[]`), ast(0, 'array', []));
1715
t.deepEqual(
1816
parse(`{"abc": 123}`),
19-
ast(0, 'record', [
20-
ast(1, 'prop', ast(1, 'data', 'abc'), ast(8, 'data', 123)),
21-
]),
17+
ast(
18+
0,
19+
'record',
20+
arr([ast(1, 'prop', ast(1, 'data', 'abc'), ast(8, 'data', 123))]),
21+
),
2222
);
23-
t.deepEqual(parse('9898n'), ast(0, 'data', BigInt(9898)));
2423
t.deepEqual(
2524
parse('["abc", 123]'),
26-
ast(0, 'array', [ast(1, 'data', 'abc'), ast(8, 'data', 123)]),
25+
ast(0, 'array', arr([ast(1, 'data', 'abc'), ast(8, 'data', 123)])),
2726
);
2827
t.deepEqual(parse(` /* nothing */ 123`), ast(16, 'data', 123));
2928
t.deepEqual(
@@ -37,7 +36,9 @@ test('data', t => {
3736
});
3837

3938
test('binops', t => {
40-
const parse = defaultJustinParser();
39+
const { parse, ast } = makeParserUtils(justin, (val, message) =>
40+
t.assert(val, message),
41+
);
4142
t.deepEqual(
4243
parse(`2 === 2`),
4344
ast(0, '===', ast(0, 'data', 2), ast(6, 'data', 2)),

0 commit comments

Comments
 (0)