@@ -91,10 +91,10 @@ function buildProperties(context) {
9191
9292module . exports = {
9393 meta : {
94- type : 'problem ' ,
94+ type : 'suggestion ' ,
9595 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. ' ,
9898 recommended : false ,
9999 url : docsUrl ( 'extensions' ) ,
100100 } ,
@@ -134,12 +134,6 @@ module.exports = {
134134 } ,
135135 ] ,
136136 } ,
137- messages : {
138- missingExtension :
139- 'Missing file extension for "{{importPath}}" (expected {{expected}}).' ,
140- unexpectedExtension :
141- 'Unexpected file extension "{{extension}}" in import of "{{importPath}}".' ,
142- } ,
143137 } ,
144138
145139 create ( context ) {
@@ -158,12 +152,7 @@ module.exports = {
158152 return getModifier ( extension ) === 'never' ;
159153 }
160154
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 ) {
167156 const fileExt = path . extname ( file ) ;
168157 const fileWithoutExtension = file . slice ( 0 , - fileExt . length ) ;
169158 const resolvedFileWithoutExtension = resolve ( fileWithoutExtension , context ) ;
@@ -201,7 +190,9 @@ module.exports = {
201190 if ( ! source || ! source . value ) { return ; }
202191
203192 const importPathWithQueryString = source . value ;
193+ const hasQuery = importPathWithQueryString . includes ( '?' ) ;
204194 const currentDir = path . dirname ( context . getFilename ( ) ) ;
195+ const isRelative = importPathWithQueryString . startsWith ( '.' ) ;
205196
206197 // If not undefined, the user decided if rules are enforced on this import
207198 const overrideAction = computeOverrideAction (
@@ -213,58 +204,82 @@ module.exports = {
213204 return ;
214205 }
215206
216- // don't enforce anything on builtins
217207 if ( ! overrideAction && isBuiltIn ( importPathWithQueryString , context . settings ) ) { return ; }
218208
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 ; }
224211
225212 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 ) ;
227215
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' ;
234220
235- // Case 1: Missing extension.
236- if ( ! extensionWithDot || ! importPath . endsWith ( extensionWithDot ) ) {
221+ if ( ! extension || ! importPath . endsWith ( `.${ extension } ` ) ) {
237222 // ignore type-only imports and exports
238223 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+ }
252252 }
253253 } else {
254254 // 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+ }
268283 }
269284 }
270285 }
0 commit comments