trickle-observe 0.2.86 → 0.2.88

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.
@@ -327,6 +327,33 @@ function findClosingBrace(source, openBrace) {
327
327
  pos++;
328
328
  pos++; // skip past /
329
329
  }
330
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
331
+ // Possible regex literal — check preceding context
332
+ let p = pos - 1;
333
+ while (p >= 0 && (source[p] === ' ' || source[p] === '\t'))
334
+ p--;
335
+ const prevCh = p >= 0 ? source[p] : '';
336
+ if ('=(!,;:?[{&|^~+-><%'.includes(prevCh) || source.slice(Math.max(0, p - 5), p + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
337
+ pos++;
338
+ while (pos < source.length) {
339
+ if (source[pos] === '\\')
340
+ pos++;
341
+ else if (source[pos] === '[') {
342
+ pos++;
343
+ while (pos < source.length && source[pos] !== ']') {
344
+ if (source[pos] === '\\')
345
+ pos++;
346
+ pos++;
347
+ }
348
+ }
349
+ else if (source[pos] === '/')
350
+ break;
351
+ pos++;
352
+ }
353
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1]))
354
+ pos++;
355
+ }
356
+ }
330
357
  pos++;
331
358
  }
332
359
  return -1; // not found
@@ -390,6 +417,11 @@ function findVarDeclarations(source, lineOffset = 0) {
390
417
  const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
391
418
  if (/^\s*require\s*\(/.test(restOfLine))
392
419
  continue;
420
+ // Skip variable declarations inside for-loop headers (for (let x = ...; ...; ...))
421
+ // The semicolon inside for(...) is NOT a statement end
422
+ const beforeDecl = source.slice(Math.max(0, vmatch.index - 50), vmatch.index);
423
+ if (/\bfor\s*\(\s*$/.test(beforeDecl))
424
+ continue;
393
425
  // Calculate line number (count newlines before this position)
394
426
  // Subtract lineOffset to map compiled line numbers back to original source lines
395
427
  let lineNo = 1;
@@ -421,8 +453,29 @@ function findVarDeclarations(source, lineOffset = 0) {
421
453
  else if (ch === '\n' && depth === 0) {
422
454
  // For semicolon-free code, the newline is the end
423
455
  // But only if the next non-whitespace isn't a continuation (., +, etc.)
456
+ // AND the previous non-whitespace isn't an operator expecting more (=, +, -, etc.)
424
457
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
425
458
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
459
+ // Also check if a recent line ends with an operator that expects a value on the next line
460
+ // Walk backwards through empty lines to find the last non-empty line
461
+ let checkPos = pos;
462
+ let lastChar = '';
463
+ for (let back = 0; back < 5; back++) {
464
+ const prevNL = source.lastIndexOf('\n', checkPos - 1);
465
+ const prevLine = source.slice(prevNL + 1, checkPos).trimEnd();
466
+ if (prevLine.length > 0) {
467
+ lastChar = prevLine[prevLine.length - 1];
468
+ break;
469
+ }
470
+ checkPos = prevNL;
471
+ if (prevNL <= 0)
472
+ break;
473
+ }
474
+ if (lastChar && '=+-*/%&|^~<>?:,({['.includes(lastChar)) {
475
+ // Line ends with operator — this is a continuation, don't end the statement
476
+ pos++;
477
+ continue;
478
+ }
426
479
  foundEnd = pos;
427
480
  break;
428
481
  }
@@ -440,6 +493,38 @@ function findVarDeclarations(source, lineOffset = 0) {
440
493
  pos++;
441
494
  }
442
495
  }
496
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
497
+ // Possible regex literal — check if the preceding non-whitespace indicates regex context
498
+ // (after =, (, ,, ;, !, &, |, ^, ~, ?, :, [, {, return, typeof, etc.)
499
+ let p = pos - 1;
500
+ while (p >= 0 && (source[p] === ' ' || source[p] === '\t'))
501
+ p--;
502
+ const prevCh = p >= 0 ? source[p] : '';
503
+ if ('=(!,;:?[{&|^~+-><%'.includes(prevCh) || source.slice(Math.max(0, p - 5), p + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
504
+ // This is a regex literal — skip to the closing /
505
+ pos++; // skip past opening /
506
+ while (pos < source.length) {
507
+ if (source[pos] === '\\') {
508
+ pos++;
509
+ } // skip escaped char
510
+ else if (source[pos] === '[') {
511
+ // Character class — skip to ]
512
+ pos++;
513
+ while (pos < source.length && source[pos] !== ']') {
514
+ if (source[pos] === '\\')
515
+ pos++;
516
+ pos++;
517
+ }
518
+ }
519
+ else if (source[pos] === '/')
520
+ break;
521
+ pos++;
522
+ }
523
+ // Skip regex flags
524
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1]))
525
+ pos++;
526
+ }
527
+ }
443
528
  else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
444
529
  // Skip line comment
445
530
  while (pos < source.length && source[pos] !== '\n')
@@ -644,9 +729,14 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
644
729
  while ((methodMatch = methodRegex.exec(classBody)) !== null) {
645
730
  const isStatic = !!methodMatch[1];
646
731
  const methodName = methodMatch[2];
647
- // Skip constructor and private methods
732
+ // Skip constructor, private methods, and JS keywords that look like method calls
648
733
  if (methodName === 'constructor' || methodName.startsWith('_'))
649
734
  continue;
735
+ if (['if', 'else', 'for', 'while', 'do', 'switch', 'case', 'return', 'throw',
736
+ 'try', 'catch', 'finally', 'with', 'new', 'delete', 'typeof', 'void',
737
+ 'yield', 'await', 'import', 'export', 'super', 'this', 'class',
738
+ 'break', 'continue', 'debugger', 'in', 'of', 'instanceof'].includes(methodName))
739
+ continue;
650
740
  // Extract param names
651
741
  const mParamStr = methodMatch[3].trim();
652
742
  const mParamNames = mParamStr
package/dist/trace-var.js CHANGED
@@ -177,8 +177,11 @@ function flushVarBuffer() {
177
177
  * More aggressive truncation than function samples since there are many more variables.
178
178
  */
179
179
  function sanitizeVarSample(value, depth = 3) {
180
- if (value === null || value === undefined)
181
- return value;
180
+ if (value === null)
181
+ return null;
182
+ // JSON.stringify drops undefined values, so use null to preserve the field
183
+ if (value === undefined)
184
+ return null;
182
185
  const t = typeof value;
183
186
  // Primitives are always safe to return at any depth
184
187
  if (t === 'string') {
@@ -632,13 +632,25 @@ function findReassignments(source) {
632
632
  const beforeOnLine = source.slice(lineStart, match.index).trim();
633
633
  if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(','))
634
634
  continue;
635
+ // Skip comma-separated multi-variable declaration continuations:
636
+ // var X = 'foo',
637
+ // Y = 'bar'; ← Y looks like a reassignment but is actually a declaration
638
+ // Detect by checking if the previous non-empty line ends with ','
639
+ if (beforeOnLine.length === 0) {
640
+ const prevLineEnd = source.lastIndexOf('\n', lineStart - 1);
641
+ if (prevLineEnd >= 0) {
642
+ const prevLine = source.slice(source.lastIndexOf('\n', prevLineEnd - 1) + 1, prevLineEnd).trimEnd();
643
+ if (prevLine.endsWith(','))
644
+ continue;
645
+ }
646
+ }
635
647
  // Calculate line number
636
648
  let lineNo = 1;
637
649
  for (let i = 0; i < match.index; i++) {
638
650
  if (source[i] === '\n')
639
651
  lineNo++;
640
652
  }
641
- // Find end of statement (same logic as findVarDeclarations)
653
+ // Find end of statement
642
654
  const startPos = match.index + match[0].length - 1;
643
655
  let pos = startPos;
644
656
  let depth = 0;
@@ -660,6 +672,24 @@ function findReassignments(source) {
660
672
  else if (ch === '\n' && depth === 0) {
661
673
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
662
674
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
675
+ // Check if a recent non-empty line ends with an operator
676
+ let checkPos2 = pos;
677
+ let lastCh2 = '';
678
+ for (let back = 0; back < 5; back++) {
679
+ const prevNL2 = source.lastIndexOf('\n', checkPos2 - 1);
680
+ const prevLine2 = source.slice(prevNL2 + 1, checkPos2).trimEnd();
681
+ if (prevLine2.length > 0) {
682
+ lastCh2 = prevLine2[prevLine2.length - 1];
683
+ break;
684
+ }
685
+ checkPos2 = prevNL2;
686
+ if (prevNL2 <= 0)
687
+ break;
688
+ }
689
+ if (lastCh2 && '=+-*/%&|^~<>?:,({['.includes(lastCh2)) {
690
+ pos++;
691
+ continue;
692
+ }
663
693
  foundEnd = pos;
664
694
  break;
665
695
  }
@@ -676,6 +706,33 @@ function findReassignments(source) {
676
706
  pos++;
677
707
  }
678
708
  }
709
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
710
+ // Possible regex literal
711
+ let rp = pos - 1;
712
+ while (rp >= 0 && (source[rp] === ' ' || source[rp] === '\t'))
713
+ rp--;
714
+ const rpCh = rp >= 0 ? source[rp] : '';
715
+ if ('=(!,;:?[{&|^~+-><%'.includes(rpCh) || source.slice(Math.max(0, rp - 5), rp + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
716
+ pos++;
717
+ while (pos < source.length) {
718
+ if (source[pos] === '\\')
719
+ pos++;
720
+ else if (source[pos] === '[') {
721
+ pos++;
722
+ while (pos < source.length && source[pos] !== ']') {
723
+ if (source[pos] === '\\')
724
+ pos++;
725
+ pos++;
726
+ }
727
+ }
728
+ else if (source[pos] === '/')
729
+ break;
730
+ pos++;
731
+ }
732
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1]))
733
+ pos++;
734
+ }
735
+ }
679
736
  else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
680
737
  while (pos < source.length && source[pos] !== '\n')
681
738
  pos++;
@@ -619,13 +619,25 @@ export function findReassignments(source) {
619
619
  const beforeOnLine = source.slice(lineStart, match.index).trim();
620
620
  if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(','))
621
621
  continue;
622
+ // Skip comma-separated multi-variable declaration continuations:
623
+ // var X = 'foo',
624
+ // Y = 'bar'; ← Y looks like a reassignment but is actually a declaration
625
+ // Detect by checking if the previous non-empty line ends with ','
626
+ if (beforeOnLine.length === 0) {
627
+ const prevLineEnd = source.lastIndexOf('\n', lineStart - 1);
628
+ if (prevLineEnd >= 0) {
629
+ const prevLine = source.slice(source.lastIndexOf('\n', prevLineEnd - 1) + 1, prevLineEnd).trimEnd();
630
+ if (prevLine.endsWith(','))
631
+ continue;
632
+ }
633
+ }
622
634
  // Calculate line number
623
635
  let lineNo = 1;
624
636
  for (let i = 0; i < match.index; i++) {
625
637
  if (source[i] === '\n')
626
638
  lineNo++;
627
639
  }
628
- // Find end of statement (same logic as findVarDeclarations)
640
+ // Find end of statement
629
641
  const startPos = match.index + match[0].length - 1;
630
642
  let pos = startPos;
631
643
  let depth = 0;
@@ -647,6 +659,24 @@ export function findReassignments(source) {
647
659
  else if (ch === '\n' && depth === 0) {
648
660
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
649
661
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
662
+ // Check if a recent non-empty line ends with an operator
663
+ let checkPos2 = pos;
664
+ let lastCh2 = '';
665
+ for (let back = 0; back < 5; back++) {
666
+ const prevNL2 = source.lastIndexOf('\n', checkPos2 - 1);
667
+ const prevLine2 = source.slice(prevNL2 + 1, checkPos2).trimEnd();
668
+ if (prevLine2.length > 0) {
669
+ lastCh2 = prevLine2[prevLine2.length - 1];
670
+ break;
671
+ }
672
+ checkPos2 = prevNL2;
673
+ if (prevNL2 <= 0)
674
+ break;
675
+ }
676
+ if (lastCh2 && '=+-*/%&|^~<>?:,({['.includes(lastCh2)) {
677
+ pos++;
678
+ continue;
679
+ }
650
680
  foundEnd = pos;
651
681
  break;
652
682
  }
@@ -663,6 +693,33 @@ export function findReassignments(source) {
663
693
  pos++;
664
694
  }
665
695
  }
696
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
697
+ // Possible regex literal
698
+ let rp = pos - 1;
699
+ while (rp >= 0 && (source[rp] === ' ' || source[rp] === '\t'))
700
+ rp--;
701
+ const rpCh = rp >= 0 ? source[rp] : '';
702
+ if ('=(!,;:?[{&|^~+-><%'.includes(rpCh) || source.slice(Math.max(0, rp - 5), rp + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
703
+ pos++;
704
+ while (pos < source.length) {
705
+ if (source[pos] === '\\')
706
+ pos++;
707
+ else if (source[pos] === '[') {
708
+ pos++;
709
+ while (pos < source.length && source[pos] !== ']') {
710
+ if (source[pos] === '\\')
711
+ pos++;
712
+ pos++;
713
+ }
714
+ }
715
+ else if (source[pos] === '/')
716
+ break;
717
+ pos++;
718
+ }
719
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1]))
720
+ pos++;
721
+ }
722
+ }
666
723
  else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
667
724
  while (pos < source.length && source[pos] !== '\n')
668
725
  pos++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.86",
3
+ "version": "0.2.88",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -334,6 +334,21 @@ function findClosingBrace(source: string, openBrace: number): number {
334
334
  pos += 2;
335
335
  while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
336
336
  pos++; // skip past /
337
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
338
+ // Possible regex literal — check preceding context
339
+ let p = pos - 1;
340
+ while (p >= 0 && (source[p] === ' ' || source[p] === '\t')) p--;
341
+ const prevCh = p >= 0 ? source[p] : '';
342
+ if ('=(!,;:?[{&|^~+-><%'.includes(prevCh) || source.slice(Math.max(0, p - 5), p + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
343
+ pos++;
344
+ while (pos < source.length) {
345
+ if (source[pos] === '\\') pos++;
346
+ else if (source[pos] === '[') { pos++; while (pos < source.length && source[pos] !== ']') { if (source[pos] === '\\') pos++; pos++; } }
347
+ else if (source[pos] === '/') break;
348
+ pos++;
349
+ }
350
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1])) pos++;
351
+ }
337
352
  }
338
353
  pos++;
339
354
  }
@@ -391,6 +406,11 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
391
406
  const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
392
407
  if (/^\s*require\s*\(/.test(restOfLine)) continue;
393
408
 
409
+ // Skip variable declarations inside for-loop headers (for (let x = ...; ...; ...))
410
+ // The semicolon inside for(...) is NOT a statement end
411
+ const beforeDecl = source.slice(Math.max(0, vmatch.index - 50), vmatch.index);
412
+ if (/\bfor\s*\(\s*$/.test(beforeDecl)) continue;
413
+
394
414
  // Calculate line number (count newlines before this position)
395
415
  // Subtract lineOffset to map compiled line numbers back to original source lines
396
416
  let lineNo = 1;
@@ -419,8 +439,28 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
419
439
  } else if (ch === '\n' && depth === 0) {
420
440
  // For semicolon-free code, the newline is the end
421
441
  // But only if the next non-whitespace isn't a continuation (., +, etc.)
442
+ // AND the previous non-whitespace isn't an operator expecting more (=, +, -, etc.)
422
443
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
423
444
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
445
+ // Also check if a recent line ends with an operator that expects a value on the next line
446
+ // Walk backwards through empty lines to find the last non-empty line
447
+ let checkPos = pos;
448
+ let lastChar = '';
449
+ for (let back = 0; back < 5; back++) {
450
+ const prevNL = source.lastIndexOf('\n', checkPos - 1);
451
+ const prevLine = source.slice(prevNL + 1, checkPos).trimEnd();
452
+ if (prevLine.length > 0) {
453
+ lastChar = prevLine[prevLine.length - 1];
454
+ break;
455
+ }
456
+ checkPos = prevNL;
457
+ if (prevNL <= 0) break;
458
+ }
459
+ if (lastChar && '=+-*/%&|^~<>?:,({['.includes(lastChar)) {
460
+ // Line ends with operator — this is a continuation, don't end the statement
461
+ pos++;
462
+ continue;
463
+ }
424
464
  foundEnd = pos;
425
465
  break;
426
466
  }
@@ -433,6 +473,30 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
433
473
  else if (source[pos] === quote) break;
434
474
  pos++;
435
475
  }
476
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
477
+ // Possible regex literal — check if the preceding non-whitespace indicates regex context
478
+ // (after =, (, ,, ;, !, &, |, ^, ~, ?, :, [, {, return, typeof, etc.)
479
+ let p = pos - 1;
480
+ while (p >= 0 && (source[p] === ' ' || source[p] === '\t')) p--;
481
+ const prevCh = p >= 0 ? source[p] : '';
482
+ if ('=(!,;:?[{&|^~+-><%'.includes(prevCh) || source.slice(Math.max(0, p - 5), p + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
483
+ // This is a regex literal — skip to the closing /
484
+ pos++; // skip past opening /
485
+ while (pos < source.length) {
486
+ if (source[pos] === '\\') { pos++; } // skip escaped char
487
+ else if (source[pos] === '[') {
488
+ // Character class — skip to ]
489
+ pos++;
490
+ while (pos < source.length && source[pos] !== ']') {
491
+ if (source[pos] === '\\') pos++;
492
+ pos++;
493
+ }
494
+ } else if (source[pos] === '/') break;
495
+ pos++;
496
+ }
497
+ // Skip regex flags
498
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1])) pos++;
499
+ }
436
500
  } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
437
501
  // Skip line comment
438
502
  while (pos < source.length && source[pos] !== '\n') pos++;
@@ -627,8 +691,12 @@ function transformCjsSource(source: string, filename: string, moduleName: string
627
691
  while ((methodMatch = methodRegex.exec(classBody)) !== null) {
628
692
  const isStatic = !!methodMatch[1];
629
693
  const methodName = methodMatch[2];
630
- // Skip constructor and private methods
694
+ // Skip constructor, private methods, and JS keywords that look like method calls
631
695
  if (methodName === 'constructor' || methodName.startsWith('_')) continue;
696
+ if (['if', 'else', 'for', 'while', 'do', 'switch', 'case', 'return', 'throw',
697
+ 'try', 'catch', 'finally', 'with', 'new', 'delete', 'typeof', 'void',
698
+ 'yield', 'await', 'import', 'export', 'super', 'this', 'class',
699
+ 'break', 'continue', 'debugger', 'in', 'of', 'instanceof'].includes(methodName)) continue;
632
700
  // Extract param names
633
701
  const mParamStr = methodMatch[3].trim();
634
702
  const mParamNames = mParamStr
package/src/trace-var.ts CHANGED
@@ -167,7 +167,9 @@ function flushVarBuffer(): void {
167
167
  * More aggressive truncation than function samples since there are many more variables.
168
168
  */
169
169
  function sanitizeVarSample(value: unknown, depth: number = 3): unknown {
170
- if (value === null || value === undefined) return value;
170
+ if (value === null) return null;
171
+ // JSON.stringify drops undefined values, so use null to preserve the field
172
+ if (value === undefined) return null;
171
173
 
172
174
  const t = typeof value;
173
175
  // Primitives are always safe to return at any depth
@@ -578,13 +578,25 @@ export function findReassignments(source: string): Array<{ lineEnd: number; varN
578
578
  const beforeOnLine = source.slice(lineStart, match.index).trim();
579
579
  if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(',')) continue;
580
580
 
581
+ // Skip comma-separated multi-variable declaration continuations:
582
+ // var X = 'foo',
583
+ // Y = 'bar'; ← Y looks like a reassignment but is actually a declaration
584
+ // Detect by checking if the previous non-empty line ends with ','
585
+ if (beforeOnLine.length === 0) {
586
+ const prevLineEnd = source.lastIndexOf('\n', lineStart - 1);
587
+ if (prevLineEnd >= 0) {
588
+ const prevLine = source.slice(source.lastIndexOf('\n', prevLineEnd - 1) + 1, prevLineEnd).trimEnd();
589
+ if (prevLine.endsWith(',')) continue;
590
+ }
591
+ }
592
+
581
593
  // Calculate line number
582
594
  let lineNo = 1;
583
595
  for (let i = 0; i < match.index; i++) {
584
596
  if (source[i] === '\n') lineNo++;
585
597
  }
586
598
 
587
- // Find end of statement (same logic as findVarDeclarations)
599
+ // Find end of statement
588
600
  const startPos = match.index + match[0].length - 1;
589
601
  let pos = startPos;
590
602
  let depth = 0;
@@ -603,6 +615,19 @@ export function findReassignments(source: string): Array<{ lineEnd: number; varN
603
615
  } else if (ch === '\n' && depth === 0) {
604
616
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
605
617
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
618
+ // Check if a recent non-empty line ends with an operator
619
+ let checkPos2 = pos;
620
+ let lastCh2 = '';
621
+ for (let back = 0; back < 5; back++) {
622
+ const prevNL2 = source.lastIndexOf('\n', checkPos2 - 1);
623
+ const prevLine2 = source.slice(prevNL2 + 1, checkPos2).trimEnd();
624
+ if (prevLine2.length > 0) { lastCh2 = prevLine2[prevLine2.length - 1]; break; }
625
+ checkPos2 = prevNL2;
626
+ if (prevNL2 <= 0) break;
627
+ }
628
+ if (lastCh2 && '=+-*/%&|^~<>?:,({['.includes(lastCh2)) {
629
+ pos++; continue;
630
+ }
606
631
  foundEnd = pos;
607
632
  break;
608
633
  }
@@ -614,6 +639,21 @@ export function findReassignments(source: string): Array<{ lineEnd: number; varN
614
639
  else if (source[pos] === quote) break;
615
640
  pos++;
616
641
  }
642
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] !== '/' && source[pos + 1] !== '*') {
643
+ // Possible regex literal
644
+ let rp = pos - 1;
645
+ while (rp >= 0 && (source[rp] === ' ' || source[rp] === '\t')) rp--;
646
+ const rpCh = rp >= 0 ? source[rp] : '';
647
+ if ('=(!,;:?[{&|^~+-><%'.includes(rpCh) || source.slice(Math.max(0, rp - 5), rp + 1).match(/\b(return|typeof|instanceof|in|of|void|delete|throw|new|case)\s*$/)) {
648
+ pos++;
649
+ while (pos < source.length) {
650
+ if (source[pos] === '\\') pos++;
651
+ else if (source[pos] === '[') { pos++; while (pos < source.length && source[pos] !== ']') { if (source[pos] === '\\') pos++; pos++; } }
652
+ else if (source[pos] === '/') break;
653
+ pos++;
654
+ }
655
+ while (pos + 1 < source.length && /[gimsuy]/.test(source[pos + 1])) pos++;
656
+ }
617
657
  } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
618
658
  while (pos < source.length && source[pos] !== '\n') pos++;
619
659
  continue;