11import path from 'path' ;
2- import fs from 'fs' ;
2+
33import minimatch from 'minimatch' ;
44import resolve from 'eslint-module-utils/resolve' ;
55import { isBuiltIn , isExternalModule , isScoped } from '../core/importType' ;
@@ -17,6 +17,7 @@ const properties = {
1717 pattern : patternProperties ,
1818 checkTypeImports : { type : 'boolean' } ,
1919 ignorePackages : { type : 'boolean' } ,
20+ fix : { type : 'boolean' } ,
2021 pathGroupOverrides : {
2122 type : 'array' ,
2223 items : {
@@ -46,6 +47,7 @@ function buildProperties(context) {
4647 defaultConfig : 'never' ,
4748 pattern : { } ,
4849 ignorePackages : false ,
50+ fix : false ,
4951 } ;
5052
5153 context . options . forEach ( ( obj ) => {
@@ -57,7 +59,7 @@ function buildProperties(context) {
5759 }
5860
5961 // If this is not the new structure, transfer all props to result.pattern
60- if ( obj . pattern === undefined && obj . ignorePackages === undefined && obj . checkTypeImports === undefined ) {
62+ if ( obj . pattern === undefined && obj . ignorePackages === undefined && obj . checkTypeImports === undefined && obj . fix === undefined ) {
6163 Object . assign ( result . pattern , obj ) ;
6264 return ;
6365 }
@@ -76,6 +78,10 @@ function buildProperties(context) {
7678 result . checkTypeImports = obj . checkTypeImports ;
7779 }
7880
81+ if ( obj . fix !== undefined ) {
82+ result . fix = obj . fix ;
83+ }
84+
7985 if ( obj . pathGroupOverrides !== undefined ) {
8086 result . pathGroupOverrides = obj . pathGroupOverrides ;
8187 }
@@ -91,11 +97,10 @@ function buildProperties(context) {
9197
9298module . exports = {
9399 meta : {
94- type : 'problem ' ,
100+ type : 'suggestion ' ,
95101 docs : {
96- description : 'Enforce that import statements either always include or never include allowed file extensions.' ,
97- category : 'Static Analysis' ,
98- recommended : false ,
102+ category : 'Style guide' ,
103+ description : 'Ensure consistent use of file extension within the import path.' ,
99104 url : docsUrl ( 'extensions' ) ,
100105 } ,
101106 fixable : 'code' ,
@@ -134,18 +139,15 @@ module.exports = {
134139 } ,
135140 ] ,
136141 } ,
137- messages : {
138- missingExtension :
139- 'Missing file extension for "{{importPath}}" (expected {{expected}}).' ,
140- unexpectedExtension :
141- 'Unexpected file extension "{{extension}}" in import of "{{importPath}}".' ,
142- } ,
143142 } ,
144143
145144 create ( context ) {
146145
147146 const props = buildProperties ( context ) ;
148147
148+ // Check if fix is enabled in options
149+ const isFixEnabled = ! ! props . fix ;
150+
149151 function getModifier ( extension ) {
150152 return props . pattern [ extension ] || props . defaultConfig ;
151153 }
@@ -158,14 +160,9 @@ module.exports = {
158160 return getModifier ( extension ) === 'never' ;
159161 }
160162
161- // Updated: This helper now determines resolvability based on the passed options.
162- // If the configured option for the extension is "never", we return true immediately.
163- function isResolvableWithoutExtension ( file , ext ) {
164- if ( isUseOfExtensionForbidden ( ext ) ) {
165- return true ;
166- }
167- const fileExt = path . extname ( file ) ;
168- const fileWithoutExtension = file . slice ( 0 , - fileExt . length ) ;
163+ function isResolvableWithoutExtension ( file ) {
164+ const extension = path . extname ( file ) ;
165+ const fileWithoutExtension = file . slice ( 0 , - extension . length ) ;
169166 const resolvedFileWithoutExtension = resolve ( fileWithoutExtension , context ) ;
170167
171168 return resolvedFileWithoutExtension === resolve ( file , context ) ;
@@ -189,19 +186,11 @@ module.exports = {
189186 }
190187 }
191188
192- function getCandidateExtension ( importPath , currentDir ) {
193- const basePath = path . resolve ( currentDir , importPath ) ;
194- const keys = Object . keys ( props . pattern ) ;
195- const valid = keys . filter ( ( key ) => fs . existsSync ( `${ basePath } .${ key } ` ) ) ;
196- return valid . length === 1 ? `.${ valid [ 0 ] } ` : null ;
197- }
198-
199189 function checkFileExtension ( source , node ) {
200190 // bail if the declaration doesn't have a source, e.g. "export { foo };", or if it's only partially typed like in an editor
201191 if ( ! source || ! source . value ) { return ; }
202192
203193 const importPathWithQueryString = source . value ;
204- const currentDir = path . dirname ( context . getFilename ( ) ) ;
205194
206195 // If not undefined, the user decided if rules are enforced on this import
207196 const overrideAction = computeOverrideAction (
@@ -223,7 +212,8 @@ module.exports = {
223212 if ( ! overrideAction && isExternalRootModule ( importPath ) ) { return ; }
224213
225214 const resolvedPath = resolve ( importPath , context ) ;
226- const extensionWithDot = path . extname ( resolvedPath || importPath ) ;
215+
216+ const extension = path . extname ( resolvedPath || importPath ) . substring ( 1 ) ;
227217
228218 // determine if this is a module
229219 const isPackage = isExternalModule (
@@ -232,38 +222,29 @@ module.exports = {
232222 context ,
233223 ) || isScoped ( importPath ) ;
234224
235- // Case 1: Missing extension.
236- if ( ! extensionWithDot || ! importPath . endsWith ( extensionWithDot ) ) {
225+ if ( ! extension || ! importPath . endsWith ( `.${ extension } ` ) ) {
237226 // ignore type-only imports and exports
238227 if ( ! props . checkTypeImports && ( node . importKind === 'type' || node . exportKind === 'type' ) ) { return ; }
239- const candidate = getCandidateExtension ( importPath , currentDir ) ;
240- if ( candidate && isUseOfExtensionRequired ( candidate . replace ( / ^ \. / , '' ) , isPackage ) ) {
228+ const extensionRequired = isUseOfExtensionRequired ( extension , ! overrideAction && isPackage ) ;
229+ const extensionForbidden = isUseOfExtensionForbidden ( extension ) ;
230+ if ( extensionRequired && ! extensionForbidden ) {
241231 context . report ( {
242- node,
243- messageId : 'missingExtension' ,
244- data : {
245- importPath : importPathWithQueryString ,
246- expected : candidate ,
247- } ,
248- fix ( fixer ) {
249- return fixer . replaceText ( source , JSON . stringify ( importPathWithQueryString + candidate ) ) ;
250- } ,
232+ node : source ,
233+ message :
234+ `Missing file extension ${ extension ? `"${ extension } " ` : '' } for "${ importPathWithQueryString } "` ,
235+ fix : isFixEnabled && extension ? ( fixer ) => fixer . replaceText ( source , JSON . stringify ( `${ importPathWithQueryString } .${ extension } ` ) ) : null ,
251236 } ) ;
252237 }
253- } else {
254- // Case 2: Unexpected extension provided.
255- const extension = extensionWithDot . substring ( 1 ) ;
256- if ( isUseOfExtensionForbidden ( extension ) && isResolvableWithoutExtension ( importPath , extension ) ) {
238+ } else if ( extension ) {
239+ if ( isUseOfExtensionForbidden ( extension ) && isResolvableWithoutExtension ( importPath ) ) {
257240 context . report ( {
258241 node : source ,
259- messageId : 'unexpectedExtension' ,
260- data : {
261- extension,
262- importPath : importPathWithQueryString ,
263- } ,
264- fix ( fixer ) {
265- return fixer . replaceText ( source , JSON . stringify ( importPath . slice ( 0 , - extensionWithDot . length ) ) ) ;
266- } ,
242+ message : `Unexpected use of file extension "${ extension } " for "${ importPathWithQueryString } "` ,
243+ fix : isFixEnabled ? ( fixer ) => {
244+ const extensionPattern = new RegExp ( `\\.${ extension } ($|\\?)` , 'g' ) ;
245+ const withoutExtension = importPathWithQueryString . replace ( extensionPattern , '$1' ) ;
246+ return fixer . replaceText ( source , JSON . stringify ( withoutExtension ) ) ;
247+ } : null ,
267248 } ) ;
268249 }
269250 }
0 commit comments