Skip to content

Commit 4875999

Browse files
committed
Add the foundation for the group-exports rule
1 parent 919e407 commit 4875999

File tree

3 files changed

+481
-0
lines changed

3 files changed

+481
-0
lines changed

docs/rules/group-exports.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# @croutonn/group-exports
2+
> (TODO: summary)
3+
4+
(TODO: why is this rule useful?)
5+
6+
## Rule Details
7+
8+
(TODO: how does this rule check code?)
9+
10+
## Options
11+
12+
(TODO: what do options exist?)

lib/rules/group-exports.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"use strict";
2+
3+
/**
4+
* @typedef {import("eslint").Rule.RuleModule} RuleModule
5+
* @typedef {import("eslint").Rule.RuleContext} RuleContext
6+
* @typedef {import("eslint").Rule.RuleListener} RuleListener
7+
* @typedef {import("estree").Node} ASTNode
8+
*/
9+
10+
const errors = {
11+
ExportNamedDeclaration: "Multiple named export declarations; consolidate all named exports into a single export declaration",
12+
AssignmentExpression: "Multiple CommonJS exports; consolidate all exports into a single assignment to `module.exports`"
13+
};
14+
15+
/**
16+
* Returns an array with names of the properties in the accessor chain for MemberExpression nodes
17+
*
18+
* Example:
19+
*
20+
* `module.exports = {}` => ['module', 'exports']
21+
* `module.exports.property = true` => ['module', 'exports', 'property']
22+
* @param {Node} node AST Node (MemberExpression)
23+
* @returns {Array} Array with the property names in the chain
24+
* @private
25+
*/
26+
function accessorChain(node) {
27+
const chain = [];
28+
let target = node;
29+
30+
do {
31+
chain.unshift(target.property.name);
32+
33+
if (target.object.type === "Identifier") {
34+
chain.unshift(target.object.name);
35+
break;
36+
}
37+
38+
target = target.object;
39+
} while (target.type === "MemberExpression");
40+
41+
return chain;
42+
}
43+
44+
45+
/**
46+
* Create Rule
47+
* @param {RuleContext} context rule context
48+
* @returns {RuleListener} rule listener
49+
*/
50+
function create(context) {
51+
const nodes = {
52+
modules: {
53+
set: new Set(),
54+
sources: {}
55+
},
56+
types: {
57+
set: new Set(),
58+
sources: {}
59+
},
60+
commonjs: {
61+
set: new Set()
62+
}
63+
};
64+
65+
return {
66+
ExportNamedDeclaration(node) {
67+
const target = node.exportKind === "type" ? nodes.types : nodes.modules;
68+
69+
if (!node.source) {
70+
target.set.add(node);
71+
} else if (Array.isArray(target.sources[node.source.value])) {
72+
target.sources[node.source.value].push(node);
73+
} else {
74+
target.sources[node.source.value] = [node];
75+
}
76+
},
77+
78+
AssignmentExpression(node) {
79+
if (node.left.type !== "MemberExpression") {
80+
return;
81+
}
82+
83+
const chain = accessorChain(node.left);
84+
85+
// Assignments to module.exports
86+
// Deeper assignments are ignored since they just modify what's already being exported
87+
// (ie. module.exports.exported.prop = true is ignored)
88+
if (chain[0] === "module" && chain[1] === "exports" && chain.length <= 3) {
89+
nodes.commonjs.set.add(node);
90+
return;
91+
}
92+
93+
// Assignments to exports (exports.* = *)
94+
if (chain[0] === "exports" && chain.length === 2) {
95+
nodes.commonjs.set.add(node);
96+
97+
}
98+
},
99+
100+
"Program:exit": function onExit() {
101+
102+
// Report multiple `export` declarations (ES2015 modules)
103+
if (nodes.modules.set.size > 1) {
104+
nodes.modules.set.forEach(node => {
105+
context.report({
106+
node,
107+
messageId: node.type
108+
});
109+
});
110+
}
111+
112+
// Report multiple `aggregated exports` from the same module (ES2015 modules)
113+
Object.values(nodes.modules.sources)
114+
.filter(nodesWithSource => Array.isArray(nodesWithSource) && nodesWithSource.length > 1).flat()
115+
.forEach(node => {
116+
context.report({
117+
node,
118+
messageId: node.type
119+
});
120+
});
121+
122+
// Report multiple `export type` declarations (FLOW ES2015 modules)
123+
if (nodes.types.set.size > 1) {
124+
nodes.types.set.forEach(node => {
125+
context.report({
126+
node,
127+
messageId: node.type
128+
});
129+
});
130+
}
131+
132+
// Report multiple `aggregated type exports` from the same module (FLOW ES2015 modules)
133+
Object.values(nodes.types.sources)
134+
.filter(nodesWithSource => Array.isArray(nodesWithSource) && nodesWithSource.length > 1).flat()
135+
.forEach(node => {
136+
context.report({
137+
node,
138+
messageId: node.type
139+
});
140+
});
141+
142+
// Report multiple `module.exports` assignments (CommonJS)
143+
if (nodes.commonjs.set.size > 1) {
144+
nodes.commonjs.set.forEach(node => {
145+
context.report({
146+
node,
147+
messageId: node.type
148+
});
149+
});
150+
}
151+
}
152+
};
153+
}
154+
155+
/**
156+
* @type RuleModule
157+
*/
158+
module.exports = {
159+
meta: {
160+
docs: {
161+
description: "Reports when named exports are not grouped together in a single export declaration or when multiple assignments to CommonJS module.exports or exports object are present in a single file",
162+
163+
category: "Best Practices",
164+
165+
recommended: true,
166+
url: "https://github.com/croutonn/eslint-plugin/blob/main/docs/rules/group-exports.md"
167+
},
168+
169+
fixable: "code",
170+
messages: errors,
171+
schema: [],
172+
173+
type: "suggestion"
174+
},
175+
176+
create
177+
};

0 commit comments

Comments
 (0)