11import type { TSESTree } from '@typescript-eslint/types' ;
2+ import type { Type , TypeChecker } from 'typescript' ;
3+
24import { createRule } from '../utils/index.js' ;
35import { ReferenceTracker } from '@eslint-community/eslint-utils' ;
46import { FindVariableContext } from '../utils/ast-utils.js' ;
57import { findVariable } from '../utils/ast-utils.js' ;
68import type { RuleContext } from '../types.js' ;
79import type { AST } from 'svelte-eslint-parser' ;
10+ import { type TSTools , getTypeScriptTools } from '../utils/ts-utils/index.js' ;
811
912export default createRule ( 'no-navigation-without-resolve' , {
1013 meta : {
@@ -48,6 +51,8 @@ export default createRule('no-navigation-without-resolve', {
4851 ]
4952 } ,
5053 create ( context ) {
54+ const tsTools = getTypeScriptTools ( context ) ;
55+
5156 let resolveReferences : Set < TSESTree . Identifier > = new Set < TSESTree . Identifier > ( ) ;
5257
5358 const ignoreGoto = context . options [ 0 ] ?. ignoreGoto ?? false ;
@@ -66,7 +71,7 @@ export default createRule('no-navigation-without-resolve', {
6671 } = extractFunctionCallReferences ( referenceTracker ) ;
6772 if ( ! ignoreGoto ) {
6873 for ( const gotoCall of gotoCalls ) {
69- checkGotoCall ( context , gotoCall , resolveReferences ) ;
74+ checkGotoCall ( context , gotoCall , resolveReferences , tsTools ) ;
7075 }
7176 }
7277 if ( ! ignorePushState ) {
@@ -75,6 +80,7 @@ export default createRule('no-navigation-without-resolve', {
7580 context ,
7681 pushStateCall ,
7782 resolveReferences ,
83+ tsTools ,
7884 'pushStateWithoutResolve'
7985 ) ;
8086 }
@@ -85,22 +91,24 @@ export default createRule('no-navigation-without-resolve', {
8591 context ,
8692 replaceStateCall ,
8793 resolveReferences ,
94+ tsTools ,
8895 'replaceStateWithoutResolve'
8996 ) ;
9097 }
9198 }
9299 } ,
93100 ...( ! ignoreLinks && {
94101 SvelteShorthandAttribute ( node ) {
95- checkLinkAttribute ( context , node , node . value , resolveReferences ) ;
102+ checkLinkAttribute ( context , node , node . value , resolveReferences , tsTools ) ;
96103 } ,
97104 SvelteAttribute ( node ) {
98105 if ( node . value . length > 0 ) {
99106 checkLinkAttribute (
100107 context ,
101108 node ,
102109 node . value [ 0 ] . type === 'SvelteMustacheTag' ? node . value [ 0 ] . expression : node . value [ 0 ] ,
103- resolveReferences
110+ resolveReferences ,
111+ tsTools
104112 ) ;
105113 }
106114 }
@@ -187,11 +195,18 @@ function extractFunctionCallReferences(referenceTracker: ReferenceTracker): {
187195function checkGotoCall (
188196 context : RuleContext ,
189197 call : TSESTree . CallExpression ,
190- resolveReferences : Set < TSESTree . Identifier >
198+ resolveReferences : Set < TSESTree . Identifier > ,
199+ tsTools : TSTools | null
191200) : void {
192201 if (
193202 call . arguments . length > 0 &&
194- ! isValueAllowed ( new FindVariableContext ( context ) , call . arguments [ 0 ] , resolveReferences , { } )
203+ ! isValueAllowed (
204+ new FindVariableContext ( context ) ,
205+ call . arguments [ 0 ] ,
206+ resolveReferences ,
207+ tsTools ,
208+ { }
209+ )
195210 ) {
196211 context . report ( { loc : call . arguments [ 0 ] . loc , messageId : 'gotoWithoutResolve' } ) ;
197212 }
@@ -201,13 +216,20 @@ function checkShallowNavigationCall(
201216 context : RuleContext ,
202217 call : TSESTree . CallExpression ,
203218 resolveReferences : Set < TSESTree . Identifier > ,
219+ tsTools : TSTools | null ,
204220 messageId : string
205221) : void {
206222 if (
207223 call . arguments . length > 0 &&
208- ! isValueAllowed ( new FindVariableContext ( context ) , call . arguments [ 0 ] , resolveReferences , {
209- allowEmpty : true
210- } )
224+ ! isValueAllowed (
225+ new FindVariableContext ( context ) ,
226+ call . arguments [ 0 ] ,
227+ resolveReferences ,
228+ tsTools ,
229+ {
230+ allowEmpty : true
231+ }
232+ )
211233 ) {
212234 context . report ( { loc : call . arguments [ 0 ] . loc , messageId } ) ;
213235 }
@@ -217,7 +239,8 @@ function checkLinkAttribute(
217239 context : RuleContext ,
218240 attribute : AST . SvelteAttribute | AST . SvelteShorthandAttribute ,
219241 value : TSESTree . Expression | AST . SvelteLiteral ,
220- resolveReferences : Set < TSESTree . Identifier >
242+ resolveReferences : Set < TSESTree . Identifier > ,
243+ tsTools : TSTools | null
221244) : void {
222245 if (
223246 attribute . parent . parent . type === 'SvelteElement' &&
@@ -226,7 +249,7 @@ function checkLinkAttribute(
226249 attribute . parent . parent . name . name === 'a' &&
227250 attribute . key . name === 'href' &&
228251 ! hasRelExternal ( new FindVariableContext ( context ) , attribute . parent ) &&
229- ! isValueAllowed ( new FindVariableContext ( context ) , value , resolveReferences , {
252+ ! isValueAllowed ( new FindVariableContext ( context ) , value , resolveReferences , tsTools , {
230253 allowAbsolute : true ,
231254 allowFragment : true ,
232255 allowNullish : true
@@ -275,6 +298,7 @@ function isValueAllowed(
275298 ctx : FindVariableContext ,
276299 value : TSESTree . CallExpressionArgument | AST . SvelteLiteral ,
277300 resolveReferences : Set < TSESTree . Identifier > ,
301+ tsTools : TSTools | null ,
278302 config : {
279303 allowAbsolute ?: boolean ;
280304 allowEmpty ?: boolean ;
@@ -287,10 +311,20 @@ function isValueAllowed(
287311 if (
288312 variable !== null &&
289313 variable . identifiers . length > 0 &&
290- variable . identifiers [ 0 ] . parent . type === 'VariableDeclarator' &&
291- variable . identifiers [ 0 ] . parent . init !== null
314+ variable . identifiers [ 0 ] . parent . type === 'VariableDeclarator'
292315 ) {
293- return isValueAllowed ( ctx , variable . identifiers [ 0 ] . parent . init , resolveReferences , config ) ;
316+ if ( expressionIsResolvedPathname ( variable . identifiers [ 0 ] , tsTools ) ) {
317+ return true ;
318+ }
319+ if ( variable . identifiers [ 0 ] . parent . init !== null ) {
320+ return isValueAllowed (
321+ ctx ,
322+ variable . identifiers [ 0 ] . parent . init ,
323+ resolveReferences ,
324+ tsTools ,
325+ config
326+ ) ;
327+ }
294328 }
295329 }
296330 if ( value . type === 'ConditionalExpression' ) {
@@ -304,6 +338,7 @@ function isValueAllowed(
304338 ( config . allowEmpty && expressionIsEmpty ( value ) ) ||
305339 ( config . allowFragment && expressionStartsWith ( ctx , value , '#' ) ) ||
306340 ( config . allowNullish && expressionIsNullish ( value ) ) ||
341+ expressionIsResolvedPathname ( value , tsTools ) ||
307342 expressionIsResolveCall ( ctx , value , resolveReferences )
308343 ) {
309344 return true ;
@@ -313,6 +348,43 @@ function isValueAllowed(
313348
314349// Helper functions
315350
351+ function expressionIsResolvedPathname (
352+ value : TSESTree . CallExpressionArgument | TSESTree . Expression | AST . SvelteLiteral ,
353+ tsTools : TSTools | null
354+ ) : boolean {
355+ if ( tsTools === null ) {
356+ return false ;
357+ }
358+ const checker = tsTools . service . program . getTypeChecker ( ) as TypeChecker & {
359+ isTypeAssignableTo ( source : Type , target : Type ) : boolean ;
360+ } ;
361+
362+ const tsNode = tsTools . service . esTreeNodeToTSNodeMap . get ( value ) ;
363+ if ( tsNode === undefined ) {
364+ return false ;
365+ }
366+ const nodeType = checker . getTypeAtLocation ( tsNode ) ;
367+
368+ const appTypesModule = checker . getAmbientModules ( ) . find ( ( m ) => m . name === '"$app/types"' ) ;
369+ if ( ! appTypesModule ) {
370+ return false ;
371+ }
372+
373+ const resolvedPathnameSymbol = checker
374+ . getExportsOfModule ( appTypesModule )
375+ . find ( ( e ) => e . name === 'ResolvedPathname' ) ;
376+ if ( ! resolvedPathnameSymbol ) {
377+ return false ;
378+ }
379+ const resolvedPathnameType = checker . getDeclaredTypeOfSymbol ( resolvedPathnameSymbol ) ;
380+
381+ // 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).
382+ return (
383+ checker . isTypeAssignableTo ( nodeType , resolvedPathnameType ) &&
384+ checker . isTypeAssignableTo ( resolvedPathnameType , nodeType )
385+ ) ;
386+ }
387+
316388function expressionIsResolveCall (
317389 ctx : FindVariableContext ,
318390 node : TSESTree . CallExpressionArgument | AST . SvelteLiteral ,
0 commit comments