Skip to content

Commit ee51581

Browse files
authored
Merge pull request #827 from collinsauve/issue414
[new] import/extensions should have a ignorePackages option. Fixes #414
2 parents eca7b4d + 81d3b36 commit ee51581

File tree

4 files changed

+201
-21
lines changed

4 files changed

+201
-21
lines changed

docs/rules/extensions.md

+40-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ In order to provide a consistent use of file extensions across your code base, t
66

77
## Rule Details
88

9-
This rule either takes one string option, one object option, or a string and an object option. If it is the string `"never"` (the default value), then the rule forbids the use for any extension. If it is the string `"always"`, then the rule enforces the use of extensions for all import statements.
9+
This rule either takes one string option, one object option, or a string and an object option. If it is the string `"never"` (the default value), then the rule forbids the use for any extension. If it is the string `"always"`, then the rule enforces the use of extensions for all import statements. If it is the string `"ignorePackages"`, then the rule enforces the use of extensions for all import statements except package imports.
1010

1111
By providing an object you can configure each extension separately, so for example `{ "js": "always", "json": "never" }` would always enforce the use of the `.js` extension but never allow the use of the `.json` extension.
1212

@@ -41,7 +41,7 @@ import foo from './foo.js';
4141

4242
import bar from './bar.json';
4343

44-
import Component from './Component.jsx'
44+
import Component from './Component.jsx';
4545

4646
import express from 'express/index.js';
4747
```
@@ -53,7 +53,7 @@ import foo from './foo';
5353

5454
import bar from './bar';
5555

56-
import Component from './Component'
56+
import Component from './Component';
5757

5858
import express from 'express/index';
5959

@@ -67,7 +67,7 @@ import foo from './foo';
6767

6868
import bar from './bar';
6969

70-
import Component from './Component'
70+
import Component from './Component';
7171

7272
import express from 'express';
7373
```
@@ -79,13 +79,48 @@ import foo from './foo.js';
7979

8080
import bar from './bar.json';
8181

82-
import Component from './Component.jsx'
82+
import Component from './Component.jsx';
8383

8484
import express from 'express/index.js';
8585

8686
import * as path from 'path';
8787
```
8888

89+
The following patterns are considered problems when configuration set to "ignorePackages":
90+
91+
```js
92+
import foo from './foo';
93+
94+
import bar from './bar';
95+
96+
import Component from './Component';
97+
98+
```
99+
100+
The following patterns are not considered problems when configuration set to "ignorePackages":
101+
102+
```js
103+
import foo from './foo.js';
104+
105+
import bar from './bar.json';
106+
107+
import Component from './Component.jsx';
108+
109+
import express from 'express';
110+
111+
```
112+
113+
The following patterns are not considered problems when configuration set to `[ 'always', {ignorePackages: true} ]`:
114+
115+
```js
116+
import Component from './Component.jsx';
117+
118+
import baz from 'foo/baz.js';
119+
120+
import express from 'express';
121+
122+
```
123+
89124
## When Not To Use It
90125

91126
If you are not concerned about a consistent usage of file extension.

src/core/importType.js

+10
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,21 @@ function isExternalModule(name, settings, path) {
3737
return externalModuleRegExp.test(name) && isExternalPath(path, name, settings)
3838
}
3939

40+
const externalModuleMainRegExp = /^[\w]((?!\/).)*$/
41+
export function isExternalModuleMain(name, settings, path) {
42+
return externalModuleMainRegExp.test(name) && isExternalPath(path, name, settings)
43+
}
44+
4045
const scopedRegExp = /^@[^/]+\/[^/]+/
4146
function isScoped(name) {
4247
return scopedRegExp.test(name)
4348
}
4449

50+
const scopedMainRegExp = /^@[^/]+\/?[^/]+$/
51+
export function isScopedMain(name) {
52+
return scopedMainRegExp.test(name)
53+
}
54+
4555
function isInternalModule(name, settings, path) {
4656
return externalModuleRegExp.test(name) && !isExternalPath(path, name, settings)
4757
}

src/rules/extensions.js

+76-16
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,56 @@
11
import path from 'path'
2-
import has from 'has'
32

43
import resolve from 'eslint-module-utils/resolve'
5-
import { isBuiltIn } from '../core/importType'
4+
import { isBuiltIn, isExternalModuleMain, isScopedMain } from '../core/importType'
65

7-
const enumValues = { enum: [ 'always', 'never' ] }
6+
const enumValues = { enum: [ 'always', 'ignorePackages', 'never' ] }
87
const patternProperties = {
98
type: 'object',
109
patternProperties: { '.*': enumValues },
1110
}
11+
const properties = {
12+
type: 'object',
13+
properties: {
14+
'pattern': patternProperties,
15+
'ignorePackages': { type: 'boolean' },
16+
},
17+
}
18+
19+
function buildProperties(context) {
20+
21+
const result = {
22+
defaultConfig: 'never',
23+
pattern: {},
24+
ignorePackages: false,
25+
}
26+
27+
context.options.forEach(obj => {
28+
29+
// If this is a string, set defaultConfig to its value
30+
if (typeof obj === 'string') {
31+
result.defaultConfig = obj
32+
return
33+
}
34+
35+
// If this is not the new structure, transfer all props to result.pattern
36+
if (obj.pattern === undefined && obj.ignorePackages === undefined) {
37+
Object.assign(result.pattern, obj)
38+
return
39+
}
40+
41+
// If pattern is provided, transfer all props
42+
if (obj.pattern !== undefined) {
43+
Object.assign(result.pattern, obj.pattern)
44+
}
45+
46+
// If ignorePackages is provided, transfer it to result
47+
if (obj.ignorePackages !== undefined) {
48+
result.ignorePackages = obj.ignorePackages
49+
}
50+
})
51+
52+
return result
53+
}
1254

1355
module.exports = {
1456
meta: {
@@ -21,6 +63,19 @@ module.exports = {
2163
items: [enumValues],
2264
additionalItems: false,
2365
},
66+
{
67+
type: 'array',
68+
items: [
69+
enumValues,
70+
properties,
71+
],
72+
additionalItems: false,
73+
},
74+
{
75+
type: 'array',
76+
items: [properties],
77+
additionalItems: false,
78+
},
2479
{
2580
type: 'array',
2681
items: [patternProperties],
@@ -39,21 +94,19 @@ module.exports = {
3994
},
4095

4196
create: function (context) {
42-
const configuration = context.options[0] || 'never'
43-
const defaultConfig = typeof configuration === 'string' ? configuration : null
44-
const modifiers = Object.assign(
45-
{},
46-
typeof configuration === 'object' ? configuration : context.options[1]
47-
)
48-
49-
function isUseOfExtensionRequired(extension) {
50-
if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig }
51-
return modifiers[extension] === 'always'
97+
98+
const props = buildProperties(context)
99+
100+
function getModifier(extension) {
101+
return props.pattern[extension] || props.defaultConfig
102+
}
103+
104+
function isUseOfExtensionRequired(extension, isPackageMain) {
105+
return getModifier(extension) === 'always' && (!props.ignorePackages || !isPackageMain)
52106
}
53107

54108
function isUseOfExtensionForbidden(extension) {
55-
if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig }
56-
return modifiers[extension] === 'never'
109+
return getModifier(extension) === 'never'
57110
}
58111

59112
function isResolvableWithoutExtension(file) {
@@ -77,8 +130,15 @@ module.exports = {
77130
// for unresolved, use source value.
78131
const extension = path.extname(resolvedPath || importPath).substring(1)
79132

133+
// determine if this is a module
134+
const isPackageMain =
135+
isExternalModuleMain(importPath, context.settings) ||
136+
isScopedMain(importPath)
137+
80138
if (!extension || !importPath.endsWith(extension)) {
81-
if (isUseOfExtensionRequired(extension) && !isUseOfExtensionForbidden(extension)) {
139+
const extensionRequired = isUseOfExtensionRequired(extension, isPackageMain)
140+
const extensionForbidden = isUseOfExtensionForbidden(extension)
141+
if (extensionRequired && !extensionForbidden) {
82142
context.report({
83143
node: source,
84144
message:

tests/src/rules/extensions.js

+75
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,37 @@ ruleTester.run('extensions', rule, {
5959
test({ code: 'import thing from "./fake-file.js"', options: [ 'always' ] }),
6060
test({ code: 'import thing from "non-package"', options: [ 'never' ] }),
6161

62+
63+
test({
64+
code: `
65+
import foo from './foo.js'
66+
import bar from './bar.json'
67+
import Component from './Component'
68+
import express from 'express'
69+
`,
70+
options: [ 'ignorePackages' ],
71+
}),
72+
73+
test({
74+
code: `
75+
import foo from './foo.js'
76+
import bar from './bar.json'
77+
import Component from './Component.jsx'
78+
import express from 'express'
79+
`,
80+
options: [ 'always', {ignorePackages: true} ],
81+
}),
82+
83+
test({
84+
code: `
85+
import foo from './foo'
86+
import bar from './bar'
87+
import Component from './Component'
88+
import express from 'express'
89+
`,
90+
options: [ 'never', {ignorePackages: true} ],
91+
}),
92+
6293
],
6394

6495
invalid: [
@@ -201,5 +232,49 @@ ruleTester.run('extensions', rule, {
201232
],
202233
}),
203234

235+
236+
test({
237+
code: `
238+
import foo from './foo.js'
239+
import bar from './bar.json'
240+
import Component from './Component'
241+
import baz from 'foo/baz'
242+
import express from 'express'
243+
`,
244+
options: [ 'always', {ignorePackages: true} ],
245+
errors: [
246+
{
247+
message: 'Missing file extension for "./Component"',
248+
line: 4,
249+
column: 31,
250+
}, {
251+
message: 'Missing file extension for "foo/baz"',
252+
line: 5,
253+
column: 25,
254+
},
255+
],
256+
}),
257+
258+
test({
259+
code: `
260+
import foo from './foo.js'
261+
import bar from './bar.json'
262+
import Component from './Component.jsx'
263+
import express from 'express'
264+
`,
265+
errors: [
266+
{
267+
message: 'Unexpected use of file extension "js" for "./foo.js"',
268+
line: 2,
269+
column: 25,
270+
}, {
271+
message: 'Unexpected use of file extension "jsx" for "./Component.jsx"',
272+
line: 4,
273+
column: 31,
274+
},
275+
],
276+
options: [ 'never', {ignorePackages: true} ],
277+
}),
278+
204279
],
205280
})

0 commit comments

Comments
 (0)