diff --git a/packages/parser-jsx/src/parser.ts b/packages/parser-jsx/src/parser.ts index 741f5b67425..2d831e22bc3 100644 --- a/packages/parser-jsx/src/parser.ts +++ b/packages/parser-jsx/src/parser.ts @@ -267,6 +267,41 @@ const allowsTextChildren = (node: JSXElement): boolean => { !HTML_ELEMENTS_WITH_ONLY_NON_TEXT_CHILDREN.includes(node.openingElement.name.name); }; +/** + * Check if the node contains any JSX elements in its descendants. + * This is used to determine if an expression will render JSX content + * rather than plain text. + */ +const containsJSXElement = (node: Node): boolean => { + if (!node || typeof node !== 'object') { + return false; + } + + if (node.type === 'JSXElement') { + return true; + } + + for (const key of Object.keys(node)) { + if (key === 'loc' || key === 'range' || key === 'parent') { + continue; // Skip location info and parent references to avoid infinite loops + } + + const value = (node as any)[key]; + + if (Array.isArray(value)) { + for (const item of value) { + if (item && typeof item === 'object' && containsJSXElement(item as Node)) { + return true; + } + } + } else if (value && typeof value === 'object' && containsJSXElement(value as Node)) { + return true; + } + } + + return false; +}; + /** * Generate an HTML document representing a fragment containing the * provided roots derived from the specified resource. @@ -309,9 +344,19 @@ export default class JSXParser extends Parser { } }, JSXExpressionContainer(node, /* istanbul ignore next */ ancestors = []) { - const data = mapExpression(node); const parent = getParentAttributeOrElement(ancestors); + /* + * Skip if this expression contains JSX elements, since those elements + * will be processed separately and adding a text placeholder would be misleading. + * See: https://github.com/webhintio/hint/issues/4624 + */ + if (containsJSXElement(node.expression)) { + return; + } + + const data = mapExpression(node); + if (parent && parent.type !== 'JSXAttribute' && allowsTextChildren(parent)) { addChild(data, parent, childMap); } diff --git a/packages/parser-jsx/tests/tests.ts b/packages/parser-jsx/tests/tests.ts index cfc41c5a190..b5f2a4fa90e 100644 --- a/packages/parser-jsx/tests/tests.ts +++ b/packages/parser-jsx/tests/tests.ts @@ -198,3 +198,42 @@ test('It translates JSX attributes to HTML attributes', async (t) => { t.is(label.getAttribute('class'), 'foo'); t.is(label.getAttribute('for'), 'bar'); }); + +/* + * Issue #4624: Doesn't recognize div inside function + * https://github.com/webhintio/hint/issues/4624 + */ +test('It omits expression placeholder when expression contains JSX (dl with map)', async (t) => { + const { document } = await parseJSX(`const jsx =
{data?.nodes?.map((item) => (
{item.name}
{item.value}
))}
;`); + const dl = document.querySelectorAll('dl')[0]; + + /* The outer expression should not produce a text node, only the inner JSX elements. */ + t.is(dl.innerHTML, '
{expression}
{expression}
'); +}); + +test('It omits expression placeholder when expression contains JSX (conditional)', async (t) => { + const { document } = await parseJSX(`const jsx =
{condition ?
Term
: null}
;`); + const dl = document.querySelectorAll('dl')[0]; + + /* The conditional expression should not produce a text node. */ + t.is(dl.innerHTML, '
Term
'); +}); + +test('It omits expression placeholder in div when expression contains JSX', async (t) => { + const { document } = await parseJSX(`const jsx =
{items.map(item => {item})}
;`); + const div = document.querySelectorAll('div')[0]; + + /* + * Even in elements that allow text children, expressions containing JSX + * should not produce a text placeholder (only the JSX elements). + */ + t.is(div.innerHTML, '{expression}'); +}); + +test('It keeps expression placeholder when expression does not contain JSX', async (t) => { + const { document } = await parseJSX(`const jsx =
{getText()}
;`); + const div = document.querySelectorAll('div')[0]; + + /* Simple function calls that don't contain JSX should still get the placeholder. */ + t.is(div.innerHTML, '{expression}'); +});