Skip to content

Commit df96f99

Browse files
committed
feat(no-navigation-without-base): added support for pushState and replaceState
1 parent 2a03e88 commit df96f99

File tree

1 file changed

+148
-66
lines changed

1 file changed

+148
-66
lines changed

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

+148-66
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import type { RuleContext } from '../types.js';
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,153 @@ 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;
127+
}
128+
const url = call.arguments[0];
129+
if (!urlStartsWithBase(url, basePathNames)) {
130+
context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' });
60131
}
61132
}
62133

63-
function checkTemplateLiteral(
134+
function checkShallowNavigationCall(
64135
context: RuleContext,
65-
path: TSESTree.TemplateLiteral,
66-
basePathNames: Set<TSESTree.Identifier>
136+
call: TSESTree.CallExpression,
137+
basePathNames: Set<TSESTree.Identifier>,
138+
messageId: string
67139
): void {
68-
const startingIdentifier = extractStartingIdentifier(path);
69-
if (startingIdentifier === undefined || !basePathNames.has(startingIdentifier)) {
70-
context.report({ loc: path.loc, messageId: 'isNotPrefixedWithBasePath' });
140+
if (call.arguments.length < 1) {
141+
return;
142+
}
143+
const url = call.arguments[0];
144+
if (!urlIsEmpty(url) && !urlStartsWithBase(url, basePathNames)) {
145+
context.report({ loc: url.loc, messageId });
71146
}
72147
}
73148

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' });
149+
// Helper functions
150+
151+
function urlStartsWithBase(
152+
url: TSESTree.CallExpressionArgument,
153+
basePathNames: Set<TSESTree.Identifier>
154+
): boolean {
155+
switch (url.type) {
156+
case 'BinaryExpression':
157+
return binaryExpressionStartsWithBase(url, basePathNames);
158+
case 'TemplateLiteral':
159+
return templateLiteralStartsWithBase(url, basePathNames);
160+
default:
161+
return false;
78162
}
79163
}
80164

81-
function extractStartingIdentifier(
165+
function binaryExpressionStartsWithBase(
166+
url: TSESTree.BinaryExpression,
167+
basePathNames: Set<TSESTree.Identifier>
168+
): boolean {
169+
return url.left.type === 'Identifier' && basePathNames.has(url.left);
170+
}
171+
172+
function templateLiteralStartsWithBase(
173+
url: TSESTree.TemplateLiteral,
174+
basePathNames: Set<TSESTree.Identifier>
175+
): boolean {
176+
const startingIdentifier = extractLiteralStartingIdentifier(url);
177+
return startingIdentifier !== undefined && basePathNames.has(startingIdentifier);
178+
}
179+
180+
function extractLiteralStartingIdentifier(
82181
templateLiteral: TSESTree.TemplateLiteral
83182
): TSESTree.Identifier | undefined {
84183
const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) =>
@@ -97,38 +196,21 @@ function extractStartingIdentifier(
97196
return undefined;
98197
}
99198

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
199+
function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
200+
return (
201+
(url.type === 'Literal' && url.value === '') ||
202+
(url.type === 'TemplateLiteral' &&
203+
url.expressions.length === 0 &&
204+
url.quasis.length === 1 &&
205+
url.quasis[0].value.raw === '')
111206
);
112207
}
113208

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-
}
209+
/*
210+
function checkLiteral(context: RuleContext, url: TSESTree.Literal): void {
211+
const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i;
212+
if (!absolutePathRegex.test(url.value?.toString() ?? '')) {
213+
context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' });
132214
}
133-
return set;
134215
}
216+
*/

0 commit comments

Comments
 (0)