From 9266fdba28910d043ac054d15e048d096a843fda Mon Sep 17 00:00:00 2001 From: Cullen Walsh Date: Fri, 18 Jul 2025 19:43:50 +0000 Subject: [PATCH] [Perf] Improve performance of removeNodesFromOriginalCode() Summary: This change replaces the existing RegExp replace() logic with concatenated string slices. This avoids reallocation the result string for each node replacement, replacing it with string slice operations (which are implemented as O(1) string views within v8) and a single .join(''), which can be optimized by the runtime to a single allocation. This probably won't make a noticable difference, but the change also simplifies some further feature work I am attempting to add. Test Plan: `yarn install && yarn run test --all` --- src/utils/remove-nodes-from-original-code.ts | 64 +++++++++----------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/utils/remove-nodes-from-original-code.ts b/src/utils/remove-nodes-from-original-code.ts index a2c2a350..73a11114 100644 --- a/src/utils/remove-nodes-from-original-code.ts +++ b/src/utils/remove-nodes-from-original-code.ts @@ -1,16 +1,7 @@ -import { - CommentBlock, - CommentLine, - Directive, - ImportDeclaration, - InterpreterDirective, - Statement, -} from '@babel/types'; +import { Comment, Node } from '@babel/types'; -/** Escapes a string literal to be passed to new RegExp. See: https://stackoverflow.com/a/6969486/480608. - * @param s the string to escape - */ -const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +type NodeOrComment = Node | Comment; +type BoundedNodeOrComment = NodeOrComment & { start: number; end: number }; /** * Removes imports from original file @@ -19,30 +10,33 @@ const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); */ export const removeNodesFromOriginalCode = ( code: string, - nodes: ( - | Statement - | CommentBlock - | Directive - | CommentLine - | ImportDeclaration - | InterpreterDirective - )[], + nodes: (Node | Comment)[], ) => { - let text = code; - for (const node of nodes) { - const start = Number(node.start); - const end = Number(node.end); - if (Number.isSafeInteger(start) && Number.isSafeInteger(end)) { - text = text.replace( - // only replace imports at the beginning of the line (ignoring whitespace) - // otherwise matching commented imports will be replaced - new RegExp( - '^\\s*' + escapeRegExp(code.substring(start, end)), - 'm', - ), - '', - ); + const ranges: { start: number; end: number }[] = nodes.filter( + (node): node is BoundedNodeOrComment => { + const start = Number(node.start); + const end = Number(node.end); + return Number.isSafeInteger(start) && Number.isSafeInteger(end); + }, + ); + ranges.sort((a, b) => a.start - b.start); + + let result: string = ''; + let idx = 0; + + for (const { start, end } of ranges) { + if (start > idx) { + result += code.slice(idx, start); + idx = start; + } + if (end > idx) { + idx = end; } } - return text; + + if (idx < code.length) { + result += code.slice(idx); + } + + return result; };