@@ -8,14 +8,19 @@ import type { RuleContext } from '../types';
8
8
export default createRule ( 'no-navigation-without-base' , {
9
9
meta : {
10
10
docs : {
11
- description : 'disallow using goto() without the base path' ,
11
+ description :
12
+ 'disallow using navigation (links, goto, pushState, replaceState) without the base path' ,
12
13
category : 'SvelteKit' ,
13
14
recommended : false
14
15
} ,
15
16
schema : [ ] ,
16
17
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."
19
24
} ,
20
25
type : 'suggestion'
21
26
} ,
@@ -26,59 +31,164 @@ export default createRule('no-navigation-without-base', {
26
31
getSourceCode ( context ) . scopeManager . globalScope !
27
32
) ;
28
33
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
+ ) ;
47
52
}
48
53
}
49
54
} ;
50
55
}
51
56
} ) ;
52
57
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 (
54
121
context : RuleContext ,
55
- path : TSESTree . BinaryExpression ,
122
+ call : TSESTree . CallExpression ,
56
123
basePathNames : Set < TSESTree . Identifier >
57
124
) : 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 ;
60
127
}
128
+ const url = call . arguments [ 0 ] ;
129
+ checkUrlStartsWithBase ( context , url , basePathNames , 'gotoNotPrefixed' ) ;
61
130
}
62
131
63
- function checkTemplateLiteral (
132
+ function checkShallowNavigationCall (
64
133
context : RuleContext ,
65
- path : TSESTree . TemplateLiteral ,
66
- basePathNames : Set < TSESTree . Identifier >
134
+ call : TSESTree . CallExpression ,
135
+ basePathNames : Set < TSESTree . Identifier > ,
136
+ messageId : string
67
137
) : 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 ;
71
144
}
145
+ checkUrlStartsWithBase ( context , url , basePathNames , messageId ) ;
72
146
}
73
147
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 } ) ;
78
165
}
79
166
}
80
167
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 (
82
192
templateLiteral : TSESTree . TemplateLiteral
83
193
) : TSESTree . Identifier | undefined {
84
194
const literalParts = [ ...templateLiteral . expressions , ...templateLiteral . quasis ] . sort ( ( a , b ) =>
@@ -97,38 +207,21 @@ function extractStartingIdentifier(
97
207
return undefined ;
98
208
}
99
209
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 === '' )
111
217
) ;
112
218
}
113
219
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' });
132
225
}
133
- return set ;
134
226
}
227
+ */
0 commit comments