diff --git a/.changeset/public-groups-prove.md b/.changeset/public-groups-prove.md
new file mode 100644
index 000000000..0a744470e
--- /dev/null
+++ b/.changeset/public-groups-prove.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix(no-navigation-without-resolve): allowing undefined and null in link hrefs
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 06ad91df8..fdf808982 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
@@ -97,9 +97,11 @@ export default createRule('no-navigation-without-resolve', {
}
if (
(node.value[0].type === 'SvelteLiteral' &&
+ !expressionIsNullish(new FindVariableContext(context), node.value[0]) &&
!expressionIsAbsolute(new FindVariableContext(context), node.value[0]) &&
!expressionIsFragment(new FindVariableContext(context), node.value[0])) ||
(node.value[0].type === 'SvelteMustacheTag' &&
+ !expressionIsNullish(new FindVariableContext(context), node.value[0].expression) &&
!expressionIsAbsolute(new FindVariableContext(context), node.value[0].expression) &&
!expressionIsFragment(new FindVariableContext(context), node.value[0].expression) &&
!isResolveCall(
@@ -263,6 +265,45 @@ function expressionIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
);
}
+function expressionIsNullish(
+ ctx: FindVariableContext,
+ url: AST.SvelteLiteral | TSESTree.Expression
+): boolean {
+ switch (url.type) {
+ case 'Identifier':
+ return identifierIsNullish(ctx, url);
+ case 'Literal':
+ return url.value === null; // Undefined is an Identifier in ESTree, null is a Literal
+ case 'TemplateLiteral':
+ return templateLiteralIsNullish(ctx, url);
+ default:
+ return false;
+ }
+}
+
+function identifierIsNullish(ctx: FindVariableContext, url: TSESTree.Identifier): boolean {
+ if (url.name === 'undefined') {
+ return true;
+ }
+ 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 expressionIsNullish(ctx, variable.identifiers[0].parent.init);
+}
+
+function templateLiteralIsNullish(
+ ctx: FindVariableContext,
+ url: TSESTree.TemplateLiteral
+): boolean {
+ return url.expressions.length === 1 && expressionIsNullish(ctx, url.expressions[0]);
+}
+
function expressionIsAbsolute(
ctx: FindVariableContext,
url: AST.SvelteLiteral | TSESTree.Expression
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-nullish-like-literal01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-nullish-like-literal01-errors.yaml
new file mode 100644
index 000000000..716ba9583
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-nullish-like-literal01-errors.yaml
@@ -0,0 +1,24 @@
+- message: Unexpected href link without resolve().
+ line: 6
+ column: 10
+ suggestions: null
+- message: Unexpected href link without resolve().
+ line: 7
+ column: 10
+ suggestions: null
+- message: Unexpected href link without resolve().
+ line: 8
+ column: 9
+ suggestions: null
+- message: Unexpected href link without resolve().
+ line: 9
+ column: 9
+ suggestions: null
+- message: Unexpected href link without resolve().
+ line: 10
+ column: 9
+ suggestions: null
+- message: Unexpected href link without resolve().
+ line: 11
+ column: 9
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-nullish-like-literal01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-nullish-like-literal01-input.svelte
new file mode 100644
index 000000000..1986f1f38
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-nullish-like-literal01-input.svelte
@@ -0,0 +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-nullish01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-nullish01-input.svelte
new file mode 100644
index 000000000..87756feb0
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-nullish01-input.svelte
@@ -0,0 +1,13 @@
+
+
+Click me!
+Click me!
+Click me!
+Click me!
+Click me!
+Click me!
+Click me!
+Click me!