Skip to content

Commit 1e06290

Browse files
authored
fix(no-navigation-without-resolve): properly detecting absolute and fragment URLs in variables (#1322)
1 parent 527b3b4 commit 1e06290

File tree

6 files changed

+99
-33
lines changed

6 files changed

+99
-33
lines changed

.changeset/hungry-rats-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': patch
3+
---
4+
5+
fix(no-navigation-without-resolve): properly detecting absolute and fragment URLs in variables

packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ export default createRule('no-navigation-without-resolve', {
9797
}
9898
if (
9999
(node.value[0].type === 'SvelteLiteral' &&
100-
!expressionIsAbsolute(node.value[0]) &&
101-
!expressionIsFragment(node.value[0])) ||
100+
!expressionIsAbsolute(new FindVariableContext(context), node.value[0]) &&
101+
!expressionIsFragment(new FindVariableContext(context), node.value[0])) ||
102102
(node.value[0].type === 'SvelteMustacheTag' &&
103-
!expressionIsAbsolute(node.value[0].expression) &&
104-
!expressionIsFragment(node.value[0].expression) &&
103+
!expressionIsAbsolute(new FindVariableContext(context), node.value[0].expression) &&
104+
!expressionIsFragment(new FindVariableContext(context), node.value[0].expression) &&
105105
!isResolveCall(
106106
new FindVariableContext(context),
107107
node.value[0].expression,
@@ -235,19 +235,19 @@ function isResolveCall(
235235
) {
236236
return true;
237237
}
238-
if (node.type === 'Identifier') {
239-
const variable = ctx.findVariable(node);
240-
if (
241-
variable !== null &&
242-
variable.identifiers.length > 0 &&
243-
variable.identifiers[0].parent.type === 'VariableDeclarator' &&
244-
variable.identifiers[0].parent.init !== null &&
245-
isResolveCall(ctx, variable.identifiers[0].parent.init, resolveReferences)
246-
) {
247-
return true;
248-
}
238+
if (node.type !== 'Identifier') {
239+
return false;
249240
}
250-
return false;
241+
const variable = ctx.findVariable(node);
242+
if (
243+
variable === null ||
244+
variable.identifiers.length === 0 ||
245+
variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
246+
variable.identifiers[0].parent.init === null
247+
) {
248+
return false;
249+
}
250+
return isResolveCall(ctx, variable.identifiers[0].parent.init, resolveReferences);
251251
}
252252

253253
function expressionIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
@@ -260,31 +260,55 @@ function expressionIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
260260
);
261261
}
262262

263-
function expressionIsAbsolute(url: AST.SvelteLiteral | TSESTree.Expression): boolean {
263+
function expressionIsAbsolute(
264+
ctx: FindVariableContext,
265+
url: AST.SvelteLiteral | TSESTree.Expression
266+
): boolean {
264267
switch (url.type) {
265268
case 'BinaryExpression':
266-
return binaryExpressionIsAbsolute(url);
269+
return binaryExpressionIsAbsolute(ctx, url);
270+
case 'Identifier':
271+
return identifierIsAbsolute(ctx, url);
267272
case 'Literal':
268273
return typeof url.value === 'string' && urlValueIsAbsolute(url.value);
269274
case 'SvelteLiteral':
270275
return urlValueIsAbsolute(url.value);
271276
case 'TemplateLiteral':
272-
return templateLiteralIsAbsolute(url);
277+
return templateLiteralIsAbsolute(ctx, url);
273278
default:
274279
return false;
275280
}
276281
}
277282

278-
function binaryExpressionIsAbsolute(url: TSESTree.BinaryExpression): boolean {
283+
function binaryExpressionIsAbsolute(
284+
ctx: FindVariableContext,
285+
url: TSESTree.BinaryExpression
286+
): boolean {
279287
return (
280-
(url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(url.left)) ||
281-
expressionIsAbsolute(url.right)
288+
(url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(ctx, url.left)) ||
289+
expressionIsAbsolute(ctx, url.right)
282290
);
283291
}
284292

285-
function templateLiteralIsAbsolute(url: TSESTree.TemplateLiteral): boolean {
293+
function identifierIsAbsolute(ctx: FindVariableContext, url: TSESTree.Identifier): boolean {
294+
const variable = ctx.findVariable(url);
295+
if (
296+
variable === null ||
297+
variable.identifiers.length === 0 ||
298+
variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
299+
variable.identifiers[0].parent.init === null
300+
) {
301+
return false;
302+
}
303+
return expressionIsAbsolute(ctx, variable.identifiers[0].parent.init);
304+
}
305+
306+
function templateLiteralIsAbsolute(
307+
ctx: FindVariableContext,
308+
url: TSESTree.TemplateLiteral
309+
): boolean {
286310
return (
287-
url.expressions.some(expressionIsAbsolute) ||
311+
url.expressions.some((expression) => expressionIsAbsolute(ctx, expression)) ||
288312
url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw))
289313
);
290314
}
@@ -293,28 +317,52 @@ function urlValueIsAbsolute(url: string): boolean {
293317
return /^[+a-z]*:/i.test(url);
294318
}
295319

296-
function expressionIsFragment(url: AST.SvelteLiteral | TSESTree.Expression): boolean {
320+
function expressionIsFragment(
321+
ctx: FindVariableContext,
322+
url: AST.SvelteLiteral | TSESTree.Expression
323+
): boolean {
297324
switch (url.type) {
298325
case 'BinaryExpression':
299-
return binaryExpressionIsFragment(url);
326+
return binaryExpressionIsFragment(ctx, url);
327+
case 'Identifier':
328+
return identifierIsFragment(ctx, url);
300329
case 'Literal':
301330
return typeof url.value === 'string' && urlValueIsFragment(url.value);
302331
case 'SvelteLiteral':
303332
return urlValueIsFragment(url.value);
304333
case 'TemplateLiteral':
305-
return templateLiteralIsFragment(url);
334+
return templateLiteralIsFragment(ctx, url);
306335
default:
307336
return false;
308337
}
309338
}
310339

311-
function binaryExpressionIsFragment(url: TSESTree.BinaryExpression): boolean {
312-
return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(url.left);
340+
function binaryExpressionIsFragment(
341+
ctx: FindVariableContext,
342+
url: TSESTree.BinaryExpression
343+
): boolean {
344+
return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(ctx, url.left);
313345
}
314346

315-
function templateLiteralIsFragment(url: TSESTree.TemplateLiteral): boolean {
347+
function identifierIsFragment(ctx: FindVariableContext, url: TSESTree.Identifier): boolean {
348+
const variable = ctx.findVariable(url);
349+
if (
350+
variable === null ||
351+
variable.identifiers.length === 0 ||
352+
variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
353+
variable.identifiers[0].parent.init === null
354+
) {
355+
return false;
356+
}
357+
return expressionIsFragment(ctx, variable.identifiers[0].parent.init);
358+
}
359+
360+
function templateLiteralIsFragment(
361+
ctx: FindVariableContext,
362+
url: TSESTree.TemplateLiteral
363+
): boolean {
316364
return (
317-
(url.expressions.length >= 1 && expressionIsFragment(url.expressions[0])) ||
365+
(url.expressions.length >= 1 && expressionIsFragment(ctx, url.expressions[0])) ||
318366
(url.quasis.length >= 1 && urlValueIsFragment(url.quasis[0].value.raw))
319367
);
320368
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
- message: Found a link with a url that isn't resolved.
2-
line: 5
2+
line: 7
33
column: 9
44
suggestions: null
55
- message: Found a link with a url that isn't resolved.
6-
line: 6
6+
line: 8
7+
column: 9
8+
suggestions: null
9+
- message: Found a link with a url that isn't resolved.
10+
line: 9
711
column: 9
812
suggestions: null
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script>
22
import { resolve } from '$app/paths';
3+
4+
const value = resolve('/foo') + '/bar';
35
</script>
46

57
<a href={resolve('/foo') + '/bar'}>Click me!</a>
68
<a href={'/foo' + resolve('/bar')}>Click me!</a>
9+
<a href={value}>Click me!</a>

packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-absolute-url01-input.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script>
22
const protocol = 'https';
3+
4+
const value = "https://svelte.dev";
35
</script>
46

57
<a href="http://svelte.dev">Click me!</a>
@@ -13,3 +15,4 @@
1315
<a href={`${protocol}://svelte.dev`}>Click me!</a>
1416
<a href="mailto:[email protected]">Click me!</a>
1517
<a href="tel:+123456789">Click me!</a>
18+
<a href={value}>Click me!</a>

packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-fragment-url01-input.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script>
22
const section = 'sectionName';
3+
4+
const value = '#section';
35
</script>
46

57
<a href="#">Click me!</a>
@@ -9,3 +11,4 @@
911
<a href={'#' + section}>Click me!</a>
1012
<a href={`#${section}`}>Click me!</a>
1113
<a href={'#user:42'}>Click me!</a>
14+
<a href={value}>Click me!</a>

0 commit comments

Comments
 (0)