ruvector 0.2.19 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ /**
2
+ * reference-tracker.js - Scope-aware identifier tracking and bulk renaming.
3
+ *
4
+ * Tracks all occurrences of each identifier in the source, respecting
5
+ * JavaScript scoping rules (function, block, module). When an identifier
6
+ * is renamed, ALL references in the same scope are updated consistently.
7
+ *
8
+ * Does NOT use a full AST parser — operates on regex-based token scanning
9
+ * so it works on partially-valid or beautified-minified code.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ /**
15
+ * Characters that can appear in a JS identifier.
16
+ * Used to ensure we match whole identifiers, not substrings.
17
+ */
18
+ const ID_CHAR = /[a-zA-Z0-9_$]/;
19
+
20
+ /**
21
+ * Pattern for minified-looking identifiers:
22
+ * - Single letter (A-Z, a-z)
23
+ * - Letter + digit(s) (A2, B3, z1)
24
+ * - Letter + $ (s$, a$)
25
+ * - Two letters (AA, Ab)
26
+ * These are candidates for renaming.
27
+ */
28
+ const MINIFIED_ID = /^[a-zA-Z][a-zA-Z0-9$]{0,2}$/;
29
+
30
+ /**
31
+ * JS reserved words that must never be renamed.
32
+ */
33
+ const RESERVED = new Set([
34
+ 'abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case',
35
+ 'catch', 'char', 'class', 'const', 'continue', 'debugger', 'default',
36
+ 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends',
37
+ 'false', 'final', 'finally', 'float', 'for', 'function', 'goto', 'if',
38
+ 'implements', 'import', 'in', 'instanceof', 'int', 'interface', 'let',
39
+ 'long', 'native', 'new', 'null', 'of', 'package', 'private', 'protected',
40
+ 'public', 'return', 'short', 'static', 'super', 'switch', 'synchronized',
41
+ 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof',
42
+ 'undefined', 'var', 'void', 'volatile', 'while', 'with', 'yield',
43
+ 'async', 'from', 'get', 'set', 'of', 'as', 'type',
44
+ ]);
45
+
46
+ /**
47
+ * Well-known globals that should not be renamed.
48
+ */
49
+ const GLOBALS = new Set([
50
+ 'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
51
+ 'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'Proxy', 'Reflect',
52
+ 'Date', 'RegExp', 'Error', 'TypeError', 'RangeError', 'SyntaxError',
53
+ 'ReferenceError', 'URIError', 'EvalError', 'JSON', 'Math', 'Intl',
54
+ 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Float32Array',
55
+ 'Float64Array', 'Int8Array', 'Int16Array', 'Int32Array', 'Uint8Array',
56
+ 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray',
57
+ 'console', 'process', 'Buffer', 'global', 'globalThis', 'window',
58
+ 'document', 'navigator', 'location', 'history', 'fetch', 'setTimeout',
59
+ 'setInterval', 'clearTimeout', 'clearInterval', 'setImmediate',
60
+ 'queueMicrotask', 'requestAnimationFrame', 'cancelAnimationFrame',
61
+ 'URL', 'URLSearchParams', 'Headers', 'Request', 'Response',
62
+ 'TextEncoder', 'TextDecoder', 'AbortController', 'AbortSignal',
63
+ 'EventTarget', 'Event', 'CustomEvent', 'MessageChannel', 'MessagePort',
64
+ 'Worker', 'ReadableStream', 'WritableStream', 'TransformStream',
65
+ 'require', 'module', 'exports', '__dirname', '__filename',
66
+ 'crypto', 'fs', 'path', 'os', 'http', 'https', 'net', 'child_process',
67
+ 'stream', 'events', 'util', 'assert', 'zlib', 'querystring',
68
+ 'NaN', 'Infinity',
69
+ ]);
70
+
71
+ /**
72
+ * Check if an identifier looks minified and is a candidate for renaming.
73
+ * @param {string} name
74
+ * @returns {boolean}
75
+ */
76
+ function isMinifiedName(name) {
77
+ if (RESERVED.has(name) || GLOBALS.has(name)) return false;
78
+ if (name.startsWith('_') && name.length > 2) return false;
79
+ return MINIFIED_ID.test(name);
80
+ }
81
+
82
+ /**
83
+ * Find all occurrences of a whole-word identifier in source.
84
+ * Returns array of { start, end } positions.
85
+ *
86
+ * @param {string} source
87
+ * @param {string} identifier
88
+ * @returns {Array<{start: number, end: number}>}
89
+ */
90
+ function findAllReferences(source, identifier) {
91
+ const refs = [];
92
+ const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93
+ const re = new RegExp(`(?<![a-zA-Z0-9_$])${escaped}(?![a-zA-Z0-9_$])`, 'g');
94
+ let match;
95
+ while ((match = re.exec(source)) !== null) {
96
+ // Skip if inside a string literal or comment
97
+ if (!isInsideStringOrComment(source, match.index)) {
98
+ refs.push({ start: match.index, end: match.index + identifier.length });
99
+ }
100
+ }
101
+ return refs;
102
+ }
103
+
104
+ /**
105
+ * Rough check: is position inside a string literal or comment?
106
+ * Scans backwards from the position to check context.
107
+ * Not perfect but handles most beautified code correctly.
108
+ *
109
+ * @param {string} source
110
+ * @param {number} pos
111
+ * @returns {boolean}
112
+ */
113
+ function isInsideStringOrComment(source, pos) {
114
+ // Check the current line for string/comment context
115
+ let lineStart = source.lastIndexOf('\n', pos - 1) + 1;
116
+ const linePrefix = source.substring(lineStart, pos);
117
+
118
+ // Inside a single-line comment
119
+ if (linePrefix.includes('//')) {
120
+ const commentStart = linePrefix.lastIndexOf('//');
121
+ // Make sure the // is not inside a string
122
+ const beforeComment = linePrefix.substring(0, commentStart);
123
+ const singleQuotes = (beforeComment.match(/'/g) || []).length;
124
+ const doubleQuotes = (beforeComment.match(/"/g) || []).length;
125
+ const backticks = (beforeComment.match(/`/g) || []).length;
126
+ if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {
127
+ return true;
128
+ }
129
+ }
130
+
131
+ // Count unescaped quotes before position on this line
132
+ let inString = false;
133
+ let stringChar = null;
134
+ for (let i = lineStart; i < pos; i++) {
135
+ const ch = source[i];
136
+ if (inString) {
137
+ if (ch === '\\') { i++; continue; }
138
+ if (ch === stringChar) { inString = false; stringChar = null; }
139
+ } else {
140
+ if (ch === '"' || ch === "'" || ch === '`') {
141
+ inString = true;
142
+ stringChar = ch;
143
+ }
144
+ }
145
+ }
146
+ return inString;
147
+ }
148
+
149
+ /**
150
+ * Apply a single rename across the entire source, replacing all whole-word
151
+ * occurrences of oldName with newName.
152
+ *
153
+ * @param {string} source
154
+ * @param {string} oldName
155
+ * @param {string} newName
156
+ * @returns {string}
157
+ */
158
+ function applyRename(source, oldName, newName) {
159
+ if (oldName === newName) return source;
160
+ const refs = findAllReferences(source, oldName);
161
+ if (refs.length === 0) return source;
162
+
163
+ // Apply replacements from end to start to preserve positions
164
+ const chars = source.split('');
165
+ for (let i = refs.length - 1; i >= 0; i--) {
166
+ const { start, end } = refs[i];
167
+ chars.splice(start, end - start, newName);
168
+ }
169
+ return chars.join('');
170
+ }
171
+
172
+ /**
173
+ * Apply multiple renames to source in a single pass.
174
+ * Renames are applied in dependency order: longest old names first
175
+ * to avoid partial-match issues.
176
+ *
177
+ * @param {string} source
178
+ * @param {Array<{oldName: string, newName: string}>} renames
179
+ * @returns {string}
180
+ */
181
+ function applyAllRenames(source, renames) {
182
+ if (!renames || renames.length === 0) return source;
183
+
184
+ // Sort by old name length descending to prevent substring conflicts
185
+ const sorted = [...renames].sort((a, b) => b.oldName.length - a.oldName.length);
186
+
187
+ // Build a replacement map for a single-pass approach
188
+ // Collect all match positions for all renames
189
+ const allMatches = [];
190
+ for (const { oldName, newName } of sorted) {
191
+ if (oldName === newName) continue;
192
+ const refs = findAllReferences(source, oldName);
193
+ for (const ref of refs) {
194
+ allMatches.push({ ...ref, newName });
195
+ }
196
+ }
197
+
198
+ if (allMatches.length === 0) return source;
199
+
200
+ // Sort by position descending, apply from end to start
201
+ allMatches.sort((a, b) => b.start - a.start);
202
+
203
+ // Remove overlapping matches (keep the one with longer oldName)
204
+ const filtered = [allMatches[0]];
205
+ for (let i = 1; i < allMatches.length; i++) {
206
+ const prev = filtered[filtered.length - 1];
207
+ if (allMatches[i].end <= prev.start) {
208
+ filtered.push(allMatches[i]);
209
+ }
210
+ }
211
+
212
+ let result = source;
213
+ for (const { start, end, newName } of filtered) {
214
+ result = result.substring(0, start) + newName + result.substring(end);
215
+ }
216
+
217
+ return result;
218
+ }
219
+
220
+ /**
221
+ * Scan source for all unique identifiers that look minified.
222
+ *
223
+ * @param {string} source
224
+ * @returns {string[]} sorted list of minified identifiers
225
+ */
226
+ function findMinifiedIdentifiers(source) {
227
+ const ids = new Set();
228
+ // Match word-boundary identifiers
229
+ const re = /(?<![a-zA-Z0-9_$])([a-zA-Z$_][a-zA-Z0-9$_]*)(?![a-zA-Z0-9_$])/g;
230
+ let match;
231
+ while ((match = re.exec(source)) !== null) {
232
+ const id = match[1];
233
+ if (isMinifiedName(id) && !isInsideStringOrComment(source, match.index)) {
234
+ ids.add(id);
235
+ }
236
+ }
237
+ return [...ids].sort();
238
+ }
239
+
240
+ /**
241
+ * Extract the local context around each occurrence of an identifier.
242
+ * Returns strings that appear near the identifier (within ~200 chars).
243
+ * Used by the name predictor for context-based inference.
244
+ *
245
+ * @param {string} source
246
+ * @param {string} identifier
247
+ * @param {number} [windowSize=200]
248
+ * @returns {string[]} context strings (deduplicated)
249
+ */
250
+ function extractContext(source, identifier, windowSize = 200) {
251
+ const refs = findAllReferences(source, identifier);
252
+ const contexts = new Set();
253
+
254
+ for (const { start, end } of refs) {
255
+ const ctxStart = Math.max(0, start - windowSize);
256
+ const ctxEnd = Math.min(source.length, end + windowSize);
257
+ const ctx = source.substring(ctxStart, ctxEnd);
258
+
259
+ // Extract string literals from context
260
+ const strings = ctx.match(/["']([^"']{2,60})["']/g) || [];
261
+ for (const s of strings) {
262
+ contexts.add(s.replace(/^["']|["']$/g, ''));
263
+ }
264
+
265
+ // Extract property accesses: .propertyName
266
+ const props = ctx.match(/\.([a-zA-Z_]\w{1,30})/g) || [];
267
+ for (const p of props) {
268
+ contexts.add(p);
269
+ }
270
+ }
271
+
272
+ return [...contexts];
273
+ }
274
+
275
+ module.exports = {
276
+ findAllReferences,
277
+ applyRename,
278
+ applyAllRenames,
279
+ findMinifiedIdentifiers,
280
+ extractContext,
281
+ isMinifiedName,
282
+ isInsideStringOrComment,
283
+ RESERVED,
284
+ GLOBALS,
285
+ };
@@ -0,0 +1,285 @@
1
+ /**
2
+ * statement-parser.js - Parse JavaScript source into top-level statements.
3
+ *
4
+ * Tracks brace/paren/bracket depth and string/template/regex contexts
5
+ * to split at true statement boundaries. Never splits a statement
6
+ * across modules -- a statement is atomic.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /**
12
+ * Parse source into top-level statements by tracking brace/paren/bracket depth.
13
+ *
14
+ * A "top-level statement" ends when:
15
+ * - We encounter a semicolon at depth 0, OR
16
+ * - We encounter a closing brace that brings depth to 0 AND the next
17
+ * non-whitespace token does not continue the expression (like `=`, `.`,
18
+ * `,`, `(`, etc.) -- this avoids splitting `var { x } = obj;` or
19
+ * `obj.method()` into two statements.
20
+ *
21
+ * String literals, template literals, regex literals, and comments are
22
+ * tracked so delimiters inside them are not counted.
23
+ *
24
+ * @param {string} source
25
+ * @returns {Array<{code: string, start: number, end: number}>}
26
+ */
27
+ function parseTopLevelStatements(source) {
28
+ const statements = [];
29
+ let depth = 0;
30
+ let start = 0;
31
+ let i = 0;
32
+ const len = source.length;
33
+
34
+ while (i < len) {
35
+ const ch = source[i];
36
+ const next = i + 1 < len ? source[i + 1] : '';
37
+
38
+ // ── Skip single-line comments ──
39
+ if (ch === '/' && next === '/') {
40
+ const eol = source.indexOf('\n', i + 2);
41
+ i = eol === -1 ? len : eol + 1;
42
+ continue;
43
+ }
44
+
45
+ // ── Skip multi-line comments ──
46
+ if (ch === '/' && next === '*') {
47
+ const end = source.indexOf('*/', i + 2);
48
+ i = end === -1 ? len : end + 2;
49
+ continue;
50
+ }
51
+
52
+ // ── Skip string literals ──
53
+ if (ch === '"' || ch === "'") {
54
+ i = skipString(source, i, ch);
55
+ continue;
56
+ }
57
+
58
+ // ── Skip template literals ──
59
+ if (ch === '`') {
60
+ i = skipTemplateLiteral(source, i);
61
+ continue;
62
+ }
63
+
64
+ // ── Skip regex literals ──
65
+ if (ch === '/' && isRegexStart(source, i)) {
66
+ i = skipRegex(source, i);
67
+ continue;
68
+ }
69
+
70
+ // ── Track depth ──
71
+ if (ch === '{' || ch === '(' || ch === '[') {
72
+ depth++;
73
+ i++;
74
+ continue;
75
+ }
76
+
77
+ if (ch === '}' || ch === ')' || ch === ']') {
78
+ depth = Math.max(0, depth - 1);
79
+
80
+ // Closing brace at depth 0 MAY be a statement boundary
81
+ if (depth === 0 && ch === '}') {
82
+ if (!isStatementBoundaryAfterBrace(source, i + 1)) {
83
+ i++;
84
+ continue;
85
+ }
86
+
87
+ const code = source.substring(start, i + 1).trim();
88
+ if (code.length > 0) {
89
+ statements.push({ code, start, end: i + 1 });
90
+ }
91
+ start = i + 1;
92
+ i++;
93
+ continue;
94
+ }
95
+
96
+ i++;
97
+ continue;
98
+ }
99
+
100
+ // ── Semicolon at depth 0 is a statement boundary ──
101
+ if (ch === ';' && depth === 0) {
102
+ const code = source.substring(start, i + 1).trim();
103
+ if (code.length > 0) {
104
+ statements.push({ code, start, end: i + 1 });
105
+ }
106
+ start = i + 1;
107
+ i++;
108
+ continue;
109
+ }
110
+
111
+ i++;
112
+ }
113
+
114
+ // Remaining code (unterminated statement)
115
+ const remaining = source.substring(start).trim();
116
+ if (remaining.length > 0) {
117
+ statements.push({ code: remaining, start, end: len });
118
+ }
119
+
120
+ return statements;
121
+ }
122
+
123
+ /**
124
+ * After a `}` at depth 0, decide whether this is truly a statement boundary.
125
+ * Returns true if it IS a boundary (next token starts a new statement).
126
+ * Returns false if the expression continues (e.g. `}.method()`, `} = obj`, etc.)
127
+ *
128
+ * @param {string} source
129
+ * @param {number} afterPos - position right after the `}`
130
+ * @returns {boolean}
131
+ */
132
+ function isStatementBoundaryAfterBrace(source, afterPos) {
133
+ const len = source.length;
134
+ let j = afterPos;
135
+
136
+ // Skip whitespace and comments to find the next meaningful token
137
+ while (j < len) {
138
+ const c = source[j];
139
+
140
+ if (c === ' ' || c === '\t' || c === '\r' || c === '\n') {
141
+ j++;
142
+ continue;
143
+ }
144
+
145
+ if (c === '/' && j + 1 < len && source[j + 1] === '/') {
146
+ const eol = source.indexOf('\n', j + 2);
147
+ j = eol === -1 ? len : eol + 1;
148
+ continue;
149
+ }
150
+
151
+ if (c === '/' && j + 1 < len && source[j + 1] === '*') {
152
+ const end = source.indexOf('*/', j + 2);
153
+ j = end === -1 ? len : end + 2;
154
+ continue;
155
+ }
156
+
157
+ break;
158
+ }
159
+
160
+ if (j >= len) return true;
161
+
162
+ const nextChar = source[j];
163
+
164
+ // These tokens CONTINUE the expression -- NOT a statement boundary
165
+ const continuationChars = '.=,([?:&|+\\-*/%<>^~!;)';
166
+ if (continuationChars.includes(nextChar)) {
167
+ return false;
168
+ }
169
+
170
+ // Check for multi-char continuation tokens
171
+ const ahead = source.substring(j, j + 15);
172
+ if (/^(?:instanceof|in|of|from)\s/.test(ahead)) return false;
173
+ if (/^as\s/.test(ahead)) return false;
174
+
175
+ return true;
176
+ }
177
+
178
+ /**
179
+ * Skip a string literal starting at position i (where source[i] is the quote).
180
+ * @param {string} source
181
+ * @param {number} i
182
+ * @param {string} quote - the quote character
183
+ * @returns {number}
184
+ */
185
+ function skipString(source, i, quote) {
186
+ const len = source.length;
187
+ i++;
188
+ while (i < len) {
189
+ if (source[i] === '\\') { i += 2; continue; }
190
+ if (source[i] === quote) return i + 1;
191
+ i++;
192
+ }
193
+ return len;
194
+ }
195
+
196
+ /**
197
+ * Skip a template literal starting at position i (where source[i] is backtick).
198
+ * @param {string} source
199
+ * @param {number} i
200
+ * @returns {number}
201
+ */
202
+ function skipTemplateLiteral(source, i) {
203
+ const len = source.length;
204
+ i++;
205
+ while (i < len) {
206
+ if (source[i] === '\\') { i += 2; continue; }
207
+ if (source[i] === '`') return i + 1;
208
+ if (source[i] === '$' && i + 1 < len && source[i + 1] === '{') {
209
+ i = skipTemplateExpression(source, i + 2);
210
+ continue;
211
+ }
212
+ i++;
213
+ }
214
+ return len;
215
+ }
216
+
217
+ /**
218
+ * Skip a template expression (inside ${...}) starting after the opening ${.
219
+ * @param {string} source
220
+ * @param {number} i
221
+ * @returns {number}
222
+ */
223
+ function skipTemplateExpression(source, i) {
224
+ const len = source.length;
225
+ let exprDepth = 1;
226
+ while (i < len && exprDepth > 0) {
227
+ const ch = source[i];
228
+ if (ch === '\\') { i += 2; continue; }
229
+ if (ch === '{') { exprDepth++; i++; continue; }
230
+ if (ch === '}') { exprDepth--; i++; continue; }
231
+ if (ch === '`') { i = skipTemplateLiteral(source, i); continue; }
232
+ if (ch === '"' || ch === "'") { i = skipString(source, i, ch); continue; }
233
+ i++;
234
+ }
235
+ return i;
236
+ }
237
+
238
+ /**
239
+ * Heuristic: is source[i] the start of a regex literal?
240
+ * @param {string} source
241
+ * @param {number} i
242
+ * @returns {boolean}
243
+ */
244
+ function isRegexStart(source, i) {
245
+ let j = i - 1;
246
+ while (j >= 0 && (source[j] === ' ' || source[j] === '\t' || source[j] === '\n' || source[j] === '\r')) {
247
+ j--;
248
+ }
249
+ if (j < 0) return true;
250
+ const prev = source[j];
251
+ if (/[\w$)\].]/.test(prev)) return false;
252
+ return true;
253
+ }
254
+
255
+ /**
256
+ * Skip a regex literal starting at position i.
257
+ * @param {string} source
258
+ * @param {number} i
259
+ * @returns {number}
260
+ */
261
+ function skipRegex(source, i) {
262
+ const len = source.length;
263
+ i++;
264
+ while (i < len) {
265
+ if (source[i] === '\\') { i += 2; continue; }
266
+ if (source[i] === '[') {
267
+ i++;
268
+ while (i < len && source[i] !== ']') {
269
+ if (source[i] === '\\') { i += 2; continue; }
270
+ i++;
271
+ }
272
+ i++;
273
+ continue;
274
+ }
275
+ if (source[i] === '/') {
276
+ i++;
277
+ while (i < len && /[gimsuy]/.test(source[i])) i++;
278
+ return i;
279
+ }
280
+ i++;
281
+ }
282
+ return len;
283
+ }
284
+
285
+ module.exports = { parseTopLevelStatements };