Skip to content

Commit 7d92cd5

Browse files
committed
chore: duplicated no-goto-without-base into no-navigation-without-base
1 parent c053ad5 commit 7d92cd5

16 files changed

+273
-3
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ These rules relate to SvelteKit and its best Practices.
410410
| Rule ID | Description | |
411411
|:--------|:------------|:---|
412412
| [svelte/no-goto-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-goto-without-base/) | disallow using goto() without the base path | |
413+
| [svelte/no-navigation-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/) | disallow using goto() without the base path | |
413414

414415
## Experimental
415416

docs/rules.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ These rules extend the rules provided by ESLint itself, or other plugins to work
109109

110110
These rules relate to SvelteKit and its best Practices.
111111

112-
| Rule ID | Description | |
113-
| :------------------------------------------------------------- | :------------------------------------------ | :-- |
114-
| [svelte/no-goto-without-base](./rules/no-goto-without-base.md) | disallow using goto() without the base path | |
112+
| Rule ID | Description | |
113+
| :------------------------------------------------------------------------- | :------------------------------------------ | :-- |
114+
| [svelte/no-goto-without-base](./rules/no-goto-without-base.md) | disallow using goto() without the base path | |
115+
| [svelte/no-navigation-without-base](./rules/no-navigation-without-base.md) | disallow using goto() without the base path | |
115116

116117
## Experimental
117118

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/no-navigation-without-base'
5+
description: 'disallow using goto() without the base path'
6+
since: 'v2.36.0-next.9'
7+
---
8+
9+
# svelte/no-navigation-without-base
10+
11+
> disallow using goto() without the base path
12+
13+
## :book: Rule Details
14+
15+
This rule reports navigation using SvelteKit's `goto()` function without prefixing a relative URL with the base path. If a non-prefixed relative URL is used for navigation, the `goto` function navigates away from the base path, which is usually not what you wanted to do (for external URLs, `window.location = url` should be used instead).
16+
17+
<!--eslint-skip-->
18+
19+
```svelte
20+
<script>
21+
/* eslint svelte/no-navigation-without-base: "error" */
22+
23+
import { goto } from '$app/navigation';
24+
import { base } from '$app/paths';
25+
import { base as baseAlias } from '$app/paths';
26+
27+
// ✓ GOOD
28+
goto(base + '/foo/');
29+
goto(`${base}/foo/`);
30+
31+
goto(baseAlias + '/foo/');
32+
goto(`${baseAlias}/foo/`);
33+
34+
goto('https://localhost/foo/');
35+
36+
// ✗ BAD
37+
goto('/foo');
38+
39+
goto('/foo/' + base);
40+
goto(`/foo/${base}`);
41+
</script>
42+
```
43+
44+
## :wrench: Options
45+
46+
Nothing.
47+
48+
## :books: Further Reading
49+
50+
- [`goto()` documentation](https://kit.svelte.dev/docs/modules#$app-navigation-goto)
51+
- [`base` documentation](https://kit.svelte.dev/docs/modules#$app-paths-base)
52+
53+
## :rocket: Version
54+
55+
This rule was introduced in eslint-plugin-svelte v2.36.0-next.9
56+
57+
## :mag: Implementation
58+
59+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts)
60+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-navigation-without-base.ts)

packages/eslint-plugin-svelte/src/rule-types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ export interface RuleOptions {
179179
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inspect/
180180
*/
181181
'svelte/no-inspect'?: Linter.RuleEntry<[]>
182+
/**
183+
* disallow using goto() without the base path
184+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/
185+
*/
186+
'svelte/no-navigation-without-base'?: Linter.RuleEntry<[]>
182187
/**
183188
* disallow use of not function in event handler
184189
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-not-function-handler/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { TSESTree } from '@typescript-eslint/types';
2+
import { createRule } from '../utils/index.js';
3+
import { ReferenceTracker } from '@eslint-community/eslint-utils';
4+
import { getSourceCode } from '../utils/compat.js';
5+
import { findVariable } from '../utils/ast-utils.js';
6+
import type { RuleContext } from '../types.js';
7+
8+
export default createRule('no-navigation-without-base', {
9+
meta: {
10+
docs: {
11+
description: 'disallow using goto() without the base path',
12+
category: 'SvelteKit',
13+
recommended: false
14+
},
15+
schema: [],
16+
messages: {
17+
isNotPrefixedWithBasePath:
18+
"Found a goto() call with a url that isn't prefixed with the base path."
19+
},
20+
type: 'suggestion'
21+
},
22+
create(context) {
23+
return {
24+
Program() {
25+
const referenceTracker = new ReferenceTracker(
26+
getSourceCode(context).scopeManager.globalScope!
27+
);
28+
const basePathNames = extractBasePathReferences(referenceTracker, context);
29+
for (const gotoCall of extractGotoReferences(referenceTracker)) {
30+
if (gotoCall.arguments.length < 1) {
31+
continue;
32+
}
33+
const path = gotoCall.arguments[0];
34+
switch (path.type) {
35+
case 'BinaryExpression':
36+
checkBinaryExpression(context, path, basePathNames);
37+
break;
38+
case 'Literal':
39+
checkLiteral(context, path);
40+
break;
41+
case 'TemplateLiteral':
42+
checkTemplateLiteral(context, path, basePathNames);
43+
break;
44+
default:
45+
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
46+
}
47+
}
48+
}
49+
};
50+
}
51+
});
52+
53+
function checkBinaryExpression(
54+
context: RuleContext,
55+
path: TSESTree.BinaryExpression,
56+
basePathNames: Set<TSESTree.Identifier>
57+
): void {
58+
if (path.left.type !== 'Identifier' || !basePathNames.has(path.left)) {
59+
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
60+
}
61+
}
62+
63+
function checkTemplateLiteral(
64+
context: RuleContext,
65+
path: TSESTree.TemplateLiteral,
66+
basePathNames: Set<TSESTree.Identifier>
67+
): void {
68+
const startingIdentifier = extractStartingIdentifier(path);
69+
if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) {
70+
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
71+
}
72+
}
73+
74+
function checkLiteral(context: RuleContext, path: TSESTree.Literal): void {
75+
const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i;
76+
if (!absolutePathRegex.test(path.value?.toString() ?? '')) {
77+
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
78+
}
79+
}
80+
81+
function extractStartingIdentifier(
82+
templateLiteral: TSESTree.TemplateLiteral
83+
): TSESTree.Identifier | undefined {
84+
const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) =>
85+
a.range[0] < b.range[0] ? -1 : 1
86+
);
87+
for (const part of literalParts) {
88+
if (part.type === 'TemplateElement' && part.value.raw === '') {
89+
// Skip empty quasi in the begining
90+
continue;
91+
}
92+
if (part.type === 'Identifier') {
93+
return part;
94+
}
95+
return undefined;
96+
}
97+
return undefined;
98+
}
99+
100+
function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.CallExpression[] {
101+
return Array.from(
102+
referenceTracker.iterateEsmReferences({
103+
'$app/navigation': {
104+
[ReferenceTracker.ESM]: true,
105+
goto: {
106+
[ReferenceTracker.CALL]: true
107+
}
108+
}
109+
}),
110+
({ node }) => node
111+
);
112+
}
113+
114+
function extractBasePathReferences(
115+
referenceTracker: ReferenceTracker,
116+
context: RuleContext
117+
): Set<TSESTree.Identifier> {
118+
const set = new Set<TSESTree.Identifier>();
119+
for (const { node } of referenceTracker.iterateEsmReferences({
120+
'$app/paths': {
121+
[ReferenceTracker.ESM]: true,
122+
base: {
123+
[ReferenceTracker.READ]: true
124+
}
125+
}
126+
})) {
127+
const variable = findVariable(context, (node as TSESTree.ImportSpecifier).local);
128+
if (!variable) continue;
129+
for (const reference of variable.references) {
130+
if (reference.identifier.type === 'Identifier') set.add(reference.identifier);
131+
}
132+
}
133+
return set;
134+
}

packages/eslint-plugin-svelte/src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import noImmutableReactiveStatements from '../rules/no-immutable-reactive-statem
3535
import noInlineStyles from '../rules/no-inline-styles.js';
3636
import noInnerDeclarations from '../rules/no-inner-declarations.js';
3737
import noInspect from '../rules/no-inspect.js';
38+
import noNavigationWithoutBase from '../rules/no-navigation-without-base.js';
3839
import noNotFunctionHandler from '../rules/no-not-function-handler.js';
3940
import noObjectInTextMustaches from '../rules/no-object-in-text-mustaches.js';
4041
import noReactiveFunctions from '../rules/no-reactive-functions.js';
@@ -103,6 +104,7 @@ export const rules = [
103104
noInlineStyles,
104105
noInnerDeclarations,
105106
noInspect,
107+
noNavigationWithoutBase,
106108
noNotFunctionHandler,
107109
noObjectInTextMustaches,
108110
noReactiveFunctions,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Found a goto() call with a url that isn't prefixed with the base path.
2+
line: 4
3+
column: 8
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import { goto as alias } from '$app/navigation';
3+
4+
alias('/foo');
5+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- message: Found a goto() call with a url that isn't prefixed with the base path.
2+
line: 5
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: 6
7+
column: 7
8+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import { base } from '$app/paths';
3+
import { goto } from '$app/navigation';
4+
5+
goto('/foo/' + base);
6+
goto(`/foo/${base}`);
7+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Found a goto() call with a url that isn't prefixed with the base path.
2+
line: 4
3+
column: 7
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import { goto } from '$app/navigation';
3+
4+
goto('/foo');
5+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
import { goto } from '$app/navigation';
3+
4+
goto('http://localhost/foo/');
5+
goto('https://localhost/foo/');
6+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
import { base as alias } from '$app/paths';
3+
import { goto } from '$app/navigation';
4+
5+
// eslint-disable-next-line prefer-template -- Testing both variants
6+
goto(alias + '/foo/');
7+
goto(`${alias}/foo/`);
8+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
import { base } from '$app/paths';
3+
import { goto } from '$app/navigation';
4+
5+
// eslint-disable-next-line prefer-template -- Testing both variants
6+
goto(base + '/foo/');
7+
goto(`${base}/foo/`);
8+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from '../../utils/eslint-compat';
2+
import rule from '../../../src/rules/no-navigation-without-base';
3+
import { loadTestCases } from '../../utils/utils';
4+
5+
const tester = new RuleTester({
6+
languageOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: 'module'
9+
}
10+
});
11+
12+
tester.run('no-navigation-without-base', rule as any, loadTestCases('no-navigation-without-base'));

0 commit comments

Comments
 (0)