@@ -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,156 @@ 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 ;
127
+ }
128
+ const url = call . arguments [ 0 ] ;
129
+ if ( ! urlStartsWithBase ( url , basePathNames ) ) {
130
+ context . report ( { loc : url . loc , messageId : 'gotoNotPrefixed' } ) ;
60
131
}
61
132
}
62
133
63
- function checkTemplateLiteral (
134
+ function checkShallowNavigationCall (
64
135
context : RuleContext ,
65
- path : TSESTree . TemplateLiteral ,
66
- basePathNames : Set < TSESTree . Identifier >
136
+ call : TSESTree . CallExpression ,
137
+ basePathNames : Set < TSESTree . Identifier > ,
138
+ messageId : string
67
139
) : 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 ) ) {
145
+ return ;
146
+ }
147
+ if ( ! urlStartsWithBase ( url , basePathNames ) ) {
148
+ context . report ( { loc : url . loc , messageId } ) ;
71
149
}
72
150
}
73
151
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 urlStartsWithBase (
155
+ url : TSESTree . CallExpressionArgument ,
156
+ basePathNames : Set < TSESTree . Identifier >
157
+ ) : boolean {
158
+ switch ( url . type ) {
159
+ case 'BinaryExpression' :
160
+ return binaryExpressionStartsWithBase ( url , basePathNames ) ;
161
+ case 'TemplateLiteral' :
162
+ return templateLiteralStartsWithBase ( url , basePathNames ) ;
163
+ default :
164
+ return false ;
78
165
}
79
166
}
80
167
81
- function extractStartingIdentifier (
168
+ function binaryExpressionStartsWithBase (
169
+ url : TSESTree . BinaryExpression ,
170
+ basePathNames : Set < TSESTree . Identifier >
171
+ ) : boolean {
172
+ return url . left . type === 'Identifier' && basePathNames . has ( url . left ) ;
173
+ }
174
+
175
+ function templateLiteralStartsWithBase (
176
+ url : TSESTree . TemplateLiteral ,
177
+ basePathNames : Set < TSESTree . Identifier >
178
+ ) : boolean {
179
+ const startingIdentifier = extractLiteralStartingIdentifier ( url ) ;
180
+ return startingIdentifier !== undefined && basePathNames . has ( startingIdentifier ) ;
181
+ }
182
+
183
+ function extractLiteralStartingIdentifier (
82
184
templateLiteral : TSESTree . TemplateLiteral
83
185
) : TSESTree . Identifier | undefined {
84
186
const literalParts = [ ...templateLiteral . expressions , ...templateLiteral . quasis ] . sort ( ( a , b ) =>
@@ -97,38 +199,21 @@ function extractStartingIdentifier(
97
199
return undefined ;
98
200
}
99
201
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
202
+ function urlIsEmpty ( url : TSESTree . CallExpressionArgument ) : boolean {
203
+ return (
204
+ ( url . type === 'Literal' && url . value === '' ) ||
205
+ ( url . type === 'TemplateLiteral' &&
206
+ url . expressions . length === 0 &&
207
+ url . quasis . length === 1 &&
208
+ url . quasis [ 0 ] . value . raw === '' )
111
209
) ;
112
210
}
113
211
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
- }
212
+ /*
213
+ function checkLiteral(context: RuleContext, url: TSESTree.Literal): void {
214
+ const absolutePathRegex = /^(?:[+a-z]+:)?\/\//i;
215
+ if (!absolutePathRegex.test(url.value?.toString() ?? '')) {
216
+ context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' });
132
217
}
133
- return set ;
134
218
}
219
+ */
0 commit comments