Skip to content

Commit 7645cf6

Browse files
tests: fix failing tests and introduce fix flag for better backward compatibility
1 parent d84fbfa commit 7645cf6

File tree

1 file changed

+35
-54
lines changed

1 file changed

+35
-54
lines changed

src/rules/extensions.js

Lines changed: 35 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'path';
2-
import fs from 'fs';
2+
33
import minimatch from 'minimatch';
44
import resolve from 'eslint-module-utils/resolve';
55
import { 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

9298
module.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

Comments
 (0)