@@ -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,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