From e7b5946da06cee75d2c1629a82c478fc13085a6b Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Fri, 3 Jul 2026 20:20:28 -0700 Subject: [PATCH] refactor(scripts): share regexp literal escaping (#99778) --- scripts/audit-seams.mjs | 11 ++++------- scripts/check-deprecated-api-usage.mjs | 5 +---- scripts/e2e/mock-openai-server.mjs | 5 ++--- scripts/e2e/openwebui-probe.mjs | 12 ++++++------ scripts/github/barnacle-auto-response.mjs | 7 ++++--- scripts/github/real-behavior-proof-policy.mjs | 11 ++++------- scripts/lib/regexp.mjs | 4 ++++ scripts/lib/stable-release-closeout.mjs | 5 +---- scripts/release-check.ts | 3 ++- scripts/runtime-postbuild.mjs | 2 +- scripts/transitive-manifest-risk-report.mjs | 5 +---- 11 files changed, 30 insertions(+), 40 deletions(-) create mode 100644 scripts/lib/regexp.mjs diff --git a/scripts/audit-seams.mjs b/scripts/audit-seams.mjs index bda525118d2f..1432c77187a4 100644 --- a/scripts/audit-seams.mjs +++ b/scripts/audit-seams.mjs @@ -10,6 +10,7 @@ import { BUNDLED_PLUGIN_ROOT_DIR, } from "./lib/bundled-plugin-paths.mjs"; import { optionalBundledClusterSet } from "./lib/optional-bundled-clusters.mjs"; +import { escapeRegExp } from "./lib/regexp.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const srcRoot = path.join(repoRoot, "src"); @@ -548,12 +549,8 @@ function splitNameTokens(name) { .filter(Boolean); } -function escapeForRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - function hasImportSource(source, specifier) { - const escaped = escapeForRegExp(specifier); + const escaped = escapeRegExp(specifier); return new RegExp(`from\\s+["']${escaped}["']|import\\s*\\(\\s*["']${escaped}["']\\s*\\)`).test( source, ); @@ -840,7 +837,7 @@ async function buildTestIndex(testFiles) { } function hasExecutableImportReference(source, importPath) { - const escapedImportPath = escapeForRegExp(importPath); + const escapedImportPath = escapeRegExp(importPath); const suffix = String.raw`(?:\.[^"'\\\`]+)?`; const patterns = [ new RegExp(String.raw`\bfrom\s*["'\`]${escapedImportPath}${suffix}["'\`]`), @@ -852,7 +849,7 @@ function hasExecutableImportReference(source, importPath) { } function hasModuleMockReference(source, importPath) { - const escapedImportPath = escapeForRegExp(importPath); + const escapedImportPath = escapeRegExp(importPath); const suffix = String.raw`(?:\.[^"'\\\`]+)?`; const patterns = [ new RegExp(String.raw`\bvi\.mock\s*\(\s*["'\`]${escapedImportPath}${suffix}["'\`]`), diff --git a/scripts/check-deprecated-api-usage.mjs b/scripts/check-deprecated-api-usage.mjs index be258f3f2ba6..bdead04c75c1 100644 --- a/scripts/check-deprecated-api-usage.mjs +++ b/scripts/check-deprecated-api-usage.mjs @@ -4,6 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import { collectDeprecatedInternalConfigApiViolations } from "./lib/deprecated-config-api-guard.mjs"; import { buildDeprecatedPluginSdkModuleSpecifiers } from "./lib/deprecated-plugin-sdk-usage.mjs"; +import { escapeRegExp } from "./lib/regexp.mjs"; const repoRoot = process.cwd(); @@ -56,10 +57,6 @@ function* walk(dir, rule) { } } -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); -} - function collectIdentifierRuleViolations(rule) { const allowedFiles = new Set(rule.allowedFiles ?? []); const pattern = new RegExp( diff --git a/scripts/e2e/mock-openai-server.mjs b/scripts/e2e/mock-openai-server.mjs index fa68788c86d6..e069dbe9b21c 100644 --- a/scripts/e2e/mock-openai-server.mjs +++ b/scripts/e2e/mock-openai-server.mjs @@ -1,6 +1,7 @@ // Mock OpenAI-compatible server for broader E2E scenarios. import { createHash } from "node:crypto"; import http from "node:http"; +import { escapeRegExp } from "../lib/regexp.mjs"; import { readTcpPortEnv } from "./lib/env-limits.mjs"; import { boundedRequestLogBody, @@ -244,9 +245,7 @@ function collectFunctionCallOutputText(body) { } function hasDeclaredTool(bodyText, name) { - return new RegExp(`"name"\\s*:\\s*"${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`, "u").test( - bodyText, - ); + return new RegExp(`"name"\\s*:\\s*"${escapeRegExp(name)}"`, "u").test(bodyText); } function mcpCodeModeApiFileEvents(body, bodyText) { diff --git a/scripts/e2e/openwebui-probe.mjs b/scripts/e2e/openwebui-probe.mjs index 99ef9161e1b7..e5e12e7886a6 100644 --- a/scripts/e2e/openwebui-probe.mjs +++ b/scripts/e2e/openwebui-probe.mjs @@ -1,5 +1,6 @@ // Probe script for OpenWebUI E2E connectivity. import { Agent, setGlobalDispatcher } from "undici"; +import { escapeRegExp } from "../lib/regexp.mjs"; import { readBoundedResponseText as readBoundedResponseTextWithLimit } from "./lib/bounded-response-text.mjs"; const baseUrl = process.env.OPENWEBUI_BASE_URL ?? ""; @@ -158,10 +159,6 @@ function sleep(ms) { }); } -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - function redactDiagnosticText(text, extraSecrets = []) { let redacted = text .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/giu, "Bearer ") @@ -194,8 +191,11 @@ function cookieSecretValues(cookieHeader) { } function authDiagnosticSecretValues(authHeaders) { - const authorization = typeof authHeaders.authorization === "string" ? authHeaders.authorization : ""; - const bearerToken = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length) : ""; + const authorization = + typeof authHeaders.authorization === "string" ? authHeaders.authorization : ""; + const bearerToken = authorization.startsWith("Bearer ") + ? authorization.slice("Bearer ".length) + : ""; const cookie = typeof authHeaders.cookie === "string" ? authHeaders.cookie : ""; return [bearerToken, authorization, cookie, ...cookieSecretValues(cookie)].filter(Boolean); } diff --git a/scripts/github/barnacle-auto-response.mjs b/scripts/github/barnacle-auto-response.mjs index 9cfe0b2b8bd3..8b3bdf9536cd 100644 --- a/scripts/github/barnacle-auto-response.mjs +++ b/scripts/github/barnacle-auto-response.mjs @@ -1,5 +1,6 @@ // Barnacle owns deterministic GitHub triage and auto-response behavior. +import { escapeRegExp } from "../lib/regexp.mjs"; import { NEEDS_PR_CONTEXT_LABEL, PROOF_SUFFICIENT_LABEL, @@ -294,7 +295,7 @@ function extractIssueFormValue(body, field) { if (!body) { return ""; } - const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const escapedField = escapeRegExp(field); const regex = new RegExp( `(?:^|\\n)###\\s+${escapedField}\\s*\\n([\\s\\S]*?)(?=\\n###\\s+|$)`, "i", @@ -317,7 +318,7 @@ function hasLinkedReference(text) { } function hasFilledTemplateLine(body, field) { - const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const escapedField = escapeRegExp(field); const regex = new RegExp(`^\\s*-\\s*${escapedField}:\\s*\\S`, "im"); return regex.test(body); } @@ -334,7 +335,7 @@ function hasMostlyBlankTemplate(body) { "Root cause", "Target test or file", ].filter((field) => { - const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const escapedField = escapeRegExp(field); const regex = new RegExp(`^\\s*-\\s*${escapedField}(?: \\([^)]*\\))?:\\s*$`, "im"); return regex.test(body); }).length; diff --git a/scripts/github/real-behavior-proof-policy.mjs b/scripts/github/real-behavior-proof-policy.mjs index e70571358fb7..2871edb91801 100644 --- a/scripts/github/real-behavior-proof-policy.mjs +++ b/scripts/github/real-behavior-proof-policy.mjs @@ -1,5 +1,6 @@ // Shared PR context and evidence policy for GitHub checks and label decisions. import { readBoundedResponseText } from "../lib/bounded-response.mjs"; +import { escapeRegExp } from "../lib/regexp.mjs"; /** ClawSweeper-owned labels that OpenClaw preserves but does not mutate. */ export const PROOF_OVERRIDE_LABEL = "proof: override"; @@ -53,10 +54,6 @@ const legacyProofFieldNames = [ const missingValueRegex = /^(?:n\/?a|none|not applicable|tbd|todo|unknown|unsure|none provided|no evidence|not tested|untested|did not test|didn't test|could not test|couldn't test|-|(?:-{3,}|\*{3,}|_{3,})|\[[^\]]*\])\.?$/i; -function escapeRegex(text) { - return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - function createTimeoutError(label, timeoutMs) { const error = new Error(`${label} timed out after ${timeoutMs}ms`); error.code = "ETIMEDOUT"; @@ -290,7 +287,7 @@ function extractMarkdownSections(headingRegex, body = "") { } export function hasAuthoredPullRequestSection(heading, body = "") { - const headingPattern = new RegExp(`^#{2,6}\\s+${escapeRegex(heading)}\\b[^\\n]*$`, "im"); + const headingPattern = new RegExp(`^#{2,6}\\s+${escapeRegExp(heading)}\\b[^\\n]*$`, "im"); return !isMissingValue(extractMarkdownSections(headingPattern, body).at(-1) ?? ""); } @@ -300,7 +297,7 @@ function extractLegacyProofSections(body = "") { function fieldLineRegex(name) { return new RegExp( - `^\\s*(?:[-*]\\s*)?(?:\\*\\*)?${escapeRegex(name)}(?:\\s*\\([^)]*\\))?(?:\\*\\*)?\\s*:\\s*(.*)$`, + `^\\s*(?:[-*]\\s*)?(?:\\*\\*)?${escapeRegExp(name)}(?:\\s*\\([^)]*\\))?(?:\\*\\*)?\\s*:\\s*(.*)$`, "i", ); } @@ -375,7 +372,7 @@ function result(status, reason, details = {}) { } function extractMarkerField(marker, name) { - const match = marker.match(new RegExp(`\\b${escapeRegex(name)}=([^\\s>]+)`, "i")); + const match = marker.match(new RegExp(`\\b${escapeRegExp(name)}=([^\\s>]+)`, "i")); return match?.[1] ?? ""; } diff --git a/scripts/lib/regexp.mjs b/scripts/lib/regexp.mjs new file mode 100644 index 000000000000..6ddc4a25ed6c --- /dev/null +++ b/scripts/lib/regexp.mjs @@ -0,0 +1,4 @@ +/** Escape text so it can be embedded literally inside a RegExp constructor pattern. */ +export function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/scripts/lib/stable-release-closeout.mjs b/scripts/lib/stable-release-closeout.mjs index 5d770db5ec22..af78be2a52e4 100644 --- a/scripts/lib/stable-release-closeout.mjs +++ b/scripts/lib/stable-release-closeout.mjs @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import { escapeRegExp } from "./regexp.mjs"; const STABLE_RELEASE_TAG_RE = /^v(?\d{4}\.\d{1,2}\.\d{1,2})(?:-[1-9]\d*)?$/u; const MAX_ROLLBACK_DRILL_AGE_MS = 90 * 24 * 60 * 60 * 1000; @@ -14,10 +15,6 @@ function parseStableReleaseTagDetails(tag) { }; } -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); -} - function sha256(value) { return createHash("sha256").update(value).digest("hex"); } diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 5ef3a789bb2e..7437bb8cc75f 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -25,6 +25,7 @@ import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH, writePackageDistInventory, } from "../src/infra/package-dist-inventory.ts"; +import { escapeRegExp } from "../src/shared/regexp.js"; import { checkCliBootstrapExternalImports } from "./check-cli-bootstrap-imports.mjs"; import { collectBundledExtensionManifestErrors, @@ -958,7 +959,7 @@ export function collectForbiddenPackContentPaths( export { collectPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs"; function extractTag(item: string, tag: string): string | null { - const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const escapedTag = escapeRegExp(tag); const regex = new RegExp(`<${escapedTag}>([^<]+)`); return regex.exec(item)?.[1]?.trim() ?? null; } diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index fafc5917b269..3598cedd34bb 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -6,6 +6,7 @@ import { performance } from "node:perf_hooks"; import { fileURLToPath, pathToFileURL } from "node:url"; import { copyBundledPluginMetadata } from "./copy-bundled-plugin-metadata.mjs"; import { copyPluginSdkRootAlias } from "./copy-plugin-sdk-root-alias.mjs"; +import { escapeRegExp } from "./lib/regexp.mjs"; import { copyStaticExtensionAssets, copyStaticExtensionAssetsToRuntimeOverlay, @@ -22,7 +23,6 @@ const ROOT_RUNTIME_ALIAS_PATTERN = /^(?.+\.(?:runtime|contract))-[A-Za-z0- const ROOT_STABLE_RUNTIME_ALIAS_PATTERN = /^.+\.(?:runtime|contract)\.js$/u; const ROOT_RUNTIME_IMPORT_SPECIFIER_PATTERN = /(["'])\.\/([^"']+\.(?:runtime|contract)-[A-Za-z0-9_-]+\.js)\1/gu; -const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); const PLUGIN_SDK_ROOT_ALIAS_OUTPUT = "dist/plugin-sdk/root-alias.cjs"; const OFFICIAL_CHANNEL_CATALOG_OUTPUT = "dist/channel-catalog.json"; const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [ diff --git a/scripts/transitive-manifest-risk-report.mjs b/scripts/transitive-manifest-risk-report.mjs index e3c0f6d26fb7..7b1fca14d050 100644 --- a/scripts/transitive-manifest-risk-report.mjs +++ b/scripts/transitive-manifest-risk-report.mjs @@ -7,6 +7,7 @@ import path from "node:path"; import process from "node:process"; import YAML from "yaml"; import { readBoundedResponseText } from "./lib/bounded-response.mjs"; +import { escapeRegExp } from "./lib/regexp.mjs"; import { parseReportCliArgs, writeReportArtifact } from "./lib/report-cli-helpers.mjs"; import { collectAllResolvedPackagesFromLockfile, @@ -132,10 +133,6 @@ function splitMinimumReleaseAgeExcludeSelector(selector) { }; } -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); -} - function packagePatternMatches(pattern, packageName) { const regex = new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`, "u"); return regex.test(packageName);