@@ -8,14 +8,19 @@ import type { RuleContext } from '../types';
88export 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