Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
epidemian
GitHub Repository: epidemian/eslint-plugin-import
Path: blob/main/src/rules/extensions.js
829 views
1
import path from 'path';
2
3
import resolve from 'eslint-module-utils/resolve';
4
import { isBuiltIn, isExternalModule, isScoped } from '../core/importType';
5
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
6
import docsUrl from '../docsUrl';
7
8
const enumValues = { enum: [ 'always', 'ignorePackages', 'never' ] };
9
const patternProperties = {
10
type: 'object',
11
patternProperties: { '.*': enumValues },
12
};
13
const properties = {
14
type: 'object',
15
properties: {
16
'pattern': patternProperties,
17
'ignorePackages': { type: 'boolean' },
18
},
19
};
20
21
function buildProperties(context) {
22
23
const result = {
24
defaultConfig: 'never',
25
pattern: {},
26
ignorePackages: false,
27
};
28
29
context.options.forEach(obj => {
30
31
// If this is a string, set defaultConfig to its value
32
if (typeof obj === 'string') {
33
result.defaultConfig = obj;
34
return;
35
}
36
37
// If this is not the new structure, transfer all props to result.pattern
38
if (obj.pattern === undefined && obj.ignorePackages === undefined) {
39
Object.assign(result.pattern, obj);
40
return;
41
}
42
43
// If pattern is provided, transfer all props
44
if (obj.pattern !== undefined) {
45
Object.assign(result.pattern, obj.pattern);
46
}
47
48
// If ignorePackages is provided, transfer it to result
49
if (obj.ignorePackages !== undefined) {
50
result.ignorePackages = obj.ignorePackages;
51
}
52
});
53
54
if (result.defaultConfig === 'ignorePackages') {
55
result.defaultConfig = 'always';
56
result.ignorePackages = true;
57
}
58
59
return result;
60
}
61
62
module.exports = {
63
meta: {
64
type: 'suggestion',
65
docs: {
66
url: docsUrl('extensions'),
67
},
68
69
schema: {
70
anyOf: [
71
{
72
type: 'array',
73
items: [enumValues],
74
additionalItems: false,
75
},
76
{
77
type: 'array',
78
items: [
79
enumValues,
80
properties,
81
],
82
additionalItems: false,
83
},
84
{
85
type: 'array',
86
items: [properties],
87
additionalItems: false,
88
},
89
{
90
type: 'array',
91
items: [patternProperties],
92
additionalItems: false,
93
},
94
{
95
type: 'array',
96
items: [
97
enumValues,
98
patternProperties,
99
],
100
additionalItems: false,
101
},
102
],
103
},
104
},
105
106
create(context) {
107
108
const props = buildProperties(context);
109
110
function getModifier(extension) {
111
return props.pattern[extension] || props.defaultConfig;
112
}
113
114
function isUseOfExtensionRequired(extension, isPackage) {
115
return getModifier(extension) === 'always' && (!props.ignorePackages || !isPackage);
116
}
117
118
function isUseOfExtensionForbidden(extension) {
119
return getModifier(extension) === 'never';
120
}
121
122
function isResolvableWithoutExtension(file) {
123
const extension = path.extname(file);
124
const fileWithoutExtension = file.slice(0, -extension.length);
125
const resolvedFileWithoutExtension = resolve(fileWithoutExtension, context);
126
127
return resolvedFileWithoutExtension === resolve(file, context);
128
}
129
130
function isExternalRootModule(file) {
131
const slashCount = file.split('/').length - 1;
132
133
if (slashCount === 0) return true;
134
if (isScoped(file) && slashCount <= 1) return true;
135
return false;
136
}
137
138
function checkFileExtension(source, node) {
139
// bail if the declaration doesn't have a source, e.g. "export { foo };", or if it's only partially typed like in an editor
140
if (!source || !source.value) return;
141
142
const importPathWithQueryString = source.value;
143
144
// don't enforce anything on builtins
145
if (isBuiltIn(importPathWithQueryString, context.settings)) return;
146
147
const importPath = importPathWithQueryString.replace(/\?(.*)$/, '');
148
149
// don't enforce in root external packages as they may have names with `.js`.
150
// Like `import Decimal from decimal.js`)
151
if (isExternalRootModule(importPath)) return;
152
153
const resolvedPath = resolve(importPath, context);
154
155
// get extension from resolved path, if possible.
156
// for unresolved, use source value.
157
const extension = path.extname(resolvedPath || importPath).substring(1);
158
159
// determine if this is a module
160
const isPackage = isExternalModule(
161
importPath,
162
context.settings,
163
resolve(importPath, context),
164
context,
165
) || isScoped(importPath);
166
167
if (!extension || !importPath.endsWith(`.${extension}`)) {
168
// ignore type-only imports
169
if (node.importKind === 'type') return;
170
const extensionRequired = isUseOfExtensionRequired(extension, isPackage);
171
const extensionForbidden = isUseOfExtensionForbidden(extension);
172
if (extensionRequired && !extensionForbidden) {
173
context.report({
174
node: source,
175
message:
176
`Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPathWithQueryString}"`,
177
});
178
}
179
} else if (extension) {
180
if (isUseOfExtensionForbidden(extension) && isResolvableWithoutExtension(importPath)) {
181
context.report({
182
node: source,
183
message: `Unexpected use of file extension "${extension}" for "${importPathWithQueryString}"`,
184
});
185
}
186
}
187
}
188
189
return moduleVisitor(checkFileExtension, { commonjs: true });
190
},
191
};
192
193