ripple 0.2.180 → 0.2.183

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.
@@ -1,8 +1,9 @@
1
- // @ts-nocheck
2
- /** @import { Program } from 'estree' */
1
+ /** @import * as AST from 'estree' */
2
+ /** @import * as ESTreeJSX from 'estree-jsx' */
3
+ /** @import { Parse } from '#parser' */
4
+
3
5
  /**
4
6
  * @import {
5
- * CommentWithLocation,
6
7
  * RipplePluginConfig
7
8
  * } from '#compiler' */
8
9
 
@@ -14,22 +15,69 @@ import { parse_style } from './style.js';
14
15
  import { walk } from 'zimmerframe';
15
16
  import { regex_newline_characters } from '../../../utils/patterns.js';
16
17
 
17
- const parser = acorn.Parser.extend(tsPlugin({ jsx: true }), RipplePlugin());
18
+ /**
19
+ * @typedef {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} AcornPlugin
20
+ */
21
+
22
+ const parser = /** @type {Parse.ParserConstructor} */ (
23
+ /** @type {unknown} */ (
24
+ acorn.Parser.extend(
25
+ tsPlugin({ jsx: true }),
26
+ /** @type {AcornPlugin} */ (/** @type {unknown} */ (RipplePlugin())),
27
+ )
28
+ )
29
+ );
30
+
31
+ /** @type {Parse.BindingType} */
32
+ const BINDING_TYPES = {
33
+ BIND_NONE: 0, // Not a binding
34
+ BIND_VAR: 1, // Var-style binding
35
+ BIND_LEXICAL: 2, // Let- or const-style binding
36
+ BIND_FUNCTION: 3, // Function declaration
37
+ BIND_SIMPLE_CATCH: 4, // Simple (identifier pattern) catch binding
38
+ BIND_OUTSIDE: 5, // Special case for function names as bound inside the function
39
+ };
40
+
41
+ /**
42
+ * @this {Parse.DestructuringErrors}
43
+ * @returns {Parse.DestructuringErrors}
44
+ */
45
+ function DestructuringErrors() {
46
+ if (!(this instanceof DestructuringErrors)) {
47
+ throw new TypeError("'DestructuringErrors' must be invoked with 'new'");
48
+ }
49
+ this.shorthandAssign = -1;
50
+ this.trailingComma = -1;
51
+ this.parenthesizedAssign = -1;
52
+ this.parenthesizedBind = -1;
53
+ this.doubleProto = -1;
54
+ return this;
55
+ }
18
56
 
19
57
  /**
20
58
  * Convert JSX node types to regular JavaScript node types
21
- * @param {any} node - The JSX node to convert
22
- * @returns {any} The converted node
59
+ * @param {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression | AST.Node} node - The JSX node to convert
60
+ * @returns {AST.Identifier | AST.MemberExpression | AST.Node} The converted node
23
61
  */
24
62
  function convert_from_jsx(node) {
63
+ /** @type {AST.Identifier | AST.MemberExpression | AST.Node} */
64
+ let converted_node;
25
65
  if (node.type === 'JSXIdentifier') {
26
- node.type = 'Identifier';
66
+ converted_node = /** @type {AST.Identifier} */ (/** @type {unknown} */ (node));
67
+ converted_node.type = 'Identifier';
27
68
  } else if (node.type === 'JSXMemberExpression') {
28
- node.type = 'MemberExpression';
29
- node.object = convert_from_jsx(node.object);
30
- node.property = convert_from_jsx(node.property);
69
+ converted_node = /** @type {AST.MemberExpression} */ (/** @type {unknown} */ (node));
70
+ converted_node.type = 'MemberExpression';
71
+ converted_node.object = /** @type {AST.Identifier | AST.MemberExpression} */ (
72
+ convert_from_jsx(converted_node.object)
73
+ );
74
+ converted_node.property = /** @type {AST.Identifier} */ (
75
+ convert_from_jsx(converted_node.property)
76
+ );
77
+ } else {
78
+ converted_node = node;
31
79
  }
32
- return node;
80
+ return converted_node;
33
81
  }
34
82
 
35
83
  const regex_whitespace_only = /\s/;
@@ -38,7 +86,7 @@ const regex_whitespace_only = /\s/;
38
86
  * Skip whitespace characters without skipping comments.
39
87
  * This is needed because Acorn's skipSpace() also skips comments, which breaks
40
88
  * parsing in certain contexts. Updates parser position and line tracking.
41
- * @param {acorn.Parser} parser
89
+ * @param {Parse.Parser} parser
42
90
  */
43
91
  function skipWhitespace(parser) {
44
92
  const originalStart = parser.start;
@@ -62,22 +110,56 @@ function skipWhitespace(parser) {
62
110
  parser.startLoc = lineInfo || acorn.getLineInfo(parser.input, parser.start);
63
111
  }
64
112
 
113
+ /**
114
+ * @param {AST.Node | null | undefined} node
115
+ * @returns {boolean}
116
+ */
65
117
  function isWhitespaceTextNode(node) {
66
118
  if (!node || node.type !== 'Text') {
67
119
  return false;
68
120
  }
69
- const value =
70
- typeof node.value === 'string' ? node.value : typeof node.raw === 'string' ? node.raw : '';
71
- return /^\s*$/.test(value);
121
+
122
+ const expr = node.expression;
123
+ if (expr && expr.type === 'Literal' && typeof expr.value === 'string') {
124
+ return /^\s*$/.test(expr.value);
125
+ }
126
+ return false;
127
+ }
128
+
129
+ /**
130
+ * @param {AST.Element} element
131
+ * @param {ESTreeJSX.JSXOpeningElement} open
132
+ */
133
+ function addOpeningAndClosing(element, open) {
134
+ const name = /** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name;
135
+
136
+ element.openingElement = open;
137
+ element.closingElement = {
138
+ type: 'JSXClosingElement',
139
+ name: open.name,
140
+ start: /** @type {AST.NodeWithLocation} */ (element).end - `</${name}>`.length,
141
+ end: element.end,
142
+ loc: {
143
+ start: {
144
+ line: element.loc.end.line,
145
+ column: element.loc.end.column - `</${name}>`.length,
146
+ },
147
+ end: {
148
+ line: element.loc.end.line,
149
+ column: element.loc.end.column,
150
+ },
151
+ },
152
+ metadata: { path: [] },
153
+ };
72
154
  }
73
155
 
74
156
  /**
75
157
  * Acorn parser plugin for Ripple syntax extensions
76
158
  * @param {RipplePluginConfig} [config] - Plugin configuration
77
- * @returns {function(any): any} Parser extension function
159
+ * @returns {(Parser: Parse.ParserConstructor) => Parse.ParserConstructor} Parser extension function
78
160
  */
79
161
  function RipplePlugin(config) {
80
- return (/** @type {typeof acorn.Parser} */ Parser) => {
162
+ return (/** @type {Parse.ParserConstructor} */ Parser) => {
81
163
  const original = acorn.Parser.prototype;
82
164
  const tt = Parser.tokTypes || acorn.tokTypes;
83
165
  const tc = Parser.tokContexts || acorn.tokContexts;
@@ -85,16 +167,23 @@ function RipplePlugin(config) {
85
167
  const tstc = Parser.acornTypeScript.tokContexts;
86
168
 
87
169
  class RippleParser extends Parser {
88
- /** @type {any[]} */
170
+ /** @type {AST.Node[]} */
89
171
  #path = [];
90
172
  #commentContextId = 0;
91
173
  #loose = false;
92
174
 
175
+ /**
176
+ * @param {Parse.Options} options
177
+ * @param {string} input
178
+ */
93
179
  constructor(options, input) {
94
180
  super(options, input);
95
181
  this.#loose = options?.rippleOptions.loose === true;
96
182
  }
97
183
 
184
+ /**
185
+ * @returns {Parse.CommentMetaData | null}
186
+ */
98
187
  #createCommentMetadata() {
99
188
  if (this.#path.length === 0) {
100
189
  return null;
@@ -114,23 +203,21 @@ function RipplePlugin(config) {
114
203
  return null;
115
204
  }
116
205
 
117
- container.metadata ??= {};
206
+ container.metadata ??= { path: [] };
118
207
  if (container.metadata.commentContainerId === undefined) {
119
208
  container.metadata.commentContainerId = ++this.#commentContextId;
120
209
  }
121
210
 
122
- return {
211
+ return /*** @type {Parse.CommentMetaData} */ ({
123
212
  containerId: container.metadata.commentContainerId,
124
- containerType: container.type,
125
213
  childIndex: children.length,
126
214
  beforeMeaningfulChild: !hasMeaningfulChildren,
127
- };
215
+ });
128
216
  }
129
217
 
130
218
  /**
131
219
  * Helper method to get the element name from a JSX identifier or member expression
132
- * @param {any} node - The node to get the name from
133
- * @returns {string | null} Element name or null
220
+ * @type {Parse.Parser['getElementName']}
134
221
  */
135
222
  getElementName(node) {
136
223
  if (!node) return null;
@@ -145,8 +232,7 @@ function RipplePlugin(config) {
145
232
 
146
233
  /**
147
234
  * Get token from character code - handles Ripple-specific tokens
148
- * @param {number} code - Character code
149
- * @returns {any} Token or calls super method
235
+ * @type {Parse.Parser['getTokenFromCode']}
150
236
  */
151
237
  getTokenFromCode(code) {
152
238
  if (code === 60) {
@@ -376,7 +462,7 @@ function RipplePlugin(config) {
376
462
 
377
463
  /**
378
464
  * Read an @ prefixed identifier
379
- * @returns {any} Token with @ identifier
465
+ * @type {Parse.Parser['readAtIdentifier']}
380
466
  */
381
467
  readAtIdentifier() {
382
468
  const start = this.pos;
@@ -410,11 +496,12 @@ function RipplePlugin(config) {
410
496
 
411
497
  /**
412
498
  * Override parseIdent to mark @ identifiers as tracked
413
- * @param {any} [liberal] - Whether to allow liberal parsing
414
- * @returns {any} Parsed identifier node
499
+ * @type {Parse.Parser['parseIdent']}
415
500
  */
416
501
  parseIdent(liberal) {
417
- const node = super.parseIdent(liberal);
502
+ const node = /** @type {AST.Identifier &AST.NodeWithLocation} */ (
503
+ super.parseIdent(liberal)
504
+ );
418
505
  if (node.name && node.name.startsWith('@')) {
419
506
  node.name = node.name.slice(1); // Remove the '@' for internal use
420
507
  node.tracked = true;
@@ -429,14 +516,7 @@ function RipplePlugin(config) {
429
516
 
430
517
  /**
431
518
  * Override parseSubscripts to handle `.@[expression]` syntax for reactive computed member access
432
- * @param {any} base - The base expression
433
- * @param {number} startPos - Start position
434
- * @param {any} startLoc - Start location
435
- * @param {boolean} noCalls - Whether calls are disallowed
436
- * @param {any} maybeAsyncArrow - Optional async arrow flag
437
- * @param {any} optionalChained - Optional chaining flag
438
- * @param {any} forInit - For-init flag
439
- * @returns {any} Parsed subscript expression
519
+ * @type {Parse.Parser['parseSubscripts']}
440
520
  */
441
521
  parseSubscripts(
442
522
  base,
@@ -458,7 +538,7 @@ function RipplePlugin(config) {
458
538
 
459
539
  // Check for @[ pattern (@ = 64, [ = 91)
460
540
  if (nextChar === 64 && charAfter === 91) {
461
- const node = this.startNodeAt(startPos, startLoc);
541
+ const node = /** @type {AST.MemberExpression} */ (this.startNodeAt(startPos, startLoc));
462
542
  node.object = base;
463
543
  node.computed = true;
464
544
  node.optional = this.type === tt.questionDot;
@@ -483,7 +563,7 @@ function RipplePlugin(config) {
483
563
  this.expect(tt.bracketR);
484
564
 
485
565
  // Finish this MemberExpression node
486
- base = this.finishNode(node, 'MemberExpression');
566
+ base = /** @type {AST.MemberExpression} */ (this.finishNode(node, 'MemberExpression'));
487
567
 
488
568
  // Recursively handle any further subscripts (chaining)
489
569
  return this.parseSubscripts(
@@ -512,10 +592,7 @@ function RipplePlugin(config) {
512
592
 
513
593
  /**
514
594
  * Parse expression atom - handles TrackedArray and TrackedObject literals
515
- * @param {any} [refDestructuringErrors]
516
- * @param {any} [forNew]
517
- * @param {any} [forInit]
518
- * @returns {any} Parsed expression atom
595
+ * @type {Parse.Parser['parseExprAtom']}
519
596
  */
520
597
  parseExprAtom(refDestructuringErrors, forNew, forInit) {
521
598
  // Check if this is @(expression) for unboxing tracked values
@@ -527,7 +604,7 @@ function RipplePlugin(config) {
527
604
  if (this.type === tt.name && this.value === '#server') {
528
605
  const node = this.startNode();
529
606
  this.next();
530
- return this.finishNode(node, 'ServerIdentifier');
607
+ return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
531
608
  }
532
609
 
533
610
  // Check if this is #Map( or #Set(
@@ -554,6 +631,7 @@ function RipplePlugin(config) {
554
631
  /**
555
632
  * Override to track parenthesized expressions in metadata
556
633
  * This allows the prettier plugin to preserve parentheses where they existed
634
+ * @type {Parse.Parser['parseParenAndDistinguishExpression']}
557
635
  */
558
636
  parseParenAndDistinguishExpression(canBeArrow, forInit) {
559
637
  const startPos = this.start;
@@ -561,8 +639,8 @@ function RipplePlugin(config) {
561
639
 
562
640
  // If the expression's start position is after the opening paren,
563
641
  // it means it was wrapped in parentheses. Mark it in metadata.
564
- if (expr && expr.start > startPos) {
565
- expr.metadata ??= {};
642
+ if (expr && /** @type {AST.NodeWithLocation} */ (expr).start > startPos) {
643
+ expr.metadata ??= { path: [] };
566
644
  expr.metadata.parenthesized = true;
567
645
  }
568
646
 
@@ -572,10 +650,10 @@ function RipplePlugin(config) {
572
650
  /**
573
651
  * Parse `@(expression)` syntax for unboxing tracked values
574
652
  * Creates a TrackedExpression node with the argument property
575
- * @returns {any} TrackedExpression node
653
+ * @type {Parse.Parser['parseTrackedExpression']}
576
654
  */
577
655
  parseTrackedExpression() {
578
- const node = this.startNode();
656
+ const node = /** @type {AST.TrackedExpression} */ (this.startNode());
579
657
  this.next(); // consume '@(' token
580
658
  node.argument = this.parseExpression();
581
659
  this.expect(tt.parenR); // expect ')'
@@ -584,9 +662,7 @@ function RipplePlugin(config) {
584
662
 
585
663
  /**
586
664
  * Override to allow TrackedExpression as a valid lvalue for update expressions
587
- * @param {any} expr - Expression to check
588
- * @param {any} bindingType - Binding type
589
- * @param {any} checkClashes - Check for clashes
665
+ * @type {Parse.Parser['checkLValSimple']}
590
666
  */
591
667
  checkLValSimple(expr, bindingType, checkClashes) {
592
668
  // Allow TrackedExpression as a valid lvalue for ++/-- operators
@@ -596,18 +672,21 @@ function RipplePlugin(config) {
596
672
  return super.checkLValSimple(expr, bindingType, checkClashes);
597
673
  }
598
674
 
675
+ /**
676
+ * @type {Parse.Parser['parseServerBlock']}
677
+ */
599
678
  parseServerBlock() {
600
- const node = this.startNode();
679
+ const node = /** @type {AST.ServerBlock} */ (this.startNode());
601
680
  this.next();
602
681
 
603
- const body = this.startNode();
682
+ const body = /** @type {AST.BlockStatement} */ (this.startNode());
604
683
  node.body = body;
605
684
  body.body = [];
606
685
 
607
686
  this.expect(tt.braceL);
608
687
  this.enterScope(0);
609
688
  while (this.type !== tt.braceR) {
610
- const stmt = this.parseStatement(null, true);
689
+ const stmt = /** @type {AST.Statement} */ (this.parseStatement(null, true));
611
690
  body.body.push(stmt);
612
691
  }
613
692
  this.next();
@@ -621,11 +700,13 @@ function RipplePlugin(config) {
621
700
  /**
622
701
  * Parse `#Map(...)` or `#Set(...)` syntax for tracked collections
623
702
  * Creates a TrackedMap or TrackedSet node with the arguments property
624
- * @param {string} type - Either 'TrackedMap' or 'TrackedSet'
625
- * @returns {any} TrackedMap or TrackedSet node
703
+ * @type {Parse.Parser['parseTrackedCollectionExpression']}
626
704
  */
627
705
  parseTrackedCollectionExpression(type) {
628
- const node = this.startNode();
706
+ const node =
707
+ /** @type {(AST.TrackedMapExpression | AST.TrackedSetExpression) & AST.NodeWithLocation} */ (
708
+ this.startNode()
709
+ );
629
710
  this.next(); // consume '#Map' or '#Set'
630
711
 
631
712
  // Check if we should NOT consume the parentheses
@@ -639,21 +720,23 @@ function RipplePlugin(config) {
639
720
  const beforeStart = this.input.substring(Math.max(0, node.start - 5), node.start);
640
721
  const isAfterNew = /new\s*$/.test(beforeStart);
641
722
 
642
- if (isAfterNew && this.type === tt.parenL) {
723
+ if (!isAfterNew) {
724
+ // If we reach here, it means #Map or #Set is being called without 'new'
725
+ // Throw a TypeError to match JavaScript class constructor behavior
726
+ const constructorName =
727
+ type === 'TrackedMapExpression' ? '#Map (TrackedMap)' : '#Set (TrackedSet)';
728
+ this.raise(
729
+ node.start,
730
+ `TypeError: Class constructor ${constructorName} cannot be invoked without 'new'`,
731
+ );
732
+ }
733
+
734
+ if (this.type === tt.parenL) {
643
735
  // Don't consume parens - they belong to NewExpression
644
736
  node.arguments = [];
645
737
  return this.finishNode(node, type);
646
738
  }
647
739
 
648
- // If we reach here, it means #Map or #Set is being called without 'new'
649
- // Throw a TypeError to match JavaScript class constructor behavior
650
- const constructorName =
651
- type === 'TrackedMapExpression' ? '#Map (TrackedMap)' : '#Set (TrackedSet)';
652
- this.raise(
653
- node.start,
654
- `TypeError: Class constructor ${constructorName} cannot be invoked without 'new'`,
655
- );
656
-
657
740
  this.expect(tt.parenL); // expect '('
658
741
 
659
742
  node.arguments = [];
@@ -680,8 +763,11 @@ function RipplePlugin(config) {
680
763
  return this.finishNode(node, type);
681
764
  }
682
765
 
766
+ /**
767
+ * @type {Parse.Parser['parseTrackedArrayExpression']}
768
+ */
683
769
  parseTrackedArrayExpression() {
684
- const node = this.startNode();
770
+ const node = /** @type {AST.TrackedArrayExpression} */ (this.startNode());
685
771
  this.next(); // consume the '#['
686
772
 
687
773
  node.elements = [];
@@ -715,8 +801,11 @@ function RipplePlugin(config) {
715
801
  return this.finishNode(node, 'TrackedArrayExpression');
716
802
  }
717
803
 
804
+ /**
805
+ * @type {Parse.Parser['parseTrackedObjectExpression']}
806
+ */
718
807
  parseTrackedObjectExpression() {
719
- const node = this.startNode();
808
+ const node = /** @type {AST.TrackedObjectExpression} */ (this.startNode());
720
809
  this.next(); // consume the '#{'
721
810
 
722
811
  node.properties = [];
@@ -740,7 +829,7 @@ function RipplePlugin(config) {
740
829
  }
741
830
  } else {
742
831
  // Regular property
743
- node.properties.push(this.parseProperty(false, {}));
832
+ node.properties.push(this.parseProperty(false, new DestructuringErrors()));
744
833
  }
745
834
  }
746
835
 
@@ -749,14 +838,10 @@ function RipplePlugin(config) {
749
838
 
750
839
  /**
751
840
  * Parse a component - common implementation used by statements, expressions, and export defaults
752
- * @param {Object} options - Parsing options
753
- * @param {boolean} [options.requireName=false] - Whether component name is required
754
- * @param {boolean} [options.isDefault=false] - Whether this is an export default component
755
- * @param {boolean} [options.declareName=false] - Whether to declare the name in scope
756
- * @returns {any} Component node
841
+ * @type {Parse.Parser['parseComponent']}
757
842
  */
758
843
  parseComponent({ requireName = false, isDefault = false, declareName = false } = {}) {
759
- const node = this.startNode();
844
+ const node = /** @type {AST.Component} */ (this.startNode());
760
845
  node.type = 'Component';
761
846
  node.css = null;
762
847
  node.default = isDefault;
@@ -766,12 +851,20 @@ function RipplePlugin(config) {
766
851
  if (requireName) {
767
852
  node.id = this.parseIdent();
768
853
  if (declareName) {
769
- this.declareName(node.id.name, 'var', node.id.start);
854
+ this.declareName(
855
+ node.id.name,
856
+ BINDING_TYPES.BIND_VAR,
857
+ /** @type {AST.NodeWithLocation} */ (node.id).start,
858
+ );
770
859
  }
771
860
  } else {
772
861
  node.id = this.type.label === 'name' ? this.parseIdent() : null;
773
862
  if (declareName && node.id) {
774
- this.declareName(node.id.name, 'var', node.id.start);
863
+ this.declareName(
864
+ node.id.name,
865
+ BINDING_TYPES.BIND_VAR,
866
+ /** @type {AST.NodeWithLocation} */ (node.id).start,
867
+ );
775
868
  }
776
869
  }
777
870
 
@@ -792,6 +885,9 @@ function RipplePlugin(config) {
792
885
  return node;
793
886
  }
794
887
 
888
+ /**
889
+ * @type {Parse.Parser['parseExportDefaultDeclaration']}
890
+ */
795
891
  parseExportDefaultDeclaration() {
796
892
  // Check if this is "export default component"
797
893
  if (this.value === 'component') {
@@ -801,6 +897,7 @@ function RipplePlugin(config) {
801
897
  return super.parseExportDefaultDeclaration();
802
898
  }
803
899
 
900
+ /** @type {Parse.Parser['parseForStatement']} */
804
901
  parseForStatement(node) {
805
902
  this.next();
806
903
  let awaitAt =
@@ -818,12 +915,16 @@ function RipplePlugin(config) {
818
915
 
819
916
  let isLet = this.isLet();
820
917
  if (this.type === tt._var || this.type === tt._const || isLet) {
821
- let init = this.startNode(),
822
- kind = isLet ? 'let' : this.value;
918
+ let init = /** @type {AST.VariableDeclaration} */ (this.startNode()),
919
+ kind = isLet ? 'let' : /** @type {AST.VariableDeclaration['kind']} */ (this.value);
823
920
  this.next();
824
921
  this.parseVar(init, true, kind);
825
922
  this.finishNode(init, 'VariableDeclaration');
826
- return this.parseForAfterInitWithIndex(node, init, awaitAt);
923
+ return this.parseForAfterInitWithIndex(
924
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
925
+ init,
926
+ awaitAt,
927
+ );
827
928
  }
828
929
 
829
930
  // Handle other cases like using declarations if they exist
@@ -836,7 +937,7 @@ function RipplePlugin(config) {
836
937
  ? 'await using'
837
938
  : null;
838
939
  if (usingKind) {
839
- let init = this.startNode();
940
+ let init = /** @type {AST.VariableDeclaration} */ (this.startNode());
840
941
  this.next();
841
942
  if (usingKind === 'await using') {
842
943
  if (!this.canAwait) {
@@ -846,13 +947,17 @@ function RipplePlugin(config) {
846
947
  }
847
948
  this.parseVar(init, true, usingKind);
848
949
  this.finishNode(init, 'VariableDeclaration');
849
- return this.parseForAfterInitWithIndex(node, init, awaitAt);
950
+ return this.parseForAfterInitWithIndex(
951
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
952
+ init,
953
+ awaitAt,
954
+ );
850
955
  }
851
956
 
852
957
  let containsEsc = this.containsEsc;
853
- let refDestructuringErrors = {};
958
+ let refDestructuringErrors = new DestructuringErrors();
854
959
  let initPos = this.start;
855
- let init =
960
+ let init_expr =
856
961
  awaitAt > -1
857
962
  ? this.parseExprSubscripts(refDestructuringErrors, 'await')
858
963
  : this.parseExpression(true, refDestructuringErrors);
@@ -864,30 +969,38 @@ function RipplePlugin(config) {
864
969
  if (awaitAt > -1) {
865
970
  // implies `ecmaVersion >= 9`
866
971
  if (this.type === tt._in) this.unexpected(awaitAt);
867
- node.await = true;
972
+ /** @type {AST.ForOfStatement} */ (node).await = true;
868
973
  } else if (isForOf && this.options.ecmaVersion >= 8) {
869
974
  if (
870
- init.start === initPos &&
975
+ init_expr.start === initPos &&
871
976
  !containsEsc &&
872
- init.type === 'Identifier' &&
873
- init.name === 'async'
977
+ init_expr.type === 'Identifier' &&
978
+ init_expr.name === 'async'
874
979
  )
875
980
  this.unexpected();
876
- else if (this.options.ecmaVersion >= 9) node.await = false;
981
+ else if (this.options.ecmaVersion >= 9)
982
+ /** @type {AST.ForOfStatement} */ (node).await = false;
877
983
  }
878
984
  if (startsWithLet && isForOf)
879
- this.raise(init.start, "The left-hand side of a for-of loop may not start with 'let'.");
880
- this.toAssignable(init, false, refDestructuringErrors);
985
+ this.raise(
986
+ /** @type {AST.NodeWithLocation} */ (init_expr).start,
987
+ "The left-hand side of a for-of loop may not start with 'let'.",
988
+ );
989
+ const init = this.toAssignable(init_expr, false, refDestructuringErrors);
881
990
  this.checkLValPattern(init);
882
- return this.parseForInWithIndex(node, init);
991
+ return this.parseForInWithIndex(
992
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
993
+ init,
994
+ );
883
995
  } else {
884
996
  this.checkExpressionErrors(refDestructuringErrors, true);
885
997
  }
886
998
 
887
999
  if (awaitAt > -1) this.unexpected(awaitAt);
888
- return this.parseFor(node, init);
1000
+ return this.parseFor(node, init_expr);
889
1001
  }
890
1002
 
1003
+ /** @type {Parse.Parser['parseForAfterInitWithIndex']} */
891
1004
  parseForAfterInitWithIndex(node, init, awaitAt) {
892
1005
  if (
893
1006
  (this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual('of'))) &&
@@ -895,15 +1008,25 @@ function RipplePlugin(config) {
895
1008
  ) {
896
1009
  if (this.options.ecmaVersion >= 9) {
897
1010
  if (this.type === tt._in) {
898
- if (awaitAt > -1) this.unexpected(awaitAt);
899
- } else node.await = awaitAt > -1;
1011
+ if (awaitAt > -1) {
1012
+ this.unexpected(awaitAt);
1013
+ }
1014
+ } else {
1015
+ /** @type {AST.ForOfStatement} */ (node).await = awaitAt > -1;
1016
+ }
900
1017
  }
901
- return this.parseForInWithIndex(node, init);
1018
+ return this.parseForInWithIndex(
1019
+ /** @type {AST.ForInStatement | AST.ForOfStatement} */ (node),
1020
+ init,
1021
+ );
1022
+ }
1023
+ if (awaitAt > -1) {
1024
+ this.unexpected(awaitAt);
902
1025
  }
903
- if (awaitAt > -1) this.unexpected(awaitAt);
904
1026
  return this.parseFor(node, init);
905
1027
  }
906
1028
 
1029
+ /** @type {Parse.Parser['parseForInWithIndex']} */
907
1030
  parseForInWithIndex(node, init) {
908
1031
  const isForIn = this.type === tt._in;
909
1032
  this.next();
@@ -918,7 +1041,7 @@ function RipplePlugin(config) {
918
1041
  init.declarations[0].id.type !== 'Identifier')
919
1042
  ) {
920
1043
  this.raise(
921
- init.start,
1044
+ /** @type {AST.NodeWithLocation} */ (init).start,
922
1045
  `${isForIn ? 'for-in' : 'for-of'} loop variable declaration may not have an initializer`,
923
1046
  );
924
1047
  }
@@ -932,8 +1055,13 @@ function RipplePlugin(config) {
932
1055
 
933
1056
  if (this.isContextual('index')) {
934
1057
  this.next(); // consume 'index'
935
- node.index = this.parseExpression();
936
- if (node.index.type !== 'Identifier') {
1058
+ /** @type {AST.ForOfStatement} */ (node).index = /** @type {AST.Identifier} */ (
1059
+ this.parseExpression()
1060
+ );
1061
+ if (
1062
+ /** @type {AST.Identifier} */ (/** @type {AST.ForOfStatement} */ (node).index)
1063
+ .type !== 'Identifier'
1064
+ ) {
937
1065
  this.raise(this.start, 'Expected identifier after "index" keyword');
938
1066
  }
939
1067
  this.eat(tt.semi);
@@ -941,7 +1069,7 @@ function RipplePlugin(config) {
941
1069
 
942
1070
  if (this.isContextual('key')) {
943
1071
  this.next(); // consume 'key'
944
- node.key = this.parseExpression();
1072
+ /** @type {AST.ForOfStatement} */ (node).key = this.parseExpression();
945
1073
  }
946
1074
 
947
1075
  if (this.isContextual('index')) {
@@ -949,16 +1077,19 @@ function RipplePlugin(config) {
949
1077
  }
950
1078
  } else if (!isForIn) {
951
1079
  // Set index to null for standard for-of loops
952
- node.index = null;
1080
+ /** @type {AST.ForOfStatement} */ (node).index = null;
953
1081
  }
954
1082
 
955
1083
  this.expect(tt.parenR);
956
- node.body = this.parseStatement('for');
1084
+ node.body = /** @type {AST.BlockStatement} */ (this.parseStatement('for'));
957
1085
  this.exitScope();
958
1086
  this.labels.pop();
959
1087
  return this.finishNode(node, isForIn ? 'ForInStatement' : 'ForOfStatement');
960
1088
  }
961
1089
 
1090
+ /**
1091
+ * @type {Parse.Parser['checkUnreserved']}
1092
+ */
962
1093
  checkUnreserved(ref) {
963
1094
  if (ref.name === 'component') {
964
1095
  this.raise(
@@ -969,6 +1100,7 @@ function RipplePlugin(config) {
969
1100
  return super.checkUnreserved(ref);
970
1101
  }
971
1102
 
1103
+ /** @type {Parse.Parser['shouldParseExportStatement']} */
972
1104
  shouldParseExportStatement() {
973
1105
  if (super.shouldParseExportStatement()) {
974
1106
  return true;
@@ -979,8 +1111,11 @@ function RipplePlugin(config) {
979
1111
  return this.type.keyword === 'var';
980
1112
  }
981
1113
 
1114
+ /**
1115
+ * @return {ESTreeJSX.JSXExpressionContainer}
1116
+ */
982
1117
  jsx_parseExpressionContainer() {
983
- let node = this.startNode();
1118
+ let node = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
984
1119
  this.next();
985
1120
  let tracked = false;
986
1121
 
@@ -1010,17 +1145,25 @@ function RipplePlugin(config) {
1010
1145
  return this.finishNode(node, 'JSXExpressionContainer');
1011
1146
  }
1012
1147
 
1148
+ /**
1149
+ * @type {Parse.Parser['jsx_parseEmptyExpression']}
1150
+ */
1013
1151
  jsx_parseEmptyExpression() {
1014
1152
  // Override to properly handle the range for JSXEmptyExpression
1015
1153
  // The range should be from after { to before }
1016
- const node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
1154
+ const node = /** @type {ESTreeJSX.JSXEmptyExpression} */ (
1155
+ this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc)
1156
+ );
1017
1157
  node.end = this.start;
1018
1158
  node.loc.end = this.startLoc;
1019
1159
  return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
1020
1160
  }
1021
1161
 
1162
+ /**
1163
+ * @type {Parse.Parser['jsx_parseTupleContainer']}
1164
+ */
1022
1165
  jsx_parseTupleContainer() {
1023
- const t = this.startNode();
1166
+ const t = /** @type {ESTreeJSX.JSXExpressionContainer} */ (this.startNode());
1024
1167
  return (
1025
1168
  this.next(),
1026
1169
  (t.expression =
@@ -1030,8 +1173,11 @@ function RipplePlugin(config) {
1030
1173
  );
1031
1174
  }
1032
1175
 
1176
+ /**
1177
+ * @type {Parse.Parser['jsx_parseAttribute']}
1178
+ */
1033
1179
  jsx_parseAttribute() {
1034
- let node = this.startNode();
1180
+ let node = /** @type {AST.RippleAttribute | ESTreeJSX.JSXAttribute} */ (this.startNode());
1035
1181
 
1036
1182
  if (this.eat(tt.braceL)) {
1037
1183
  if (this.value === 'ref') {
@@ -1042,57 +1188,71 @@ function RipplePlugin(config) {
1042
1188
  '"ref" is a Ripple keyword and must be used in the form {ref fn}',
1043
1189
  );
1044
1190
  }
1045
- node.argument = this.parseMaybeAssign();
1191
+ /** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
1046
1192
  this.expect(tt.braceR);
1047
- return this.finishNode(node, 'RefAttribute');
1193
+ return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
1048
1194
  } else if (this.type === tt.ellipsis) {
1049
1195
  this.expect(tt.ellipsis);
1050
- node.argument = this.parseMaybeAssign();
1196
+ /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1051
1197
  this.expect(tt.braceR);
1052
1198
  return this.finishNode(node, 'SpreadAttribute');
1053
1199
  } else if (this.lookahead().type === tt.ellipsis) {
1054
1200
  this.expect(tt.ellipsis);
1055
- node.argument = this.parseMaybeAssign();
1201
+ /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
1056
1202
  this.expect(tt.braceR);
1057
1203
  return this.finishNode(node, 'SpreadAttribute');
1058
1204
  } else {
1059
- const id = this.parseIdentNode();
1205
+ const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
1060
1206
  id.tracked = false;
1061
1207
  if (id.name.startsWith('@')) {
1062
1208
  id.tracked = true;
1063
1209
  id.name = id.name.slice(1);
1064
1210
  }
1065
1211
  this.finishNode(id, 'Identifier');
1066
- node.name = id;
1067
- node.value = id;
1068
- node.shorthand = true; // Mark as shorthand since name and value are the same
1212
+ /** @type {AST.Attribute} */ (node).name = id;
1213
+ /** @type {AST.Attribute} */ (node).value = id;
1214
+ /** @type {AST.Attribute} */ (node).shorthand = true; // Mark as shorthand since name and value are the same
1069
1215
  this.next();
1070
1216
  this.expect(tt.braceR);
1071
1217
  return this.finishNode(node, 'Attribute');
1072
1218
  }
1073
1219
  }
1074
- node.name = this.jsx_parseNamespacedName();
1075
- node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
1220
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
1221
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).value =
1222
+ /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
1223
+ this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
1224
+ );
1076
1225
  return this.finishNode(node, 'JSXAttribute');
1077
1226
  }
1078
1227
 
1228
+ /**
1229
+ * @type {Parse.Parser['jsx_parseNamespacedName']}
1230
+ */
1079
1231
  jsx_parseNamespacedName() {
1080
1232
  const base = this.jsx_parseIdentifier();
1081
1233
  if (!this.eat(tt.colon)) return base;
1082
- const node = this.startNodeAt(base.start, base.loc.start);
1234
+ const node = /** @type {ESTreeJSX.JSXNamespacedName} */ (
1235
+ this.startNodeAt(
1236
+ /** @type {AST.NodeWithLocation} */ (base).start,
1237
+ /** @type {AST.NodeWithLocation} */ (base).loc.start,
1238
+ )
1239
+ );
1083
1240
  node.namespace = base;
1084
1241
  node.name = this.jsx_parseIdentifier();
1085
1242
  return this.finishNode(node, 'JSXNamespacedName');
1086
1243
  }
1087
1244
 
1245
+ /**
1246
+ * @type {Parse.Parser['jsx_parseIdentifier']}
1247
+ */
1088
1248
  jsx_parseIdentifier() {
1089
- const node = this.startNode();
1249
+ const node = /** @type {ESTreeJSX.JSXIdentifier} */ (this.startNode());
1090
1250
 
1091
1251
  if (this.type.label === '@') {
1092
1252
  this.next(); // consume @
1093
1253
 
1094
1254
  if (this.type === tt.name || this.type === tstt.jsxName) {
1095
- node.name = this.value;
1255
+ node.name = /** @type {string} */ (this.value);
1096
1256
  node.tracked = true;
1097
1257
  this.next();
1098
1258
  } else {
@@ -1102,13 +1262,13 @@ function RipplePlugin(config) {
1102
1262
  } else if (
1103
1263
  (this.type === tt.name || this.type === tstt.jsxName) &&
1104
1264
  this.value &&
1105
- this.value.startsWith('@')
1265
+ /** @type {string} */ (this.value).startsWith('@')
1106
1266
  ) {
1107
- node.name = this.value.substring(1);
1267
+ node.name = /** @type {string} */ (this.value).substring(1);
1108
1268
  node.tracked = true;
1109
1269
  this.next();
1110
1270
  } else if (this.type === tt.name || this.type.keyword || this.type === tstt.jsxName) {
1111
- node.name = this.value;
1271
+ node.name = /** @type {string} */ (this.value);
1112
1272
  node.tracked = false; // Explicitly mark as not tracked
1113
1273
  this.next();
1114
1274
  } else {
@@ -1118,6 +1278,9 @@ function RipplePlugin(config) {
1118
1278
  return this.finishNode(node, 'JSXIdentifier');
1119
1279
  }
1120
1280
 
1281
+ /**
1282
+ * @type {Parse.Parser['jsx_parseElementName']}
1283
+ */
1121
1284
  jsx_parseElementName() {
1122
1285
  if (this.type === tstt.jsxTagEnd) {
1123
1286
  return '';
@@ -1130,7 +1293,12 @@ function RipplePlugin(config) {
1130
1293
  }
1131
1294
 
1132
1295
  if (this.eat(tt.dot)) {
1133
- let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
1296
+ let memberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1297
+ this.startNodeAt(
1298
+ /** @type {AST.NodeWithLocation} */ (node).start,
1299
+ /** @type {AST.NodeWithLocation} */ (node).loc.start,
1300
+ )
1301
+ );
1134
1302
  memberExpr.object = node;
1135
1303
 
1136
1304
  // Check for .@[expression] syntax for tracked computed member access
@@ -1151,8 +1319,8 @@ function RipplePlugin(config) {
1151
1319
  this.expect(tt.bracketL);
1152
1320
 
1153
1321
  // Parse the expression inside brackets
1154
- memberExpr.property = this.parseExpression();
1155
- memberExpr.property.tracked = true;
1322
+ memberExpr.property = /** @type {ESTreeJSX.JSXIdentifier} */ (this.parseExpression());
1323
+ /** @type {AST.TrackedNode} */ (memberExpr.property).tracked = true;
1156
1324
 
1157
1325
  // Expect closing bracket
1158
1326
  this.expect(tt.bracketR);
@@ -1167,9 +1335,11 @@ function RipplePlugin(config) {
1167
1335
  memberExpr.computed = false;
1168
1336
  }
1169
1337
  while (this.eat(tt.dot)) {
1170
- let newMemberExpr = this.startNodeAt(
1171
- memberExpr.start,
1172
- memberExpr.loc && memberExpr.loc.start,
1338
+ let newMemberExpr = /** @type {ESTreeJSX.JSXMemberExpression} */ (
1339
+ this.startNodeAt(
1340
+ /** @type {AST.NodeWithLocation} */ (memberExpr).start,
1341
+ /** @type {AST.NodeWithLocation} */ (memberExpr).loc.start,
1342
+ )
1173
1343
  );
1174
1344
  newMemberExpr.object = memberExpr;
1175
1345
  newMemberExpr.property = this.jsx_parseIdentifier();
@@ -1181,13 +1351,17 @@ function RipplePlugin(config) {
1181
1351
  return node;
1182
1352
  }
1183
1353
 
1354
+ /** @type {Parse.Parser['jsx_parseAttributeValue']} */
1184
1355
  jsx_parseAttributeValue() {
1185
1356
  switch (this.type) {
1186
1357
  case tt.braceL:
1187
1358
  const t = this.jsx_parseExpressionContainer();
1188
1359
  return (
1189
- 'JSXEmptyExpression' === t.expression.type &&
1190
- this.raise(t.start, 'attributes must only be assigned a non-empty expression'),
1360
+ t.expression.type === 'JSXEmptyExpression' &&
1361
+ this.raise(
1362
+ /** @type {AST.NodeWithLocation} */ (t).start,
1363
+ 'attributes must only be assigned a non-empty expression',
1364
+ ),
1191
1365
  t
1192
1366
  );
1193
1367
  case tstt.jsxTagStart:
@@ -1198,6 +1372,9 @@ function RipplePlugin(config) {
1198
1372
  }
1199
1373
  }
1200
1374
 
1375
+ /**
1376
+ * @type {Parse.Parser['parseTryStatement']}
1377
+ */
1201
1378
  parseTryStatement(node) {
1202
1379
  this.next();
1203
1380
  node.block = this.parseBlock();
@@ -1211,7 +1388,7 @@ function RipplePlugin(config) {
1211
1388
  }
1212
1389
 
1213
1390
  if (this.type === tt._catch) {
1214
- const clause = this.startNode();
1391
+ const clause = /** @type {AST.CatchClause} */ (this.startNode());
1215
1392
  this.next();
1216
1393
  if (this.eat(tt.parenL)) {
1217
1394
  clause.param = this.parseCatchClauseParam();
@@ -1229,11 +1406,15 @@ function RipplePlugin(config) {
1229
1406
  node.finalizer = this.eat(tt._finally) ? this.parseBlock() : null;
1230
1407
 
1231
1408
  if (!node.handler && !node.finalizer && !node.pending) {
1232
- this.raise(node.start, 'Missing catch or finally clause');
1409
+ this.raise(
1410
+ /** @type {AST.NodeWithLocation} */ (node).start,
1411
+ 'Missing catch or finally clause',
1412
+ );
1233
1413
  }
1234
1414
  return this.finishNode(node, 'TryStatement');
1235
1415
  }
1236
1416
 
1417
+ /** @type {Parse.Parser['jsx_readToken']} */
1237
1418
  jsx_readToken() {
1238
1419
  const inside_tsx_compat = this.#path.findLast((n) => n.type === 'TsxCompat');
1239
1420
  if (inside_tsx_compat) {
@@ -1386,64 +1567,83 @@ function RipplePlugin(config) {
1386
1567
  }
1387
1568
  }
1388
1569
 
1570
+ /**
1571
+ * @type {Parse.Parser['parseElement']}
1572
+ */
1389
1573
  parseElement() {
1390
1574
  const inside_head = this.#path.findLast(
1391
1575
  (n) => n.type === 'Element' && n.id.type === 'Identifier' && n.id.name === 'head',
1392
1576
  );
1393
1577
  // Adjust the start so we capture the `<` as part of the element
1394
- const prev_pos = this.pos;
1395
- this.pos = this.start - 1;
1396
- const position = this.curPosition();
1397
- this.pos = prev_pos;
1398
-
1399
- const element = this.startNode();
1400
- element.start = position.index;
1401
- element.loc.start = position;
1402
- element.metadata = {};
1578
+ const start = this.start - 1;
1579
+ const position = new acorn.Position(this.curLine, start - this.lineStart);
1580
+
1581
+ const element = /** @type {AST.Element | AST.TsxCompat} */ (this.startNode());
1582
+ element.start = start;
1583
+ /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
1584
+ element.metadata = { path: [] };
1403
1585
  element.children = [];
1404
1586
 
1405
1587
  // Check if this is a <script> or <style> tag
1406
1588
  const tagName = this.value;
1407
1589
  const isScriptOrStyle = tagName === 'script' || tagName === 'style';
1408
-
1590
+ /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */
1409
1591
  let open;
1410
1592
  if (isScriptOrStyle) {
1411
1593
  // Manually parse opening tag to avoid jsx_parseOpeningElementAt consuming content
1412
- const tagStart = this.start;
1413
- const tagEndPos = this.input.indexOf('>', tagStart);
1594
+ const tagEndPos = this.input.indexOf('>', start) + 1; // +1 as end positions are exclusive
1595
+ const opening_position = new acorn.Position(position.line, position.column);
1596
+ const end_position = acorn.getLineInfo(this.input, tagEndPos);
1414
1597
 
1415
1598
  open = {
1416
1599
  type: 'JSXOpeningElement',
1417
- name: { type: 'JSXIdentifier', name: tagName },
1600
+ name: {
1601
+ type: 'JSXIdentifier',
1602
+ name: tagName,
1603
+ metadata: { path: [] },
1604
+ start: start + 1,
1605
+ end: tagEndPos - 1,
1606
+ loc: {
1607
+ start: { ...position, column: position.column + 1 },
1608
+ end: {
1609
+ ...end_position,
1610
+ column: end_position.column - 1,
1611
+ },
1612
+ },
1613
+ },
1418
1614
  attributes: [],
1419
1615
  selfClosing: false,
1420
- end: tagEndPos + 1,
1616
+ start,
1617
+ end: tagEndPos,
1421
1618
  loc: {
1422
- end: {
1423
- line: this.curLine,
1424
- column: tagEndPos + 1,
1425
- },
1619
+ start: opening_position,
1620
+ end: end_position,
1426
1621
  },
1622
+ metadata: { path: [] },
1427
1623
  };
1428
1624
 
1429
1625
  // Position after the '>'
1430
- this.pos = tagEndPos + 1;
1626
+ this.pos = tagEndPos;
1431
1627
 
1432
1628
  // Add opening and closing for easier location tracking
1433
1629
  // TODO: we should also parse attributes inside the opening tag
1434
1630
  } else {
1435
- open = this.jsx_parseOpeningElementAt();
1631
+ open =
1632
+ /** @type {ReturnType<Parse.Parser['jsx_parseOpeningElementAt']> & AST.NodeWithLocation} */ (
1633
+ this.jsx_parseOpeningElementAt()
1634
+ );
1436
1635
  }
1437
1636
 
1438
1637
  // Check if this is a namespaced element (tsx:react)
1439
1638
  const is_tsx_compat = open.name.type === 'JSXNamespacedName';
1440
1639
 
1441
1640
  if (is_tsx_compat) {
1442
- element.type = 'TsxCompat';
1443
- element.kind = open.name.name.name; // e.g., "react" from "tsx:react"
1641
+ const namespace_node = /** @type {ESTreeJSX.JSXNamespacedName} */ (open.name);
1642
+ /** @type {AST.TsxCompat} */ (element).type = 'TsxCompat';
1643
+ /** @type {AST.TsxCompat} */ (element).kind = namespace_node.name.name; // e.g., "react" from "tsx:react"
1444
1644
 
1445
1645
  if (open.selfClosing) {
1446
- const tagName = open.name.namespace.name + ':' + open.name.name.name;
1646
+ const tagName = namespace_node.namespace.name + ':' + namespace_node.name.name;
1447
1647
  this.raise(
1448
1648
  open.start,
1449
1649
  `TSX compatibility elements cannot be self-closing. '<${tagName} />' must have a closing tag '</${tagName}>'.`,
@@ -1457,65 +1657,34 @@ function RipplePlugin(config) {
1457
1657
 
1458
1658
  for (const attr of open.attributes) {
1459
1659
  if (attr.type === 'JSXAttribute') {
1460
- attr.type = 'Attribute';
1660
+ /** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
1461
1661
  if (attr.name.type === 'JSXIdentifier') {
1462
- attr.name.type = 'Identifier';
1662
+ /** @type {AST.Identifier} */ (/** @type {unknown} */ (attr.name)).type =
1663
+ 'Identifier';
1463
1664
  }
1464
1665
  if (attr.value !== null) {
1465
1666
  if (attr.value.type === 'JSXExpressionContainer') {
1466
- attr.value = attr.value.expression;
1667
+ /** @type {ESTreeJSX.JSXExpressionContainer['expression']} */ (attr.value) =
1668
+ attr.value.expression;
1467
1669
  }
1468
1670
  }
1469
1671
  }
1470
1672
  }
1471
1673
 
1472
1674
  if (!is_tsx_compat) {
1473
- if (open.name.type === 'JSXIdentifier') {
1474
- open.name.type = 'Identifier';
1475
- }
1476
- element.id = convert_from_jsx(open.name);
1675
+ /** @type {AST.Element} */ (element).id = /** @type {AST.Identifier} */ (
1676
+ convert_from_jsx(/** @type {ESTreeJSX.JSXIdentifier} */ (open.name))
1677
+ );
1477
1678
  element.selfClosing = open.selfClosing;
1478
1679
  }
1479
1680
 
1480
1681
  element.attributes = open.attributes;
1481
- element.metadata ??= {};
1682
+ element.metadata ??= { path: [] };
1482
1683
  element.metadata.commentContainerId = ++this.#commentContextId;
1483
1684
  // Store opening tag's end position for use in loose mode when element is unclosed
1484
1685
  element.metadata.openingTagEnd = open.end;
1485
1686
  element.metadata.openingTagEndLoc = open.loc.end;
1486
1687
 
1487
- function addOpeningAndClosing() {
1488
- //TODO: once parse attributes of style and script
1489
- // we should the open element loc properly
1490
- const name = open.name.name;
1491
- open.loc = {
1492
- start: {
1493
- line: element.loc.start.line,
1494
- column: element.loc.start.column,
1495
- },
1496
- end: {
1497
- line: element.loc.start.line,
1498
- column: element.loc.start.column + `${name}>`.length,
1499
- },
1500
- };
1501
-
1502
- element.openingElement = open;
1503
- element.closingElement = {
1504
- type: 'JSXClosingElement',
1505
- name: open.name,
1506
- loc: {
1507
- start: {
1508
- line: element.loc.end.line,
1509
- column: element.loc.end.column - `</${name}>`.length,
1510
- },
1511
- end: {
1512
- line: element.loc.end.line,
1513
- column: element.loc.end.column,
1514
- },
1515
- },
1516
- };
1517
- }
1518
-
1519
1688
  if (element.selfClosing) {
1520
1689
  this.#path.pop();
1521
1690
 
@@ -1524,7 +1693,7 @@ function RipplePlugin(config) {
1524
1693
  this.next();
1525
1694
  }
1526
1695
  } else {
1527
- if (open.name.name === 'script') {
1696
+ if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'script') {
1528
1697
  let content = '';
1529
1698
 
1530
1699
  // TODO implement this where we get a string for content of the content of the script tag
@@ -1551,10 +1720,10 @@ function RipplePlugin(config) {
1551
1720
  this.next();
1552
1721
  }
1553
1722
 
1554
- element.content = content;
1723
+ /** @type {AST.Element} */ (element).content = content;
1555
1724
  this.finishNode(element, 'Element');
1556
- addOpeningAndClosing(element, open);
1557
- } else if (open.name.name === 'style') {
1725
+ addOpeningAndClosing(/** @type {AST.Element} */ (element), open);
1726
+ } else if (/** @type {ESTreeJSX.JSXIdentifier} */ (open.name).name === 'style') {
1558
1727
  // jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
1559
1728
  // So backtrack to the end of the <style> tag to make sure everything is included
1560
1729
  const start = open.end;
@@ -1562,7 +1731,9 @@ function RipplePlugin(config) {
1562
1731
  const end = input.indexOf('</style>');
1563
1732
  const content = input.slice(0, end);
1564
1733
 
1565
- const component = this.#path.findLast((n) => n.type === 'Component');
1734
+ const component = /** @type {AST.Component} */ (
1735
+ this.#path.findLast((n) => n.type === 'Component')
1736
+ );
1566
1737
  const parsed_css = parse_style(content, { loose: this.#loose });
1567
1738
 
1568
1739
  if (!inside_head) {
@@ -1590,7 +1761,9 @@ function RipplePlugin(config) {
1590
1761
  }
1591
1762
  // This node is used for Prettier - always add parsed CSS as children
1592
1763
  // for proper formatting, regardless of whether it's inside head or not
1593
- element.children = [parsed_css];
1764
+ /** @type {AST.Element} */ (element).children = [
1765
+ /** @type {AST.Node} */ (/** @type {unknown} */ (parsed_css)),
1766
+ ];
1594
1767
 
1595
1768
  // Ensure we escape JSX <tag></tag> context
1596
1769
  const curContext = this.curContext();
@@ -1599,13 +1772,13 @@ function RipplePlugin(config) {
1599
1772
  this.context.pop();
1600
1773
  }
1601
1774
 
1602
- element.css = content;
1775
+ /** @type {AST.Element} */ (element).css = content;
1603
1776
  this.finishNode(element, 'Element');
1604
- addOpeningAndClosing(element, open);
1777
+ addOpeningAndClosing(/** @type {AST.Element} */ (element), open);
1605
1778
  return element;
1606
1779
  } else {
1607
1780
  this.enterScope(0);
1608
- this.parseTemplateBody(element.children);
1781
+ this.parseTemplateBody(/** @type {AST.Element} */ (element).children);
1609
1782
  this.exitScope();
1610
1783
 
1611
1784
  if (element.type === 'TsxCompat') {
@@ -1669,6 +1842,9 @@ function RipplePlugin(config) {
1669
1842
  return element;
1670
1843
  }
1671
1844
 
1845
+ /**
1846
+ * @type {Parse.Parser['parseTemplateBody']}
1847
+ */
1672
1848
  parseTemplateBody(body) {
1673
1849
  const inside_func =
1674
1850
  this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
@@ -1720,13 +1896,13 @@ function RipplePlugin(config) {
1720
1896
  }
1721
1897
 
1722
1898
  if (text) {
1723
- const node = {
1899
+ const node = /** @type {ESTreeJSX.JSXText} */ ({
1724
1900
  type: 'JSXText',
1725
1901
  value: text,
1726
1902
  raw: text,
1727
1903
  start,
1728
1904
  end: this.pos,
1729
- };
1905
+ });
1730
1906
  body.push(node);
1731
1907
  }
1732
1908
 
@@ -1739,7 +1915,9 @@ function RipplePlugin(config) {
1739
1915
  // Keep JSXEmptyExpression as-is (for prettier to handle comments)
1740
1916
  // but convert other expressions to Text/Html nodes
1741
1917
  if (node.expression.type !== 'JSXEmptyExpression') {
1742
- node.type = node.html ? 'Html' : 'Text';
1918
+ /** @type {AST.Html | AST.TextNode} */ (/** @type {unknown} */ (node)).type = node.html
1919
+ ? 'Html'
1920
+ : 'Text';
1743
1921
  delete node.html;
1744
1922
  }
1745
1923
  body.push(node);
@@ -1749,7 +1927,10 @@ function RipplePlugin(config) {
1749
1927
  this.next();
1750
1928
  if (this.value === '/') {
1751
1929
  this.next();
1752
- const closingTag = this.jsx_parseElementName();
1930
+ const closingTag =
1931
+ /** @type {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXNamespacedName | ESTreeJSX.JSXMemberExpression} */ (
1932
+ this.jsx_parseElementName()
1933
+ );
1753
1934
  this.exprAllowed = true;
1754
1935
 
1755
1936
  // Validate that the closing tag matches the opening tag
@@ -1761,7 +1942,9 @@ function RipplePlugin(config) {
1761
1942
  this.raise(this.start, 'Unexpected closing tag');
1762
1943
  }
1763
1944
 
1945
+ /** @type {string | null} */
1764
1946
  let openingTagName;
1947
+ /** @type {string | null} */
1765
1948
  let closingTagName;
1766
1949
 
1767
1950
  if (currentElement.type === 'TsxCompat') {
@@ -1808,9 +1991,11 @@ function RipplePlugin(config) {
1808
1991
  // Mark as unclosed and adjust location
1809
1992
  elem.unclosed = true;
1810
1993
  const position = this.curPosition();
1811
- position.line = elem.metadata.openingTagEndLoc.line;
1812
- position.column = elem.metadata.openingTagEndLoc.column;
1813
- elem.loc.end = position;
1994
+ position.line = /** @type {AST.Position} */ (elem.metadata.openingTagEndLoc).line;
1995
+ position.column = /** @type {AST.Position} */ (
1996
+ elem.metadata.openingTagEndLoc
1997
+ ).column;
1998
+ /** @type {AST.NodeWithLocation} */ (elem).loc.end = position;
1814
1999
  elem.end = elem.metadata.openingTagEnd;
1815
2000
 
1816
2001
  this.#path.pop(); // Remove from stack
@@ -1841,24 +2026,29 @@ function RipplePlugin(config) {
1841
2026
  this.parseTemplateBody(body);
1842
2027
  }
1843
2028
 
2029
+ /**
2030
+ * @type {Parse.Parser['parseStatement']}
2031
+ */
1844
2032
  parseStatement(context, topLevel, exports) {
1845
2033
  if (
1846
2034
  context !== 'for' &&
1847
2035
  context !== 'if' &&
1848
2036
  this.context.at(-1) === tc.b_stat &&
1849
2037
  this.type === tt.braceL &&
1850
- this.context.some((c) => c === tstt.tc_expr)
2038
+ this.context.some((c) => c === tstc.tc_expr)
1851
2039
  ) {
1852
2040
  this.next();
1853
2041
  const node = this.jsx_parseExpressionContainer();
1854
2042
  // Keep JSXEmptyExpression as-is (don't convert to Text)
1855
2043
  if (node.expression.type !== 'JSXEmptyExpression') {
1856
- node.type = 'Text';
2044
+ /** @type {AST.TextNode} */ (/** @type {unknown} */ (node)).type = 'Text';
1857
2045
  }
1858
2046
  this.next();
1859
2047
  this.context.pop();
1860
2048
  this.context.pop();
1861
- return node;
2049
+ return /** @type {ESTreeJSX.JSXEmptyExpression | AST.TextNode | ESTreeJSX.JSXExpressionContainer} */ (
2050
+ /** @type {unknown} */ (node)
2051
+ );
1862
2052
  }
1863
2053
 
1864
2054
  if (this.value === '#server') {
@@ -1875,7 +2065,7 @@ function RipplePlugin(config) {
1875
2065
  // interfering with legitimate decorator syntax
1876
2066
  this.skip_decorator = true;
1877
2067
  const expressionResult = this.tryParse(() => {
1878
- const node = this.startNode();
2068
+ const node = /** @type {AST.ExpressionStatement} */ (this.startNode());
1879
2069
  this.next();
1880
2070
  // Force expression context to ensure @ is tokenized correctly
1881
2071
  const old_expr_allowed = this.exprAllowed;
@@ -1883,17 +2073,19 @@ function RipplePlugin(config) {
1883
2073
  node.expression = this.parseExpression();
1884
2074
 
1885
2075
  if (node.expression.type === 'UpdateExpression') {
2076
+ /** @type {AST.Expression} */
1886
2077
  let object = node.expression.argument;
1887
2078
  while (object.type === 'MemberExpression') {
1888
- object = object.object;
2079
+ object = /** @type {AST.Expression} */ (object.object);
1889
2080
  }
1890
2081
  if (object.type === 'Identifier') {
1891
2082
  object.tracked = true;
1892
2083
  }
1893
2084
  } else if (node.expression.type === 'AssignmentExpression') {
2085
+ /** @type {AST.Expression | AST.Pattern | AST.Identifier} */
1894
2086
  let object = node.expression.left;
1895
2087
  while (object.type === 'MemberExpression') {
1896
- object = object.object;
2088
+ object = /** @type {AST.Expression} */ (object.object);
1897
2089
  }
1898
2090
  if (object.type === 'Identifier') {
1899
2091
  object.tracked = true;
@@ -1931,12 +2123,15 @@ function RipplePlugin(config) {
1931
2123
  return super.parseStatement(context, topLevel, exports);
1932
2124
  }
1933
2125
 
2126
+ /**
2127
+ * @type {Parse.Parser['parseBlock']}
2128
+ */
1934
2129
  parseBlock(createNewLexicalScope, node, exitStrict) {
1935
2130
  const parent = this.#path.at(-1);
1936
2131
 
1937
2132
  if (parent?.type === 'Component' || parent?.type === 'Element') {
1938
2133
  if (createNewLexicalScope === void 0) createNewLexicalScope = true;
1939
- if (node === void 0) node = this.startNode();
2134
+ if (node === void 0) node = /** @type {AST.BlockStatement} */ (this.startNode());
1940
2135
 
1941
2136
  node.body = [];
1942
2137
  this.expect(tt.braceL);
@@ -1961,7 +2156,7 @@ function RipplePlugin(config) {
1961
2156
  }
1962
2157
  }
1963
2158
 
1964
- return RippleParser;
2159
+ return /** @type {Parse.ParserConstructor} */ (RippleParser);
1965
2160
  };
1966
2161
  }
1967
2162
 
@@ -1970,11 +2165,15 @@ function RipplePlugin(config) {
1970
2165
  * to add them after the fact. They are needed in order to support `ripple-ignore` comments
1971
2166
  * in JS code and so that `prettier-plugin` doesn't remove all comments when formatting.
1972
2167
  * @param {string} source
1973
- * @param {CommentWithLocation[]} comments
2168
+ * @param {AST.CommentWithLocation[]} comments
1974
2169
  * @param {number} [index=0] - Starting index
1975
- * @returns {{ onComment: Function, add_comments: Function }} Comment handler functions
1976
2170
  */
1977
2171
  function get_comment_handlers(source, comments, index = 0) {
2172
+ /**
2173
+ * @param {string} text
2174
+ * @param {number} startIndex
2175
+ * @returns {string | null}
2176
+ */
1978
2177
  function getNextNonWhitespaceCharacter(text, startIndex) {
1979
2178
  for (let i = startIndex; i < text.length; i++) {
1980
2179
  const char = text[i];
@@ -1986,6 +2185,9 @@ function get_comment_handlers(source, comments, index = 0) {
1986
2185
  }
1987
2186
 
1988
2187
  return {
2188
+ /**
2189
+ * @type {Parse.Options['onComment']}
2190
+ */
1989
2191
  onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
1990
2192
  if (block && /\n/.test(value)) {
1991
2193
  let a = start;
@@ -2004,12 +2206,16 @@ function get_comment_handlers(source, comments, index = 0) {
2004
2206
  start,
2005
2207
  end,
2006
2208
  loc: {
2007
- start: /** @type {import('acorn').Position} */ (start_loc),
2008
- end: /** @type {import('acorn').Position} */ (end_loc),
2209
+ start: start_loc,
2210
+ end: end_loc,
2009
2211
  },
2010
2212
  context: metadata ?? null,
2011
2213
  });
2012
2214
  },
2215
+
2216
+ /**
2217
+ * @param {AST.Node} ast
2218
+ */
2013
2219
  add_comments: (ast) => {
2014
2220
  if (comments.length === 0) return;
2015
2221
 
@@ -2024,14 +2230,13 @@ function get_comment_handlers(source, comments, index = 0) {
2024
2230
  context,
2025
2231
  }));
2026
2232
 
2233
+ /**
2234
+ * @param {AST.Node} ast
2235
+ */
2027
2236
  walk(ast, null, {
2237
+ /** @param {AST.Node} node */
2028
2238
  _(node, { next, path }) {
2029
- let comment;
2030
-
2031
- const metadata =
2032
- /** @type {{ commentContainerId?: number, elementLeadingComments?: CommentWithLocation[] }} */ (
2033
- node?.metadata
2034
- );
2239
+ const metadata = node?.metadata;
2035
2240
 
2036
2241
  if (metadata && metadata.commentContainerId !== undefined) {
2037
2242
  while (
@@ -2040,15 +2245,17 @@ function get_comment_handlers(source, comments, index = 0) {
2040
2245
  comments[0].context.containerId === metadata.commentContainerId &&
2041
2246
  comments[0].context.beforeMeaningfulChild
2042
2247
  ) {
2043
- const elementComment = /** @type {CommentWithLocation & { context?: any }} */ (
2044
- comments.shift()
2045
- );
2248
+ const elementComment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2249
+
2046
2250
  (metadata.elementLeadingComments ||= []).push(elementComment);
2047
2251
  }
2048
2252
  }
2049
2253
 
2050
- while (comments[0] && comments[0].start < node.start) {
2051
- comment = /** @type {CommentWithLocation} */ (comments.shift());
2254
+ while (
2255
+ comments[0] &&
2256
+ comments[0].start < /** @type {AST.NodeWithLocation} */ (node).start
2257
+ ) {
2258
+ const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2052
2259
 
2053
2260
  // Skip leading comments for BlockStatement that is a function body
2054
2261
  // These comments should be dangling on the function instead
@@ -2067,21 +2274,20 @@ function get_comment_handlers(source, comments, index = 0) {
2067
2274
  }
2068
2275
  }
2069
2276
 
2070
- if (comment.loc) {
2071
- const ancestorElements = path
2072
- .filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
2073
- .sort((a, b) => a.loc.start.line - b.loc.start.line);
2277
+ const ancestorElements = /** @type {(AST.Element & AST.NodeWithLocation)[]} */ (
2278
+ path.filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
2279
+ ).sort((a, b) => a.loc.start.line - b.loc.start.line);
2074
2280
 
2075
- const targetAncestor = ancestorElements.find(
2076
- (ancestor) => comment.loc.start.line < ancestor.loc.start.line,
2077
- );
2281
+ const targetAncestor = ancestorElements.find(
2282
+ (ancestor) => comment.loc.start.line < ancestor.loc.start.line,
2283
+ );
2078
2284
 
2079
- if (targetAncestor) {
2080
- targetAncestor.metadata ??= {};
2081
- (targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
2082
- continue;
2083
- }
2285
+ if (targetAncestor) {
2286
+ targetAncestor.metadata ??= { path: [] };
2287
+ (targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
2288
+ continue;
2084
2289
  }
2290
+
2085
2291
  (node.leadingComments ||= []).push(comment);
2086
2292
  }
2087
2293
 
@@ -2090,8 +2296,12 @@ function get_comment_handlers(source, comments, index = 0) {
2090
2296
  if (comments[0]) {
2091
2297
  if (node.type === 'BlockStatement' && node.body.length === 0) {
2092
2298
  // Collect all comments that fall within this empty block
2093
- while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
2094
- comment = /** @type {CommentWithLocation} */ (comments.shift());
2299
+ while (
2300
+ comments[0] &&
2301
+ comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
2302
+ comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
2303
+ ) {
2304
+ const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2095
2305
  (node.innerComments ||= []).push(comment);
2096
2306
  }
2097
2307
  if (node.innerComments && node.innerComments.length > 0) {
@@ -2103,10 +2313,10 @@ function get_comment_handlers(source, comments, index = 0) {
2103
2313
  // Collect all comments that fall within this JSXEmptyExpression
2104
2314
  while (
2105
2315
  comments[0] &&
2106
- comments[0].start >= node.start &&
2107
- comments[0].end <= node.end
2316
+ comments[0].start >= /** @type {AST.NodeWithLocation} */ (node).start &&
2317
+ comments[0].end <= /** @type {AST.NodeWithLocation} */ (node).end
2108
2318
  ) {
2109
- comment = /** @type {CommentWithLocation} */ (comments.shift());
2319
+ const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2110
2320
  (node.innerComments ||= []).push(comment);
2111
2321
  }
2112
2322
  if (node.innerComments && node.innerComments.length > 0) {
@@ -2116,8 +2326,12 @@ function get_comment_handlers(source, comments, index = 0) {
2116
2326
  // Handle empty Element nodes the same way as empty BlockStatements
2117
2327
  if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
2118
2328
  // Collect all comments that fall within this empty element
2119
- while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
2120
- comment = /** @type {CommentWithLocation} */ (comments.shift());
2329
+ while (
2330
+ comments[0] &&
2331
+ comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
2332
+ comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
2333
+ ) {
2334
+ const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2121
2335
  (node.innerComments ||= []).push(comment);
2122
2336
  }
2123
2337
  if (node.innerComments && node.innerComments.length > 0) {
@@ -2125,57 +2339,58 @@ function get_comment_handlers(source, comments, index = 0) {
2125
2339
  }
2126
2340
  }
2127
2341
 
2128
- const parent = /** @type {any} */ (path.at(-1));
2342
+ const parent = /** @type {AST.Node & AST.NodeWithLocation} */ (path.at(-1));
2129
2343
 
2130
2344
  if (parent === undefined || node.end !== parent.end) {
2131
2345
  const slice = source.slice(node.end, comments[0].start);
2132
2346
 
2133
2347
  // Check if this node is the last item in an array-like structure
2134
2348
  let is_last_in_array = false;
2135
- let array_prop = null;
2349
+ /** @type {(AST.Node | null)[] | null} */
2350
+ let node_array = null;
2351
+ let isParam = false;
2352
+ let isArgument = false;
2353
+ let isSwitchCaseSibling = false;
2136
2354
 
2137
2355
  if (
2138
- parent?.type === 'BlockStatement' ||
2139
- parent?.type === 'Program' ||
2140
- parent?.type === 'Component' ||
2141
- parent?.type === 'ClassBody'
2142
- ) {
2143
- array_prop = 'body';
2144
- } else if (parent?.type === 'SwitchStatement') {
2145
- array_prop = 'cases';
2146
- } else if (parent?.type === 'SwitchCase') {
2147
- array_prop = 'consequent';
2148
- } else if (
2149
- parent?.type === 'ArrayExpression' ||
2150
- parent?.type === 'TrackedArrayExpression'
2356
+ parent.type === 'BlockStatement' ||
2357
+ parent.type === 'Program' ||
2358
+ parent.type === 'Component' ||
2359
+ parent.type === 'ClassBody'
2151
2360
  ) {
2152
- array_prop = 'elements';
2361
+ node_array = parent.body;
2362
+ } else if (parent.type === 'SwitchStatement') {
2363
+ node_array = parent.cases;
2364
+ isSwitchCaseSibling = true;
2365
+ } else if (parent.type === 'SwitchCase') {
2366
+ node_array = parent.consequent;
2153
2367
  } else if (
2154
- parent?.type === 'ObjectExpression' ||
2155
- parent?.type === 'TrackedObjectExpression'
2368
+ parent.type === 'ArrayExpression' ||
2369
+ parent.type === 'TrackedArrayExpression'
2156
2370
  ) {
2157
- array_prop = 'properties';
2371
+ node_array = parent.elements;
2158
2372
  } else if (
2159
- parent?.type === 'FunctionDeclaration' ||
2160
- parent?.type === 'FunctionExpression' ||
2161
- parent?.type === 'ArrowFunctionExpression'
2373
+ parent.type === 'ObjectExpression' ||
2374
+ parent.type === 'TrackedObjectExpression'
2162
2375
  ) {
2163
- array_prop = 'params';
2376
+ node_array = parent.properties;
2164
2377
  } else if (
2165
- parent?.type === 'CallExpression' ||
2166
- parent?.type === 'OptionalCallExpression' ||
2167
- parent?.type === 'NewExpression'
2378
+ parent.type === 'FunctionDeclaration' ||
2379
+ parent.type === 'FunctionExpression' ||
2380
+ parent.type === 'ArrowFunctionExpression'
2168
2381
  ) {
2169
- array_prop = 'arguments';
2382
+ node_array = parent.params;
2383
+ isParam = true;
2384
+ } else if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
2385
+ node_array = parent.arguments;
2386
+ isArgument = true;
2170
2387
  }
2171
- if (array_prop && Array.isArray(parent[array_prop])) {
2172
- is_last_in_array =
2173
- parent[array_prop].indexOf(node) === parent[array_prop].length - 1;
2388
+
2389
+ if (node_array && Array.isArray(node_array)) {
2390
+ is_last_in_array = node_array.indexOf(node) === node_array.length - 1;
2174
2391
  }
2175
2392
 
2176
2393
  if (is_last_in_array) {
2177
- const isParam = array_prop === 'params';
2178
- const isArgument = array_prop === 'arguments';
2179
2394
  if (isParam || isArgument) {
2180
2395
  while (comments.length) {
2181
2396
  const potentialComment = comments[0];
@@ -2186,7 +2401,7 @@ function get_comment_handlers(source, comments, index = 0) {
2186
2401
  const nextChar = getNextNonWhitespaceCharacter(source, potentialComment.end);
2187
2402
  if (nextChar === ')') {
2188
2403
  (node.trailingComments ||= []).push(
2189
- /** @type {CommentWithLocation} */ (comments.shift()),
2404
+ /** @type {AST.CommentWithLocation} */ (comments.shift()),
2190
2405
  );
2191
2406
  continue;
2192
2407
  }
@@ -2207,7 +2422,7 @@ function get_comment_handlers(source, comments, index = 0) {
2207
2422
  end = comment.end;
2208
2423
  }
2209
2424
  }
2210
- } else if (node.end <= comments[0].start) {
2425
+ } else if (/** @type {AST.NodeWithLocation} */ (node).end <= comments[0].start) {
2211
2426
  const onlySimpleWhitespace = /^[,) \t]*$/.test(slice);
2212
2427
  const onlyWhitespace = /^\s*$/.test(slice);
2213
2428
  const hasBlankLine = /\n\s*\n/.test(slice);
@@ -2217,7 +2432,6 @@ function get_comment_handlers(source, comments, index = 0) {
2217
2432
  nodeEndLine !== null &&
2218
2433
  commentStartLine !== null &&
2219
2434
  commentStartLine === nodeEndLine + 1;
2220
- const isSwitchCaseSibling = array_prop === 'cases';
2221
2435
 
2222
2436
  if (isSwitchCaseSibling && !is_last_in_array) {
2223
2437
  if (
@@ -2225,7 +2439,9 @@ function get_comment_handlers(source, comments, index = 0) {
2225
2439
  commentStartLine !== null &&
2226
2440
  nodeEndLine === commentStartLine
2227
2441
  ) {
2228
- node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
2442
+ node.trailingComments = [
2443
+ /** @type {AST.CommentWithLocation} */ (comments.shift()),
2444
+ ];
2229
2445
  }
2230
2446
  return;
2231
2447
  }
@@ -2237,14 +2453,9 @@ function get_comment_handlers(source, comments, index = 0) {
2237
2453
  // Check if this is a block comment that's inline with the next statement
2238
2454
  // e.g., /** @type {SomeType} */ (a) = 5;
2239
2455
  // These should be leading comments, not trailing
2240
- if (
2241
- comments[0].type === 'Block' &&
2242
- !is_last_in_array &&
2243
- array_prop &&
2244
- parent[array_prop]
2245
- ) {
2246
- const currentIndex = parent[array_prop].indexOf(node);
2247
- const nextSibling = parent[array_prop][currentIndex + 1];
2456
+ if (comments[0].type === 'Block' && !is_last_in_array && node_array) {
2457
+ const currentIndex = node_array.indexOf(node);
2458
+ const nextSibling = node_array[currentIndex + 1];
2248
2459
 
2249
2460
  if (nextSibling && nextSibling.loc) {
2250
2461
  const commentEndLine = comments[0].loc?.end?.line;
@@ -2260,21 +2471,22 @@ function get_comment_handlers(source, comments, index = 0) {
2260
2471
 
2261
2472
  // For function parameters, only attach as trailing comment if it's on the same line
2262
2473
  // Comments on next line after comma should be leading comments of next parameter
2263
- const isParam = array_prop === 'params';
2264
2474
  if (isParam) {
2265
2475
  // Check if comment is on same line as the node
2266
2476
  const nodeEndLine = source.slice(0, node.end).split('\n').length;
2267
2477
  const commentStartLine = source.slice(0, comments[0].start).split('\n').length;
2268
2478
  if (nodeEndLine === commentStartLine) {
2269
2479
  node.trailingComments = [
2270
- /** @type {CommentWithLocation} */ (comments.shift()),
2480
+ /** @type {AST.CommentWithLocation} */ (comments.shift()),
2271
2481
  ];
2272
2482
  }
2273
2483
  // Otherwise leave it for next parameter's leading comments
2274
2484
  } else {
2275
- node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
2485
+ node.trailingComments = [
2486
+ /** @type {AST.CommentWithLocation} */ (comments.shift()),
2487
+ ];
2276
2488
  }
2277
- } else if (hasBlankLine && onlyWhitespace && array_prop && parent[array_prop]) {
2489
+ } else if (hasBlankLine && onlyWhitespace && node_array) {
2278
2490
  // When there's a blank line between node and comment(s),
2279
2491
  // check if there's also a blank line after the comment(s) before the next node
2280
2492
  // If so, attach comments as trailing to preserve the grouping
@@ -2289,8 +2501,8 @@ function get_comment_handlers(source, comments, index = 0) {
2289
2501
  return;
2290
2502
  }
2291
2503
 
2292
- const currentIndex = parent[array_prop].indexOf(node);
2293
- const nextSibling = parent[array_prop][currentIndex + 1];
2504
+ const currentIndex = node_array.indexOf(node);
2505
+ const nextSibling = node_array[currentIndex + 1];
2294
2506
 
2295
2507
  if (nextSibling && nextSibling.loc) {
2296
2508
  // Find where the comment block ends
@@ -2336,7 +2548,9 @@ function get_comment_handlers(source, comments, index = 0) {
2336
2548
  if (!commentsInsideElement) {
2337
2549
  // Attach all the comments as trailing
2338
2550
  for (let i = 0; i <= lastCommentIndex; i++) {
2339
- (node.trailingComments ||= []).push(comments.shift());
2551
+ (node.trailingComments ||= []).push(
2552
+ /** @type {AST.CommentWithLocation} */ (comments.shift()),
2553
+ );
2340
2554
  }
2341
2555
  }
2342
2556
  }
@@ -2355,10 +2569,10 @@ function get_comment_handlers(source, comments, index = 0) {
2355
2569
  * Parse Ripple source code into an AST
2356
2570
  * @param {string} source
2357
2571
  * @param {ParseOptions} [options]
2358
- * @returns {Program}
2572
+ * @returns {AST.Program}
2359
2573
  */
2360
2574
  export function parse(source, options) {
2361
- /** @type {CommentWithLocation[]} */
2575
+ /** @type {AST.CommentWithLocation[]} */
2362
2576
  const comments = [];
2363
2577
 
2364
2578
  // Preprocess step 1: Add trailing commas to single-parameter generics followed by (
@@ -2413,7 +2627,7 @@ export function parse(source, options) {
2413
2627
  sourceType: 'module',
2414
2628
  ecmaVersion: 13,
2415
2629
  locations: true,
2416
- onComment: /** @type {any} */ (onComment),
2630
+ onComment,
2417
2631
  rippleOptions: {
2418
2632
  loose: options?.loose || false,
2419
2633
  },
@@ -2424,5 +2638,5 @@ export function parse(source, options) {
2424
2638
 
2425
2639
  add_comments(ast);
2426
2640
 
2427
- return /** @type {Program} */ (ast);
2641
+ return ast;
2428
2642
  }