Skip to content

Commit 645a55d

Browse files
committed
Add exports-last rule
1 parent df9bb15 commit 645a55d

File tree

6 files changed

+304
-0
lines changed

6 files changed

+304
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ See also [Configuring ESLint](https://eslint.org/docs/user-guide/configuring).
4848

4949
| Rule ID | Description | |
5050
|:--------|:------------|:--:|
51+
| [@croutonn/exports-last](./docs/rules/exports-last.md) | This rule enforces that all exports are declared at the bottom of the file. This rule will report any export declarations that comes before any non-export statements. | ⭐️✒️ |
5152
| [@croutonn/group-exports](./docs/rules/group-exports.md) | 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 | ⭐️✒️ |
5253
| [@croutonn/jsx-a11y-anchor-has-content](./docs/rules/jsx-a11y-anchor-has-content.md) | Enforce that anchors have content and that the content is accessible to screen readers | |
5354
| [@croutonn/jsx-a11y-control-has-associated-label](./docs/rules/jsx-a11y-control-has-associated-label.md) | Enforce that a control (an interactive element) has a text label | |

docs/rules/exports-last.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# @croutonn/exports-last
2+
> This rule enforces that all exports are declared at the bottom of the file. This rule will report any export declarations that comes before any non-export statements.
3+
> - ⭐️ This rule is included in `plugin:@croutonn/recommended` preset.
4+
> - ✒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
5+
6+
## Rule Details
7+
8+
## This will be reported
9+
10+
```JS
11+
12+
const bool = true
13+
14+
export default bool
15+
16+
const str = 'foo'
17+
18+
```
19+
20+
```JS
21+
22+
export const bool = true
23+
24+
const str = 'foo'
25+
26+
```
27+
28+
## This will not be reported
29+
30+
```JS
31+
const arr = ['bar']
32+
33+
export const bool = true
34+
35+
export default bool
36+
37+
export function func() {
38+
console.log('Hello World 🌍')
39+
}
40+
41+
export const str = 'foo'
42+
```
43+
44+
## Options
45+
46+
No options
47+
48+
## When Not To Use It
49+
50+
If you don't mind exports being sprinkled throughout a file, you may not want to enable this rule.
51+
52+
#### ES6 exports only
53+
54+
The exports-last rule is currently only working on ES6 exports. You may not want to enable this rule if you're using CommonJS exports.
55+
56+
If you need CommonJS support feel free to open an issue or create a PR.
57+
58+
## Implementation
59+
60+
- [Rule source](../../lib/rules/exports-last.js)
61+
- [Test source](../../tests/lib/rules/exports-last.js)

lib/configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
"react/prop-types": "off",
88
"@croutonn/func-style": ["error", "arrow"],
99
"@croutonn/group-exports": ["error"],
10+
"@croutonn/exports-last": ["error"],
1011
"@croutonn/typescript-react-component-type": ["error", "raw"],
1112
"@croutonn/typescript-react-require-props-suffix": ["error"],
1213
"@croutonn/typescript-react-require-props-type": ["error"]

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
recommended: require("./configs/recommended")
77
},
88
rules: {
9+
"exports-last": require("./rules/exports-last"),
910
"func-style": require("./rules/func-style"),
1011
"group-exports": require("./rules/group-exports"),
1112
"jsx-a11y-anchor-has-content": require("./rules/jsx-a11y-anchor-has-content"),

lib/rules/exports-last.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"use strict";
2+
3+
/**
4+
* @typedef {import("eslint").Rule.RuleModule} RuleModule
5+
* @typedef {import("estree").Node} ASTNode
6+
* @typedef {import("eslint").Rule.RuleFixer} RuleFixer
7+
* @typedef {import("eslint").Rule.RuleFix} RuleFix
8+
*/
9+
10+
const errors = {
11+
error: "Export statements should appear at the end of the file"
12+
};
13+
14+
/**
15+
* @param {{type: string}} options
16+
* @returns {boolean}
17+
*/
18+
function isNonExportStatement({ type }) {
19+
return type !== "ExportDefaultDeclaration" &&
20+
type !== "ExportNamedDeclaration" &&
21+
type !== "ExportAllDeclaration";
22+
}
23+
24+
/**
25+
* @type RuleModule
26+
*/
27+
module.exports = {
28+
meta: {
29+
docs: {
30+
description: "This rule enforces that all exports are declared at the bottom of the file. This rule will report any export declarations that comes before any non-export statements.",
31+
32+
category: "Best Practices",
33+
34+
recommended: true,
35+
url: "https://github.com/croutonn/eslint-plugin/blob/main/docs/rules/exports-last.md"
36+
},
37+
38+
fixable: "code",
39+
messages: errors,
40+
schema: [],
41+
42+
type: "suggestion"
43+
},
44+
45+
create(context) {
46+
const sourceCode = context.getSourceCode();
47+
48+
return {
49+
Program({ body }) {
50+
const lastNonExportStatementIndex = body.reduce((acc, item, index) => {
51+
if (isNonExportStatement(item)) {
52+
return index;
53+
}
54+
return acc;
55+
}, -1);
56+
57+
if (lastNonExportStatementIndex === -1) {
58+
return;
59+
}
60+
61+
const newCode = body.slice(0, lastNonExportStatementIndex + 1).map(node => ({
62+
node,
63+
code: sourceCode.getText(node),
64+
isNonExportStatement: isNonExportStatement(node)
65+
})).sort((a, b) => {
66+
if (!a.isNonExportStatement && b.isNonExportStatement) {
67+
return 1;
68+
}
69+
if (a.isNonExportStatement === b.isNonExportStatement) {
70+
return 0;
71+
}
72+
return -1;
73+
}).map(node => node.code).join("\n");
74+
75+
const start = body[0].range[0];
76+
const end = body.slice(0, lastNonExportStatementIndex + 1).slice(-1)[0].range[1];
77+
78+
let fixed = false;
79+
80+
/**
81+
* @param {RuleFixer} fixer
82+
* @returns {RuleFix}
83+
*/
84+
function fix(fixer) {
85+
if (fixed) {
86+
return null;
87+
}
88+
fixed = true;
89+
return fixer.replaceTextRange([start, end], newCode);
90+
}
91+
92+
body.slice(0, lastNonExportStatementIndex).forEach(node => {
93+
if (isNonExportStatement(node)) {
94+
return;
95+
}
96+
context.report({
97+
node,
98+
messageId: "error",
99+
fix
100+
});
101+
});
102+
}
103+
};
104+
}
105+
};

tests/lib/rules/exports-last.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"use strict";
2+
3+
const { RuleTester } = require("eslint");
4+
const rule = require("../../../lib/rules/exports-last");
5+
6+
/**
7+
* @param {string} type
8+
* @returns {messageId: string, type: string}
9+
*/
10+
function error(type) {
11+
return {
12+
messageId: "error",
13+
type
14+
};
15+
}
16+
17+
new RuleTester({
18+
parser: require.resolve("@typescript-eslint/parser"),
19+
parserOptions: {
20+
ecmaVersion: 2015,
21+
ecmaFeatures: {
22+
jsx: true
23+
},
24+
lib: ["dom", "dom.iterable", "esnext"],
25+
sourceType: "module"
26+
}
27+
}).run("exports-last", rule, {
28+
valid: [
29+
30+
// // Empty file
31+
// "// comment",
32+
// `
33+
// const foo = 'bar'
34+
// const bar = 'baz'
35+
// `,
36+
// `
37+
// const foo = 'bar'
38+
// export {foo}
39+
// `,
40+
// `
41+
// const foo = 'bar'
42+
// export default foo
43+
// `,
44+
45+
// // Only exports
46+
// `
47+
// export default foo
48+
// export const bar = true
49+
// `,
50+
// `
51+
// const foo = 'bar'
52+
// export default foo
53+
// export const bar = true
54+
// `,
55+
56+
// // Multiline export
57+
// `
58+
// const foo = 'bar'
59+
// export default function bar () {
60+
// const very = 'multiline'
61+
// }
62+
// export const baz = true
63+
// `,
64+
65+
// // Many exports
66+
// `
67+
// const foo = 'bar'
68+
// export default foo
69+
// export const so = 'many'
70+
// export const exports = ':)'
71+
// export const i = 'cant'
72+
// export const even = 'count'
73+
// export const how = 'many'
74+
// `,
75+
76+
// // Export all
77+
// `
78+
// export * from './foo'
79+
// `
80+
],
81+
invalid: [
82+
83+
// Default export before variable declaration
84+
{
85+
code: `export default 'bar'
86+
const bar = true`,
87+
output: `const bar = true
88+
export default 'bar'`,
89+
errors: [error("ExportDefaultDeclaration")]
90+
},
91+
92+
// Named export before variable declaration
93+
{
94+
code: `export const foo = 'bar'
95+
const bar = true`,
96+
output: `const bar = true
97+
export const foo = 'bar'`,
98+
errors: [error("ExportNamedDeclaration")]
99+
},
100+
101+
// Export all before variable declaration
102+
{
103+
code: `export * from './foo'
104+
const bar = true`,
105+
output: `const bar = true
106+
export * from './foo'`,
107+
errors: [error("ExportAllDeclaration")]
108+
},
109+
110+
// Many exports arround variable declaration
111+
{
112+
code: `export default 'such foo many bar'
113+
export const so = 'many'
114+
const foo = 'bar'
115+
export const exports = ':)'
116+
const hoge = 'bar'
117+
export const i = 'cant'
118+
export const even = 'count'
119+
export const how = 'many'`,
120+
output: `const foo = 'bar'
121+
const hoge = 'bar'
122+
export default 'such foo many bar'
123+
export const so = 'many'
124+
export const exports = ':)'
125+
export const i = 'cant'
126+
export const even = 'count'
127+
export const how = 'many'`,
128+
errors: [
129+
error("ExportDefaultDeclaration"),
130+
error("ExportNamedDeclaration"),
131+
error("ExportNamedDeclaration")
132+
]
133+
}
134+
]
135+
});

0 commit comments

Comments
 (0)