Skip to content

Commit 3fa81d2

Browse files
committed
Implement extensions rule
This fixes #209 and fixes #204.
1 parent 60ceb16 commit 3fa81d2

File tree

9 files changed

+279
-0
lines changed

9 files changed

+279
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
4848
* Ensure all imports appear before other statements ([`imports-first`])
4949
* Report repeated import of the same module in multiple places ([`no-duplicates`])
5050
* Report namespace imports ([`no-namespace`])
51+
* Ensure consistent use of file extension within the import path ([`extensions`])
5152

5253
[`imports-first`]: ./docs/rules/imports-first.md
5354
[`no-duplicates`]: ./docs/rules/no-duplicates.md
5455
[`no-namespace`]: ./docs/rules/no-namespace.md
56+
[`extensions`]: ./docs/rules/extensions.md
5557

5658

5759
## Installation

docs/rules/extensions.md

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# extensions - Ensure consistent use of file extension within the import path
2+
3+
Some file resolve algorithms allow you to omit the file extension within the import source path. For example the `node` resolver can resolve `./foo/bar` to the absolute path `/User/someone/foo/bar.js` because the `.js` extension is resolved automatically by default. Depending on the resolver you can configure more extensions to get resolved automatically.
4+
5+
In order to provide a consistent use of file extensions across your code base, this rule can enforce or disallow the use of certain file extensions.
6+
7+
## Rule Details
8+
9+
This rule has one option which could be either a string or an object. If it is `"never"` (the default value) the rule forbids the use for any extension. If `"always"` then the rule enforces the use of extensions for all import statements.
10+
11+
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.
12+
13+
### Exception
14+
15+
When disallowing the use of certain extensions this rule makes an exception and allows the use of extension when the file would not be resolvable without extension.
16+
17+
For example, given the following folder structure:
18+
19+
```
20+
├── foo
21+
│   ├── bar.js
22+
│   ├── bar.json
23+
```
24+
25+
and this import statement:
26+
27+
```js
28+
import bar from './foo/bar.json';
29+
```
30+
31+
then the extension can’t be omitted because it would then resolve to `./foo/bar.js`.
32+
33+
### Examples
34+
35+
The following patterns are considered problems when configuration set to "never":
36+
37+
```js
38+
import foo from './foo.js';
39+
40+
import bar from './bar.json';
41+
42+
import Component from './Component.jsx'
43+
44+
import express from 'express/index.js';
45+
```
46+
47+
The following patterns are not considered problems when configuration set to "never":
48+
49+
```js
50+
import foo from './foo';
51+
52+
import bar from './bar';
53+
54+
import Component from './Component'
55+
56+
import express from 'express/index';
57+
```
58+
59+
The following patterns are considered problems when configuration set to "always":
60+
61+
```js
62+
import foo from './foo';
63+
64+
import bar from './bar';
65+
66+
import Component from './Component'
67+
68+
import express from 'express';
69+
```
70+
71+
The following patterns are not considered problems when configuration set to "always":
72+
73+
```js
74+
import foo from './foo.js';
75+
76+
import bar from './bar.json';
77+
78+
import Component from './Component.jsx'
79+
80+
import express from 'express/index.js';
81+
```
82+
83+
## When Not To Use It
84+
85+
If you are not concerned about a consistent usage of file extension.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"es6-set": "^0.1.4",
7171
"es6-symbol": "*",
7272
"eslint-import-resolver-node": "^0.2.0",
73+
"lodash.endswith": "^4.0.1",
7374
"object-assign": "^4.0.1"
7475
}
7576
}

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const rules = {
55
'namespace': require('./rules/namespace'),
66
'no-namespace': require('./rules/no-namespace'),
77
'export': require('./rules/export'),
8+
'extensions': require('./rules/extensions'),
89

910
'no-named-as-default': require('./rules/no-named-as-default'),
1011
'no-named-as-default-member': require('./rules/no-named-as-default-member'),

src/rules/extensions.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import path from 'path'
2+
import resolve from '../core/resolve'
3+
import endsWith from 'lodash.endsWith'
4+
5+
module.exports = function (context) {
6+
const configuration = context.options[0] || 'never'
7+
8+
function isUseOfExtensionEnforced(extension) {
9+
if (typeof configuration === 'object') {
10+
return configuration[extension] === 'always'
11+
}
12+
13+
return configuration === 'always'
14+
}
15+
16+
function isResolvableWithoutExtension(file) {
17+
const extension = path.extname(file)
18+
const fileWithoutExtension = file.slice(0, -extension.length)
19+
const resolvedFileWithoutExtension = resolve(fileWithoutExtension, context)
20+
21+
return resolvedFileWithoutExtension === resolve(file, context)
22+
}
23+
24+
function checkFileExtension(node) {
25+
const { source } = node
26+
const importPath = source.value
27+
const resolvedPath = resolve(importPath, context)
28+
const extension = path.extname(resolvedPath).substring(1)
29+
30+
if (!endsWith(importPath, extension)) {
31+
if (isUseOfExtensionEnforced(extension)) {
32+
context.report({
33+
node: source,
34+
message: `Missing file extension "${extension}" for "${importPath}"`,
35+
})
36+
}
37+
} else {
38+
if (!isUseOfExtensionEnforced(extension) && isResolvableWithoutExtension(importPath)) {
39+
context.report({
40+
node: source,
41+
message: `Unexpected use of file extension "${extension}" for "${importPath}"`,
42+
})
43+
}
44+
}
45+
}
46+
47+
return {
48+
ImportDeclaration: checkFileExtension,
49+
}
50+
}
51+
52+
module.exports.schema = [
53+
{
54+
oneOf: [
55+
{
56+
enum: [ 'always', 'never' ],
57+
},
58+
{
59+
type: 'object',
60+
patternProperties: {
61+
'.*': { enum: [ 'always', 'never' ] },
62+
},
63+
},
64+
],
65+
},
66+
]

tests/files/bar.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

tests/files/bar.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default null

tests/files/file.with.dot.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default null

tests/src/rules/extensions.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { RuleTester } from 'eslint'
2+
import rule from 'rules/extensions';
3+
import { test } from '../utils';
4+
5+
const ruleTester = new RuleTester()
6+
7+
ruleTester.run('extensions', rule, {
8+
valid: [
9+
test({ code: 'import a from "a"' }),
10+
test({ code: 'import dot from "./file.with.dot"' }),
11+
test({
12+
code: 'import a from "a/index.js"',
13+
options: [ 'always' ]
14+
}),
15+
test({
16+
code: 'import dot from "./file.with.dot.js"',
17+
options: [ 'always' ]
18+
}),
19+
test({
20+
code: [
21+
'import a from "a"',
22+
'import packageConfig from "./package.json"',
23+
].join('\n'),
24+
options: [ { json: 'always', js: 'never' } ]
25+
}),
26+
test({
27+
code: [
28+
'import lib from "./bar"',
29+
'import component from "./bar.jsx"',
30+
'import data from "./bar.json"'
31+
].join('\n'),
32+
options: [ 'never' ],
33+
settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }
34+
})
35+
],
36+
37+
invalid: [
38+
test({
39+
code: 'import a from "a/index.js"',
40+
errors: [ {
41+
message: 'Unexpected use of file extension "js" for "a/index.js"',
42+
line: 1,
43+
column: 15
44+
} ]
45+
}),
46+
test({
47+
code: 'import a from "a"',
48+
options: [ 'always' ],
49+
errors: [ {
50+
message: 'Missing file extension "js" for "a"',
51+
line: 1,
52+
column: 15
53+
} ]
54+
}),
55+
test({
56+
code: 'import dot from "./file.with.dot"',
57+
options: [ "always" ],
58+
errors: [
59+
{
60+
message: 'Missing file extension "js" for "./file.with.dot"',
61+
line: 1,
62+
column: 17
63+
}
64+
]
65+
}),
66+
test({
67+
code: [
68+
'import a from "a/index.js"',
69+
'import packageConfig from "./package"',
70+
].join('\n'),
71+
options: [ { json: 'always', js: 'never' } ],
72+
settings: { 'import/resolve': { 'extensions': [ '.js', '.json' ] } },
73+
errors: [
74+
{
75+
message: 'Unexpected use of file extension "js" for "a/index.js"',
76+
line: 1,
77+
column: 15
78+
},
79+
{
80+
message: 'Missing file extension "json" for "./package"',
81+
line: 2,
82+
column: 27
83+
}
84+
]
85+
}),
86+
test({
87+
code: [
88+
'import lib from "./bar.js"',
89+
'import component from "./bar.jsx"',
90+
'import data from "./bar.json"'
91+
].join('\n'),
92+
options: [ 'never' ],
93+
settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } },
94+
errors: [
95+
{
96+
message: 'Unexpected use of file extension "js" for "./bar.js"',
97+
line: 1,
98+
column: 17
99+
}
100+
]
101+
}),
102+
test({
103+
code: [
104+
'import lib from "./bar.js"',
105+
'import component from "./bar.jsx"',
106+
'import data from "./bar.json"'
107+
].join('\n'),
108+
options: [ { json: 'always', js: 'never', jsx: 'never' } ],
109+
settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } },
110+
errors: [
111+
{
112+
message: 'Unexpected use of file extension "js" for "./bar.js"',
113+
line: 1,
114+
column: 17
115+
}
116+
]
117+
})
118+
119+
]
120+
})
121+

0 commit comments

Comments
 (0)