Skip to content

Commit a95d368

Browse files
committed
chore: duplicated no-goto-without-base into no-navigation-without-base
1 parent 1f6a193 commit a95d368

16 files changed

+277
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ These rules relate to SvelteKit and its best Practices.
474474
| Rule ID | Description | |
475475
|:--------|:------------|:---|
476476
| [svelte/no-goto-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-goto-without-base/) | disallow using goto() without the base path | |
477+
| [svelte/no-navigation-without-base](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/) | disallow using goto() without the base path | |
477478

478479
## Experimental
479480

docs/rules.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,10 @@ These rules extend the rules provided by ESLint itself, or other plugins to work
108108

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

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

115116
## Experimental
116117

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
<ESLintCodeBlock>
18+
19+
<!--eslint-skip-->
20+
21+
```svelte
22+
<script>
23+
/* eslint svelte/no-navigation-without-base: "error" */
24+
25+
import { goto } from '$app/navigation';
26+
import { base } from '$app/paths';
27+
import { base as baseAlias } from '$app/paths';
28+
29+
// ✓ GOOD
30+
goto(base + '/foo/');
31+
goto(`${base}/foo/`);
32+
33+
goto(baseAlias + '/foo/');
34+
goto(`${baseAlias}/foo/`);
35+
36+
goto('https://localhost/foo/');
37+
38+
// ✗ BAD
39+
goto('/foo');
40+
41+
goto('/foo/' + base);
42+
goto(`/foo/${base}`);
43+
</script>
44+
```
45+
46+
</ESLintCodeBlock>
47+
48+
## :wrench: Options
49+
50+
Nothing.
51+
52+
## :books: Further Reading
53+
54+
- [`goto()` documentation](https://kit.svelte.dev/docs/modules#$app-navigation-goto)
55+
- [`base` documentation](https://kit.svelte.dev/docs/modules#$app-paths-base)
56+
57+
## :rocket: Version
58+
59+
This rule was introduced in eslint-plugin-svelte v2.36.0-next.9
60+
61+
## :mag: Implementation
62+
63+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-navigation-without-base.ts)
64+
- [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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ export interface RuleOptions {
174174
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inspect/
175175
*/
176176
'svelte/no-inspect'?: Linter.RuleEntry<[]>
177+
/**
178+
* disallow using goto() without the base path
179+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-navigation-without-base/
180+
*/
181+
'svelte/no-navigation-without-base'?: Linter.RuleEntry<[]>
177182
/**
178183
* disallow use of not function in event handler
179184
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-not-function-handler/
Lines changed: 134 additions & 0 deletions
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';
3+
import { ReferenceTracker } from '@eslint-community/eslint-utils';
4+
import { getSourceCode } from '../utils/compat';
5+
import { findVariable } from '../utils/ast-utils';
6+
import type { RuleContext } from '../types';
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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import noImmutableReactiveStatements from '../rules/no-immutable-reactive-statem
3434
import noInlineStyles from '../rules/no-inline-styles';
3535
import noInnerDeclarations from '../rules/no-inner-declarations';
3636
import noInspect from '../rules/no-inspect';
37+
import noNavigationWithoutBase from '../rules/no-navigation-without-base';
3738
import noNotFunctionHandler from '../rules/no-not-function-handler';
3839
import noObjectInTextMustaches from '../rules/no-object-in-text-mustaches';
3940
import noReactiveFunctions from '../rules/no-reactive-functions';
@@ -101,6 +102,7 @@ export const rules = [
101102
noInlineStyles,
102103
noInnerDeclarations,
103104
noInspect,
105+
noNavigationWithoutBase,
104106
noNotFunctionHandler,
105107
noObjectInTextMustaches,
106108
noReactiveFunctions,
Lines changed: 4 additions & 0 deletions
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
Lines changed: 5 additions & 0 deletions
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>
Lines changed: 8 additions & 0 deletions
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>

0 commit comments

Comments
 (0)