Skip to content

Commit 0906e67

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

File tree

1 file changed

+162
-65
lines changed

1 file changed

+162
-65
lines changed

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

Lines changed: 162 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,167 @@ 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+
console.log(url);
149+
checkUrlStartsWithBase(context, url, basePathNames, messageId);
72150
}
73151

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

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

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

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

0 commit comments

Comments
 (0)