11import type { TSESTree } from '@typescript-eslint/types' ;
2+
23import { createRule } from '../utils/index.js' ;
34import { ReferenceTracker } from '@eslint-community/eslint-utils' ;
45import { FindVariableContext } from '../utils/ast-utils.js' ;
56import { findVariable } from '../utils/ast-utils.js' ;
67import type { RuleContext } from '../types.js' ;
78import type { AST } from 'svelte-eslint-parser' ;
9+ import { type TSTools , getTypeScriptTools } from '../utils/ts-utils/index.js' ;
810
911export default createRule ( 'no-navigation-without-resolve' , {
1012 meta : {
@@ -48,6 +50,8 @@ export default createRule('no-navigation-without-resolve', {
4850 ]
4951 } ,
5052 create ( context ) {
53+ const tsTools = getTypeScriptTools ( context ) ;
54+
5155 let resolveReferences : Set < TSESTree . Identifier > = new Set < TSESTree . Identifier > ( ) ;
5256
5357 const ignoreGoto = context . options [ 0 ] ?. ignoreGoto ?? false ;
@@ -66,7 +70,7 @@ export default createRule('no-navigation-without-resolve', {
6670 } = extractFunctionCallReferences ( referenceTracker ) ;
6771 if ( ! ignoreGoto ) {
6872 for ( const gotoCall of gotoCalls ) {
69- checkGotoCall ( context , gotoCall , resolveReferences ) ;
73+ checkGotoCall ( context , gotoCall , resolveReferences , tsTools ) ;
7074 }
7175 }
7276 if ( ! ignorePushState ) {
@@ -75,6 +79,7 @@ export default createRule('no-navigation-without-resolve', {
7579 context ,
7680 pushStateCall ,
7781 resolveReferences ,
82+ tsTools ,
7883 'pushStateWithoutResolve'
7984 ) ;
8085 }
@@ -85,22 +90,24 @@ export default createRule('no-navigation-without-resolve', {
8590 context ,
8691 replaceStateCall ,
8792 resolveReferences ,
93+ tsTools ,
8894 'replaceStateWithoutResolve'
8995 ) ;
9096 }
9197 }
9298 } ,
9399 ...( ! ignoreLinks && {
94100 SvelteShorthandAttribute ( node ) {
95- checkLinkAttribute ( context , node , node . value , resolveReferences ) ;
101+ checkLinkAttribute ( context , node , node . value , resolveReferences , tsTools ) ;
96102 } ,
97103 SvelteAttribute ( node ) {
98104 if ( node . value . length > 0 ) {
99105 checkLinkAttribute (
100106 context ,
101107 node ,
102108 node . value [ 0 ] . type === 'SvelteMustacheTag' ? node . value [ 0 ] . expression : node . value [ 0 ] ,
103- resolveReferences
109+ resolveReferences ,
110+ tsTools
104111 ) ;
105112 }
106113 }
@@ -187,11 +194,18 @@ function extractFunctionCallReferences(referenceTracker: ReferenceTracker): {
187194function checkGotoCall (
188195 context : RuleContext ,
189196 call : TSESTree . CallExpression ,
190- resolveReferences : Set < TSESTree . Identifier >
197+ resolveReferences : Set < TSESTree . Identifier > ,
198+ tsTools : TSTools | null
191199) : void {
192200 if (
193201 call . arguments . length > 0 &&
194- ! isValueAllowed ( new FindVariableContext ( context ) , call . arguments [ 0 ] , resolveReferences , { } )
202+ ! isValueAllowed (
203+ new FindVariableContext ( context ) ,
204+ call . arguments [ 0 ] ,
205+ resolveReferences ,
206+ tsTools ,
207+ { }
208+ )
195209 ) {
196210 context . report ( { loc : call . arguments [ 0 ] . loc , messageId : 'gotoWithoutResolve' } ) ;
197211 }
@@ -201,13 +215,20 @@ function checkShallowNavigationCall(
201215 context : RuleContext ,
202216 call : TSESTree . CallExpression ,
203217 resolveReferences : Set < TSESTree . Identifier > ,
218+ tsTools : TSTools | null ,
204219 messageId : string
205220) : void {
206221 if (
207222 call . arguments . length > 0 &&
208- ! isValueAllowed ( new FindVariableContext ( context ) , call . arguments [ 0 ] , resolveReferences , {
209- allowEmpty : true
210- } )
223+ ! isValueAllowed (
224+ new FindVariableContext ( context ) ,
225+ call . arguments [ 0 ] ,
226+ resolveReferences ,
227+ tsTools ,
228+ {
229+ allowEmpty : true
230+ }
231+ )
211232 ) {
212233 context . report ( { loc : call . arguments [ 0 ] . loc , messageId } ) ;
213234 }
@@ -217,7 +238,8 @@ function checkLinkAttribute(
217238 context : RuleContext ,
218239 attribute : AST . SvelteAttribute | AST . SvelteShorthandAttribute ,
219240 value : TSESTree . Expression | AST . SvelteLiteral ,
220- resolveReferences : Set < TSESTree . Identifier >
241+ resolveReferences : Set < TSESTree . Identifier > ,
242+ tsTools : TSTools | null
221243) : void {
222244 if (
223245 attribute . parent . parent . type === 'SvelteElement' &&
@@ -226,7 +248,7 @@ function checkLinkAttribute(
226248 attribute . parent . parent . name . name === 'a' &&
227249 attribute . key . name === 'href' &&
228250 ! hasRelExternal ( new FindVariableContext ( context ) , attribute . parent ) &&
229- ! isValueAllowed ( new FindVariableContext ( context ) , value , resolveReferences , {
251+ ! isValueAllowed ( new FindVariableContext ( context ) , value , resolveReferences , tsTools , {
230252 allowAbsolute : true ,
231253 allowFragment : true ,
232254 allowNullish : true
@@ -275,6 +297,7 @@ function isValueAllowed(
275297 ctx : FindVariableContext ,
276298 value : TSESTree . CallExpressionArgument | AST . SvelteLiteral ,
277299 resolveReferences : Set < TSESTree . Identifier > ,
300+ tsTools : TSTools | null ,
278301 config : {
279302 allowAbsolute ?: boolean ;
280303 allowEmpty ?: boolean ;
@@ -287,23 +310,34 @@ function isValueAllowed(
287310 if (
288311 variable !== null &&
289312 variable . identifiers . length > 0 &&
290- variable . identifiers [ 0 ] . parent . type === 'VariableDeclarator' &&
291- variable . identifiers [ 0 ] . parent . init !== null
313+ variable . identifiers [ 0 ] . parent . type === 'VariableDeclarator'
292314 ) {
293- return isValueAllowed ( ctx , variable . identifiers [ 0 ] . parent . init , resolveReferences , config ) ;
315+ if ( expressionIsResolvedPathname ( variable . identifiers [ 0 ] , tsTools ) ) {
316+ return true ;
317+ }
318+ if ( variable . identifiers [ 0 ] . parent . init !== null ) {
319+ return isValueAllowed (
320+ ctx ,
321+ variable . identifiers [ 0 ] . parent . init ,
322+ resolveReferences ,
323+ tsTools ,
324+ config
325+ ) ;
326+ }
294327 }
295328 }
296329 if ( value . type === 'ConditionalExpression' ) {
297330 return (
298- isValueAllowed ( ctx , value . consequent , resolveReferences , config ) &&
299- isValueAllowed ( ctx , value . alternate , resolveReferences , config )
331+ isValueAllowed ( ctx , value . consequent , resolveReferences , tsTools , config ) &&
332+ isValueAllowed ( ctx , value . alternate , resolveReferences , tsTools , config )
300333 ) ;
301334 }
302335 if (
303336 ( config . allowAbsolute && expressionIsAbsoluteUrl ( ctx , value ) ) ||
304337 ( config . allowEmpty && expressionIsEmpty ( value ) ) ||
305338 ( config . allowFragment && expressionStartsWith ( ctx , value , '#' ) ) ||
306339 ( config . allowNullish && expressionIsNullish ( value ) ) ||
340+ expressionIsResolvedPathname ( value , tsTools ) ||
307341 expressionIsResolveCall ( ctx , value , resolveReferences )
308342 ) {
309343 return true ;
@@ -313,6 +347,41 @@ function isValueAllowed(
313347
314348// Helper functions
315349
350+ function expressionIsResolvedPathname (
351+ value : TSESTree . CallExpressionArgument | TSESTree . Expression | AST . SvelteLiteral ,
352+ tsTools : TSTools | null
353+ ) : boolean {
354+ if ( tsTools === null ) {
355+ return false ;
356+ }
357+ const checker = tsTools . service . program . getTypeChecker ( ) ;
358+
359+ const tsNode = tsTools . service . esTreeNodeToTSNodeMap . get ( value ) ;
360+ if ( tsNode === undefined ) {
361+ return false ;
362+ }
363+ const nodeType = checker . getTypeAtLocation ( tsNode ) ;
364+
365+ const appTypesModule = checker . getAmbientModules ( ) . find ( ( m ) => m . name === '"$app/types"' ) ;
366+ if ( ! appTypesModule ) {
367+ return false ;
368+ }
369+
370+ const resolvedPathnameSymbol = checker
371+ . getExportsOfModule ( appTypesModule )
372+ . find ( ( e ) => e . name === 'ResolvedPathname' ) ;
373+ if ( ! resolvedPathnameSymbol ) {
374+ return false ;
375+ }
376+ const resolvedPathnameType = checker . getDeclaredTypeOfSymbol ( resolvedPathnameSymbol ) ;
377+
378+ // getTypeAtLocation returns the resolved (structural) type without alias information, so we cannot compare aliasSymbols directly. Instead we check structural equivalence by testing assignability in both directions: this correctly rejects strict subtypes like Pathname (Pathname ⊂ ResolvedPathname, so only one direction holds).
379+ return (
380+ checker . isTypeAssignableTo ( nodeType , resolvedPathnameType ) &&
381+ checker . isTypeAssignableTo ( resolvedPathnameType , nodeType )
382+ ) ;
383+ }
384+
316385function expressionIsResolveCall (
317386 ctx : FindVariableContext ,
318387 node : TSESTree . CallExpressionArgument | AST . SvelteLiteral ,
0 commit comments