@@ -91,10 +91,10 @@ function buildProperties(context) {
91
91
92
92
module . exports = {
93
93
meta : {
94
- type : 'problem ' ,
94
+ type : 'suggestion ' ,
95
95
docs : {
96
- description : 'Enforce that import statements either always include or never include allowed file extensions. ' ,
97
- category : 'Static Analysis ' ,
96
+ category : 'Style guide ' ,
97
+ description : 'Ensure consistent use of file extension within the import path. ' ,
98
98
recommended : false ,
99
99
url : docsUrl ( 'extensions' ) ,
100
100
} ,
@@ -134,12 +134,6 @@ module.exports = {
134
134
} ,
135
135
] ,
136
136
} ,
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
137
} ,
144
138
145
139
create ( context ) {
@@ -158,12 +152,7 @@ module.exports = {
158
152
return getModifier ( extension ) === 'never' ;
159
153
}
160
154
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
- }
155
+ function isResolvableWithoutExtension ( file ) {
167
156
const fileExt = path . extname ( file ) ;
168
157
const fileWithoutExtension = file . slice ( 0 , - fileExt . length ) ;
169
158
const resolvedFileWithoutExtension = resolve ( fileWithoutExtension , context ) ;
@@ -201,7 +190,9 @@ module.exports = {
201
190
if ( ! source || ! source . value ) { return ; }
202
191
203
192
const importPathWithQueryString = source . value ;
193
+ const hasQuery = importPathWithQueryString . includes ( '?' ) ;
204
194
const currentDir = path . dirname ( context . getFilename ( ) ) ;
195
+ const isRelative = importPathWithQueryString . startsWith ( '.' ) ;
205
196
206
197
// If not undefined, the user decided if rules are enforced on this import
207
198
const overrideAction = computeOverrideAction (
@@ -213,58 +204,82 @@ module.exports = {
213
204
return ;
214
205
}
215
206
216
- // don't enforce anything on builtins
217
207
if ( ! overrideAction && isBuiltIn ( importPathWithQueryString , context . settings ) ) { return ; }
218
208
219
- const importPath = importPathWithQueryString . replace ( / \? ( .* ) $ / , '' ) ;
220
-
221
- // don't enforce in root external packages as they may have names with `.js`.
222
- // Like `import Decimal from decimal.js`)
223
- if ( ! overrideAction && isExternalRootModule ( importPath ) ) { return ; }
209
+ const importPath = importPathWithQueryString . replace ( / \? ( .* ) $ / , '' ) . trim ( ) ;
210
+ if ( ! overrideAction && isExternalRootModule ( importPath ) && ! ( props . checkTypeImports && ( node . importKind === 'type' || node . exportKind === 'type' ) ) ) { return ; }
224
211
225
212
const resolvedPath = resolve ( importPath , context ) ;
226
- const extensionWithDot = path . extname ( resolvedPath || importPath ) ;
213
+ const isPackage = isExternalModule ( importPath , resolvedPath , context ) || isScoped ( importPath ) ;
214
+ const extension = path . extname ( resolvedPath || importPath ) . slice ( 1 ) ;
227
215
228
- // determine if this is a module
229
- const isPackage = isExternalModule (
230
- importPath ,
231
- resolve ( importPath , context ) ,
232
- context ,
233
- ) || isScoped ( importPath ) ;
216
+ const sourceCode = context . getSourceCode ( ) ;
217
+ const fileHasExports = sourceCode . ast . body . some ( ( n ) => n . type . indexOf ( 'Export' ) === 0 ) ;
218
+ const isExport = node && node . type && node . type . indexOf ( 'Export' ) === 0 ;
219
+ const isImportDeclaration = node && node . type === 'ImportDeclaration' ;
234
220
235
- // Case 1: Missing extension.
236
- if ( ! extensionWithDot || ! importPath . endsWith ( extensionWithDot ) ) {
221
+ if ( ! extension || ! importPath . endsWith ( `.${ extension } ` ) ) {
237
222
// ignore type-only imports and exports
238
223
if ( ! props . checkTypeImports && ( node . importKind === 'type' || node . exportKind === 'type' ) ) { return ; }
239
- const candidate = getCandidateExtension ( importPath , currentDir ) ;
240
- if ( candidate && isUseOfExtensionRequired ( candidate . replace ( / ^ \. / , '' ) , isPackage ) ) {
241
- 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
- } ,
251
- } ) ;
224
+ let candidate = extension ? `.${ extension } ` : getCandidateExtension ( importPath , currentDir ) ;
225
+ if ( ! candidate && isUseOfExtensionRequired ( 'js' , isPackage ) ) { candidate = '.js' ; }
226
+ if ( candidate && isUseOfExtensionRequired ( candidate . slice ( 1 ) , isPackage ) ) {
227
+ if ( isExport || hasQuery || ! isImportDeclaration && fileHasExports || ! Object . prototype . hasOwnProperty . call ( props . pattern , candidate . slice ( 1 ) ) || ! isRelative || isPackage ) {
228
+ context . report ( {
229
+ node : source ,
230
+ message : `Missing file extension ${ extension ? `"${ extension } " ` : '' } for "${ importPathWithQueryString } "` ,
231
+ data : {
232
+ importPath : importPathWithQueryString ,
233
+ expected : candidate ,
234
+ } ,
235
+ } ) ;
236
+ } else {
237
+ context . report ( {
238
+ node : source ,
239
+ message : `Missing file extension ${ extension ? `"${ extension } " ` : '' } for "${ importPathWithQueryString } "` ,
240
+ data : {
241
+ importPath : importPathWithQueryString ,
242
+ expected : candidate ,
243
+ } ,
244
+ fix ( fixer ) {
245
+ return fixer . replaceText (
246
+ source ,
247
+ JSON . stringify ( importPathWithQueryString + candidate ) ,
248
+ ) ;
249
+ } ,
250
+ } ) ;
251
+ }
252
252
}
253
253
} else {
254
254
// Case 2: Unexpected extension provided.
255
- const extension = extensionWithDot . substring ( 1 ) ;
256
- if ( isUseOfExtensionForbidden ( extension ) && isResolvableWithoutExtension ( importPath , extension ) ) {
257
- context . report ( {
258
- 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
- } ,
267
- } ) ;
255
+ if ( isUseOfExtensionForbidden ( extension ) && isResolvableWithoutExtension ( importPath ) ) {
256
+ if ( isExport || hasQuery || ! isImportDeclaration && fileHasExports || ! Object . prototype . hasOwnProperty . call ( props . pattern , extension ) || ! isRelative || isPackage ) {
257
+ context . report ( {
258
+ node : source ,
259
+ message : `Unexpected use of file extension "${ extension } " for "${ importPathWithQueryString } "` ,
260
+ data : {
261
+ extension,
262
+ importPath : importPathWithQueryString ,
263
+ } ,
264
+ } ) ;
265
+ } else {
266
+ context . report ( {
267
+ node : source ,
268
+ message : `Unexpected use of file extension "${ extension } " for "${ importPathWithQueryString } "` ,
269
+ data : {
270
+ extension,
271
+ importPath : importPathWithQueryString ,
272
+ } ,
273
+ fix ( fixer ) {
274
+ return fixer . replaceText (
275
+ source ,
276
+ JSON . stringify (
277
+ importPath . slice ( 0 , - ( extension . length + 1 ) ) ,
278
+ ) ,
279
+ ) ;
280
+ } ,
281
+ } ) ;
282
+ }
268
283
}
269
284
}
270
285
}
0 commit comments