Skip to content

Accept targeted literal .replace() escapes in require-escaped-regexp-interpolation - #49674

Merged
pelikhan merged 5 commits into
mainfrom
copilot/fix-regexp-escapability-issue
Aug 2, 2026
Merged

Accept targeted literal .replace() escapes in require-escaped-regexp-interpolation#49674
pelikhan merged 5 commits into
mainfrom
copilot/fix-regexp-escapability-issue

Conversation

Copilot AI commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

require-escaped-regexp-interpolation only recognized the canonical global metacharacter escape form (replace(/[...]/g, "\\$&")). That caused false positives for narrower but correct inline escapes such as replace(".", "\\."), including the live interpolate_prompt.cjs call site.

  • Rule behavior

    • Broaden isRegexEscapeReplaceCall() to accept two safe shapes:
      • existing global metacharacter escape: replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
      • targeted literal escape: replace("<literal>", "\\<literal>") and equivalent regex-literal search forms
    • Keep matching narrow by requiring:
      • a fixed literal search value
      • a replacement that is exactly \ + the same literal
      • the escaped portion to begin with a regex metacharacter
    • Continue rejecting arbitrary .replace() calls that do not actually neutralize regex meaning
  • Coverage

    • Add valid cases for:
      • string-literal search: .replace(".", "\\.")
      • regex-literal search: .replace(/\./g, "\\.")
    • Add a regression case proving .replace(".", ".") is still flagged
  • Example

    const exprForm = `experiments.${experimentName}`;
    
    // now recognized as already escaped
    const conditionPattern = new RegExp(
      `(\\{\\{#if[^}]*?)${exprForm.replace(".", "\\.")}`,
      "gi",
    );

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 10.4 AIC · ⌖ 6.45 AIC · ⊞ 8.2K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 25.2 AIC · ⌖ 10.7 AIC · ⊞ 8.2K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix regex escaping issue in require-escaped-regexp-interpolation Accept targeted literal .replace() escapes in require-escaped-regexp-interpolation Aug 1, 2026
Copilot AI requested a review from pelikhan August 1, 2026 22:44
@pelikhan
pelikhan marked this pull request as ready for review August 2, 2026 01:17
Copilot AI review requested due to automatic review settings August 2, 2026 01:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Broadens the ESLint rule to recognize targeted literal regex escaping.

Changes:

  • Adds string and regex-literal escape detection.
  • Adds valid and invalid regression tests.
Show a summary per file
File Description
eslint-factory/src/rules/require-escaped-regexp-interpolation.ts Implements targeted escape recognition.
eslint-factory/src/rules/require-escaped-regexp-interpolation.test.ts Tests accepted and rejected replacements.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Suppressed comments (2)

eslint-factory/src/rules/require-escaped-regexp-interpolation.ts:57

  • The regex flags are ignored, so a sticky search such as value.replace(/\./y, "\\.") is accepted. For value === "a.b", that search fails at index 0 and returns the unescaped value, while the rule suppresses the diagnostic. Reject y (or whitelist only flags that preserve the fixed-literal search behavior) and cover the sticky case with an invalid test.
  if (node.type !== AST_NODE_TYPES.Literal || !("regex" in node) || !node.regex) return null;
  return decodeFixedLiteralRegexPattern(node.regex.pattern);

eslint-factory/src/rules/require-escaped-regexp-interpolation.ts:82

  • Comparing the literal replacement value does not account for String.prototype.replace replacement tokens. A search consisting of $ followed by a backtick, paired with the corresponding backslash-prefixed replacement, passes these checks because $ is a regex metacharacter; however, the $-backtick token expands to the text before the match rather than emitting the searched literal. The $' token analogously expands the suffix. Reject targeted searches that create these special replacement tokens, and add invalid regression cases.
function isLiteralRegexEscapeReplacement(search: string, replacement: string): boolean {
  if (search.length === 0 || replacement !== `\\${search}`) return false;
  if (!REGEXP_META_CHARS.has(search[0])) return false;
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

return replacement.type === AST_NODE_TYPES.Literal && typeof replacement.value === "string" && replacement.value === "\\$&";
const search = getFixedLiteralSearchText(args[0]);
const replacement = getStringLiteralValue(args[1]);
if (replacement === "\\$&") return true;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 82e1c94. The unconditional branch is replaced by isCanonicalMetacharEscapeRegex(args[0]), which requires the search to be exactly /[.*+?^${}()|[\]\\]/g with flags === "g". This rejects narrower patterns like /./ and sticky-flag variants. Three new invalid regression tests cover: non-canonical search, sticky flag, and $' replacement token.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot quick triage for this PR:

Run details: https://github.com/github/gh-aw/actions/runs/30727625239

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 10.4 AIC · ⌖ 6.45 AIC · ⊞ 8.2K ·
Comment /souschef to run again

Copilot AI and others added 2 commits August 2, 2026 02:06
…ent-token safety

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
…ent; guard against replacement tokens

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot quick triage for this PR:

  • Review feedback still worth closing out before merge:
    1. copilot-pull-request-reviewer — address the sticky-flag (/.../y) false negative and add an invalid r...

Addressed in commit fix(eslint-rule): validate canonical metachar regex for \\$& replacement; guard against replacement tokens:

  1. Unconditional "\\$&" branch / sticky-flag false negative — the if (replacement === "\\$&") return true; branch is now return isCanonicalMetacharEscapeRegex(args[0]), which requires the search to be exactly /[.*+?^${}()|[\]\\]/g (canonical pattern + flags === "g"). This rejects both narrow patterns like /./ and sticky-flag variants like /[...]/y. Regression tests added for both cases.

  2. Replacement-token cases ($', $`, $&, $1$9, $<) — added containsReplacementToken() guard in isLiteralRegexEscapeReplacement so that a replacement like "\\$'" (where $' expands to the string after the match) is rejected. Regression test for replace("$'", "\\$'") added.

Also corrected a pre-existing bug in the valid test cases where the canonical regex had too many backslashes and didn't match the actual AST pattern.

Copilot AI requested a review from gh-aw-bot August 2, 2026 02:18
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot quick triage for this PR:

  • Review feedback still worth closing out before merge:
    1. copilot-pull-request-reviewer — your latest reply says the sticky-flag false negative and replacement-token edge cases were addressed. Please verify those reviewer concerns are fully resolved on the current head and summarize any remaining code changes still needed. Accept targeted literal .replace() escapes in require-escaped-regexp-interpolation #49674
  • Branch refresh: GitHub branch update was requested from this run.
  • Please run the pr-finisher skill before handoff and summarize any remaining maintainer action.

Run details: https://github.com/github/gh-aw/actions/runs/30731801766

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 25.2 AIC · ⌖ 10.7 AIC · ⊞ 8.2K ·
Comment /souschef to run again

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot quick triage for this PR:

  • Review feedback still worth closing out before merge:
    1. copilot-pull-request-reviewer — your latest reply says the sticky-flag false negative and replacement-...

Both reviewer concerns are fully resolved on the current HEAD (82e1c94):

  1. Sticky-flag false negativeisRegexEscapeReplaceCall now calls isCanonicalMetacharEscapeRegex(args[0]) when replacement === "\\$&", which requires node.regex.flags === "g" exactly. /[...]/y (sticky) and any other flag variant are rejected. Regression test added: replace(/[.*+?^${}()|[\]\\]/y, "\\$&") → flagged.

  2. Replacement-token casescontainsReplacementToken() guards isLiteralRegexEscapeReplacement, rejecting replacements containing $&, $', $`, $<, $1$9. Regression test added: replace("$'", "\\$'") → flagged.

The one remaining open review thread (PRRT_kwDOPc1QR86VsX34) is already outdated (code it referenced was changed) and has been replied to; it needs a maintainer to resolve it via the GitHub UI.

Local validation status: make fmt ✅ · make lint ✅ · eslint-factory unit tests 438/443 pass ✅ (5 pre-existing require-fs-io-try-catch failures unrelated to this PR). PR is mergeable_state: clean, not a draft, no conflicts. CI must be re-triggered by a maintainer before merge.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🔍 Triage Summary

Field Value
Category bug
Risk low
Priority medium
Score 40/100 (impact 15 + urgency 10 + quality 15)
Recommended action batch_review
Batch eslint-monster

Adjusts require-escaped-regexp-interpolation rule to accept literal .replace() escapes. CI green.

Automated triage — see the triage report for full context.

Generated by 🔧 PR Triage Agent · auto · 40.3 AIC · ⌖ 4.01 AIC · ⊞ 8K ·

@pelikhan
pelikhan merged commit a2f797d into main Aug 2, 2026
1 check passed
@pelikhan
pelikhan deleted the copilot/fix-regexp-escapability-issue branch August 2, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants