Skip to content

Commit 22e103c

Browse files
Completely rework group-exports implementation
1 parent 05aad15 commit 22e103c

File tree

2 files changed

+106
-54
lines changed

2 files changed

+106
-54
lines changed

src/rules/group-exports.js

+65-54
Original file line numberDiff line numberDiff line change
@@ -2,96 +2,107 @@ const meta = {}
22
/* eslint-disable max-len */
33
const errors = {
44
ExportNamedDeclaration: 'Multiple named export declarations; consolidate all named exports into a single export declaration',
5-
MemberExpression: 'Multiple CommonJS exports; consolidate all exports into a single assignment to `module.exports`',
5+
AssignmentExpression: 'Multiple CommonJS exports; consolidate all exports into a single assignment to `module.exports`',
66
}
77
/* eslint-enable max-len */
8-
const parents = [
9-
'AssignmentExpression',
10-
'MemberExpression',
11-
]
128

139
/**
14-
* Determine how many property accesses precede this node
10+
* Returns an array with names of the properties in the accessor chain for MemberExpression nodes
1511
*
16-
* For example, `module.exports` = 1, `module.exports.something` = 2 and so on.
12+
* Example:
1713
*
18-
* @param {Object} node The node being visited
19-
* @return {Number}
14+
* `module.exports = {}` => ['module', 'exports']
15+
* `module.exports.property = true` => ['module', 'exports', 'property']
16+
*
17+
* @param {Node} node AST Node (MemberExpression)
18+
* @return {Array} Array with the property names in the chain
2019
* @private
2120
*/
22-
function accessorDepth(node) {
23-
let depth = 0
21+
function accessorChain(node) {
22+
const chain = []
2423

25-
while (node.type === 'MemberExpression') {
26-
depth++
27-
node = node.parent
28-
}
24+
do {
25+
chain.unshift(node.property.name)
26+
27+
if (node.object.type === 'Identifier') {
28+
chain.unshift(node.object.name)
29+
break
30+
}
2931

30-
return depth
32+
node = node.object
33+
} while (node.type === 'MemberExpression')
34+
35+
return chain
3136
}
3237

3338
function create(context) {
34-
const named = new Set()
35-
const commonjs = {
36-
defaultExportIsObject: false,
39+
const nodes = {
40+
modules: new Set(),
41+
commonjs: {
42+
named: new Set(),
43+
default: null,
44+
},
3745
}
3846

3947
return {
4048
ExportNamedDeclaration(node) {
41-
named.add(node)
49+
nodes.modules.add(node)
4250
},
4351

44-
MemberExpression(node) {
45-
const parent = node.parent
46-
47-
// These are not the parents you are looking for
48-
if (parents.indexOf(parent.type) === -1) {
52+
AssignmentExpression(node) {
53+
if (node.left.type !== 'MemberExpression') {
4954
return
5055
}
5156

52-
if (parent.type === 'AssignmentExpression') {
53-
// Member expressions on the right side of the assignment do not interest us
54-
if (parent.left !== node) {
57+
const chain = accessorChain(node.left)
58+
59+
// Assignments to module.exports
60+
if (chain[0] === 'module' && chain[1] === 'exports') {
61+
// Adding properties to module.exports (module.exports.* = *)
62+
if (chain.length === 3) {
63+
nodes.commonjs.named.add(node)
5564
return
5665
}
5766

58-
// Special case: when assigning an object literal to `module.exports`, treat it as a named
59-
// export declaration
60-
if (parent.right.type === 'ObjectExpression') {
61-
commonjs.defaultExportIsObject = true
62-
named.add(node)
67+
// Direct assignments to module.exports (module.exports = *)
68+
if (chain.length === 2) {
69+
// Assigning an object literal is treated as a named export declaration
70+
if (node.right.type === 'ObjectExpression') {
71+
nodes.commonjs.named.add(node)
72+
return
73+
}
74+
75+
// Assigning anything else (string, function, bool) is treated as a default export
76+
// declaration
77+
nodes.commonjs.default = node
6378
return
6479
}
65-
}
6680

67-
const depth = accessorDepth(node)
68-
69-
// Assignments to module.exports
70-
// Only treat assignments to `module.exports` object as named exports, ie.
71-
// module.exports.named = true
72-
// And ignore direct assignments and assignments more deep, ie.
73-
// module.exports = {}
74-
// module.exports.named.property = true
75-
if (node.object.name === 'module' && node.property.name === 'exports' && depth === 2) {
76-
named.add(node)
77-
return
81+
// Deeper assignments are ignored since they just modify what's already being exported
7882
}
7983

80-
// Assignments to exports
81-
if (node.object.name === 'exports' && depth === 1) {
82-
named.add(node)
84+
// Assignments to exports (exports.* = *)
85+
if (chain[0] === 'exports' && chain.length === 2) {
86+
nodes.commonjs.named.add(node)
8387
return
8488
}
8589
},
8690

8791
'Program:exit': function onExit() {
88-
if (named.size > 1) {
89-
for (const node of named) {
90-
// If the "default" CommonJS export was not an object literal, do not report anything
91-
if (node.type !== 'ExportNamedDeclaration' && !commonjs.defaultExportIsObject) {
92-
continue
93-
}
92+
// Report multiple `export` declarations (ES2015 modules)
93+
if (nodes.modules.size > 1) {
94+
for (const node of nodes.modules) {
95+
context.report({
96+
node,
97+
message: errors[node.type],
98+
})
99+
}
100+
}
94101

102+
// Report multiple `module.exports` assignments (CommonJS) unless the module contains a
103+
// "default" export (ie. it exports something else than object literal - a string, fn, etc.)
104+
if (!nodes.commonjs.default && nodes.commonjs.named.size > 1) {
105+
for (const node of nodes.commonjs.named) {
95106
context.report({
96107
node,
97108
message: errors[node.type],

tests/src/rules/group-exports.js

+41
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ ruleTester.run('group-exports', rule, {
7676
module.exports = () => {}
7777
module.exports.attached = true
7878
` }),
79+
test({ code: `
80+
module.exports = () => {}
81+
exports.test = true
82+
exports.another = true
83+
` }),
7984
test({ code: `
8085
module.exports = "non-object"
8186
module.exports.attached = true
@@ -96,6 +101,22 @@ ruleTester.run('group-exports', rule, {
96101
const another = true
97102
export default {}
98103
` }),
104+
test({ code: `
105+
module.something.else = true
106+
module.something.different = true
107+
` }),
108+
test({ code: `
109+
module.exports.test = true
110+
module.something.different = true
111+
` }),
112+
test({ code: `
113+
exports.test = true
114+
module.something.different = true
115+
` }),
116+
test({ code: `
117+
unrelated = 'assignment'
118+
module.exports.test = true
119+
` }),
99120
],
100121
invalid: [
101122
test({
@@ -140,5 +161,25 @@ ruleTester.run('group-exports', rule, {
140161
errors.commonjs,
141162
],
142163
}),
164+
test({
165+
code: `
166+
module.exports.test = true
167+
module.exports.another = true
168+
`,
169+
errors: [
170+
errors.commonjs,
171+
errors.commonjs,
172+
],
173+
}),
174+
test({
175+
code: `
176+
exports.test = true
177+
module.exports.another = true
178+
`,
179+
errors: [
180+
errors.commonjs,
181+
errors.commonjs,
182+
],
183+
}),
143184
],
144185
})

0 commit comments

Comments
 (0)