Rule
require-escaped-regexp-interpolation (shipped 2026-07-30, first review).
Problem
isRegexEscapeReplaceCall() in eslint-factory/src/rules/require-escaped-regexp-interpolation.ts requires the .replace() call's second argument to be the exact literal string "\$&" — i.e. it only recognizes the "escape every regex metacharacter globally" idiom (x.replace(/[.*+?^${}()|[\]\\]/g, "\$&")). Any other equally-valid, narrower inline escape is rejected as "unescaped", even when it demonstrably neutralizes the only metacharacter the value can contain.
Grounded evidence
actions/setup/js/interpolate_prompt.cjs:238-240:
const experimentName = key.substring("GH_AW_EXPERIMENTS_".length).toLowerCase(); // env-var suffix: [A-Z0-9_] only
const exprForm = `experiments.${experimentName}`; // exactly one literal "."
const conditionPattern = new RegExp(`(\\{\\{#if[^}]*?)${exprForm.replace(".", "\\.")}`, "gi");
exprForm.replace(".", "\\.") deliberately escapes the single literal . that exprForm is guaranteed to contain (from the "experiments." prefix — experimentName comes from an uppercase, underscore-only env-var name and cannot itself contain regex metacharacters). This is a correct, intentional escape for this call site. isRegexEscapeReplaceCall doesn't recognize it because the replacement isn't exactly "\$&", so the rule reports unescapedInterpolation on already-escaped, safe code — a false positive in a live, in-scope production file (this file is not a .test.cjs, so it is linted).
Ask
Broaden isRegexEscapeReplaceCall to also accept the single-character/literal escape form: a .replace() call whose search argument is a string or regex literal for one specific character (or short literal), and whose replacement is a backslash followed by exactly that same literal (e.g. .replace(".", "\\."), .replace(/\./g, "\\.")). Keep the existing global-metachar-class + "\$&" form as the other accepted shape.
Acceptance criteria
- New valid-case test:
new RegExp(\...${x.replace(".", "\.")}`)(and ideally the regex-literal-search variantx.replace(/./g, "\.")`) no longer flagged.
- Existing invalid-case tests (bare unescaped identifiers,
escapeHtml, mixed escaped/unescaped) remain flagged — do not widen to accept arbitrary .replace() calls regardless of the replacement value.
- Re-verify
interpolate_prompt.cjs:240 is no longer flagged after the fix (grounding check).
- Consider documenting the recognized
.replace() shapes in the rule's require-escaped-regexp-interpolation README section once it's added (see doc-debt: this rule and 8 others shipped since the last README update are currently undocumented in eslint-factory/README.md's rule table).
Generated by 🤖 ESLint Refiner · agent · 216.4 AIC · ⌖ 6.19 AIC · ⊞ 5.1K · ◷
Rule
require-escaped-regexp-interpolation(shipped 2026-07-30, first review).Problem
isRegexEscapeReplaceCall()ineslint-factory/src/rules/require-escaped-regexp-interpolation.tsrequires the.replace()call's second argument to be the exact literal string"\$&"— i.e. it only recognizes the "escape every regex metacharacter globally" idiom (x.replace(/[.*+?^${}()|[\]\\]/g, "\$&")). Any other equally-valid, narrower inline escape is rejected as "unescaped", even when it demonstrably neutralizes the only metacharacter the value can contain.Grounded evidence
actions/setup/js/interpolate_prompt.cjs:238-240:exprForm.replace(".", "\\.")deliberately escapes the single literal.thatexprFormis guaranteed to contain (from the"experiments."prefix —experimentNamecomes from an uppercase, underscore-only env-var name and cannot itself contain regex metacharacters). This is a correct, intentional escape for this call site.isRegexEscapeReplaceCalldoesn't recognize it because the replacement isn't exactly"\$&", so the rule reportsunescapedInterpolationon already-escaped, safe code — a false positive in a live, in-scope production file (this file is not a.test.cjs, so it is linted).Ask
Broaden
isRegexEscapeReplaceCallto also accept the single-character/literal escape form: a.replace()call whose search argument is a string or regex literal for one specific character (or short literal), and whose replacement is a backslash followed by exactly that same literal (e.g..replace(".", "\\."),.replace(/\./g, "\\.")). Keep the existing global-metachar-class +"\$&"form as the other accepted shape.Acceptance criteria
new RegExp(\...${x.replace(".", "\.")}`)(and ideally the regex-literal-search variantx.replace(/./g, "\.")`) no longer flagged.escapeHtml, mixed escaped/unescaped) remain flagged — do not widen to accept arbitrary.replace()calls regardless of the replacement value.interpolate_prompt.cjs:240is no longer flagged after the fix (grounding check)..replace()shapes in the rule'srequire-escaped-regexp-interpolationREADME section once it's added (see doc-debt: this rule and 8 others shipped since the last README update are currently undocumented ineslint-factory/README.md's rule table).