Skip to content

Commit d4bed97

Browse files
New: group-exports rule
1 parent c975742 commit d4bed97

File tree

6 files changed

+284
-0
lines changed

6 files changed

+284
-0
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
55

66
## [Unreleased]
7+
### Added
8+
- Add [`group-exports`] rule: style-guide rule to report use of multiple named exports ([#721], thanks [@Alaneor])
9+
710
### Changed
811
- [`no-extraneous-dependencies`]: use `read-pkg-up` to simplify finding + loading `package.json` ([#680], thanks [@wtgtybhertgeghgtwtg])
912

@@ -376,7 +379,9 @@ for info on changes for earlier releases.
376379
[`no-webpack-loader-syntax`]: ./docs/rules/no-webpack-loader-syntax.md
377380
[`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md
378381
[`unambiguous`]: ./docs/rules/unambiguous.md
382+
[`group-exports`]: ./docs/rules/group-exports.md
379383

384+
[#721]: https://github.com/benmosher/eslint-plugin-import/pull/721
380385
[#680]: https://github.com/benmosher/eslint-plugin-import/pull/680
381386
[#654]: https://github.com/benmosher/eslint-plugin-import/pull/654
382387
[#639]: https://github.com/benmosher/eslint-plugin-import/pull/639
@@ -561,3 +566,4 @@ for info on changes for earlier releases.
561566
[@ntdb]: https://github.com/ntdb
562567
[@jakubsta]: https://github.com/jakubsta
563568
[@wtgtybhertgeghgtwtg]: https://github.com/wtgtybhertgeghgtwtg
569+
[@Alaneor]: https://github.com/Alaneor

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
7676
* Limit the maximum number of dependencies a module can have ([`max-dependencies`])
7777
* Forbid unassigned imports ([`no-unassigned-import`])
7878
* Forbid named default exports ([`no-named-default`])
79+
* Prefer single named export declaration ([`group-exports`])
7980

8081
[`first`]: ./docs/rules/first.md
8182
[`no-duplicates`]: ./docs/rules/no-duplicates.md
@@ -87,6 +88,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
8788
[`max-dependencies`]: ./docs/rules/max-dependencies.md
8889
[`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md
8990
[`no-named-default`]: ./docs/rules/no-named-default.md
91+
[`group-exports`]: ./docs/rules/group-exports.md
9092

9193
## Installation
9294

docs/rules/group-exports.md

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# group-exports
2+
3+
Reports when multiple named exports or CommonJS assignments are present in a single file and when the default export is not adjacent to the named export.
4+
5+
**Rationale:** An `export` declaration or `module.exports` assignment can appear anywhere in the code. By requiring a single export declaration along with the default export being placed next to the named export, your exports will remain at one place, making it easier to see what exports a module provides.
6+
7+
## Rule Details
8+
9+
This rule warns whenever a single file contains multiple named exports or assignments to `module.exports` (or `exports`) and when the default export is not adjacent to the named export.
10+
11+
### Valid
12+
13+
```js
14+
// Default export is adjacent to named export -> ok
15+
export default function test() {}
16+
// A single named export -> ok
17+
export const valid = true
18+
```
19+
20+
```js
21+
const first = true
22+
const second = true
23+
24+
// A single named export -> ok
25+
export {
26+
first,
27+
second,
28+
}
29+
```
30+
31+
```js
32+
// A single exports assignment -> ok
33+
module.exports = {
34+
first: true,
35+
second: true
36+
}
37+
```
38+
39+
### Invalid
40+
41+
```js
42+
// Multiple named exports -> not ok!
43+
export const first = true
44+
export const second = true
45+
```
46+
47+
```js
48+
// Default export is not adjacent to the named export -> not ok!
49+
export default {}
50+
const first = true
51+
export { first }
52+
```
53+
54+
```js
55+
// Multiple exports assignments -> not ok!
56+
exports.first = true
57+
exports.second = true
58+
```
59+
60+
## When Not To Use It
61+
62+
If you do not mind having your exports spread across the file, you can safely turn this rule off.

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const rules = {
99
'extensions': require('./rules/extensions'),
1010
'no-restricted-paths': require('./rules/no-restricted-paths'),
1111
'no-internal-modules': require('./rules/no-internal-modules'),
12+
'group-exports': require('./rules/group-exports'),
1213

1314
'no-named-default': require('./rules/no-named-default'),
1415
'no-named-as-default': require('./rules/no-named-as-default'),

src/rules/group-exports.js

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
const meta = {}
2+
const errors = {
3+
ExportNamedDeclaration:
4+
'Multiple named export declarations; consolidate all named exports into a single statement',
5+
ExportDefaultDeclaration:
6+
'Default export declaration should be adjacent to named export',
7+
MemberExpression:
8+
'Multiple CommonJS exports; consolidate all exports into a single assignment to ' +
9+
'`module.exports`',
10+
}
11+
12+
/**
13+
* Check if two nodes are adjacent (only whitespace between them)
14+
*
15+
* The two nodes do not have to be sorted in the order they appear in the code.
16+
*
17+
* @param {Object} opts Options for the check
18+
* @param {Object} opts.context The context of the nodes
19+
* @param {Object} opts.first The first node
20+
* @param {Object} opts.second The second node
21+
* @return {Boolean}
22+
* @private
23+
*/
24+
function isAdjacent(opts = {}) {
25+
const sourceCode = opts.context.getSourceCode()
26+
27+
if (sourceCode.getTokensBetween(opts.first, opts.second).length ||
28+
sourceCode.getTokensBetween(opts.second, opts.first).length) {
29+
return false
30+
}
31+
32+
return true
33+
}
34+
35+
/**
36+
* Determine how many property accesses precede this node
37+
*
38+
* For example, `module.exports` = 1, `module.exports.something` = 2 and so on.
39+
*
40+
* @param {Object} node The node being visited
41+
* @return {Number}
42+
* @private
43+
*/
44+
function accessorDepth(node) {
45+
let depth = 0
46+
47+
while (node.type === 'MemberExpression') {
48+
depth++
49+
node = node.parent
50+
}
51+
52+
return depth
53+
}
54+
55+
function create(context) {
56+
const exports = {
57+
named: new Set(),
58+
default: null,
59+
last: null,
60+
}
61+
62+
return {
63+
ExportDefaultDeclaration(node) {
64+
exports.default = node
65+
},
66+
67+
ExportNamedDeclaration(node) {
68+
exports.named.add(node)
69+
exports.last = node
70+
},
71+
72+
MemberExpression(node) {
73+
if (['MemberExpression', 'AssignmentExpression'].indexOf(node.parent.type) === -1) {
74+
return
75+
}
76+
77+
// Member expressions on the right side of the assignment do not interest us
78+
if (node.parent.type === 'AssignmentExpression' && node.parent.left !== node) {
79+
return
80+
}
81+
82+
if (node.object.name === 'module' && node.property.name === 'exports') {
83+
// module.exports.exported.*: assignments this deep should not be considered as exports
84+
if (accessorDepth(node) > 2) {
85+
return
86+
}
87+
88+
return void exports.named.add(node)
89+
}
90+
91+
if (node.object.name === 'exports') {
92+
// exports.exported.*: assignments this deep should not be considered as exports
93+
if (accessorDepth(node) > 1) {
94+
return
95+
}
96+
97+
return void exports.named.add(node)
98+
}
99+
},
100+
101+
'Program:exit': function onExit() {
102+
if (exports.named.size > 1) {
103+
for (const node of exports.named) {
104+
context.report({
105+
node,
106+
message: errors[node.type],
107+
})
108+
}
109+
}
110+
111+
// There is exactly one named export and a default export -> check if they are adjacent
112+
if (exports.default && exports.last && exports.named.size === 1) {
113+
const adjacent = isAdjacent({
114+
context,
115+
first: exports.default,
116+
second: exports.last,
117+
})
118+
119+
if (!adjacent) {
120+
context.report({
121+
node: exports.default,
122+
message: errors[exports.default.type],
123+
})
124+
}
125+
}
126+
},
127+
}
128+
}
129+
130+
export default {
131+
meta,
132+
create,
133+
}

tests/src/rules/group-exports.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { test } from '../utils'
2+
import { RuleTester } from 'eslint'
3+
import rule from 'rules/group-exports'
4+
5+
const errors = {
6+
named:
7+
'Multiple named export declarations; consolidate all named exports into a single statement',
8+
default:
9+
'Default export declaration should be adjacent to named export',
10+
commonjs:
11+
'Multiple CommonJS exports; consolidate all exports into a single assignment to ' +
12+
'`module.exports`',
13+
}
14+
const ruleTester = new RuleTester()
15+
16+
ruleTester.run('group-exports', rule, {
17+
valid: [
18+
test({ code: 'export const test = true' }),
19+
test({ code: 'export default {}\nexport const test = true' }),
20+
test({ code: [
21+
'const first = true',
22+
'const second = true',
23+
'export { first,\nsecond }',
24+
].join('\n') }),
25+
test({ code: 'export default {}\n/* test */\nexport const test = true'}),
26+
test({ code: 'export default {}\n// test\nexport const test = true'}),
27+
test({ code: 'export const test = true\n/* test */\nexport default {}'}),
28+
test({ code: 'export const test = true\n// test\nexport default {}'}),
29+
test({ code: 'module.exports = {} '}),
30+
test({ code: 'module.exports = { test: true,\nanother: false }' }),
31+
test({ code: 'exports.test = true' }),
32+
33+
test({ code: 'module.exports = {}\nconst test = module.exports' }),
34+
test({ code: 'exports.test = true\nconst test = exports.test' }),
35+
test({ code: 'module.exports = {}\nmodule.exports.too.deep = true' }),
36+
test({ code: 'module.exports = {}\nexports.too.deep = true' }),
37+
],
38+
invalid: [
39+
test({
40+
code: [
41+
'export const test = true',
42+
'export const another = true',
43+
].join('\n'),
44+
errors: [
45+
errors.named,
46+
errors.named,
47+
],
48+
}),
49+
test({
50+
code: [
51+
'module.exports = {}',
52+
'module.exports.test = true',
53+
'module.exports.another = true',
54+
].join('\n'),
55+
errors: [
56+
errors.commonjs,
57+
errors.commonjs,
58+
errors.commonjs,
59+
],
60+
}),
61+
test({
62+
code: [
63+
'module.exports = {}',
64+
'module.exports = {}',
65+
].join('\n'),
66+
errors: [
67+
errors.commonjs,
68+
errors.commonjs,
69+
],
70+
}),
71+
test({
72+
code: 'export default {}\nconst test = true\nexport { test }',
73+
errors: [errors.default],
74+
}),
75+
test({
76+
code: 'const test = true\nexport { test }\nconst another = true\nexport default {}',
77+
errors: [errors.default],
78+
}),
79+
],
80+
})

0 commit comments

Comments
 (0)