1
1
import path from 'path' ;
2
- import fs from 'fs' ;
2
+
3
3
import minimatch from 'minimatch' ;
4
4
import resolve from 'eslint-module-utils/resolve' ;
5
5
import { isBuiltIn , isExternalModule , isScoped } from '../core/importType' ;
@@ -17,6 +17,7 @@ const properties = {
17
17
pattern : patternProperties ,
18
18
checkTypeImports : { type : 'boolean' } ,
19
19
ignorePackages : { type : 'boolean' } ,
20
+ fix : { type : 'boolean' } ,
20
21
pathGroupOverrides : {
21
22
type : 'array' ,
22
23
items : {
@@ -46,6 +47,7 @@ function buildProperties(context) {
46
47
defaultConfig : 'never' ,
47
48
pattern : { } ,
48
49
ignorePackages : false ,
50
+ fix : false ,
49
51
} ;
50
52
51
53
context . options . forEach ( ( obj ) => {
@@ -57,7 +59,7 @@ function buildProperties(context) {
57
59
}
58
60
59
61
// 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 ) {
61
63
Object . assign ( result . pattern , obj ) ;
62
64
return ;
63
65
}
@@ -76,6 +78,10 @@ function buildProperties(context) {
76
78
result . checkTypeImports = obj . checkTypeImports ;
77
79
}
78
80
81
+ if ( obj . fix !== undefined ) {
82
+ result . fix = obj . fix ;
83
+ }
84
+
79
85
if ( obj . pathGroupOverrides !== undefined ) {
80
86
result . pathGroupOverrides = obj . pathGroupOverrides ;
81
87
}
@@ -91,11 +97,10 @@ function buildProperties(context) {
91
97
92
98
module . exports = {
93
99
meta : {
94
- type : 'problem ' ,
100
+ type : 'suggestion ' ,
95
101
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.' ,
99
104
url : docsUrl ( 'extensions' ) ,
100
105
} ,
101
106
fixable : 'code' ,
@@ -134,18 +139,15 @@ module.exports = {
134
139
} ,
135
140
] ,
136
141
} ,
137
- messages : {
138
- missingExtension :
139
- 'Missing file extension for "{{importPath}}" (expected {{expected}}).' ,
140
- unexpectedExtension :
141
- 'Unexpected file extension "{{extension}}" in import of "{{importPath}}".' ,
142
- } ,
143
142
} ,
144
143
145
144
create ( context ) {
146
145
147
146
const props = buildProperties ( context ) ;
148
147
148
+ // Check if fix is enabled in options
149
+ const isFixEnabled = ! ! props . fix ;
150
+
149
151
function getModifier ( extension ) {
150
152
return props . pattern [ extension ] || props . defaultConfig ;
151
153
}
@@ -158,14 +160,9 @@ module.exports = {
158
160
return getModifier ( extension ) === 'never' ;
159
161
}
160
162
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 ) ;
169
166
const resolvedFileWithoutExtension = resolve ( fileWithoutExtension , context ) ;
170
167
171
168
return resolvedFileWithoutExtension === resolve ( file , context ) ;
@@ -189,19 +186,11 @@ module.exports = {
189
186
}
190
187
}
191
188
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
-
199
189
function checkFileExtension ( source , node ) {
200
190
// bail if the declaration doesn't have a source, e.g. "export { foo };", or if it's only partially typed like in an editor
201
191
if ( ! source || ! source . value ) { return ; }
202
192
203
193
const importPathWithQueryString = source . value ;
204
- const currentDir = path . dirname ( context . getFilename ( ) ) ;
205
194
206
195
// If not undefined, the user decided if rules are enforced on this import
207
196
const overrideAction = computeOverrideAction (
@@ -223,7 +212,8 @@ module.exports = {
223
212
if ( ! overrideAction && isExternalRootModule ( importPath ) ) { return ; }
224
213
225
214
const resolvedPath = resolve ( importPath , context ) ;
226
- const extensionWithDot = path . extname ( resolvedPath || importPath ) ;
215
+
216
+ const extension = path . extname ( resolvedPath || importPath ) . substring ( 1 ) ;
227
217
228
218
// determine if this is a module
229
219
const isPackage = isExternalModule (
@@ -232,38 +222,29 @@ module.exports = {
232
222
context ,
233
223
) || isScoped ( importPath ) ;
234
224
235
- // Case 1: Missing extension.
236
- if ( ! extensionWithDot || ! importPath . endsWith ( extensionWithDot ) ) {
225
+ if ( ! extension || ! importPath . endsWith ( `.${ extension } ` ) ) {
237
226
// ignore type-only imports and exports
238
227
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 ) {
241
231
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 ,
251
236
} ) ;
252
237
}
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 ) ) {
257
240
context . report ( {
258
241
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 ,
267
248
} ) ;
268
249
}
269
250
}
0 commit comments