@@ -5,7 +5,7 @@ import type { RuleFixer } from '@typescript-eslint/utils/ts-eslint'
55import  {  minimatch  }  from  'minimatch' 
66import  type  {  MinimatchOptions  }  from  'minimatch' 
77
8- import  type  {  FileExtension ,   RuleContext  }  from  '../types.js' 
8+ import  type  {  RuleContext  }  from  '../types.js' 
99import  { 
1010  isBuiltIn , 
1111  isExternalModule , 
@@ -105,7 +105,12 @@ export interface NormalizedOptions {
105105  fix ?: boolean 
106106} 
107107
108- export  type  MessageId  =  'missing'  |  'missingKnown'  |  'unexpected'  |  'addMissing' 
108+ export  type  MessageId  = 
109+   |  'missing' 
110+   |  'missingKnown' 
111+   |  'unexpected' 
112+   |  'addMissing' 
113+   |  'removeUnexpected' 
109114
110115function  buildProperties ( context : RuleContext < MessageId ,  Options > )  { 
111116  const  result : Required < NormalizedOptions >  =  { 
@@ -188,6 +193,20 @@ function computeOverrideAction(
188193  } 
189194} 
190195
196+ /** 
197+  * Replaces the import path in a source string with a new import path. 
198+  * 
199+  * @param  source - The original source string containing the import statement. 
200+  * @param  importPath - The new import path to replace the existing one. 
201+  * @returns  The updated source string with the replaced import path. 
202+  */ 
203+ function  replaceImportPath ( source : string ,  importPath : string )  { 
204+   return  source . replace ( 
205+     / ^ ( [ ' " ] ) ( .+ ) \1$ / , 
206+     ( _ ,  quote : string )  =>  `${ quote } ${ importPath } ${ quote }  , 
207+   ) 
208+ } 
209+ 
191210export  default  createRule < Options ,  MessageId > ( { 
192211  name : 'extensions' , 
193212  meta : { 
@@ -236,27 +255,26 @@ export default createRule<Options, MessageId>({
236255        'Unexpected use of file extension "{{extension}}" for "{{importPath}}"' , 
237256      addMissing :
238257        'Add "{{extension}}" file extension from "{{importPath}}" into "{{fixedImportPath}}"' , 
258+       removeUnexpected :
259+         'Remove unexpected "{{extension}}" file extension from "{{importPath}}" into "{{fixedImportPath}}"' , 
239260    } , 
240261  } , 
241262  defaultOptions : [ ] , 
242263  create ( context )  { 
243264    const  props  =  buildProperties ( context ) 
244265
245-     function  getModifier ( extension : FileExtension )  { 
266+     function  getModifier ( extension : string )  { 
246267      return  props . pattern [ extension ]  ||  props . defaultConfig 
247268    } 
248269
249-     function  isUseOfExtensionRequired ( 
250-       extension : FileExtension , 
251-       isPackage : boolean , 
252-     )  { 
270+     function  isUseOfExtensionRequired ( extension : string ,  isPackage : boolean )  { 
253271      return  ( 
254272        getModifier ( extension )  ===  'always'  && 
255273        ( ! props . ignorePackages  ||  ! isPackage ) 
256274      ) 
257275    } 
258276
259-     function  isUseOfExtensionForbidden ( extension : FileExtension )  { 
277+     function  isUseOfExtensionForbidden ( extension : string )  { 
260278      return  getModifier ( extension )  ===  'never' 
261279    } 
262280
@@ -297,7 +315,11 @@ export default createRule<Options, MessageId>({
297315          return 
298316        } 
299317
300-         const  importPath  =  importPathWithQueryString . replace ( / \? ( .* ) $ / ,  '' ) 
318+         const  { 
319+           pathname : importPath , 
320+           query, 
321+           hash, 
322+         }  =  parsePath ( importPathWithQueryString ) 
301323
302324        // don't enforce in root external packages as they may have names with `.js`. 
303325        // Like `import Decimal from decimal.js`) 
@@ -309,9 +331,7 @@ export default createRule<Options, MessageId>({
309331
310332        // get extension from resolved path, if possible. 
311333        // for unresolved, use source value. 
312-         const  extension  =  path 
313-           . extname ( resolvedPath  ||  importPath ) 
314-           . slice ( 1 )  as  FileExtension 
334+         const  extension  =  path . extname ( resolvedPath  ||  importPath ) . slice ( 1 ) 
315335
316336        // determine if this is a module 
317337        const  isPackage  = 
@@ -336,16 +356,15 @@ export default createRule<Options, MessageId>({
336356          ) 
337357          const  extensionForbidden  =  isUseOfExtensionForbidden ( extension ) 
338358          if  ( extensionRequired  &&  ! extensionForbidden )  { 
339-             const  {  pathname,  query,  hash }  =  parsePath ( 
340-               importPathWithQueryString , 
341-             ) 
342359            const  fixedImportPath  =  stringifyPath ( { 
343360              pathname : `${  
344-                 / ( [ \\ / ] | [ \\ / ] ? \. ? \. ) $ / . test ( pathname )  
361+                 / ( [ \\ / ] | [ \\ / ] ? \. ? \. ) $ / . test ( importPath )  
345362                  ? `${   
346-                       pathname . endsWith ( '/' )  ? pathname . slice ( 0 ,  - 1 )  : pathname  
363+                       importPath . endsWith ( '/' )  
364+                         ? importPath . slice ( 0 ,  - 1 )  
365+                         : importPath  
347366                    }  /index.${ extension } 
348-                   : `${ pathname } ${ extension }   
367+                   : `${ importPath } ${ extension }   
349368              }  `, 
350369              query, 
351370              hash, 
@@ -354,7 +373,7 @@ export default createRule<Options, MessageId>({
354373              fix ( fixer : RuleFixer )  { 
355374                return  fixer . replaceText ( 
356375                  source , 
357-                   JSON . stringify ( fixedImportPath ) , 
376+                   replaceImportPath ( source . raw ,   fixedImportPath ) , 
358377                ) 
359378              } , 
360379            } 
@@ -376,7 +395,7 @@ export default createRule<Options, MessageId>({
376395                          data : { 
377396                            extension, 
378397                            importPath : importPathWithQueryString , 
379-                             fixedImportPath :  fixedImportPath , 
398+                             fixedImportPath, 
380399                          } , 
381400                        } , 
382401                      ] , 
@@ -388,21 +407,68 @@ export default createRule<Options, MessageId>({
388407          isUseOfExtensionForbidden ( extension )  && 
389408          isResolvableWithoutExtension ( importPath ) 
390409        )  { 
410+           const  fixedPathname  =  importPath . slice ( 0 ,  - ( extension . length  +  1 ) ) 
411+           const  isIndex  =  fixedPathname . endsWith ( '/index' ) 
412+           const  fixedImportPath  =  stringifyPath ( { 
413+             pathname : isIndex  ? fixedPathname . slice ( 0 ,  - 6 )  : fixedPathname , 
414+             query, 
415+             hash, 
416+           } ) 
417+           const  fixOrSuggest  =  { 
418+             fix ( fixer : RuleFixer )  { 
419+               return  fixer . replaceText ( 
420+                 source , 
421+                 replaceImportPath ( source . raw ,  fixedImportPath ) , 
422+               ) 
423+             } , 
424+           } 
425+           const  commonSuggestion  =  { 
426+             ...fixOrSuggest , 
427+             messageId : 'removeUnexpected'  as  const , 
428+             data : { 
429+               extension, 
430+               importPath : importPathWithQueryString , 
431+               fixedImportPath, 
432+             } , 
433+           } 
391434          context . report ( { 
392435            node : source , 
393436            messageId : 'unexpected' , 
394437            data : { 
395438              extension, 
396439              importPath : importPathWithQueryString , 
397440            } , 
398-             ...( props . fix  &&  { 
399-               fix ( fixer )  { 
400-                 return  fixer . replaceText ( 
401-                   source , 
402-                   JSON . stringify ( importPath . slice ( 0 ,  - ( extension . length  +  1 ) ) ) , 
403-                 ) 
404-               } , 
405-             } ) , 
441+             ...( props . fix 
442+               ? fixOrSuggest 
443+               : { 
444+                   suggest : [ 
445+                     commonSuggestion , 
446+                     isIndex  &&  { 
447+                       ...commonSuggestion , 
448+                       fix ( fixer : RuleFixer )  { 
449+                         return  fixer . replaceText ( 
450+                           source , 
451+                           replaceImportPath ( 
452+                             source . raw , 
453+                             stringifyPath ( { 
454+                               pathname : fixedPathname , 
455+                               query, 
456+                               hash, 
457+                             } ) , 
458+                           ) , 
459+                         ) 
460+                       } , 
461+                       data : { 
462+                         ...commonSuggestion . data , 
463+                         fixedImportPath : stringifyPath ( { 
464+                           pathname : fixedPathname , 
465+                           query, 
466+                           hash, 
467+                         } ) , 
468+                       } , 
469+                     } , 
470+                   ] . filter ( Boolean ) , 
471+                 } ) , 
406472          } ) 
407473        } 
408474      } , 
0 commit comments