Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ ruleTester.run('prefer-docusaurus-link', rule, {
code: '<a href="tel:123456789">Call</a>',
options: [{ignoreFullyResolved: true}],
},
{
// eslint-disable-next-line no-template-curly-in-string
code: '<a href={`https://x.com/${"docu" + "saurus"} ${"rex"}`}>Twitter</a>',
options: [{ignoreFullyResolved: true}],
},
{
code: '<a href={"https://x.com/" + "docusaurus"}>X</a>',
options: [{ignoreFullyResolved: true}],
},
],
invalid: [
{
Expand Down Expand Up @@ -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: '<a href={`https://x.com/${"docu" + "saurus"} ${"rex"}`}>Twitter</a>',
// Dynamic template literals that can't be resolved should be reported
code: '<a href={`https://github.com/${repo}`}>GitHub</a>',
options: [{ignoreFullyResolved: true}],
errors: errorsJSX,
},
{
// Partially resolved but not fully (path starts with /)
code: '<a href={`/${path}`}>Internal</a>',
options: [{ignoreFullyResolved: true}],
errors: errorsJSX,
},
Expand Down
49 changes: 38 additions & 11 deletions packages/eslint-plugin/src/rules/no-html-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Options, MessageIds>({
name: 'no-html-links',
meta: {
Expand Down Expand Up @@ -81,17 +116,9 @@ export default createRule<Options, MessageIds>({
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;
}
}
}
Expand Down