@tsrx/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +52 -0
- package/src/analyze/css-analyze.js +160 -0
- package/src/analyze/validation.js +167 -0
- package/src/comment-utils.js +91 -0
- package/src/constants.js +21 -0
- package/src/errors.js +82 -0
- package/src/helpers.d.ts +11 -0
- package/src/identifier-utils.js +80 -0
- package/src/index.js +141 -0
- package/src/parse/index.js +772 -0
- package/src/parse/style.js +704 -0
- package/src/scope.js +476 -0
- package/src/source-map-utils.js +358 -0
- package/src/transform/segments.js +2140 -0
- package/src/transform/stylesheet.js +545 -0
- package/src/utils/ast.js +273 -0
- package/src/utils/builders.js +1287 -0
- package/src/utils/escaping.js +26 -0
- package/src/utils/events.js +154 -0
- package/src/utils/hashing.js +15 -0
- package/src/utils/normalize_css_property_name.js +23 -0
- package/src/utils/patterns.js +24 -0
- package/src/utils/sanitize_template_string.js +7 -0
- package/src/utils.js +165 -0
- package/types/acorn.d.ts +3 -0
- package/types/estree-jsx.d.ts +3 -0
- package/types/estree.d.ts +3 -0
- package/types/index.d.ts +1550 -0
- package/types/parse.d.ts +1722 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import * as AST from 'estree'
|
|
3
|
+
@import * as ESTreeJSX from 'estree-jsx'
|
|
4
|
+
@import { Parse } from '../../types/parse'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as acorn from 'acorn';
|
|
8
|
+
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
|
9
|
+
import { walk } from 'zimmerframe';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {(BaseParser: typeof acorn.Parser) => typeof acorn.Parser} AcornPlugin
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {Parse.BindingType} */
|
|
16
|
+
export const BINDING_TYPES = {
|
|
17
|
+
BIND_NONE: 0, // Not a binding
|
|
18
|
+
BIND_VAR: 1, // Var-style binding
|
|
19
|
+
BIND_LEXICAL: 2, // Let- or const-style binding
|
|
20
|
+
BIND_FUNCTION: 3, // Function declaration
|
|
21
|
+
BIND_SIMPLE_CATCH: 4, // Simple (identifier pattern) catch binding
|
|
22
|
+
BIND_OUTSIDE: 5, // Special case for function names as bound inside the function
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @this {Parse.DestructuringErrors}
|
|
27
|
+
* @returns {Parse.DestructuringErrors}
|
|
28
|
+
*/
|
|
29
|
+
export function DestructuringErrors() {
|
|
30
|
+
if (!(this instanceof DestructuringErrors)) {
|
|
31
|
+
throw new TypeError("'DestructuringErrors' must be invoked with 'new'");
|
|
32
|
+
}
|
|
33
|
+
this.shorthandAssign = -1;
|
|
34
|
+
this.trailingComma = -1;
|
|
35
|
+
this.parenthesizedAssign = -1;
|
|
36
|
+
this.parenthesizedBind = -1;
|
|
37
|
+
this.doubleProto = -1;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert JSX node types to regular JavaScript node types
|
|
43
|
+
* @param {ESTreeJSX.JSXIdentifier | ESTreeJSX.JSXMemberExpression | AST.Node} node - The JSX node to convert
|
|
44
|
+
* @returns {AST.Identifier | AST.MemberExpression | AST.Node} The converted node
|
|
45
|
+
*/
|
|
46
|
+
export function convert_from_jsx(node) {
|
|
47
|
+
/** @type {AST.Identifier | AST.MemberExpression | AST.Node} */
|
|
48
|
+
let converted_node;
|
|
49
|
+
if (node.type === 'JSXIdentifier') {
|
|
50
|
+
converted_node = /** @type {AST.Identifier} */ (/** @type {unknown} */ (node));
|
|
51
|
+
converted_node.type = 'Identifier';
|
|
52
|
+
} else if (node.type === 'JSXMemberExpression') {
|
|
53
|
+
converted_node = /** @type {AST.MemberExpression} */ (/** @type {unknown} */ (node));
|
|
54
|
+
converted_node.type = 'MemberExpression';
|
|
55
|
+
converted_node.object = /** @type {AST.Identifier | AST.MemberExpression} */ (
|
|
56
|
+
convert_from_jsx(converted_node.object)
|
|
57
|
+
);
|
|
58
|
+
converted_node.property = /** @type {AST.Identifier} */ (
|
|
59
|
+
convert_from_jsx(converted_node.property)
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
converted_node = node;
|
|
63
|
+
}
|
|
64
|
+
return converted_node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const regex_whitespace_only = /\s/;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Skip whitespace characters without skipping comments.
|
|
71
|
+
* This is needed because Acorn's skipSpace() also skips comments, which breaks
|
|
72
|
+
* parsing in certain contexts. Updates parser position and line tracking.
|
|
73
|
+
* @param {Parse.Parser} parser
|
|
74
|
+
*/
|
|
75
|
+
export function skipWhitespace(parser) {
|
|
76
|
+
const originalStart = parser.start;
|
|
77
|
+
/** @type {acorn.Position | undefined} */
|
|
78
|
+
let lineInfo;
|
|
79
|
+
while (
|
|
80
|
+
parser.start < parser.input.length &&
|
|
81
|
+
regex_whitespace_only.test(parser.input[parser.start])
|
|
82
|
+
) {
|
|
83
|
+
parser.start++;
|
|
84
|
+
}
|
|
85
|
+
// Update line tracking if whitespace was skipped
|
|
86
|
+
if (parser.start !== originalStart) {
|
|
87
|
+
lineInfo = acorn.getLineInfo(parser.input, parser.start);
|
|
88
|
+
if (parser.pos <= parser.start) {
|
|
89
|
+
parser.curLine = lineInfo.line;
|
|
90
|
+
parser.lineStart = parser.start - lineInfo.column;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
parser.startLoc = lineInfo || acorn.getLineInfo(parser.input, parser.start);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {AST.Node | null | undefined} node
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
export function isWhitespaceTextNode(node) {
|
|
102
|
+
if (!node || node.type !== 'Text') {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const expr = node.expression;
|
|
107
|
+
if (expr && expr.type === 'Literal' && typeof expr.value === 'string') {
|
|
108
|
+
return /^\s*$/.test(expr.value);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a parser by composing Acorn with TypeScript/JSX support and optional framework plugins.
|
|
115
|
+
*
|
|
116
|
+
* This is the core factory for building tsrx-based parsers. Framework plugins (like RipplePlugin)
|
|
117
|
+
* extend the base parser with framework-specific syntax.
|
|
118
|
+
*
|
|
119
|
+
* @param {...(AcornPlugin | Function)} plugins - Framework parser plugins to compose
|
|
120
|
+
* @returns {(source: string, filename?: string, options?: any) => AST.Program} A parse function
|
|
121
|
+
*/
|
|
122
|
+
export function createParser(...plugins) {
|
|
123
|
+
const parser = /** @type {Parse.ParserConstructor} */ (
|
|
124
|
+
/** @type {unknown} */ (
|
|
125
|
+
acorn.Parser.extend(
|
|
126
|
+
tsPlugin({ jsx: true }),
|
|
127
|
+
...plugins.map((p) => /** @type {AcornPlugin} */ (/** @type {unknown} */ (p))),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} source
|
|
134
|
+
* @param {string} [filename]
|
|
135
|
+
* @param {any} [options]
|
|
136
|
+
* @returns {AST.Program}
|
|
137
|
+
*/
|
|
138
|
+
return function parse(source, filename, options) {
|
|
139
|
+
/** @type {AST.CommentWithLocation[]} */
|
|
140
|
+
const comments = [];
|
|
141
|
+
const output_comments = options?.comments;
|
|
142
|
+
|
|
143
|
+
const { onComment, add_comments } = get_comment_handlers(source, comments);
|
|
144
|
+
/** @type {AST.Program} */
|
|
145
|
+
let ast;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
ast = parser.parse(source, {
|
|
149
|
+
sourceType: 'module',
|
|
150
|
+
ecmaVersion: 13,
|
|
151
|
+
allowReturnOutsideFunction: true,
|
|
152
|
+
locations: true,
|
|
153
|
+
onComment,
|
|
154
|
+
rippleOptions: {
|
|
155
|
+
filename,
|
|
156
|
+
errors: options?.errors ?? [],
|
|
157
|
+
loose: options?.loose || false,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
} catch (e) {
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (output_comments) {
|
|
165
|
+
for (let i = 0; i < comments.length; i++) {
|
|
166
|
+
output_comments.push(comments[i]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
add_comments(ast);
|
|
171
|
+
|
|
172
|
+
return ast;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create comment handlers for tracking and attaching comments to AST nodes.
|
|
178
|
+
* Used by parse functions to collect and attach comments during parsing.
|
|
179
|
+
* @param {string} source - The source code being parsed
|
|
180
|
+
* @param {AST.CommentWithLocation[]} comments - Array to collect comments into
|
|
181
|
+
* @param {number} [index=0] - Starting index for comment filtering
|
|
182
|
+
* @returns {{ onComment: Parse.Options['onComment'], add_comments: (ast: AST.Node | AST.CSS.StyleSheet) => void }}
|
|
183
|
+
*/
|
|
184
|
+
export function get_comment_handlers(source, comments, index = 0) {
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} text
|
|
187
|
+
* @param {number} startIndex
|
|
188
|
+
* @returns {string | null}
|
|
189
|
+
*/
|
|
190
|
+
function getNextNonWhitespaceCharacter(text, startIndex) {
|
|
191
|
+
for (let i = startIndex; i < text.length; i++) {
|
|
192
|
+
const char = text[i];
|
|
193
|
+
if (char !== ' ' && char !== '\t' && char !== '\n' && char !== '\r') {
|
|
194
|
+
return char;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
/**
|
|
202
|
+
* @type {Parse.Options['onComment']}
|
|
203
|
+
*/
|
|
204
|
+
onComment: (block, value, start, end, start_loc, end_loc, metadata) => {
|
|
205
|
+
if (block && /\n/.test(value)) {
|
|
206
|
+
let a = start;
|
|
207
|
+
while (a > 0 && source[a - 1] !== '\n') a -= 1;
|
|
208
|
+
|
|
209
|
+
let b = a;
|
|
210
|
+
while (/[ \t]/.test(source[b])) b += 1;
|
|
211
|
+
|
|
212
|
+
const indentation = source.slice(a, b);
|
|
213
|
+
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
comments.push({
|
|
217
|
+
type: block ? 'Block' : 'Line',
|
|
218
|
+
value,
|
|
219
|
+
start,
|
|
220
|
+
end,
|
|
221
|
+
loc: {
|
|
222
|
+
start: start_loc,
|
|
223
|
+
end: end_loc,
|
|
224
|
+
},
|
|
225
|
+
context: metadata ?? null,
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {AST.Node | AST.CSS.StyleSheet} ast
|
|
231
|
+
*/
|
|
232
|
+
add_comments: (ast) => {
|
|
233
|
+
if (comments.length === 0) return;
|
|
234
|
+
|
|
235
|
+
comments = comments
|
|
236
|
+
.filter((comment) => comment.start >= index)
|
|
237
|
+
.map(({ type, value, start, end, loc, context }) => ({
|
|
238
|
+
type,
|
|
239
|
+
value,
|
|
240
|
+
start,
|
|
241
|
+
end,
|
|
242
|
+
loc,
|
|
243
|
+
context,
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
walk(ast, null, {
|
|
247
|
+
_(node, { next, path }) {
|
|
248
|
+
const metadata = /** @type {AST.Node} */ (node)?.metadata;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if a comment is inside an attribute expression
|
|
252
|
+
* of any ancestor Elements.
|
|
253
|
+
* @returns {boolean}
|
|
254
|
+
*/
|
|
255
|
+
function isCommentInsideAttributeExpression() {
|
|
256
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
257
|
+
const ancestor = path[i];
|
|
258
|
+
if (
|
|
259
|
+
ancestor &&
|
|
260
|
+
(ancestor.type === 'JSXAttribute' ||
|
|
261
|
+
ancestor.type === 'Attribute' ||
|
|
262
|
+
ancestor.type === 'JSXExpressionContainer')
|
|
263
|
+
) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if a comment is inside any attribute of ancestor Elements,
|
|
272
|
+
* but NOT if we're currently traversing inside that attribute.
|
|
273
|
+
* @param {AST.CommentWithLocation} comment
|
|
274
|
+
* @returns {boolean}
|
|
275
|
+
*/
|
|
276
|
+
function isCommentInsideUnvisitedAttribute(comment) {
|
|
277
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
278
|
+
const ancestor = path[i];
|
|
279
|
+
// we would definitely reach the attribute first before getting to the element
|
|
280
|
+
if (ancestor.type === 'JSXAttribute' || ancestor.type === 'Attribute') {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
if (ancestor && ancestor.type === 'Element') {
|
|
284
|
+
for (const attr of /** @type {(AST.Attribute & AST.NodeWithLocation)[]} */ (
|
|
285
|
+
ancestor.attributes
|
|
286
|
+
)) {
|
|
287
|
+
if (comment.start >= attr.start && comment.end <= attr.end) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* If a comment is located between an empty Element's opening and closing tags,
|
|
298
|
+
* attach it to the Element as `innerComments`.
|
|
299
|
+
* @param {AST.CommentWithLocation} comment
|
|
300
|
+
* @returns {AST.Element | null}
|
|
301
|
+
*/
|
|
302
|
+
function getEmptyElementInnerCommentTarget(comment) {
|
|
303
|
+
const element = /** @type {AST.Element | undefined} */ (
|
|
304
|
+
path.findLast((ancestor) => ancestor && ancestor.type === 'Element')
|
|
305
|
+
);
|
|
306
|
+
if (
|
|
307
|
+
!element ||
|
|
308
|
+
element.children.length > 0 ||
|
|
309
|
+
!element.closingElement ||
|
|
310
|
+
!(
|
|
311
|
+
comment.start >= /** @type {AST.NodeWithLocation} */ (element.openingElement).end &&
|
|
312
|
+
comment.end <= /** @type {AST.NodeWithLocation} */ (element).end
|
|
313
|
+
)
|
|
314
|
+
) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return element;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Skip CSS nodes entirely - they use CSS-local positions (relative to
|
|
322
|
+
// the <style> tag content) which would incorrectly match against
|
|
323
|
+
// absolute source positions of JS/HTML comments. Also consume any
|
|
324
|
+
// CSS comments (which have absolute positions) that fall within the
|
|
325
|
+
// parent <style> element's content range so they don't leak to
|
|
326
|
+
// subsequent JS nodes.
|
|
327
|
+
if (node.type === 'StyleSheet') {
|
|
328
|
+
const styleElement = /** @type {AST.Element & AST.NodeWithLocation | undefined} */ (
|
|
329
|
+
path.findLast(
|
|
330
|
+
(ancestor) =>
|
|
331
|
+
ancestor &&
|
|
332
|
+
ancestor.type === 'Element' &&
|
|
333
|
+
ancestor.id &&
|
|
334
|
+
/** @type {AST.Identifier} */ (ancestor.id).name === 'style',
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
if (styleElement) {
|
|
338
|
+
const cssStart =
|
|
339
|
+
/** @type {AST.NodeWithLocation} */ (styleElement.openingElement)?.end ??
|
|
340
|
+
styleElement.start;
|
|
341
|
+
const cssEnd =
|
|
342
|
+
/** @type {AST.NodeWithLocation} */ (styleElement.closingElement)?.start ??
|
|
343
|
+
styleElement.end;
|
|
344
|
+
while (comments[0] && comments[0].start >= cssStart && comments[0].end <= cssEnd) {
|
|
345
|
+
comments.shift();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (metadata && metadata.commentContainerId !== undefined) {
|
|
352
|
+
// For empty template elements, keep comments as `innerComments`.
|
|
353
|
+
// The Prettier plugin uses `innerComments` to preserve them and
|
|
354
|
+
// to avoid collapsing the element into self-closing syntax.
|
|
355
|
+
const isEmptyElement =
|
|
356
|
+
node.type === 'Element' && (!node.children || node.children.length === 0);
|
|
357
|
+
if (!isEmptyElement) {
|
|
358
|
+
while (
|
|
359
|
+
comments[0] &&
|
|
360
|
+
comments[0].context &&
|
|
361
|
+
comments[0].context.containerId === metadata.commentContainerId &&
|
|
362
|
+
comments[0].context.beforeMeaningfulChild
|
|
363
|
+
) {
|
|
364
|
+
// Check that the comment is actually in this element's own content
|
|
365
|
+
// area, not positionally inside a child element. This handles the
|
|
366
|
+
// case where jsx_parseOpeningElementAt() triggers jsx_readToken()
|
|
367
|
+
// before the child element is pushed to the parser's #path, causing
|
|
368
|
+
// comments inside the child to get the parent's containerId.
|
|
369
|
+
const commentStart = comments[0].start;
|
|
370
|
+
const isInsideChildElement = /** @type {AST.NodeWithChildren} */ (
|
|
371
|
+
node
|
|
372
|
+
).children?.some(
|
|
373
|
+
(child) =>
|
|
374
|
+
child &&
|
|
375
|
+
child.start !== undefined &&
|
|
376
|
+
child.end !== undefined &&
|
|
377
|
+
commentStart >= child.start &&
|
|
378
|
+
commentStart < child.end,
|
|
379
|
+
);
|
|
380
|
+
if (isInsideChildElement) break;
|
|
381
|
+
|
|
382
|
+
const elementComment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
383
|
+
|
|
384
|
+
(metadata.elementLeadingComments ||= []).push(elementComment);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
while (
|
|
390
|
+
comments[0] &&
|
|
391
|
+
comments[0].start < /** @type {AST.NodeWithLocation} */ (node).start
|
|
392
|
+
) {
|
|
393
|
+
// Skip comments that are inside an attribute of an ancestor Element.
|
|
394
|
+
// Since zimmerframe visits children before attributes, we need to leave
|
|
395
|
+
// these comments for when the attribute nodes are visited.
|
|
396
|
+
if (
|
|
397
|
+
isCommentInsideUnvisitedAttribute(
|
|
398
|
+
/** @type {AST.CommentWithLocation} */ (comments[0]),
|
|
399
|
+
)
|
|
400
|
+
) {
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const maybeInner = getEmptyElementInnerCommentTarget(
|
|
405
|
+
/** @type {AST.CommentWithLocation} */ (comments[0]),
|
|
406
|
+
);
|
|
407
|
+
if (maybeInner) {
|
|
408
|
+
(maybeInner.innerComments ||= []).push(
|
|
409
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
410
|
+
);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
415
|
+
|
|
416
|
+
// Skip leading comments for BlockStatement that is a function body
|
|
417
|
+
// These comments should be dangling on the function instead
|
|
418
|
+
if (node.type === 'BlockStatement') {
|
|
419
|
+
const parent = path.at(-1);
|
|
420
|
+
if (
|
|
421
|
+
parent &&
|
|
422
|
+
(parent.type === 'FunctionDeclaration' ||
|
|
423
|
+
parent.type === 'FunctionExpression' ||
|
|
424
|
+
parent.type === 'ArrowFunctionExpression') &&
|
|
425
|
+
parent.body === node
|
|
426
|
+
) {
|
|
427
|
+
// This is a function body - don't attach comment, let it be handled by function
|
|
428
|
+
(parent.comments ||= []).push(comment);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (isCommentInsideAttributeExpression()) {
|
|
434
|
+
(node.leadingComments ||= []).push(comment);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const ancestorElements = /** @type {(AST.Element & AST.NodeWithLocation)[]} */ (
|
|
439
|
+
path.filter((ancestor) => ancestor && ancestor.type === 'Element' && ancestor.loc)
|
|
440
|
+
).sort((a, b) => a.loc.start.line - b.loc.start.line);
|
|
441
|
+
|
|
442
|
+
const targetAncestor = ancestorElements.find(
|
|
443
|
+
(ancestor) => comment.loc.start.line < ancestor.loc.start.line,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (targetAncestor) {
|
|
447
|
+
targetAncestor.metadata ??= { path: [] };
|
|
448
|
+
(targetAncestor.metadata.elementLeadingComments ||= []).push(comment);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
(node.leadingComments ||= []).push(comment);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
next();
|
|
456
|
+
|
|
457
|
+
if (comments[0]) {
|
|
458
|
+
if (node.type === 'Program' && node.body.length === 0) {
|
|
459
|
+
// Collect all comments in an empty program (file with only comments)
|
|
460
|
+
while (comments.length) {
|
|
461
|
+
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
462
|
+
(node.innerComments ||= []).push(comment);
|
|
463
|
+
}
|
|
464
|
+
if (node.innerComments && node.innerComments.length > 0) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (node.type === 'BlockStatement' && node.body.length === 0) {
|
|
469
|
+
// Collect all comments that fall within this empty block
|
|
470
|
+
while (
|
|
471
|
+
comments[0] &&
|
|
472
|
+
comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
|
|
473
|
+
comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
|
|
474
|
+
) {
|
|
475
|
+
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
476
|
+
(node.innerComments ||= []).push(comment);
|
|
477
|
+
}
|
|
478
|
+
if (node.innerComments && node.innerComments.length > 0) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Handle JSXEmptyExpression - these represent {/* comment */} in JSX
|
|
483
|
+
if (node.type === 'JSXEmptyExpression') {
|
|
484
|
+
// Collect all comments that fall within this JSXEmptyExpression
|
|
485
|
+
while (
|
|
486
|
+
comments[0] &&
|
|
487
|
+
comments[0].start >= /** @type {AST.NodeWithLocation} */ (node).start &&
|
|
488
|
+
comments[0].end <= /** @type {AST.NodeWithLocation} */ (node).end
|
|
489
|
+
) {
|
|
490
|
+
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
491
|
+
(node.innerComments ||= []).push(comment);
|
|
492
|
+
}
|
|
493
|
+
if (node.innerComments && node.innerComments.length > 0) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Handle empty Element nodes the same way as empty BlockStatements
|
|
498
|
+
if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
|
|
499
|
+
// Collect all comments that fall within this empty element
|
|
500
|
+
while (
|
|
501
|
+
comments[0] &&
|
|
502
|
+
comments[0].start < /** @type {AST.NodeWithLocation} */ (node).end &&
|
|
503
|
+
comments[0].end < /** @type {AST.NodeWithLocation} */ (node).end
|
|
504
|
+
) {
|
|
505
|
+
const comment = /** @type {AST.CommentWithLocation} */ (comments.shift());
|
|
506
|
+
(node.innerComments ||= []).push(comment);
|
|
507
|
+
}
|
|
508
|
+
if (node.innerComments && node.innerComments.length > 0) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const parent = /** @type {AST.Node & AST.NodeWithLocation} */ (path.at(-1));
|
|
514
|
+
|
|
515
|
+
if (parent === undefined || node.end !== parent.end) {
|
|
516
|
+
const slice = source.slice(node.end, comments[0].start);
|
|
517
|
+
|
|
518
|
+
// Check if this node is the last item in an array-like structure
|
|
519
|
+
let is_last_in_array = false;
|
|
520
|
+
/** @type {(AST.Node | null)[] | null} */
|
|
521
|
+
let node_array = null;
|
|
522
|
+
let isParam = false;
|
|
523
|
+
let isArgument = false;
|
|
524
|
+
let isSwitchCaseSibling = false;
|
|
525
|
+
|
|
526
|
+
if (parent) {
|
|
527
|
+
if (
|
|
528
|
+
parent.type === 'BlockStatement' ||
|
|
529
|
+
parent.type === 'Program' ||
|
|
530
|
+
parent.type === 'Component' ||
|
|
531
|
+
parent.type === 'ClassBody'
|
|
532
|
+
) {
|
|
533
|
+
node_array = parent.body;
|
|
534
|
+
} else if (parent.type === 'SwitchStatement') {
|
|
535
|
+
node_array = parent.cases;
|
|
536
|
+
isSwitchCaseSibling = true;
|
|
537
|
+
} else if (parent.type === 'SwitchCase') {
|
|
538
|
+
node_array = parent.consequent;
|
|
539
|
+
} else if (parent.type === 'ArrayExpression') {
|
|
540
|
+
node_array = parent.elements;
|
|
541
|
+
} else if (parent.type === 'ObjectExpression') {
|
|
542
|
+
node_array = parent.properties;
|
|
543
|
+
} else if (
|
|
544
|
+
parent.type === 'FunctionDeclaration' ||
|
|
545
|
+
parent.type === 'FunctionExpression' ||
|
|
546
|
+
parent.type === 'ArrowFunctionExpression'
|
|
547
|
+
) {
|
|
548
|
+
node_array = parent.params;
|
|
549
|
+
isParam = true;
|
|
550
|
+
} else if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
|
|
551
|
+
node_array = parent.arguments;
|
|
552
|
+
isArgument = true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (node_array && Array.isArray(node_array)) {
|
|
557
|
+
is_last_in_array = node_array.indexOf(node) === node_array.length - 1;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (is_last_in_array) {
|
|
561
|
+
if (isParam || isArgument) {
|
|
562
|
+
while (comments.length) {
|
|
563
|
+
const potentialComment = comments[0];
|
|
564
|
+
if (parent && potentialComment.start >= parent.end) {
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const maybeInner = getEmptyElementInnerCommentTarget(potentialComment);
|
|
569
|
+
if (maybeInner) {
|
|
570
|
+
(maybeInner.innerComments ||= []).push(
|
|
571
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
572
|
+
);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const nextChar = getNextNonWhitespaceCharacter(source, potentialComment.end);
|
|
577
|
+
if (nextChar === ')') {
|
|
578
|
+
(node.trailingComments ||= []).push(
|
|
579
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
580
|
+
);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
// Special case: There can be multiple trailing comments after the last node in a block,
|
|
588
|
+
// and they can be separated by newlines
|
|
589
|
+
while (comments.length) {
|
|
590
|
+
const comment = comments[0];
|
|
591
|
+
if (parent && comment.start >= parent.end) break;
|
|
592
|
+
|
|
593
|
+
const maybeInner = getEmptyElementInnerCommentTarget(comment);
|
|
594
|
+
if (maybeInner) {
|
|
595
|
+
(maybeInner.innerComments ||= []).push(
|
|
596
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
597
|
+
);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
(node.trailingComments ||= []).push(comment);
|
|
602
|
+
comments.shift();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} else if (/** @type {AST.NodeWithLocation} */ (node).end <= comments[0].start) {
|
|
606
|
+
const maybeInner = getEmptyElementInnerCommentTarget(
|
|
607
|
+
/** @type {AST.CommentWithLocation} */ (comments[0]),
|
|
608
|
+
);
|
|
609
|
+
if (maybeInner) {
|
|
610
|
+
(maybeInner.innerComments ||= []).push(
|
|
611
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
612
|
+
);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const onlySimpleWhitespace = /^[,) \t]*$/.test(slice);
|
|
617
|
+
const onlyWhitespace = /^\s*$/.test(slice);
|
|
618
|
+
const hasBlankLine = /\n\s*\n/.test(slice);
|
|
619
|
+
const nodeEndLine = node.loc?.end?.line ?? null;
|
|
620
|
+
const commentStartLine = comments[0].loc?.start?.line ?? null;
|
|
621
|
+
const isImmediateNextLine =
|
|
622
|
+
nodeEndLine !== null &&
|
|
623
|
+
commentStartLine !== null &&
|
|
624
|
+
commentStartLine === nodeEndLine + 1;
|
|
625
|
+
|
|
626
|
+
if (isSwitchCaseSibling && !is_last_in_array) {
|
|
627
|
+
if (
|
|
628
|
+
nodeEndLine !== null &&
|
|
629
|
+
commentStartLine !== null &&
|
|
630
|
+
nodeEndLine === commentStartLine
|
|
631
|
+
) {
|
|
632
|
+
node.trailingComments = [
|
|
633
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (
|
|
640
|
+
onlySimpleWhitespace ||
|
|
641
|
+
(onlyWhitespace && !hasBlankLine && isImmediateNextLine)
|
|
642
|
+
) {
|
|
643
|
+
// Check if this is a block comment that's inline with the next statement
|
|
644
|
+
// e.g., /** @type {SomeType} */ (a) = 5;
|
|
645
|
+
// These should be leading comments, not trailing
|
|
646
|
+
if (comments[0].type === 'Block' && !is_last_in_array && node_array) {
|
|
647
|
+
const currentIndex = node_array.indexOf(node);
|
|
648
|
+
const nextSibling = node_array[currentIndex + 1];
|
|
649
|
+
|
|
650
|
+
if (nextSibling && nextSibling.loc) {
|
|
651
|
+
const commentEndLine = comments[0].loc?.end?.line;
|
|
652
|
+
const nextSiblingStartLine = nextSibling.loc?.start?.line;
|
|
653
|
+
|
|
654
|
+
// If comment ends on same line as next sibling starts, it's inline with next
|
|
655
|
+
if (commentEndLine === nextSiblingStartLine) {
|
|
656
|
+
// Leave it for next sibling's leading comments
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// For function parameters, only attach as trailing comment if it's on the same line
|
|
663
|
+
// Comments on next line after comma should be leading comments of next parameter
|
|
664
|
+
if (isParam) {
|
|
665
|
+
// Check if comment is on same line as the node
|
|
666
|
+
const nodeEndLine = source.slice(0, node.end).split('\n').length;
|
|
667
|
+
const commentStartLine = source.slice(0, comments[0].start).split('\n').length;
|
|
668
|
+
if (nodeEndLine === commentStartLine) {
|
|
669
|
+
node.trailingComments = [
|
|
670
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
671
|
+
];
|
|
672
|
+
}
|
|
673
|
+
// Otherwise leave it for next parameter's leading comments
|
|
674
|
+
} else {
|
|
675
|
+
// Line comments on the next line should be leading comments
|
|
676
|
+
// for the next statement, not trailing comments for this one.
|
|
677
|
+
// Only attach as trailing if:
|
|
678
|
+
// 1. It's on the same line as this node, OR
|
|
679
|
+
// 2. This is the last item in the array (no next sibling to attach to)
|
|
680
|
+
const commentOnSameLine =
|
|
681
|
+
nodeEndLine !== null &&
|
|
682
|
+
commentStartLine !== null &&
|
|
683
|
+
nodeEndLine === commentStartLine;
|
|
684
|
+
|
|
685
|
+
if (commentOnSameLine || is_last_in_array) {
|
|
686
|
+
node.trailingComments = [
|
|
687
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
688
|
+
];
|
|
689
|
+
}
|
|
690
|
+
// Otherwise leave it for next sibling's leading comments
|
|
691
|
+
}
|
|
692
|
+
} else if (hasBlankLine && onlyWhitespace && node_array) {
|
|
693
|
+
// When there's a blank line between node and comment(s),
|
|
694
|
+
// check if there's also a blank line after the comment(s) before the next node
|
|
695
|
+
// If so, attach comments as trailing to preserve the grouping
|
|
696
|
+
// Only do this for statement-level contexts (BlockStatement, Program),
|
|
697
|
+
// not for Element children or other contexts
|
|
698
|
+
const isStatementContext =
|
|
699
|
+
parent.type === 'BlockStatement' || parent.type === 'Program';
|
|
700
|
+
|
|
701
|
+
// Don't apply for Component - let Prettier handle comment attachment there
|
|
702
|
+
// Component bodies have different comment handling via metadata.elementLeadingComments
|
|
703
|
+
if (!isStatementContext) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const currentIndex = node_array.indexOf(node);
|
|
708
|
+
const nextSibling = node_array[currentIndex + 1];
|
|
709
|
+
|
|
710
|
+
if (nextSibling && nextSibling.loc) {
|
|
711
|
+
// Find where the comment block ends
|
|
712
|
+
let lastCommentIndex = 0;
|
|
713
|
+
let lastCommentEnd = comments[0].end;
|
|
714
|
+
|
|
715
|
+
// Collect consecutive comments (without blank lines between them)
|
|
716
|
+
while (comments[lastCommentIndex + 1]) {
|
|
717
|
+
const currentComment = comments[lastCommentIndex];
|
|
718
|
+
const nextComment = comments[lastCommentIndex + 1];
|
|
719
|
+
const sliceBetween = source.slice(currentComment.end, nextComment.start);
|
|
720
|
+
|
|
721
|
+
// If there's a blank line, stop
|
|
722
|
+
if (/\n\s*\n/.test(sliceBetween)) {
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
lastCommentIndex++;
|
|
727
|
+
lastCommentEnd = nextComment.end;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Check if there's a blank line after the last comment and before next sibling
|
|
731
|
+
const sliceAfterComments = source.slice(lastCommentEnd, nextSibling.start);
|
|
732
|
+
const hasBlankLineAfter = /\n\s*\n/.test(sliceAfterComments);
|
|
733
|
+
|
|
734
|
+
if (hasBlankLineAfter) {
|
|
735
|
+
// Don't attach comments as trailing if next sibling is an Element
|
|
736
|
+
// and any comment falls within the Element's line range
|
|
737
|
+
// This means the comments are inside the Element (between opening and closing tags)
|
|
738
|
+
const nextIsElement = nextSibling.type === 'Element';
|
|
739
|
+
const commentsInsideElement =
|
|
740
|
+
nextIsElement &&
|
|
741
|
+
nextSibling.loc &&
|
|
742
|
+
comments.some((c) => {
|
|
743
|
+
if (!c.loc) return false;
|
|
744
|
+
// Check if comment is on a line between Element's start and end lines
|
|
745
|
+
return (
|
|
746
|
+
c.loc.start.line >= nextSibling.loc.start.line &&
|
|
747
|
+
c.loc.end.line <= nextSibling.loc.end.line
|
|
748
|
+
);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
if (!commentsInsideElement) {
|
|
752
|
+
// Attach all the comments as trailing
|
|
753
|
+
for (let i = 0; i <= lastCommentIndex; i++) {
|
|
754
|
+
(node.trailingComments ||= []).push(
|
|
755
|
+
/** @type {AST.CommentWithLocation} */ (comments.shift()),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Re-export acorn utilities that plugins may need
|
|
772
|
+
export { acorn, tsPlugin };
|