@@ -8,14 +8,19 @@ import type { RuleContext } from '../types.js';
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,153 @@ 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 ) && ! urlStartsWithBase ( url , basePathNames ) ) {
145
+ context . report ( { loc : url . loc , messageId } ) ;
71
146
}
72
147
}
73
148
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 ;
78
162
}
79
163
}
80
164
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 (
82
181
templateLiteral : TSESTree . TemplateLiteral
83
182
) : TSESTree . Identifier | undefined {
84
183
const literalParts = [ ...templateLiteral . expressions , ...templateLiteral . quasis ] . sort ( ( a , b ) =>
@@ -97,38 +196,21 @@ function extractStartingIdentifier(
97
196
return undefined ;
98
197
}
99
198
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 === '' )
111
206
) ;
112
207
}
113
208
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' });
132
214
}
133
- return set ;
134
215
}
216
+ */
0 commit comments