Skip to content

Commit bd57e4f

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

File tree

1 file changed

+159
-66
lines changed

1 file changed

+159
-66
lines changed

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

Lines changed: 159 additions & 66 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
},
@@ -26,59 +31,164 @@ export default createRule('no-navigation-without-base', {
2631
getSourceCode(context).scopeManager.globalScope!
2732
);
2833
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-
}
34+
const {
35+
goto: gotoCalls,
36+
pushState: pushStateCalls,
37+
replaceState: replaceStateCalls
38+
} = extractFunctionCallReferences(referenceTracker);
39+
for (const gotoCall of gotoCalls) {
40+
checkGotoCall(context, gotoCall, basePathNames);
41+
}
42+
for (const pushStateCall of pushStateCalls) {
43+
checkShallowNavigationCall(context, pushStateCall, basePathNames, 'pushStateNotPrefixed');
44+
}
45+
for (const replaceStateCall of replaceStateCalls) {
46+
checkShallowNavigationCall(
47+
context,
48+
replaceStateCall,
49+
basePathNames,
50+
'replaceStateNotPrefixed'
51+
);
4752
}
4853
}
4954
};
5055
}
5156
});
5257

53-
function checkBinaryExpression(
58+
// Extract all imports of the base path
59+
60+
function extractBasePathReferences(
61+
referenceTracker: ReferenceTracker,
62+
context: RuleContext
63+
): Set<TSESTree.Identifier> {
64+
const set = new Set<TSESTree.Identifier>();
65+
for (const { node } of referenceTracker.iterateEsmReferences({
66+
'$app/paths': {
67+
[ReferenceTracker.ESM]: true,
68+
base: {
69+
[ReferenceTracker.READ]: true
70+
}
71+
}
72+
})) {
73+
const variable = findVariable(context, (node as TSESTree.ImportSpecifier).local);
74+
if (!variable) continue;
75+
for (const reference of variable.references) {
76+
if (reference.identifier.type === 'Identifier') set.add(reference.identifier);
77+
}
78+
}
79+
return set;
80+
}
81+
82+
// Extract all references to goto, pushState and replaceState
83+
84+
function extractFunctionCallReferences(referenceTracker: ReferenceTracker): {
85+
goto: TSESTree.CallExpression[];
86+
pushState: TSESTree.CallExpression[];
87+
replaceState: TSESTree.CallExpression[];
88+
} {
89+
const rawReferences = Array.from(
90+
referenceTracker.iterateEsmReferences({
91+
'$app/navigation': {
92+
[ReferenceTracker.ESM]: true,
93+
goto: {
94+
[ReferenceTracker.CALL]: true
95+
},
96+
pushState: {
97+
[ReferenceTracker.CALL]: true
98+
},
99+
replaceState: {
100+
[ReferenceTracker.CALL]: true
101+
}
102+
}
103+
})
104+
);
105+
return {
106+
goto: rawReferences
107+
.filter(({ path }) => path[path.length - 1] === 'goto')
108+
.map(({ node }) => node),
109+
pushState: rawReferences
110+
.filter(({ path }) => path[path.length - 1] === 'pushState')
111+
.map(({ node }) => node),
112+
replaceState: rawReferences
113+
.filter(({ path }) => path[path.length - 1] === 'replaceState')
114+
.map(({ node }) => node)
115+
};
116+
}
117+
118+
// Actual function checking
119+
120+
function checkGotoCall(
54121
context: RuleContext,
55-
path: TSESTree.BinaryExpression,
122+
call: TSESTree.CallExpression,
56123
basePathNames: Set<TSESTree.Identifier>
57124
): void {
58-
if (path.left.type !== 'Identifier' || !basePathNames.has(path.left)) {
59-
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
125+
if (call.arguments.length < 1) {
126+
return;
60127
}
128+
const url = call.arguments[0];
129+
checkUrlStartsWithBase(context, url, basePathNames, 'gotoNotPrefixed');
61130
}
62131

63-
function checkTemplateLiteral(
132+
function checkShallowNavigationCall(
64133
context: RuleContext,
65-
path: TSESTree.TemplateLiteral,
66-
basePathNames: Set<TSESTree.Identifier>
134+
call: TSESTree.CallExpression,
135+
basePathNames: Set<TSESTree.Identifier>,
136+
messageId: string
67137
): void {
68-
const startingIdentifier = extractStartingIdentifier(path);
69-
if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) {
70-
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
138+
if (call.arguments.length < 1) {
139+
return;
140+
}
141+
const url = call.arguments[0];
142+
if (urlIsEmpty(url)) {
143+
return;
71144
}
145+
checkUrlStartsWithBase(context, url, basePathNames, messageId);
72146
}
73147

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

81-
function extractStartingIdentifier(
168+
function checkBinaryExpressionStartsWithBase(
169+
context: RuleContext,
170+
url: TSESTree.BinaryExpression,
171+
basePathNames: Set<TSESTree.Identifier>,
172+
messageId: string
173+
): void {
174+
if (url.left.type !== 'Identifier' || !basePathNames.has(url.left)) {
175+
context.report({ loc: url.loc, messageId });
176+
}
177+
}
178+
179+
function checkTemplateLiteralStartsWithBase(
180+
context: RuleContext,
181+
url: TSESTree.TemplateLiteral,
182+
basePathNames: Set<TSESTree.Identifier>,
183+
messageId: string
184+
): void {
185+
const startingIdentifier = extractLiteralStartingIdentifier(url);
186+
if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) {
187+
context.report({ loc: url.loc, messageId });
188+
}
189+
}
190+
191+
function extractLiteralStartingIdentifier(
82192
templateLiteral: TSESTree.TemplateLiteral
83193
): TSESTree.Identifier | undefined {
84194
const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) =>
@@ -97,38 +207,21 @@ function extractStartingIdentifier(
97207
return undefined;
98208
}
99209

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
210+
function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
211+
return (
212+
(url.type === 'Literal' && url.value === '') ||
213+
(url.type === 'TemplateLiteral' &&
214+
url.expressions.length === 0 &&
215+
url.quasis.length === 1 &&
216+
url.quasis[0].value.raw === '')
111217
);
112218
}
113219

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

0 commit comments

Comments
 (0)