thuban 0.3.2 → 0.3.3

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.
@@ -149,21 +149,34 @@ class CopyPasteDriftDetector {
149
149
  }
150
150
 
151
151
  _normalize(code) {
152
- return code
152
+ // Two-pass normalization:
153
+ // Pass 1: collect all declared variable names
154
+ const declaredNames = [];
155
+ const declPattern = /(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
156
+ let m;
157
+ while ((m = declPattern.exec(code)) !== null) {
158
+ if (!declaredNames.includes(m[1])) declaredNames.push(m[1]);
159
+ }
160
+
161
+ let normalized = code
153
162
  // Remove comments
154
163
  .replace(/\/\/.*$/gm, '')
155
164
  .replace(/\/\*[\s\S]*?\*\//g, '')
156
- // Normalize whitespace
157
- .replace(/\s+/g, ' ')
158
- // Normalize variable names to placeholders
159
- .replace(/(?:const|let|var)\s+([a-zA-Z_$]\w*)/g, 'const VAR')
160
165
  // Normalize string literals
161
166
  .replace(/'[^']*'/g, 'STR')
162
167
  .replace(/"[^"]*"/g, 'STR')
163
168
  .replace(/`[^`]*`/g, 'STR')
164
169
  // Normalize numbers
165
- .replace(/\b\d+\.?\d*\b/g, 'NUM')
166
- .trim();
170
+ .replace(/\b\d+\.?\d*\b/g, 'NUM');
171
+
172
+ // Pass 2: replace ALL occurrences of each declared name with VAR_N
173
+ declaredNames.forEach((name, idx) => {
174
+ const nameRegex = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
175
+ normalized = normalized.replace(nameRegex, `VAR_${idx}`);
176
+ });
177
+
178
+ // Normalize whitespace
179
+ return normalized.replace(/\s+/g, ' ').trim();
167
180
  }
168
181
 
169
182
  _tokenize(normalized) {
@@ -218,10 +231,23 @@ class CopyPasteDriftDetector {
218
231
 
219
232
  _detectFunction(line) {
220
233
  const trimmed = line.trim();
234
+ // JavaScript/TypeScript: function/const/let/var
221
235
  let match = trimmed.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
222
236
  if (match) return { name: match[1] };
223
237
  match = trimmed.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/);
224
238
  if (match) return { name: match[1] };
239
+ // Kotlin: fun
240
+ match = trimmed.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
241
+ if (match) return { name: match[1] };
242
+ // Rust: fn
243
+ match = trimmed.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
244
+ if (match) return { name: match[1] };
245
+ // PHP: function
246
+ match = trimmed.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
247
+ if (match) return { name: match[1] };
248
+ // Ruby: def
249
+ match = trimmed.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/);
250
+ if (match) return { name: match[1] };
225
251
  return null;
226
252
  }
227
253