Skip to content

Commit e81b9d2

Browse files
authored
Merge pull request #9 from hildjj/features
features
2 parents 8c456e3 + e802e5d commit e81b9d2

12 files changed

+615
-98
lines changed

.github/workflows/publish.yml

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
build:
1212
runs-on: ubuntu-latest
1313
permissions:
14-
contents: read
14+
contents: write
1515
id-token: write
1616
steps:
1717
- uses: actions/checkout@v4
@@ -25,6 +25,12 @@ jobs:
2525
- run: npm run build
2626
- run: npm run test
2727
- run: npm pkg delete devDependencies scripts packageManager
28+
- name: Deploy Docs
29+
uses: peaceiris/actions-gh-pages@v4
30+
with:
31+
github_token: ${{ secrets.GITHUB_TOKEN }}
32+
publish_dir: docs
33+
publish_branch: gh-pages
2834
- run: npm publish --access public --provenance
2935
env:
3036
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
coverage/
22
node_modules/
33
.vscode/
4+
docs/

.npmignore

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
.ncurc
55
.vscode/
66
coverage/
7+
docs/
78
eslint.config.js
89
test/
910
tsconfig.json
11+
typedoc.config.js

README.md

+32-9
Original file line numberDiff line numberDiff line change
@@ -38,39 +38,62 @@ Each test definition has the following type:
3838
```ts
3939
export type PeggyTestOptions<T> = {
4040
/**
41-
* Which valid start rule to use? Default: grammar
42-
* default start rule.
41+
* Which valid start rule to use? Default: grammar default start rule.
4342
*/
4443
startRule?: string | undefined;
4544
/**
4645
* If specified, check this against the startRule.
4746
*/
4847
validInput?: string | undefined;
4948
/**
50-
* What result should startRule return for validInput?
51-
* Default: validInput.
49+
* What result should startRule return for validInput? Default:
50+
* validInput.
5251
*/
53-
validResult?: T | undefined;
52+
validResult?: T | ((res: T) => any) | undefined;
5453
/**
55-
* If specified, ensure that the grammar fails
56-
* on this input.
54+
* If specified, ensure that the grammar fails on this input.
5755
*/
5856
invalidInput?: string | undefined;
5957
/**
6058
* Expected peg$maxFailPos.
6159
*/
6260
peg$maxFailPos?: number | undefined;
6361
/**
64-
* What to append to validInput to make it
65-
* invalid, so that library mode will return a prefix match.
62+
* What to append to validInput to make it invalid, so that library mode
63+
* will return a prefix match.
6664
*/
6765
invalid?: string | undefined;
66+
/**
67+
* If any test has this set to true, only run the tests with this set to
68+
* true.
69+
*/
70+
only?: boolean | undefined;
71+
/**
72+
* If true, skip this test.
73+
*/
74+
skip?: boolean | undefined;
6875
/**
6976
* Extra options to pass to parse(), overriding whatever else this library
7077
* would have otherwise used.
7178
*/
7279
options?: (import("peggy").ParserOptions & ExtraParserOptions) | undefined;
7380
};
81+
82+
export type ExtraParserOptions = {
83+
/**
84+
* In the augmented code only, use this function as the start rule rather
85+
* than the default. This gives access to functions that are NOT valid
86+
* start rules for internal testing.
87+
*/
88+
peg$startRuleFunction?: string | undefined;
89+
/**
90+
* Number of times for each of the given rules to succeed before they
91+
* fail. Only applies in the augmented code.
92+
*/
93+
peg$failAfter?: {
94+
[ruleName: string]: number;
95+
} | undefined;
96+
};
7497
```
7598
7699
## Runtime support

lib/index.d.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @typedef {object} TestPeggyOptions
33
* @prop {boolean} [noDelete] Do not delete the generated file.
44
* @prop {boolean} [noMap] Do not add a sourcemap to the generated file.
5-
* Defaults to true if peggy$debugger is set on any start, otherwise false.
5+
* Defaults to true if peg$debugger is set on any start, otherwise false.
66
* @prop {boolean} [noGenerate] Do not generate a file, only run tests on the
77
* original.
88
* @prop {boolean} [noOriginal] Do not run tests on the original code, only
@@ -32,6 +32,14 @@ export type ExtraParserOptions = {
3232
* to functions that are NOT valid start rules for internal testing.
3333
*/
3434
peg$startRuleFunction?: string | undefined;
35+
/**
36+
* Number of times for each of the
37+
* given rules to succeed before they fail. Only applies in the augmented
38+
* code.
39+
*/
40+
peg$failAfter?: {
41+
[ruleName: string]: number;
42+
} | undefined;
3543
};
3644
export type PeggyTestOptions<T> = {
3745
/**
@@ -62,6 +70,15 @@ export type PeggyTestOptions<T> = {
6270
* invalid, so that library mode will return a prefix match.
6371
*/
6472
invalid?: string | undefined;
73+
/**
74+
* If any test has this set to true, only run the
75+
* tests with this set to true.
76+
*/
77+
only?: boolean | undefined;
78+
/**
79+
* If true, skip this test.
80+
*/
81+
skip?: boolean | undefined;
6582
/**
6683
* Extra options to pass to parse(), overriding whatever else this library
6784
* would have otherwise used.
@@ -82,7 +99,7 @@ export type TestPeggyOptions = {
8299
noDelete?: boolean | undefined;
83100
/**
84101
* Do not add a sourcemap to the generated file.
85-
* Defaults to true if peggy$debugger is set on any start, otherwise false.
102+
* Defaults to true if peg$debugger is set on any start, otherwise false.
86103
*/
87104
noMap?: boolean | undefined;
88105
/**

lib/index.js

+82-12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ let counter = 0;
2626
* @prop {string} [peg$startRuleFunction] In the augmented code only, use this
2727
* function as the start rule rather than the default. This gives access
2828
* to functions that are NOT valid start rules for internal testing.
29+
* @prop {{[ruleName: string]: number}} [peg$failAfter] Number of times for each of the
30+
* given rules to succeed before they fail. Only applies in the augmented
31+
* code.
2932
*/
3033

3134
/**
@@ -41,6 +44,9 @@ let counter = 0;
4144
* @prop {number} [peg$maxFailPos = 0] Expected peg$maxFailPos.
4245
* @prop {string} [invalid = "\uffff"] What to append to validInput to make it
4346
* invalid, so that library mode will return a prefix match.
47+
* @prop {boolean} [only] If any test has this set to true, only run the
48+
* tests with this set to true.
49+
* @prop {boolean} [skip] If true, skip this test.
4450
* @prop {import('peggy').ParserOptions & ExtraParserOptions} [options = {}]
4551
* Extra options to pass to parse(), overriding whatever else this library
4652
* would have otherwise used.
@@ -63,28 +69,49 @@ let counter = 0;
6369
* @param {TestCounts} counts
6470
*/
6571
function checkParserStarts(grammar, starts, modified, counts) {
72+
let count = 0;
73+
const M = modified ? "M_" : "U_";
74+
const only = starts.some(s => s.only);
6675
for (const start of starts) {
76+
let source = `${M}valid_${count++}`;
6777
const startRule = start.startRule || undefined; // NOT `??`
6878
const peg$maxFailPos = start.peg$maxFailPos ?? undefined;
69-
const options = start.options ?? {};
79+
const options = start.options ?? Object.create(null);
7080
const invalid = start.invalid ?? INVALID;
71-
if (!modified && options.peg$startRuleFunction) {
81+
if (start.skip || (only && !start.only)) {
7282
continue;
7383
}
84+
if (!modified) {
85+
if (options.peg$startRuleFunction || options.peg$failAfter) {
86+
continue;
87+
}
88+
}
7489
if (typeof start.validInput === "string") {
7590
// Note: validResult might be specified as undefined.
7691
const expected = Object.prototype.hasOwnProperty.call(start, "validResult")
7792
? start.validResult
7893
: start.validInput;
7994

80-
let res = grammar.parse(start.validInput, { startRule, ...options });
95+
// eslint-disable-next-line no-useless-assignment
96+
let res = undefined;
97+
try {
98+
res = grammar.parse(start.validInput, {
99+
startRule,
100+
grammarSource: source,
101+
...options,
102+
});
103+
} catch (er) {
104+
const e = /** @type {import('peggy').parser.SyntaxError} */ (er);
105+
e.message = e.format([{ source, text: start.validInput }]);
106+
throw e;
107+
}
81108
if (typeof expected === "string") {
82-
equal(res, expected);
109+
equal(res, expected, `${source} (eq): "${start.validInput}"`);
83110
} else if (typeof expected === "function") {
84111
// @ts-expect-error Can't figure this out
85112
res = expected(res);
86113
} else {
87-
deepEqual(res, expected);
114+
deepEqual(res, expected, `${source} (dp_eq): "${start.validInput}"`);
88115
}
89116

90117
const expectedLib = {
@@ -96,37 +123,51 @@ function checkParserStarts(grammar, starts, modified, counts) {
96123
if (peg$maxFailPos === undefined) {
97124
delete expectedLib.peg$maxFailPos;
98125
}
126+
source = `${M}valid_library_${count}`;
127+
// No exception in library mode
99128
let lib = grammar.parse(start.validInput, {
100129
peg$library: true,
101130
startRule,
131+
grammarSource: source,
102132
...options,
103133
});
134+
if (lib.peg$result === lib.peg$FAILED) {
135+
throw new Error(`Failed in library context: ${source}`);
136+
}
137+
138+
// @ts-expect-error This is for testing.
104139
delete lib.peg$maxFailExpected;
105140
if (peg$maxFailPos === undefined) {
141+
// @ts-expect-error This is for testing.
106142
delete lib.peg$maxFailPos;
107143
}
108144

109145
if (typeof expected === "function") {
110146
// @ts-ignore
111147
lib.peg$result = expected(lib.peg$result);
112148
}
113-
deepEqual(lib, expectedLib);
149+
deepEqual(lib, expectedLib, `${source} (dp_eq): "${start.validInput}"`);
114150

115151
if (invalid) {
152+
source = `${M}valid+_${count}`;
116153
lib = grammar.parse(start.validInput + invalid, {
117154
peg$library: true,
118155
startRule,
156+
grammarSource: source,
119157
...options,
120158
});
159+
160+
// @ts-expect-error This is for testing.
121161
delete lib.peg$maxFailExpected;
122162
if (peg$maxFailPos === undefined) {
163+
// @ts-expect-error This is for testing.
123164
delete lib.peg$maxFailPos;
124165
}
125166
if (typeof expected === "function") {
126167
// @ts-ignore
127168
lib.peg$result = expected(lib.peg$result);
128169
}
129-
deepEqual(lib, expectedLib);
170+
deepEqual(lib, expectedLib, `${source} (dp_eq): "${start.validInput}"`);
130171

131172
throws(() => grammar.parse(start.validInput + invalid, {
132173
startRule,
@@ -143,9 +184,11 @@ function checkParserStarts(grammar, starts, modified, counts) {
143184
startRule,
144185
...options,
145186
});
146-
fail("Cannot reach here");
187+
fail("Expected error but none received");
147188
} catch (er) {
148-
ok(er instanceof grammar.SyntaxError);
189+
if (!(er instanceof grammar.SyntaxError)) {
190+
throw er;
191+
}
149192
equal(typeof er.format, "function");
150193
let fmt = er.format([{ source: "test", text: start.invalidInput }]);
151194
equal(typeof fmt, "string");
@@ -186,7 +229,7 @@ function checkParserStarts(grammar, starts, modified, counts) {
186229
* @typedef {object} TestPeggyOptions
187230
* @prop {boolean} [noDelete] Do not delete the generated file.
188231
* @prop {boolean} [noMap] Do not add a sourcemap to the generated file.
189-
* Defaults to true if peggy$debugger is set on any start, otherwise false.
232+
* Defaults to true if peg$debugger is set on any start, otherwise false.
190233
* @prop {boolean} [noGenerate] Do not generate a file, only run tests on the
191234
* original.
192235
* @prop {boolean} [noOriginal] Do not run tests on the original code, only
@@ -274,13 +317,31 @@ export async function testPeggy(grammarUrl, starts, opts) {
274317
if (/^\s*peg\$result = peg\$startRuleFunction\(\);/.test(line)) {
275318
src.add(`\
276319
//#region Inserted by @peggyjs/coverage
320+
let replacements = Object.create(null);
277321
(() => {
278322
if (options.peg$debugger) {
279323
debugger;
280324
}
281325
if (options.peg$startRuleFunction) {
282326
peg$startRuleFunction = eval(options.peg$startRuleFunction); // Ew.
283327
}
328+
if (options.peg$failAfter) {
329+
for (const [name, count] of Object.entries(options.peg$failAfter)) {
330+
const fn = eval(name); // Ew, again.
331+
replacements[name] = fn;
332+
if (typeof fn !== 'function') {
333+
throw new Error('Unknown function for peg$failAfter: ' + name);
334+
}
335+
let remains = count;
336+
const replace = name + \` = (...args) => {
337+
if (remains-- > 0) {
338+
return fn.call(this, ...args)
339+
}
340+
return peg$FAILED;
341+
}\`;
342+
eval(replace); // We've already called eval twice. What's one more?
343+
}
344+
}
284345
285346
text();
286347
offset();
@@ -346,8 +407,18 @@ export async function testPeggy(grammarUrl, starts, opts) {
346407
//#endregion
347408
348409
`);
410+
src.add(new SourceNode(lineNum++, 0, grammarPath, line));
411+
src.add(`
412+
//#region Inserted by @peggyjs/coverage
413+
for (const [name, orig] of Object.entries(replacements)) {
414+
eval(name + '= orig');
415+
}
416+
replacements = Object.create(null);
417+
//#endregion
418+
`);
419+
} else {
420+
src.add(new SourceNode(lineNum++, 0, grammarPath, line));
349421
}
350-
src.add(new SourceNode(lineNum++, 0, grammarPath, line));
351422
}
352423

353424
const withMap = src.toStringWithSourceMap();
@@ -362,7 +433,6 @@ export async function testPeggy(grammarUrl, starts, opts) {
362433
${start} sourceMappingURL=data:application/json;charset=utf-8;base64,${map}
363434
`;
364435
}
365-
366436
await fs.writeFile(modifiedPath, code);
367437
try {
368438
const agrammar = /** @type {Parser} */ (

package.json

+7-4
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,23 @@
2828
"test": "c8 node --test test/*.test.js"
2929
},
3030
"dependencies": {
31-
"peggy": "4.1.1",
31+
"peggy": "4.2.0",
3232
"source-map-generator": "0.8.0"
3333
},
3434
"devDependencies": {
3535
"@peggyjs/eslint-config": "5.0.1",
36-
"@types/node": "22.9.0",
36+
"@types/node": "22.9.1",
3737
"c8": "10.1.2",
38-
"eslint": "9.14.0",
38+
"eslint": "9.15.0",
39+
"typedoc": "0.26.11",
3940
"typescript": "5.6.3"
4041
},
41-
"packageManager": "pnpm@9.12.3",
42+
"packageManager": "pnpm@9.14.2",
4243
"pnpm": {
4344
"overrides": {
45+
"@eslint/plugin-kit": "^0.2.3",
4446
"braces": "^3.0.3",
47+
"cross-spawn": "^7.0.6",
4548
"micromatch": "^4.0.8"
4649
}
4750
},

0 commit comments

Comments
 (0)