Skip to content

Commit 88bebc1

Browse files
authored
fix: handle node and children recursively (#396)
1 parent 2ffe1cb commit 88bebc1

File tree

10 files changed

+43197
-127
lines changed

10 files changed

+43197
-127
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
]
8989
},
9090
"typeCoverage": {
91-
"atLeast": 99.95,
91+
"atLeast": 100,
9292
"cache": true,
9393
"detail": true,
9494
"ignoreAsAssertion": true,

packages/eslint-mdx/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
},
3232
"dependencies": {
3333
"cosmiconfig": "^7.0.1",
34+
"estree-util-visit": "^1.1.0",
3435
"remark-mdx": "^2.1.1",
3536
"remark-parse": "^10.0.1",
3637
"remark-stringify": "^10.0.2",

packages/eslint-mdx/src/worker.ts

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
ObjectExpression,
1717
Program,
1818
SpreadElement,
19+
TemplateElement,
1920
} from 'estree'
2021
import type { JSXClosingElement, JSXElement, JSXFragment } from 'estree-jsx'
2122
import type { BlockContent, PhrasingContent } from 'mdast'
@@ -111,8 +112,9 @@ export const getRemarkProcessor = async (
111112

112113
if (result) {
113114
/* istanbul ignore next */
114-
const { plugins = [], settings } = (result.config ||
115-
{}) as Partial<RemarkConfig>
115+
const { plugins = [], settings } =
116+
// type-coverage:ignore-next-line -- cosmiconfig's typings issue
117+
(result.config || {}) as Partial<RemarkConfig>
116118

117119
// disable this rule automatically since we already have a parser option `extensions`
118120
// only disable this plugin if there are at least one plugin enabled
@@ -292,44 +294,51 @@ runAsWorker(
292294

293295
processed.add(node)
294296

295-
const children =
296-
'children' in node
297+
function handleChildren(node: BlockContent | PhrasingContent) {
298+
return 'children' in node
297299
? (node.children as Array<BlockContent | PhrasingContent>).reduce<
298300
JSXElement['children']
299301
>((acc, child) => {
300302
processed.add(child)
301303

302-
if (!child.data || 'estree' in child.data || !child.data) {
303-
return acc
304-
}
304+
if (child.data && 'estree' in child.data && child.data.estree) {
305+
const estree = child.data.estree as Program
305306

306-
const estree = (child.data.estree || {
307-
body: [],
308-
comments: [],
309-
}) as Program
307+
assert(estree.body.length <= 1)
310308

311-
assert(estree.body.length <= 1)
309+
const expStat = estree.body[0] as ExpressionStatement
312310

313-
const expStat = estree.body[0] as
314-
| ExpressionStatement
315-
| undefined
311+
if (expStat) {
312+
const expression =
313+
expStat.expression as BaseExpression as JSXElement['children'][number]
314+
acc.push(expression)
315+
}
316316

317-
if (expStat) {
318-
const expression =
319-
expStat.expression as BaseExpression as JSXElement['children'][number]
320-
acc.push(expression)
317+
comments.push(...estree.comments)
318+
} else {
319+
const expression = handleNode(
320+
child,
321+
) as JSXElement['children'][number]
322+
if (expression) {
323+
acc.push(expression)
324+
}
321325
}
322326

323-
comments.push(...estree.comments)
324-
325327
return acc
326328
}, [])
327329
: []
330+
}
331+
332+
function handleNode(node: BlockContent | PhrasingContent) {
333+
if (
334+
node.type !== 'mdxJsxTextElement' &&
335+
node.type !== 'mdxJsxFlowElement'
336+
) {
337+
return
338+
}
339+
340+
const children = handleChildren(node)
328341

329-
if (
330-
node.type === 'mdxJsxTextElement' ||
331-
node.type === 'mdxJsxFlowElement'
332-
) {
333342
const nodePos = node.position
334343

335344
const nodeStart = nodePos.start.offset
@@ -546,15 +555,20 @@ runAsWorker(
546555
expression = jsxFrg
547556
}
548557

558+
return expression
559+
}
560+
561+
const expression = handleNode(node) as Expression
562+
563+
if (expression) {
549564
body.push({
550-
...normalizePosition(nodePos),
565+
...normalizePosition(node.position),
551566
type: 'ExpressionStatement',
552-
expression: expression as Expression,
567+
expression: handleNode(node) as Expression,
553568
})
554-
return
555569
}
556570

557-
const estree = (node.data.estree || {
571+
const estree = (node.data?.estree || {
558572
body: [],
559573
comments: [],
560574
}) as Program
@@ -564,6 +578,46 @@ runAsWorker(
564578
})
565579
}
566580

581+
const { visit: visitEstree } = await loadEsmModule<
582+
typeof import('estree-util-visit')
583+
>('estree-util-visit')
584+
585+
visitEstree(
586+
{
587+
type: 'Program',
588+
sourceType: 'module',
589+
body,
590+
},
591+
node => {
592+
if (node.type !== 'TemplateElement') {
593+
return
594+
}
595+
596+
/**
597+
* Copied from @see https://github.com/eslint/espree/blob/main/lib/espree.js#L206-L220
598+
*/
599+
const templateElement = node as TemplateElement
600+
601+
const startOffset = -1
602+
const endOffset = templateElement.tail ? 1 : 2
603+
604+
// @ts-expect-error - unavailable for typing from estree
605+
templateElement.start += startOffset
606+
// @ts-expect-error - unavailable for typing from estree
607+
templateElement.end += endOffset
608+
609+
if (templateElement.range) {
610+
templateElement.range[0] += startOffset
611+
templateElement.range[1] += endOffset
612+
}
613+
614+
if (templateElement.loc) {
615+
templateElement.loc.start.column += startOffset
616+
templateElement.loc.end.column += endOffset
617+
}
618+
},
619+
)
620+
567621
for (const token of restoreTokens(text, root, sharedTokens, tt, visit)) {
568622
tokenTranslator.onToken(token, {
569623
ecmaVersion: 'latest',

0 commit comments

Comments
 (0)