links-notation 0.21.2 → 0.21.3
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/dist/index.js +295 -118
- package/package.json +1 -1
- package/src/grammar.pegjs +46 -3
- package/src/parser-generated.js +297 -113
- package/tests/NestingDepth.test.js +124 -0
- package/tests/support/nestingDepthWorker.js +15 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Reading nested groups must take time that grows with the size of the
|
|
2
|
+
// document, not with two to the power of its nesting depth
|
|
3
|
+
// (https://github.com/link-foundation/links-notation/issues/314).
|
|
4
|
+
//
|
|
5
|
+
// Before the fix, every level of nesting doubled the work: a nine-byte document
|
|
6
|
+
// took seconds. Each case is parsed on its own thread and fails when it has not
|
|
7
|
+
// finished within the budget, so a regression fails instead of hanging.
|
|
8
|
+
|
|
9
|
+
import { test, expect } from 'bun:test';
|
|
10
|
+
import { clearTimeout, setTimeout } from 'node:timers';
|
|
11
|
+
import { URL } from 'node:url';
|
|
12
|
+
import { Worker } from 'node:worker_threads';
|
|
13
|
+
import { Parser } from '../src/Parser.js';
|
|
14
|
+
import { formatLinks } from '../src/Link.js';
|
|
15
|
+
|
|
16
|
+
// Generous enough for a slow machine, and far below what the exponential
|
|
17
|
+
// reading took at the depths used here.
|
|
18
|
+
const BUDGET_MS = 5000;
|
|
19
|
+
|
|
20
|
+
// The depth used for the shapes that used to take exponential time.
|
|
21
|
+
const DEEP = 32;
|
|
22
|
+
|
|
23
|
+
// A depth that shows the time grows with the size of the document.
|
|
24
|
+
const VERY_DEEP = 256;
|
|
25
|
+
|
|
26
|
+
function withinBudget(what, source) {
|
|
27
|
+
const worker = new Worker(
|
|
28
|
+
new URL('./support/nestingDepthWorker.js', import.meta.url)
|
|
29
|
+
);
|
|
30
|
+
let timer;
|
|
31
|
+
const timeout = new Promise((_, reject) => {
|
|
32
|
+
timer = setTimeout(
|
|
33
|
+
() => reject(new Error(`${what} did not finish within ${BUDGET_MS} ms`)),
|
|
34
|
+
BUDGET_MS
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
const result = new Promise((resolve, reject) => {
|
|
38
|
+
worker.once('message', resolve);
|
|
39
|
+
worker.once('error', reject);
|
|
40
|
+
});
|
|
41
|
+
worker.postMessage(source);
|
|
42
|
+
return Promise.race([result, timeout]).finally(() => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
worker.terminate();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const closed = (depth) => `${'('.repeat(depth)}a${')'.repeat(depth)}`;
|
|
49
|
+
|
|
50
|
+
const valueAfter = (depth) => `${'('.repeat(depth)}a${') b'.repeat(depth)}`;
|
|
51
|
+
|
|
52
|
+
const unclosed = (depth) => `${'('.repeat(depth)}a`;
|
|
53
|
+
|
|
54
|
+
// Unclosed groups on lines that are each indented one space deeper.
|
|
55
|
+
const indentedUnclosed = (depth) =>
|
|
56
|
+
Array.from({ length: depth }, (_, level) => `${' '.repeat(level)}(a`).join(
|
|
57
|
+
'\n'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
test(
|
|
61
|
+
'closed groups read in linear time',
|
|
62
|
+
async () => {
|
|
63
|
+
const source = closed(DEEP);
|
|
64
|
+
const result = await withinBudget('closed groups', source);
|
|
65
|
+
expect(result).toEqual({ formatted: source });
|
|
66
|
+
},
|
|
67
|
+
BUDGET_MS * 2
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
test(
|
|
71
|
+
'values after groups read in linear time',
|
|
72
|
+
async () => {
|
|
73
|
+
const source = valueAfter(DEEP);
|
|
74
|
+
const result = await withinBudget('values after groups', source);
|
|
75
|
+
expect(result).toEqual({ formatted: `(${source})` });
|
|
76
|
+
},
|
|
77
|
+
BUDGET_MS * 2
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
test(
|
|
81
|
+
'unclosed groups fail in linear time',
|
|
82
|
+
async () => {
|
|
83
|
+
const result = await withinBudget('unclosed groups', unclosed(DEEP));
|
|
84
|
+
expect(result).toEqual({ error: 'ParseError' });
|
|
85
|
+
},
|
|
86
|
+
BUDGET_MS * 2
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
test(
|
|
90
|
+
'unclosed groups on indented lines fail in linear time',
|
|
91
|
+
async () => {
|
|
92
|
+
const result = await withinBudget(
|
|
93
|
+
'unclosed indented groups',
|
|
94
|
+
indentedUnclosed(DEEP)
|
|
95
|
+
);
|
|
96
|
+
expect(result).toEqual({ error: 'ParseError' });
|
|
97
|
+
},
|
|
98
|
+
BUDGET_MS * 2
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
test(
|
|
102
|
+
'values after very deep groups read in linear time',
|
|
103
|
+
async () => {
|
|
104
|
+
const source = valueAfter(VERY_DEEP);
|
|
105
|
+
const result = await withinBudget('very deep values after groups', source);
|
|
106
|
+
expect(result).toEqual({ formatted: `(${source})` });
|
|
107
|
+
},
|
|
108
|
+
BUDGET_MS * 2
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
test('group followed by values keeps its structure', () => {
|
|
112
|
+
const cases = [
|
|
113
|
+
['(a) b', '((a) b)'],
|
|
114
|
+
['(a) (b) c', '((a) (b) c)'],
|
|
115
|
+
['((a) b) c', '(((a) b) c)'],
|
|
116
|
+
['(a: b) c', '((a: b) c)'],
|
|
117
|
+
['(a)\n(b) c', '(a)\n((b) c)'],
|
|
118
|
+
['x\n (a) b\n (c)', '(x)\n((x) ((a) b))\n((x) (c))'],
|
|
119
|
+
];
|
|
120
|
+
const parser = new Parser();
|
|
121
|
+
for (const [source, expected] of cases) {
|
|
122
|
+
expect(formatLinks(parser.parse(source))).toBe(expected);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Parses the documents it is sent on its own thread, so a test can stop a
|
|
2
|
+
// parse that takes too long instead of hanging.
|
|
3
|
+
import { parentPort } from 'node:worker_threads';
|
|
4
|
+
import { Parser } from '../../src/Parser.js';
|
|
5
|
+
import { formatLinks } from '../../src/Link.js';
|
|
6
|
+
|
|
7
|
+
const parser = new Parser();
|
|
8
|
+
|
|
9
|
+
parentPort.on('message', (source) => {
|
|
10
|
+
try {
|
|
11
|
+
parentPort.postMessage({ formatted: formatLinks(parser.parse(source)) });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
parentPort.postMessage({ error: error.name });
|
|
14
|
+
}
|
|
15
|
+
});
|