diff --git a/packages/eslint-plugin/src/rules/__tests__/no-html-links.test.ts b/packages/eslint-plugin/src/rules/__tests__/no-html-links.test.ts index a14e2c6e524c..81d215353d67 100644 --- a/packages/eslint-plugin/src/rules/__tests__/no-html-links.test.ts +++ b/packages/eslint-plugin/src/rules/__tests__/no-html-links.test.ts @@ -43,6 +43,15 @@ ruleTester.run('prefer-docusaurus-link', rule, { code: 'Call', options: [{ignoreFullyResolved: true}], }, + { + // eslint-disable-next-line no-template-curly-in-string + code: 'Twitter', + options: [{ignoreFullyResolved: true}], + }, + { + code: 'X', + options: [{ignoreFullyResolved: true}], + }, ], invalid: [ { @@ -79,10 +88,14 @@ ruleTester.run('prefer-docusaurus-link', rule, { errors: errorsJSX, }, { - // TODO we might want to make this test pass - // Can template literals be statically pre-evaluated? (Babel can do it) - // eslint-disable-next-line no-template-curly-in-string - code: 'Twitter', + // Dynamic template literals that can't be resolved should be reported + code: 'GitHub', + options: [{ignoreFullyResolved: true}], + errors: errorsJSX, + }, + { + // Partially resolved but not fully (path starts with /) + code: 'Internal', options: [{ignoreFullyResolved: true}], errors: errorsJSX, }, diff --git a/packages/eslint-plugin/src/rules/no-html-links.ts b/packages/eslint-plugin/src/rules/no-html-links.ts index ca40a486ff16..c6c1edde6395 100644 --- a/packages/eslint-plugin/src/rules/no-html-links.ts +++ b/packages/eslint-plugin/src/rules/no-html-links.ts @@ -29,6 +29,41 @@ function isFullyResolvedUrl(urlString: string): boolean { return false; } +function staticPreEvaluate(node: TSESTree.Node): string | null { + if (node.type === 'Literal' && typeof node.value === 'string') { + return node.value; + } + if (node.type === 'TemplateLiteral') { + let result = ''; + for (let i = 0; i < node.quasis.length; i++) { + const quasi = node.quasis[i]; + if (quasi) { + result += quasi.value.raw; + } + if (i < node.expressions.length) { + const expr = node.expressions[i]; + if (!expr) { + return null; + } + const evaluated = staticPreEvaluate(expr); + if (evaluated === null) { + return null; + } + result += String(evaluated); + } + } + return result; + } + if (node.type === 'BinaryExpression' && node.operator === '+') { + const left = staticPreEvaluate(node.left); + const right = staticPreEvaluate(node.right); + if (left !== null && right !== null) { + return String(left) + String(right); + } + } + return null; +} + export default createRule({ name: 'no-html-links', meta: { @@ -81,17 +116,9 @@ export default createRule({ if (hrefAttr?.value?.type === 'JSXExpressionContainer') { const container: TSESTree.JSXExpressionContainer = hrefAttr.value; const {expression} = container; - if (expression.type === 'TemplateLiteral') { - // Simple static string template literals - if ( - expression.expressions.length === 0 && - expression.quasis.length === 1 && - expression.quasis[0]?.type === 'TemplateElement' && - isFullyResolvedUrl(String(expression.quasis[0].value.raw)) - ) { - return; - } - // TODO add more complex TemplateLiteral cases here + const evaluated = staticPreEvaluate(expression); + if (evaluated !== null && isFullyResolvedUrl(evaluated)) { + return; } } }