diff --git a/.changeset/hungry-rats-arrive.md b/.changeset/hungry-rats-arrive.md
new file mode 100644
index 000000000..7c9d612a8
--- /dev/null
+++ b/.changeset/hungry-rats-arrive.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix(no-navigation-without-resolve): properly detecting absolute and fragment URLs in variables
diff --git a/.changeset/icy-planets-know.md b/.changeset/icy-planets-know.md
new file mode 100644
index 000000000..d32303d87
--- /dev/null
+++ b/.changeset/icy-planets-know.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+feat(no-navigation-without-resolve): checking link shorthand attributes
diff --git a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts
index 110576e22..7ac4658e8 100644
--- a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts
+++ b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts
@@ -84,6 +84,26 @@ export default createRule('no-navigation-without-resolve', {
}
}
},
+ SvelteShorthandAttribute(node) {
+ if (
+ context.options[0]?.ignoreLinks === true ||
+ node.parent.parent.type !== 'SvelteElement' ||
+ node.parent.parent.kind !== 'html' ||
+ node.parent.parent.name.type !== 'SvelteName' ||
+ node.parent.parent.name.name !== 'a' ||
+ node.key.name !== 'href' ||
+ node.value.type !== 'Identifier'
+ ) {
+ return;
+ }
+ if (
+ !expressionIsAbsolute(node.value, context) &&
+ !expressionIsFragment(node.value, context) &&
+ !isResolveCall(context, node.value, resolveReferences)
+ ) {
+ context.report({ loc: node.loc, messageId: 'linkWithoutResolve' });
+ }
+ },
SvelteAttribute(node) {
if (
context.options[0]?.ignoreLinks === true ||
@@ -97,11 +117,11 @@ export default createRule('no-navigation-without-resolve', {
}
if (
(node.value[0].type === 'SvelteLiteral' &&
- !expressionIsAbsolute(node.value[0]) &&
- !expressionIsFragment(node.value[0])) ||
+ !expressionIsAbsolute(new FindVariableContext(context), node.value[0]) &&
+ !expressionIsFragment(new FindVariableContext(context), node.value[0])) ||
(node.value[0].type === 'SvelteMustacheTag' &&
- !expressionIsAbsolute(node.value[0].expression) &&
- !expressionIsFragment(node.value[0].expression) &&
+ !expressionIsAbsolute(new FindVariableContext(context), node.value[0].expression) &&
+ !expressionIsFragment(new FindVariableContext(context), node.value[0].expression) &&
!isResolveCall(
new FindVariableContext(context),
node.value[0].expression,
@@ -260,31 +280,55 @@ function expressionIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
);
}
-function expressionIsAbsolute(url: AST.SvelteLiteral | TSESTree.Expression): boolean {
+function expressionIsAbsolute(
+ ctx: FindVariableContext,
+ url: AST.SvelteLiteral | TSESTree.Expression
+): boolean {
switch (url.type) {
case 'BinaryExpression':
- return binaryExpressionIsAbsolute(url);
+ return binaryExpressionIsAbsolute(ctx, url);
+ case 'Identifier':
+ return identifierIsAbsolute(ctx, url);
case 'Literal':
return typeof url.value === 'string' && urlValueIsAbsolute(url.value);
case 'SvelteLiteral':
return urlValueIsAbsolute(url.value);
case 'TemplateLiteral':
- return templateLiteralIsAbsolute(url);
+ return templateLiteralIsAbsolute(ctx, url);
default:
return false;
}
}
-function binaryExpressionIsAbsolute(url: TSESTree.BinaryExpression): boolean {
+function binaryExpressionIsAbsolute(
+ ctx: FindVariableContext,
+ url: TSESTree.BinaryExpression
+): boolean {
return (
- (url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(url.left)) ||
- expressionIsAbsolute(url.right)
+ (url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(ctx, url.left)) ||
+ expressionIsAbsolute(ctx, url.right)
);
}
-function templateLiteralIsAbsolute(url: TSESTree.TemplateLiteral): boolean {
+function identifierIsAbsolute(ctx: FindVariableContext, url: TSESTree.Identifier): boolean {
+ const variable = ctx.findVariable(url);
+ if (
+ variable === null ||
+ variable.identifiers.length === 0 ||
+ variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
+ variable.identifiers[0].parent.init === null
+ ) {
+ return false;
+ }
+ return expressionIsAbsolute(ctx, variable.identifiers[0].parent.init);
+}
+
+function templateLiteralIsAbsolute(
+ ctx: FindVariableContext,
+ url: TSESTree.TemplateLiteral
+): boolean {
return (
- url.expressions.some(expressionIsAbsolute) ||
+ url.expressions.some((expression) => expressionIsAbsolute(ctx, expression)) ||
url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw))
);
}
@@ -293,28 +337,52 @@ function urlValueIsAbsolute(url: string): boolean {
return /^[+a-z]*:/i.test(url);
}
-function expressionIsFragment(url: AST.SvelteLiteral | TSESTree.Expression): boolean {
+function expressionIsFragment(
+ ctx: FindVariableContext,
+ url: AST.SvelteLiteral | TSESTree.Expression
+): boolean {
switch (url.type) {
case 'BinaryExpression':
- return binaryExpressionIsFragment(url);
+ return binaryExpressionIsFragment(ctx, url);
+ case 'Identifier':
+ return identifierIsFragment(ctx, url);
case 'Literal':
return typeof url.value === 'string' && urlValueIsFragment(url.value);
case 'SvelteLiteral':
return urlValueIsFragment(url.value);
case 'TemplateLiteral':
- return templateLiteralIsFragment(url);
+ return templateLiteralIsFragment(ctx, url);
default:
return false;
}
}
-function binaryExpressionIsFragment(url: TSESTree.BinaryExpression): boolean {
- return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(url.left);
+function binaryExpressionIsFragment(
+ ctx: FindVariableContext,
+ url: TSESTree.BinaryExpression
+): boolean {
+ return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(ctx, url.left);
+}
+
+function identifierIsFragment(ctx: FindVariableContext, url: TSESTree.Identifier): boolean {
+ const variable = ctx.findVariable(url);
+ if (
+ variable === null ||
+ variable.identifiers.length === 0 ||
+ variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
+ variable.identifiers[0].parent.init === null
+ ) {
+ return false;
+ }
+ return expressionIsFragment(ctx, variable.identifiers[0].parent.init);
}
-function templateLiteralIsFragment(url: TSESTree.TemplateLiteral): boolean {
+function templateLiteralIsFragment(
+ ctx: FindVariableContext,
+ url: TSESTree.TemplateLiteral
+): boolean {
return (
- (url.expressions.length >= 1 && expressionIsFragment(url.expressions[0])) ||
+ (url.expressions.length >= 1 && expressionIsFragment(ctx, url.expressions[0])) ||
(url.quasis.length >= 1 && urlValueIsFragment(url.quasis[0].value.raw))
);
}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-errors.yaml
index 7b8b81c68..2049a0dbf 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-errors.yaml
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-errors.yaml
@@ -1,8 +1,16 @@
- message: Found a link with a url that isn't resolved.
- line: 5
+ line: 8
column: 9
suggestions: null
- message: Found a link with a url that isn't resolved.
- line: 6
+ line: 9
column: 9
suggestions: null
+- message: Found a link with a url that isn't resolved.
+ line: 10
+ column: 9
+ suggestions: null
+- message: Found a link with a url that isn't resolved.
+ line: 11
+ column: 4
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-input.svelte
index 1ab747a3c..661e8c14e 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-resolve01-input.svelte
@@ -1,6 +1,11 @@
Click me!
Click me!
+Click me!
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-errors.yaml
index f6888e8c1..f5adbd301 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-errors.yaml
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-errors.yaml
@@ -1,10 +1,6 @@
-- message: Found a link with a url that isn't resolved.
- line: 5
- column: 10
- suggestions: null
- message: Found a link with a url that isn't resolved.
line: 6
- column: 9
+ column: 10
suggestions: null
- message: Found a link with a url that isn't resolved.
line: 7
@@ -18,3 +14,11 @@
line: 9
column: 9
suggestions: null
+- message: Found a link with a url that isn't resolved.
+ line: 10
+ column: 4
+ suggestions: null
+- message: Found a link with a url that isn't resolved.
+ line: 11
+ column: 9
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-input.svelte
index 2a2d1d418..8e955d59a 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-with-fragment01-input.svelte
@@ -1,9 +1,11 @@
Click me!
Click me!
Click me!
Click me!
+Click me!
Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-errors.yaml
index f6888e8c1..f5adbd301 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-errors.yaml
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-errors.yaml
@@ -1,10 +1,6 @@
-- message: Found a link with a url that isn't resolved.
- line: 5
- column: 10
- suggestions: null
- message: Found a link with a url that isn't resolved.
line: 6
- column: 9
+ column: 10
suggestions: null
- message: Found a link with a url that isn't resolved.
line: 7
@@ -18,3 +14,11 @@
line: 9
column: 9
suggestions: null
+- message: Found a link with a url that isn't resolved.
+ line: 10
+ column: 4
+ suggestions: null
+- message: Found a link with a url that isn't resolved.
+ line: 11
+ column: 9
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-input.svelte
index 4ae6bc7ec..2dab745ff 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-without-resolve01-input.svelte
@@ -1,9 +1,11 @@
Click me!
Click me!
Click me!
Click me!
+Click me!
Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-absolute-url01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-absolute-url01-input.svelte
index 115109a28..cbc35c4a0 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-absolute-url01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-absolute-url01-input.svelte
@@ -1,5 +1,8 @@
Click me!
@@ -13,3 +16,5 @@
Click me!
Click me!
Click me!
+Click me!
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-fragment-url01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-fragment-url01-input.svelte
index 9c931237b..2e35b7630 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-fragment-url01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-fragment-url01-input.svelte
@@ -1,5 +1,8 @@
Click me!
@@ -9,3 +12,5 @@
Click me!
Click me!
Click me!
+Click me!
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-resolved01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-resolved01-input.svelte
index 2561d7490..0ea7fca3a 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-resolved01-input.svelte
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-resolved01-input.svelte
@@ -2,7 +2,9 @@
import { resolve } from '$app/paths';
const value = resolve('/foo/');
+ const href = resolve('/foo/');
Click me!
Click me!
+Click me!