From ef9e3f60aee5eb353425c8a75db9b51d7c4a8ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Thu, 4 Sep 2025 15:12:26 +0200 Subject: [PATCH 1/3] test(no-navigation-without-resolve): added tests with shorthand properties --- .../invalid/link-partial-resolve01-errors.yaml | 10 +++++++--- .../invalid/link-partial-resolve01-input.svelte | 2 ++ .../invalid/link-with-fragment01-errors.yaml | 14 +++++++++----- .../invalid/link-with-fragment01-input.svelte | 2 ++ .../invalid/link-without-resolve01-errors.yaml | 14 +++++++++----- .../invalid/link-without-resolve01-input.svelte | 2 ++ .../valid/link-absolute-url01-input.svelte | 2 ++ .../valid/link-fragment-url01-input.svelte | 2 ++ .../valid/link-resolved01-input.svelte | 2 ++ 9 files changed, 37 insertions(+), 13 deletions(-) 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 98768b3c2..ad7407ad2 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,12 +1,16 @@ - message: Unexpected href link without resolve(). - line: 7 + line: 8 column: 9 suggestions: null - message: Unexpected href link without resolve(). - line: 8 + line: 9 column: 9 suggestions: null - message: Unexpected href link without resolve(). - line: 9 + line: 10 column: 9 suggestions: null +- message: Unexpected href link without resolve(). + 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 6994a2cb1..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 @@ -2,8 +2,10 @@ import { resolve } from '$app/paths'; const value = resolve('/foo') + '/bar'; + const href = resolve('/foo') + '/bar'; 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 52e9ee73e..a714ee1d9 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: Unexpected href link without resolve(). - line: 5 - column: 10 - suggestions: null - message: Unexpected href link without resolve(). line: 6 - column: 9 + column: 10 suggestions: null - message: Unexpected href link without resolve(). line: 7 @@ -18,3 +14,11 @@ line: 9 column: 9 suggestions: null +- message: Unexpected href link without resolve(). + line: 10 + column: 4 + 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-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 52e9ee73e..a714ee1d9 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: Unexpected href link without resolve(). - line: 5 - column: 10 - suggestions: null - message: Unexpected href link without resolve(). line: 6 - column: 9 + column: 10 suggestions: null - message: Unexpected href link without resolve(). line: 7 @@ -18,3 +14,11 @@ line: 9 column: 9 suggestions: null +- message: Unexpected href link without resolve(). + line: 10 + column: 4 + 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-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 4b57ff9ba..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 @@ -2,6 +2,7 @@ const protocol = 'https'; const value = "https://svelte.dev"; + const href = "https://svelte.dev"; Click me! @@ -16,3 +17,4 @@ 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 cf56203a5..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 @@ -2,6 +2,7 @@ const section = 'sectionName'; const value = '#section'; + const href = '#section'; Click me! @@ -12,3 +13,4 @@ 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! From 3243dbb9d31ce3ebd5dece752799639179ee2d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Thu, 4 Sep 2025 16:13:57 +0200 Subject: [PATCH 2/3] feat(no-navigation-without-resolve): checking link shorthand attributes --- .changeset/icy-planets-know.md | 5 +++++ .../rules/no-navigation-without-resolve.ts | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 .changeset/icy-planets-know.md 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 06ad91df8..296e0027a 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(new FindVariableContext(context), node.value) && + !expressionIsFragment(new FindVariableContext(context), node.value) && + !isResolveCall(new FindVariableContext(context), node.value, resolveReferences) + ) { + context.report({ loc: node.loc, messageId: 'linkWithoutResolve' }); + } + }, SvelteAttribute(node) { if ( context.options[0]?.ignoreLinks === true || From 85a8111bb70b0a4ba333b4fdec6f6df2bcfc910a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20D=C4=9Bdi=C4=8D?= Date: Sun, 5 Oct 2025 10:23:01 +0200 Subject: [PATCH 3/3] chore(no-navigation-without-resolve): extracted options and defaults --- .../src/rules/no-navigation-without-resolve.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 296e0027a..24aa56085 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 @@ -49,6 +49,12 @@ export default createRule('no-navigation-without-resolve', { }, create(context) { let resolveReferences: Set = new Set(); + + const ignoreGoto = context.options[0]?.ignoreGoto ?? false; + const ignorePushState = context.options[0]?.ignorePushState ?? false; + const ignoreReplaceState = context.options[0]?.ignoreReplaceState ?? false; + const ignoreLinks = context.options[0]?.ignoreLinks ?? false; + return { Program() { const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope!); @@ -58,12 +64,12 @@ export default createRule('no-navigation-without-resolve', { pushState: pushStateCalls, replaceState: replaceStateCalls } = extractFunctionCallReferences(referenceTracker); - if (context.options[0]?.ignoreGoto !== true) { + if (!ignoreGoto) { for (const gotoCall of gotoCalls) { checkGotoCall(context, gotoCall, resolveReferences); } } - if (context.options[0]?.ignorePushState !== true) { + if (!ignorePushState) { for (const pushStateCall of pushStateCalls) { checkShallowNavigationCall( context, @@ -73,7 +79,7 @@ export default createRule('no-navigation-without-resolve', { ); } } - if (context.options[0]?.ignoreReplaceState !== true) { + if (!ignoreReplaceState) { for (const replaceStateCall of replaceStateCalls) { checkShallowNavigationCall( context, @@ -86,7 +92,7 @@ export default createRule('no-navigation-without-resolve', { }, SvelteShorthandAttribute(node) { if ( - context.options[0]?.ignoreLinks === true || + ignoreLinks || node.parent.parent.type !== 'SvelteElement' || node.parent.parent.kind !== 'html' || node.parent.parent.name.type !== 'SvelteName' || @@ -106,7 +112,7 @@ export default createRule('no-navigation-without-resolve', { }, SvelteAttribute(node) { if ( - context.options[0]?.ignoreLinks === true || + ignoreLinks || node.parent.parent.type !== 'SvelteElement' || node.parent.parent.kind !== 'html' || node.parent.parent.name.type !== 'SvelteName' ||