Skip to content

Commit d2ae12b

Browse files
rob-gordonclaude
andauthored
fix(parse): keep getFeatureData's global regex a literal (#65)
* fix(parse): keep getFeatureData's global regex a literal 0.15.0 built getFeatureData's regex with new RegExp(featuresRe, "gs"). A transpiler that polyfills named capture groups only wraps regex literals. CRA's production Babel does this, so in flowchart.fun's production bundle the copy came back as a plain native RegExp, and match.groups was undefined. The loop then hit `if (!match.groups) continue;` on an empty match at lastIndex 0 and never ended, which froze the page on its first parse. The global regex is now its own literal, featuresReGlobal, next to featuresRe. A test keeps the two sources identical. A match without groups now breaks out of the loop instead of continuing, because an empty match never advances lastIndex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(lint): ban building a RegExp from a non-literal Consumers that transpile named capture groups, like CRA's production Babel, only wrap regex literals. A RegExp built from another regex loses match.groups, which is how 0.15.0 froze flowchart.fun's production build. The lookbehind rule guards against the same kind of consumer break. Run against 0.15.0's getFeatureData.ts, this rule flags line 5. It flags nothing in the current tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3e520aa commit d2ae12b

4 files changed

Lines changed: 26 additions & 5 deletions

File tree

‎graph-selector/.eslintrc.cjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ module.exports = {
2020
caughtErrorsIgnorePattern: "^_",
2121
},
2222
],
23+
"no-restricted-syntax": [
24+
"error",
25+
{
26+
selector:
27+
":matches(NewExpression, CallExpression)[callee.name='RegExp'] > :not(Literal, TemplateLiteral).arguments:first-child",
28+
message:
29+
"Write a regex literal instead. Consumers that transpile named groups (CRA's Babel) wrap literals only, so a RegExp built from another regex loses match.groups.",
30+
},
31+
],
2332
},
2433
env: {
2534
browser: true,

‎graph-selector/src/getFeatureData.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { describe, expect, test } from "vitest";
22

33
import { getFeatureData } from "./getFeatureData";
4+
import { featuresRe, featuresReGlobal } from "./regexps";
45

56
describe("getFeatureData", () => {
7+
test("its global regex is featuresRe with the g flag", () => {
8+
expect(featuresReGlobal.source).toBe(featuresRe.source);
9+
expect(featuresReGlobal.flags).toBe("gs");
10+
});
11+
612
test("it parses IDs", () => {
713
const { id, line } = getFeatureData("#hello");
814
expect(id).toBe("hello");

‎graph-selector/src/getFeatureData.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import { featuresRe } from "./regexps";
1+
import { featuresReGlobal as re } from "./regexps";
22
import { Data, Descriptor } from "./types";
33

4-
// compiled once; lastIndex is reset per call so no state leaks between calls
5-
const re = new RegExp(featuresRe, "gs");
6-
74
/**
85
* Given a line, it returns the feaures from the line
96
*/
@@ -17,7 +14,8 @@ export function getFeatureData(_line: string) {
1714
let attributes = "";
1815

1916
while ((match = re.exec(line)) != null) {
20-
if (!match.groups) continue;
17+
// an empty match never advances lastIndex, so continuing here would loop forever
18+
if (!match.groups) break;
2119
if (!match.groups.replace) break;
2220

2321
// if (match.groups.pointer) pointers.push(match.groups.pointer);

‎graph-selector/src/regexps.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ export const getFeaturesIndex = (line: string) => {
1515
*/
1616
export const featuresRe =
1717
/(?<replace>(?<id>#[\w-]+)?(?<classes>(\.[a-zA-Z]{1}[\w-]*)*)?(?<attributes>(\[[^\]=]+(?<attributeValue>=(".*"|'.*'|.*))?\])*))/s;
18+
19+
/**
20+
* featuresRe with the g flag, for getFeatureData's exec loop. Keep it a literal: a transpiler
21+
* that polyfills named groups (CRA's Babel does) wraps literals only, and a copy made with
22+
* new RegExp(featuresRe, "gs") comes back without match.groups.
23+
*/
24+
export const featuresReGlobal =
25+
/(?<replace>(?<id>#[\w-]+)?(?<classes>(\.[a-zA-Z]{1}[\w-]*)*)?(?<attributes>(\[[^\]=]+(?<attributeValue>=(".*"|'.*'|.*))?\])*))/gs;

0 commit comments

Comments
 (0)