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;
}
}
}