trickle-observe 0.2.87 → 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
@@ -429,9 +456,21 @@ function findVarDeclarations(source, lineOffset = 0) {
429
456
  // AND the previous non-whitespace isn't an operator expecting more (=, +, -, etc.)
430
457
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
431
458
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
432
- // Also check if the line ends with an operator that expects a value on the next line
433
- const prevOnLine = source.slice(source.lastIndexOf('\n', pos - 1) + 1, pos).trimEnd();
434
- const lastChar = prevOnLine[prevOnLine.length - 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
+ }
435
474
  if (lastChar && '=+-*/%&|^~<>?:,({['.includes(lastChar)) {
436
475
  // Line ends with operator — this is a continuation, don't end the statement
437
476
  pos++;
@@ -454,6 +493,38 @@ function findVarDeclarations(source, lineOffset = 0) {
454
493
  pos++;
455
494
  }
456
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
+ }
457
528
  else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
458
529
  // Skip line comment
459
530
  while (pos < source.length && source[pos] !== '\n')
@@ -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.87",
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
  }
@@ -427,9 +442,20 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
427
442
  // AND the previous non-whitespace isn't an operator expecting more (=, +, -, etc.)
428
443
  const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
429
444
  if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
430
- // Also check if the line ends with an operator that expects a value on the next line
431
- const prevOnLine = source.slice(source.lastIndexOf('\n', pos - 1) + 1, pos).trimEnd();
432
- const lastChar = prevOnLine[prevOnLine.length - 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
+ }
433
459
  if (lastChar && '=+-*/%&|^~<>?:,({['.includes(lastChar)) {
434
460
  // Line ends with operator — this is a continuation, don't end the statement
435
461
  pos++;
@@ -447,6 +473,30 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
447
473
  else if (source[pos] === quote) break;
448
474
  pos++;
449
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
+ }
450
500
  } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
451
501
  // Skip line comment
452
502
  while (pos < source.length && source[pos] !== '\n') pos++;
@@ -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;