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!