Skip to content

Commit 01e7a9a

Browse files
committed
feat(no-navigation-without-base): added support for pushState and replaceState
1 parent 21046f5 commit 01e7a9a

File tree

1 file changed

+161
-65
lines changed

1 file changed

+161
-65
lines changed

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

Lines changed: 161 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import type { RuleContext } from '../types';
88
export default createRule('no-navigation-without-base', {
99
meta: {
1010
docs: {
11-
description: 'disallow using goto() without the base path',
11+
description:
12+
'disallow using navigation (links, goto, pushState, replaceState) without the base path',
1213
category: 'SvelteKit',
1314
recommended: false
1415
},
1516
schema: [],
1617
messages: {
17-
isNotPrefixedWithBasePath:
18-
"Found a goto() call with a url that isn't prefixed with the base path."
18+
gotoNotPrefixed: "Found a goto() call with a url that isn't prefixed with the base path.",
19+
linkNotPrefixed: "Found a link with a url that isn't prefixed with the base path.",
20+
pushStateNotPrefixed:
21+
"Found a pushState() call with a url that isn't prefixed with the base path.",
22+
replaceStateNotPrefixed:
23+
"Found a replaceState() call with a url that isn't prefixed with the base path."
1924
},
2025
type: 'suggestion'
2126
},
@@ -27,58 +32,166 @@ export default createRule('no-navigation-without-base', {
2732
);
2833
const basePathNames = extractBasePathReferences(referenceTracker, context);
2934
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-
}
35+
checkGotoCall(context, gotoCall, basePathNames);
36+
}
37+
for (const pushStateCall of extractPushStateReferences(referenceTracker)) {
38+
checkShallowNavigationCall(context, pushStateCall, basePathNames, 'pushStateNotPrefixed');
39+
}
40+
for (const replaceStateCall of extractReplaceStateReferences(referenceTracker)) {
41+
checkShallowNavigationCall(
42+
context,
43+
replaceStateCall,
44+
basePathNames,
45+
'replaceStateNotPrefixed'
46+
);
4747
}
4848
}
4949
};
5050
}
5151
});
5252

53-
function checkBinaryExpression(
53+
// Extract all imports of the base path
54+
55+
function extractBasePathReferences(
56+
referenceTracker: ReferenceTracker,
57+
context: RuleContext
58+
): Set<TSESTree.Identifier> {
59+
const set = new Set<TSESTree.Identifier>();
60+
for (const { node } of referenceTracker.iterateEsmReferences({
61+
'$app/paths': {
62+
[ReferenceTracker.ESM]: true,
63+
base: {
64+
[ReferenceTracker.READ]: true
65+
}
66+
}
67+
})) {
68+
const variable = findVariable(context, (node as TSESTree.ImportSpecifier).local);
69+
if (!variable) continue;
70+
for (const reference of variable.references) {
71+
if (reference.identifier.type === 'Identifier') set.add(reference.identifier);
72+
}
73+
}
74+
return set;
75+
}
76+
77+
// Actual function checking
78+
79+
function extractGotoReferences(referenceTracker: ReferenceTracker): TSESTree.CallExpression[] {
80+
return Array.from(
81+
referenceTracker.iterateEsmReferences({
82+
'$app/navigation': {
83+
[ReferenceTracker.ESM]: true,
84+
goto: {
85+
[ReferenceTracker.CALL]: true
86+
}
87+
}
88+
}),
89+
({ node }) => node
90+
);
91+
}
92+
93+
function checkGotoCall(
5494
context: RuleContext,
55-
path: TSESTree.BinaryExpression,
95+
call: TSESTree.CallExpression,
5696
basePathNames: Set<TSESTree.Identifier>
5797
): void {
58-
if (path.left.type !== 'Identifier' || !basePathNames.has(path.left)) {
59-
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
98+
if (call.arguments.length < 1) {
99+
return;
60100
}
101+
const url = call.arguments[0];
102+
checkUrlStartsWithBase(context, url, basePathNames, 'gotoNotPrefixed');
103+
}
104+
105+
function extractPushStateReferences(referenceTracker: ReferenceTracker): TSESTree.CallExpression[] {
106+
return Array.from(
107+
referenceTracker.iterateEsmReferences({
108+
'$app/navigation': {
109+
[ReferenceTracker.ESM]: true,
110+
pushState: {
111+
[ReferenceTracker.CALL]: true
112+
}
113+
}
114+
}),
115+
({ node }) => node
116+
);
61117
}
62118

63-
function checkTemplateLiteral(
119+
function extractReplaceStateReferences(
120+
referenceTracker: ReferenceTracker
121+
): TSESTree.CallExpression[] {
122+
return Array.from(
123+
referenceTracker.iterateEsmReferences({
124+
'$app/navigation': {
125+
[ReferenceTracker.ESM]: true,
126+
replaceState: {
127+
[ReferenceTracker.CALL]: true
128+
}
129+
}
130+
}),
131+
({ node }) => node
132+
);
133+
}
134+
135+
function checkShallowNavigationCall(
64136
context: RuleContext,
65-
path: TSESTree.TemplateLiteral,
66-
basePathNames: Set<TSESTree.Identifier>
137+
call: TSESTree.CallExpression,
138+
basePathNames: Set<TSESTree.Identifier>,
139+
messageId: string
67140
): void {
68-
const startingIdentifier = extractStartingIdentifier(path);
69-
if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) {
70-
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
141+
if (call.arguments.length < 1) {
142+
return;
71143
}
144+
const url = call.arguments[0];
145+
if (urlIsEmpty(url)) {
146+
return;
147+
}
148+
checkUrlStartsWithBase(context, url, basePathNames, messageId);
72149
}
73150

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' });
151+
// Helper functions
152+
153+
function checkUrlStartsWithBase(
154+
context: RuleContext,
155+
url: TSESTree.CallExpressionArgument,
156+
basePathNames: Set<TSESTree.Identifier>,
157+
messageId: string
158+
): void {
159+
switch (url.type) {
160+
case 'BinaryExpression':
161+
checkBinaryExpressionStartsWithBase(context, url, basePathNames, messageId);
162+
break;
163+
case 'TemplateLiteral':
164+
checkTemplateLiteralStartsWithBase(context, url, basePathNames, messageId);
165+
break;
166+
default:
167+
context.report({ loc: url.loc, messageId });
78168
}
79169
}
80170

81-
function extractStartingIdentifier(
171+
function checkBinaryExpressionStartsWithBase(
172+
context: RuleContext,
173+
url: TSESTree.BinaryExpression,
174+
basePathNames: Set<TSESTree.Identifier>,
175+
messageId: string
176+
): void {
177+
if (url.left.type !== 'Identifier' || !basePathNames.has(url.left)) {
178+
context.report({ loc: url.loc, messageId });
179+
}
180+
}
181+
182+
function checkTemplateLiteralStartsWithBase(
183+
context: RuleContext,
184+
url: TSESTree.TemplateLiteral,
185+
basePathNames: Set<TSESTree.Identifier>,
186+
messageId: string
187+
): void {
188+
const startingIdentifier = extractLiteralStartingIdentifier(url);
189+
if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) {
190+
context.report({ loc: url.loc, messageId });
191+
}
192+
}
193+
194+
function extractLiteralStartingIdentifier(
82195
templateLiteral: TSESTree.TemplateLiteral
83196
): TSESTree.Identifier | undefined {
84197
const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) =>
@@ -97,38 +210,21 @@ function extractStartingIdentifier(
97210
return undefined;
98211
}
99212

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
213+
function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
214+
return (
215+
(url.type === 'Literal' && url.value === '') ||
216+
(url.type === 'TemplateLiteral' &&
217+
url.expressions.length === 0 &&
218+
url.quasis.length === 1 &&
219+
url.quasis[0].value.raw === '')
111220
);
112221
}
113222

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-
}
223+
/*
224+
function checkLiteral(context: RuleContext, url: TSESTree.Literal): void {
225+
const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i;
226+
if (!absolutePathRegex.test(url.value?.toString() ?? '')) {
227+
context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' });
132228
}
133-
return set;
134229
}
230+
*/

0 commit comments

Comments
 (0)