/
group-exports.js
153 lines (135 loc) · 4.01 KB
/
group-exports.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import docsUrl from '../docsUrl'
import values from 'object.values'
import flat from 'array.prototype.flat'
const meta = {
type: 'suggestion',
docs: {
url: docsUrl('group-exports'),
},
}
/* eslint-disable max-len */
const errors = {
ExportNamedDeclaration: 'Multiple named export declarations; consolidate all named exports into a single export declaration',
AssignmentExpression: 'Multiple CommonJS exports; consolidate all exports into a single assignment to `module.exports`',
}
/* eslint-enable max-len */
/**
* Returns an array with names of the properties in the accessor chain for MemberExpression nodes
*
* Example:
*
* `module.exports = {}` => ['module', 'exports']
* `module.exports.property = true` => ['module', 'exports', 'property']
*
* @param {Node} node AST Node (MemberExpression)
* @return {Array} Array with the property names in the chain
* @private
*/
function accessorChain(node) {
const chain = []
do {
chain.unshift(node.property.name)
if (node.object.type === 'Identifier') {
chain.unshift(node.object.name)
break
}
node = node.object
} while (node.type === 'MemberExpression')
return chain
}
function create(context) {
const nodes = {
modules: {
set: new Set(),
sources: {},
},
types: {
set: new Set(),
sources: {},
},
commonjs: {
set: new Set(),
},
}
return {
ExportNamedDeclaration(node) {
let target = node.exportKind === 'type' ? nodes.types : nodes.modules
if (!node.source) {
target.set.add(node)
} else if (Array.isArray(target.sources[node.source.value])) {
target.sources[node.source.value].push(node)
} else {
target.sources[node.source.value] = [node]
}
},
AssignmentExpression(node) {
if (node.left.type !== 'MemberExpression') {
return
}
const chain = accessorChain(node.left)
// Assignments to module.exports
// Deeper assignments are ignored since they just modify what's already being exported
// (ie. module.exports.exported.prop = true is ignored)
if (chain[0] === 'module' && chain[1] === 'exports' && chain.length <= 3) {
nodes.commonjs.set.add(node)
return
}
// Assignments to exports (exports.* = *)
if (chain[0] === 'exports' && chain.length === 2) {
nodes.commonjs.set.add(node)
return
}
},
'Program:exit': function onExit() {
// Report multiple `export` declarations (ES2015 modules)
if (nodes.modules.set.size > 1) {
nodes.modules.set.forEach(node => {
context.report({
node,
message: errors[node.type],
})
})
}
// Report multiple `aggregated exports` from the same module (ES2015 modules)
flat(values(nodes.modules.sources)
.filter(nodesWithSource => Array.isArray(nodesWithSource) && nodesWithSource.length > 1))
.forEach((node) => {
context.report({
node,
message: errors[node.type],
})
})
// Report multiple `export type` declarations (FLOW ES2015 modules)
if (nodes.types.set.size > 1) {
nodes.types.set.forEach(node => {
context.report({
node,
message: errors[node.type],
})
})
}
// Report multiple `aggregated type exports` from the same module (FLOW ES2015 modules)
flat(values(nodes.types.sources)
.filter(nodesWithSource => Array.isArray(nodesWithSource) && nodesWithSource.length > 1))
.forEach((node) => {
context.report({
node,
message: errors[node.type],
})
})
// Report multiple `module.exports` assignments (CommonJS)
if (nodes.commonjs.set.size > 1) {
nodes.commonjs.set.forEach(node => {
context.report({
node,
message: errors[node.type],
})
})
}
},
}
}
module.exports = {
meta,
create,
}