Skip to content

fix(eslint): require-escaped-regexp-interpolation — const-binding resolution to eliminate false positives - #49782

Merged
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-fix-require-escaped-regexp-interpol
Aug 2, 2026
Merged

fix(eslint): require-escaped-regexp-interpolation — const-binding resolution to eliminate false positives#49782
pelikhan merged 2 commits into
mainfrom
copilot/eslint-factory-fix-require-escaped-regexp-interpol

Conversation

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

isRecognizedAsEscaped performed no data-flow tracking, only inspecting the expression node at the interpolation site. Two patterns produced live false positives in actions/setup/js: a const variable holding the result of an escape-helper call (non-escaped* name), and const literals that can never contain regex metacharacters.

Rule changes (require-escaped-regexp-interpolation.ts)

  • findConstInitializer(sourceCode, node) — walks the ESLint scope chain from an Identifier to its const declarator; returns the initializer only when the binding has a single definition and zero post-init writes (same soundness bar as require-spawnsync-error-check's alias tracking).
  • isLiteralSafeForRegexp(node) — accepts number, boolean, and metachar-free string literals as inherently safe.
  • isRecognizedAsEscaped extended with one level of const-binding resolution: if the expression is an identifier whose const initializer is itself escape-recognized or a safe literal, it's accepted. let/var, reassigned const, and non-resolvable initializers remain flagged.
// previously flagged — now accepted
const regexPattern = escapeRegExpChars(pattern);
new RegExp(`^${regexPattern}$`);

const COMMENT_MEMORY_TAG = "gh-aw-comment-memory";
const MAX_MEMORY_ID_LENGTH = 128;
new RegExp(`${COMMENT_MEMORY_TAG}:([^\n]{1,${MAX_MEMORY_ID_LENGTH}})\n`);

// still flagged — no unsound widening
let regexPattern = escapeRegExpChars(pattern);   // let
const raw = someOtherFn(input);                  // non-escape initializer
const PATTERN = "file.*txt";                     // contains metacharacters

Boundary tests added

New valid cases covering both code paths; new invalid cases asserting let bindings, reassigned const, metachar-containing string literals, and non-escape initializers are still reported.

glob_pattern_helpers.cjs line 74

regexPattern is a reassigned let whose value is an intentionally-built regex (glob */**[^/]*/.* after escaping other metacharacters). Added // eslint-disable-next-line gh-aw-custom/require-escaped-regexp-interpolation with an explanatory comment since the rule is correctly detecting the metacharacters — the suppression is intentional, not a workaround.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for starting this fix! 🚀 I see this is a draft PR with a detailed task checklist for the eslint-factory rule improvement (addressing #49281).

Status: This PR is currently in draft mode with no code changes yet. The task checklist outlines the work needed:

  • Extend isRecognizedAsEscaped for const variables
  • Add helper functions for literal safety checking
  • Add test coverage
  • Resolve specific false positives

What's needed to move forward:

  • Implement the actual code changes in eslint-factory/src/rules/require-escaped-regexp-interpolation.ts and related files
  • Add the new test cases covering the code paths mentioned in the checklist
  • Run npm test in eslint-factory to verify all tests pass
  • Mark the PR as ready for review when implementation is complete

Since this is an agentic workflow PR, keep @pelikhan in the loop as you progress through the implementation tasks. The structure looks solid — just needs the code to follow! 👍

Generated by ✅ Contribution Check · auto · 49 AIC · ⌖ 4.83 AIC · ⊞ 8.8K ·

…st bindings

- Add `findConstInitializer` to resolve const variable bindings via scope analysis
- Add `isLiteralSafeForRegexp` to recognize safe literal values (numbers, booleans, metachar-free strings)
- Extend `isRecognizedAsEscaped` with one level of const-binding resolution: const vars initialized to an escape-helper call or safe literal are now treated as safe
- Add TSESLint import for SourceCode type
- Add test coverage for all new valid and invalid paths
- Fix glob_pattern_helpers.cjs line 74 with eslint-disable comment (intentional regex pattern building)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix false positives in require-escaped-regexp-interpolation rule fix(eslint): require-escaped-regexp-interpolation — const-binding resolution to eliminate false positives Aug 2, 2026
Copilot AI requested a review from pelikhan August 2, 2026 13:28
@pelikhan
pelikhan marked this pull request as ready for review August 2, 2026 13:42
Copilot AI review requested due to automatic review settings August 2, 2026 13:42

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

Improves the regex-interpolation ESLint rule by resolving immutable constants while preserving warnings for mutable or unsafe values.

Changes:

  • Resolves one-level const initializers.
  • Adds literal-safety handling and boundary tests.
  • Documents an intentional glob-regex suppression.
Show a summary per file
File Description
eslint-factory/src/rules/require-escaped-regexp-interpolation.ts Adds constant and literal resolution.
eslint-factory/src/rules/require-escaped-regexp-interpolation.test.ts Adds valid and invalid boundary cases.
actions/setup/js/glob_pattern_helpers.cjs Suppresses an intentional regex interpolation.

Review details

Tip

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

function isLiteralSafeForRegexp(node: TSESTree.Node): boolean {
if (node.type !== AST_NODE_TYPES.Literal) return false;
const { value } = node;
if (typeof value === "number" || typeof value === "boolean") return true;
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (0 additions detected in default business logic paths).

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions github-actions Bot 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.

The overall approach is sound — const-binding resolution is implemented conservatively (single def, no post-init writes, one level deep) and the test coverage is thorough.

One blocking issue: isLiteralSafeForRegexp incorrectly treats all numeric literals as safe. Numbers like 1.5 or 1e21 stringify to "1.5" and "1e+21" in template literals, both containing the . metacharacter — which is in REGEXP_META_CHARS. The fix is to check the string representation of the number: String(value), running it through the same per-character metacharacter check used for strings. The existing inline review comment on line 148 captures this.

Once that is corrected the PR is ready to merge.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 25.2 AIC · ⌖ 12.1 AIC · ⊞ 5.4K

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 7 new test(s): 7 design, 0 implementation, 0 violation(s).

📊 Metrics (7 tests)
Metric Value
Analyzed 7 (Go: 0, JS/TS: 7)
✅ Design 7 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 7 (100%)
Duplicate clusters 0
Inflation No (77 test lines : 78 prod lines = 0.99:1)
🚨 Violations 0

Verdict

passed. All 7 new tests verify the const-binding resolution design contract added in this PR. Tests comprehensively cover valid cases (const from escape-helpers, literals, numerics) and invalid cases (let variables, reassignments, metacharacter literals, non-escape functions). Zero implementation-detail tests; 100% design coverage. Test-to-production ratio well under 2:1 threshold.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 23.5 AIC · ⌖ 7.85 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot 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.

✅ Test Quality Sentinel: 100/100 — Excellent. All 7 new tests verify the const-binding resolution design contract. 100% design test coverage, 0% implementation tests (threshold: 30%). Test-to-production ratio 0.99:1 (threshold: 2:1). No violations.

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /diagnosing-bugs — requesting changes on two correctness gaps before merge.

📋 Key Themes & Highlights

Issues

  • isEscapedNameReference(init) omitted (line 215): the escapedX naming convention is not tested against the resolved initializer, leaving const bar = escapedPattern as a false positive.
  • Numeric literals with metachar coercions (line 148): 1.5 coerces to "1.5" and 1e21 to "1e+21", both containing . or +; the blanket typeof value === "number" guard silently suppresses these.

Positive Highlights

  • ✅ Scope-chain walk with single-def + zero-post-init-write guard is a sound soundness bar — matches the existing alias-tracking pattern in the codebase.
  • ✅ One-level-only resolution is a deliberate and appropriate conservatism.
  • ✅ Test suite covers let, reassigned const, metachar strings, and non-escape initializers — good boundary coverage.
  • ✅ The glob_pattern_helpers.cjs disable comment is well-explained and intentional.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 36.2 AIC · ⌖ 8.12 AIC · ⊞ 7.1K
Comment /matt to run again

if (sourceCode && node.type === AST_NODE_TYPES.Identifier) {
const init = findConstInitializer(sourceCode, node);
if (init !== null) {
if (isEscapeHelperCall(init) || isRegexEscapeReplaceCall(init) || isLiteralSafeForRegexp(init)) return true;

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.

[/diagnosing-bugs] isEscapedNameReference(init) is missing from the initializer check — const bar = escapedFoo; remains a false positive because escapedFoo is an Identifier whose name starts with escaped, yet only isEscapeHelperCall, isRegexEscapeReplaceCall, and isLiteralSafeForRegexp are tested against init.

💡 Suggested fix
if (isEscapeHelperCall(init) || isRegexEscapeReplaceCall(init) || isEscapedNameReference(init) || isLiteralSafeForRegexp(init)) return true;

Add a matching test case:

"const escapedName = escapedPattern; new RegExp(`^${escapedName}$`);"

@copilot please address this.

function isLiteralSafeForRegexp(node: TSESTree.Node): boolean {
if (node.type !== AST_NODE_TYPES.Literal) return false;
const { value } = node;
if (typeof value === "number" || typeof value === "boolean") return true;

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.

[/diagnosing-bugs] (Corroborating the existing comment) Number literals that contain . or + after coercion (1.5"1.5", 1e21"1e+21") will silently pass isLiteralSafeForRegexp even though their string representation contains regex metacharacters. The literal value 1.5 passes the typeof value === "number" guard but its regex expansion is 1.5, where . matches any character.

💡 Suggested fix

Convert the number to its string representation and run it through the metachar check:

if (typeof value === "number") {
  const s = String(value);
  for (const char of s) {
    if (REGEXP_META_CHARS.has(char)) return false;
  }
  return true;
}

Or restrict to integer literals only:

if (typeof value === "number") return Number.isInteger(value) && value >= 0;

Add test cases:

// invalid — 1.5 coerces to "1.5" which contains "."
"const STEP = 1.5; new RegExp(`a{${STEP}}`);"

@copilot please address this.

@github-actions github-actions Bot 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.

Review: no new blocking issues found

The const-binding resolution logic (findConstInitializer) is sound for its stated one-level scope, and test coverage for let/reassignment/non-escape-initializer/metachar-literal cases is solid.

💡 Notes
  • There is already an unresolved review comment (id 3699223566) on line 148 pointing out that isLiteralSafeForRegexp treats all number/boolean literals as regex-safe without validating their stringified form — 1e21"1e+21" and any decimal like 1.5"1.5" both contain regex metacharacters (+, .). I confirmed this is a real false negative (String(1e21) = "1e+21", String(1.5) = "1.5"). This should be fixed before merge, but I'm not duplicating that comment.
  • The single-level-only const resolution (no recursive const b = a chaining) is explicitly documented as an intentional scope limitation in the docstring, not a defect — acceptable as-is.
  • glob_pattern_helpers.cjs already has broad existing test coverage for globPatternToRegex, so the new eslint-disable suppression there is adequately backstopped.

No new comments added since the only confirmed correctness issue (numeric-literal false negative) is already tracked by an existing pending review comment.

🔎 Code quality review by PR Code Quality Reviewer · auto · 105.5 AIC · ⌖ 6.63 AIC · ⊞ 7.9K
Comment /review to run again

@pelikhan
pelikhan merged commit 5ec8470 into main Aug 2, 2026
56 of 58 checks passed
@pelikhan
pelikhan deleted the copilot/eslint-factory-fix-require-escaped-regexp-interpol branch August 2, 2026 14:09
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.84.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants