ruvector 0.2.27 → 0.2.29
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.
- package/LICENSE +21 -21
- package/README.md +2270 -2270
- package/bin/cli.js +9570 -9479
- package/bin/mcp-server.js +3854 -3854
- package/dist/core/intelligence-engine.d.ts +13 -0
- package/dist/core/intelligence-engine.d.ts.map +1 -1
- package/dist/core/intelligence-engine.js +38 -0
- package/dist/core/onnx/bundled-parallel.mjs +164 -164
- package/dist/core/onnx/embed-worker.mjs +67 -67
- package/dist/core/onnx/loader.js +434 -434
- package/dist/core/onnx/package.json +3 -3
- package/dist/core/onnx/pkg/LICENSE +21 -21
- package/dist/core/onnx/pkg/loader.js +348 -348
- package/dist/core/onnx/pkg/package.json +3 -3
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm.d.ts +112 -112
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm.js +5 -5
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm_bg.js +638 -638
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm_bg.wasm.d.ts +29 -29
- package/dist/core/onnx-embedder.d.ts.map +1 -1
- package/dist/core/onnx-embedder.js +24 -30
- package/dist/core/parallel-workers.js +439 -439
- package/dist/workers/benchmark.js +15 -15
- package/package.json +122 -122
- package/src/decompiler/api-prober.js +302 -302
- package/src/decompiler/index.js +463 -463
- package/src/decompiler/metrics.js +86 -86
- package/src/decompiler/model-decompiler.js +423 -423
- package/src/decompiler/module-splitter.js +498 -498
- package/src/decompiler/module-tree.js +142 -142
- package/src/decompiler/name-predictor.js +400 -400
- package/src/decompiler/npm-fetch.js +176 -176
- package/src/decompiler/reconstructor.js +499 -499
- package/src/decompiler/reference-tracker.js +285 -285
- package/src/decompiler/statement-parser.js +285 -285
- package/src/decompiler/style-improver.js +438 -438
- package/src/decompiler/subcategories.js +339 -339
- package/src/decompiler/validator.js +379 -379
- package/src/decompiler/witness.js +140 -140
- package/wasm/package.json +26 -26
- package/wasm/ruvector_decompiler_wasm.d.ts +27 -27
- package/wasm/ruvector_decompiler_wasm.js +220 -220
- package/wasm/ruvector_decompiler_wasm_bg.wasm.d.ts +16 -16
- package/dist/core/onnx/pkg/ruvector.db +0 -0
|
@@ -1,285 +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
|
-
};
|
|
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
|
+
};
|