Skip to content

Commit daa932b

Browse files
committed
feat(no-navigation-without-base): added support for urls defined in variables
1 parent 86898b2 commit daa932b

13 files changed

+110
-25
lines changed

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

+54-19
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,14 @@ export default createRule('no-navigation-without-base', {
9696
}
9797
const hrefValue = node.value[0];
9898
if (hrefValue.type === 'SvelteLiteral') {
99-
if (!urlIsAbsolute(hrefValue)) {
99+
if (!expressionIsAbsolute(hrefValue)) {
100100
context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
101101
}
102102
return;
103103
}
104104
if (
105-
!urlStartsWithBase(hrefValue.expression, basePathNames) &&
106-
!urlIsAbsolute(hrefValue.expression)
105+
!expressionStartsWithBase(context, hrefValue.expression, basePathNames) &&
106+
!expressionIsAbsolute(hrefValue.expression)
107107
) {
108108
context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
109109
}
@@ -183,7 +183,7 @@ function checkGotoCall(
183183
return;
184184
}
185185
const url = call.arguments[0];
186-
if (!urlStartsWithBase(url, basePathNames)) {
186+
if (url.type === 'SpreadElement' || !expressionStartsWithBase(context, url, basePathNames)) {
187187
context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' });
188188
}
189189
}
@@ -198,45 +198,59 @@ function checkShallowNavigationCall(
198198
return;
199199
}
200200
const url = call.arguments[0];
201-
if (!urlIsEmpty(url) && !urlStartsWithBase(url, basePathNames)) {
201+
if (
202+
url.type === 'SpreadElement' ||
203+
(!expressionIsEmpty(url) && !expressionStartsWithBase(context, url, basePathNames))
204+
) {
202205
context.report({ loc: url.loc, messageId });
203206
}
204207
}
205208

206209
// Helper functions
207210

208-
function urlStartsWithBase(
209-
url: TSESTree.CallExpressionArgument,
211+
function expressionStartsWithBase(
212+
context: RuleContext,
213+
url: TSESTree.Expression,
210214
basePathNames: Set<TSESTree.Identifier>
211215
): boolean {
212216
switch (url.type) {
213217
case 'BinaryExpression':
214-
return binaryExpressionStartsWithBase(url, basePathNames);
218+
return binaryExpressionStartsWithBase(context, url, basePathNames);
219+
case 'Identifier':
220+
return variableStartsWithBase(context, url, basePathNames);
215221
case 'TemplateLiteral':
216-
return templateLiteralStartsWithBase(url, basePathNames);
222+
return templateLiteralStartsWithBase(context, url, basePathNames);
217223
default:
218224
return false;
219225
}
220226
}
221227

222228
function binaryExpressionStartsWithBase(
229+
context: RuleContext,
223230
url: TSESTree.BinaryExpression,
224231
basePathNames: Set<TSESTree.Identifier>
225232
): boolean {
226-
return url.left.type === 'Identifier' && basePathNames.has(url.left);
233+
return (
234+
url.left.type !== 'PrivateIdentifier' &&
235+
expressionStartsWithBase(context, url.left, basePathNames)
236+
);
227237
}
228238

229239
function templateLiteralStartsWithBase(
240+
context: RuleContext,
230241
url: TSESTree.TemplateLiteral,
231242
basePathNames: Set<TSESTree.Identifier>
232243
): boolean {
233-
const startingIdentifier = extractLiteralStartingIdentifier(url);
234-
return startingIdentifier !== undefined && basePathNames.has(startingIdentifier);
244+
const startingIdentifier = extractLiteralStartingExpression(url);
245+
return (
246+
startingIdentifier !== undefined &&
247+
expressionStartsWithBase(context, startingIdentifier, basePathNames)
248+
);
235249
}
236250

237-
function extractLiteralStartingIdentifier(
251+
function extractLiteralStartingExpression(
238252
templateLiteral: TSESTree.TemplateLiteral
239-
): TSESTree.Identifier | undefined {
253+
): TSESTree.Expression | undefined {
240254
const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) =>
241255
a.range[0] < b.range[0] ? -1 : 1
242256
);
@@ -245,15 +259,35 @@ function extractLiteralStartingIdentifier(
245259
// Skip empty quasi in the begining
246260
continue;
247261
}
248-
if (part.type === 'Identifier') {
262+
if (part.type !== 'TemplateElement') {
249263
return part;
250264
}
251265
return undefined;
252266
}
253267
return undefined;
254268
}
255269

256-
function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
270+
function variableStartsWithBase(
271+
context: RuleContext,
272+
url: TSESTree.Identifier,
273+
basePathNames: Set<TSESTree.Identifier>
274+
): boolean {
275+
if (basePathNames.has(url)) {
276+
return true;
277+
}
278+
const variable = findVariable(context, url);
279+
if (
280+
variable === null ||
281+
variable.identifiers.length !== 1 ||
282+
variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
283+
variable.identifiers[0].parent.init === null
284+
) {
285+
return false;
286+
}
287+
return expressionStartsWithBase(context, variable.identifiers[0].parent.init, basePathNames);
288+
}
289+
290+
function expressionIsEmpty(url: TSESTree.Expression): boolean {
257291
return (
258292
(url.type === 'Literal' && url.value === '') ||
259293
(url.type === 'TemplateLiteral' &&
@@ -263,7 +297,7 @@ function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
263297
);
264298
}
265299

266-
function urlIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean {
300+
function expressionIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean {
267301
switch (url.type) {
268302
case 'BinaryExpression':
269303
return binaryExpressionIsAbsolute(url);
@@ -280,13 +314,14 @@ function urlIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean {
280314

281315
function binaryExpressionIsAbsolute(url: TSESTree.BinaryExpression): boolean {
282316
return (
283-
(url.left.type !== 'PrivateIdentifier' && urlIsAbsolute(url.left)) || urlIsAbsolute(url.right)
317+
(url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(url.left)) ||
318+
expressionIsAbsolute(url.right)
284319
);
285320
}
286321

287322
function templateLiteralIsAbsolute(url: TSESTree.TemplateLiteral): boolean {
288323
return (
289-
url.expressions.some(urlIsAbsolute) ||
324+
url.expressions.some(expressionIsAbsolute) ||
290325
url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw))
291326
);
292327
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
- message: Found a goto() call with a url that isn't prefixed with the base path.
2-
line: 4
2+
line: 6
3+
column: 7
4+
suggestions: null
5+
- message: Found a goto() call with a url that isn't prefixed with the base path.
6+
line: 7
37
column: 7
48
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script>
22
import { goto } from '$app/navigation';
33
4+
const value = "/foo";
5+
46
goto('/foo');
7+
goto(value);
58
</script>
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
- message: Found a link with a url that isn't prefixed with the base path.
2-
line: 1
2+
line: 4
33
column: 10
44
suggestions: null
55
- message: Found a link with a url that isn't prefixed with the base path.
6-
line: 2
6+
line: 5
77
column: 9
88
suggestions: null
99
- message: Found a link with a url that isn't prefixed with the base path.
10-
line: 3
10+
line: 6
11+
column: 9
12+
suggestions: null
13+
- message: Found a link with a url that isn't prefixed with the base path.
14+
line: 7
1115
column: 9
1216
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
<script>
2+
const value = "/foo";
3+
</script>
14
<a href="/foo">Click me!</a>
25
<a href={'/foo'}>Click me!</a>
36
<a href={'/' + 'foo'}>Click me!</a>
7+
<a href={value}>Click me!</a>
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
- message: Found a pushState() call with a url that isn't prefixed with the base path.
2-
line: 4
2+
line: 6
3+
column: 12
4+
suggestions: null
5+
- message: Found a pushState() call with a url that isn't prefixed with the base path.
6+
line: 7
37
column: 12
48
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script>
22
import { pushState } from '$app/navigation';
33
4+
const value = "/foo";
5+
46
pushState('/foo');
7+
pushState(value);
58
</script>
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
- message: Found a replaceState() call with a url that isn't prefixed with the
22
base path.
3-
line: 4
3+
line: 6
4+
column: 15
5+
suggestions: null
6+
- message: Found a replaceState() call with a url that isn't prefixed with the
7+
base path.
8+
line: 7
49
column: 15
510
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<script>
22
import { replaceState } from '$app/navigation';
33
4+
const value = "/foo";
5+
46
replaceState('/foo');
7+
replaceState(value);
58
</script>

packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/goto-base-prefixed01-input.svelte

+5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import { base } from '$app/paths';
33
import { goto } from '$app/navigation';
44
5+
const value1 = base + '/foo/';
6+
const value2 = `${base}/foo/`;
7+
58
// eslint-disable-next-line prefer-template -- Testing both variants
69
goto(base + '/foo/');
710
goto(`${base}/foo/`);
11+
goto(value1);
12+
goto(value2);
813
</script>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
<script>
22
import { base } from '$app/paths';
3+
4+
const value1 = base + '/foo/';
5+
const value2 = `${base}/foo/`;
36
</script>
47

58
<a href={base + '/foo/'}>Click me!</a>
69
<a href={`${base}/foo/`}>Click me!</a>
10+
<a href={value1}>Click me!</a>
11+
<a href={value2}>Click me!</a>

packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/pushState-base-prefixed01-input.svelte

+5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import { base } from '$app/paths';
33
import { pushState } from '$app/navigation';
44
5+
const value1 = base + '/foo/';
6+
const value2 = `${base}/foo/`;
7+
58
// eslint-disable-next-line prefer-template -- Testing both variants
69
pushState(base + '/foo/');
710
pushState(`${base}/foo/`);
11+
pushState(value1);
12+
pushState(value2);
813
</script>

packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-base/valid/replaceState-base-prefixed01-input.svelte

+5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import { base } from '$app/paths';
33
import { replaceState } from '$app/navigation';
44
5+
const value1 = base + '/foo/';
6+
const value2 = `${base}/foo/`;
7+
58
// eslint-disable-next-line prefer-template -- Testing both variants
69
replaceState(base + '/foo/');
710
replaceState(`${base}/foo/`);
11+
replaceState(value1);
12+
replaceState(value2);
813
</script>

0 commit comments

Comments
 (0)