-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New: group-exports
rule
#721
Conversation
Why would this be preferred over Specifically, why would exporting an object be better than named exports? |
Well, first things first: const first = true
export {
first,
} Here, I am not "exporting an object" - I am simply declaring all named exports at one place. The above is equivalent to export const first = true The reason to use this rule primarily comes from some developers exporting at various places in a file. This makes it a bit hard to find out what exactly is exported from a particular module (because you have to scan through the whole file to find all the named exports). Preferring a singular This rule is not preferred over
|
Negligent performance theorizing shouldn't be used to inform code decisions, especially since v8 isn't the only place this code will be run. I could certainly understand a rule that requires that all export statements in a file appear grouped - that seems to achieve your goal without requiring that people export an object (a bag of things), ie, without requiring that people forfeit the benefit of using named exports. |
I would like to stress out that you are not forfeiting the benefit of named exports by doing |
Ah, I misunderstood. Your rule isn't suggesting that people switch from multiple named exports to an export of an object - it's suggesting that people consolidate all named exports into a single statement? |
Yes, that's exactly the aim - to have all the exports in one place so that it's absolutely obvious what is being exported. 🎉 As per your other comment, it could be worthwhile considering implementing a rule which requires all |
Then in that case, I think this is an excellent rule, but that the rule name needs a bit of work :-) Maybe If we wanted to combine this, with the ability to require grouping all exports, then perhaps |
Sounds like a good idea! Do you think it would be useful to give users option to specify if the default export should be before or after the named exports? E.g. Also, what should happen if |
prefer-single-export
rulegroup-exports
rule
I have implemented a simplified version of the default export placement check - for now, it requires the default export to be adjacent to the named export (either before or after it). To make it simple, it does not check the placement if there are still multiple named exports. We may decide how this should behave in such situation (maybe it should then be adjacent to the last named export?). Let me know what you think so far. We can discuss the options we would like to provide for this rule, then I will extend the current implementation with those options. Thanks! |
tests/src/rules/group-exports.js
Outdated
'export const another = true', | ||
].join('\n'), | ||
errors: [ | ||
'Multiple named export declarations', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's have this error message contain something actionable - maybe "Multiple named export declarations; consolidate all named exports into a single statement"
Also, could this be autofixable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Messages updated as requested.
Autofixing would be quite difficult, it seems - we have to
- remove
export
from in front of the things being exported - add one extra
export
declaration somewhere into the file with all the exported names
The first step seems easy, but I am unsure how to do the second, given you can do only one fix per rule violation.
I guess we could use the last found export and do a full replacement of the source where we append the export at the end...Not sure if this is a good idea, though - if the code we replace contains other issues, I have no idea how ESLint would solve that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be pretty hard to auto-fix yes :/
tests/src/rules/group-exports.js
Outdated
errors: [ | ||
'Multiple CommonJS exports', | ||
'Multiple CommonJS exports', | ||
'Multiple CommonJS exports', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similarly here, "Multiple CommonJS exports; do a single assignment to module.exports
for all of your exports."
Also, what about assigning to exports
without the module
prefix?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assignment to the exports
object is covered here.
I just pushed another update to this PR, incorporating remaining suggestions and fixing some issues:
|
docs/rules/group-exports.md
Outdated
const first = true | ||
const second = true | ||
|
||
// A single named export -> ok |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
technically this is a single named export statement but it's two named exports - can we add "statement"?
docs/rules/group-exports.md
Outdated
### Invalid | ||
|
||
```js | ||
// Multiple named exports -> not ok! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Multiple named export statements"
src/rules/group-exports.js
Outdated
ExportDefaultDeclaration: | ||
'Default export declaration should be adjacent to named export', | ||
MemberExpression: | ||
'Multiple CommonJS exports; consolidate all exports into a single assignment to ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason this isn't on a single line? if max-len
is complaining, let's enable the option that ignores long strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put those in a /* eslint-disable max-len */
block.
src/rules/group-exports.js
Outdated
return | ||
} | ||
|
||
return void exports.named.add(node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these using return void
instead of doing two things in two statements? Namely, exports.named.add(node); return;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personal preference, I guess. I like to return together with the last statement in the function. Now they are split into separate lines.
tests/src/rules/group-exports.js
Outdated
default: | ||
'Default export declaration should be adjacent to named export', | ||
commonjs: | ||
'Multiple CommonJS exports; consolidate all exports into a single assignment to ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here re, let's let this be a single line
Updated with recent comments. I pushed it as a separate commit, figured it would be easier to review incrementally than going through the whole thing again and again. Once you are ready to merge, I can squash them all into a single commit. |
README.md
Outdated
@@ -76,6 +76,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a | |||
* Limit the maximum number of dependencies a module can have ([`max-dependencies`]) | |||
* Forbid unassigned imports ([`no-unassigned-import`]) | |||
* Forbid named default exports ([`no-named-default`]) | |||
* Prefer single named export declaration ([`group-exports`]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, thanks @Alaneor! I think the rule idea is interesting :)
Prefer grouping the export of named exports as a single named export declaration
?
There may be too many uses of 'export' but I think this is clearer, otherwise people might think it's to limit the number of named exports to one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to Prefer named exports to be grouped together in a single export declaration
docs/rules/group-exports.md
Outdated
@@ -0,0 +1,62 @@ | |||
# group-exports | |||
|
|||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be a potentially neat idea to enforce having both the default export and the named exports located next to eah other, but IMO this is out of the scope of the rule. Maybe some people would want to have this enforced but still be allowed to write code like
export const foo = 1;
export const bar = 2;
export default
IMO, this should be a whole different rule. And it's already being tackled in #632 (it enforces they are the last thing in the file, but the same idea is applied).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checks about grouping named and default exports next to each other have been removed.
docs/rules/group-exports.md
Outdated
@@ -0,0 +1,62 @@ | |||
# group-exports | |||
|
|||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reports when multiple named exports
. Add something like instead of one grouped named exports declaration
. The idea is rather to enforce an alternative way of doing things than straight out forbidding something, and I think the description could be more explicit about that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reworded: Reports when named exports are not grouped together in a single export
statement or when multiple assignments to CommonJS module.exports
or exports
object are present in a single file.
docs/rules/group-exports.md
Outdated
|
||
## Rule Details | ||
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assignments to a property of module.exports
(or exports
).
I agree that writing
module.exports = A
module.exports = B
is an error, but this should be handled by a different rule (there may already be one, haven't checked yet)
tests/src/rules/group-exports.js
Outdated
'const first = true', | ||
'const second = true', | ||
'export { first,\nsecond }', | ||
].join('\n') }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of joining strings, use template literals. Simplifies writing tests.
test({ code: `
const first = true
const second = true
export {
first,
second
}`
),
Do the same thing for every test where you have \n
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, thanks!
```js | ||
// Multiple exports assignments -> not ok! | ||
exports.first = true | ||
exports.second = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a it more complicated for CommonJS. If you wish for your module to export a function, but also some other things under "named" exports
module.exports = function foo() {};
module.exports.first = true;
module.exports.second = true;
You could potentially do this like in the following example, but that's pretty odd to satisfy this rule.
module.exports = Object.assign(function foo {}, {first: true, second: true});
IMO, this rule should either:
- only reports errors for ES modules
- only report the use of several instructions like
module.exports.foo = true
if there is no other direct assignment (module.exports = foo
). <-- I'd go for this as I myself prefer groupingmodule.exports = { foo, bar }
when possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have refactored the rule to only do anything if the thing assigned to module.exports
is an object literal or there is nothing being assigned to that property. If it's anything else (function, string, what have you) it will not report any errors.
So, this is still an error:
module.exports = {}
module.exports.prop = true
But this is now ok:
module.exports = () => {}
module.exports.first = true
module.exports.second = true
I guess this is the best of both approaches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about exports = {}
or exports.foo = {}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ljharb exports = {}
will not export anything nor will it modify whatever value is in module.exports
, the exports
variable is but a local reference to the default module.exports
object.
As for exports.foo = {}
, that's basically a named export of an object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@robertrossmann yes, i'm aware of that; it could still be invalid, but doesn't necessarily have to be covered by this rule.
imo exports.foo = {}
should be treated the same as module.exports.foo = {}
.
If someone wants to export a function with extra properties, they should do something like const foo = () => {}; foo.first = true; foo.second = true; module.exports = foo;
- I don't think the rule should allow multiple module.exports mutations under any circumstances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ljharb assignments to exports
and module.exports
are indeed treated the same. 😄 I have added more tests to validate this.
So
exports.first = true
module.exports.second = true
will cause this rule to issue a warning for both lines.
Currently the rule will ignore situations in which you assign something else than an object literal to module.exports
(the rule will ignore all CommonJS module operations), so
module.exports = function test() {}
module.exports.extra = true
module.exports.field = true
will not cause any warnings. Do you think it should warn even in situations like these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I do. I don't see why a non-object-literal is any better or worse of a pattern than an object literal here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has this been addressed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - the rule now does not care what is being assigned to module.exports
. Some tests for that behaviour can be found here.
src/rules/group-exports.js
Outdated
} | ||
|
||
function create(context) { | ||
const exports = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you rename this to something else? exports
is what I consider a "semi-reserved" keyword in Node, better to name it something else to avoid confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed a bunch of now-obsolete code and ultimately renamed the Set to named
, since we are now only tracking named exports.
src/rules/group-exports.js
Outdated
|
||
if (node.object.name === 'module' && node.property.name === 'exports') { | ||
// module.exports.exported.*: assignments this deep should not be considered as exports | ||
if (accessorDepth(node) > 2) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add tests for this? Multiple instructions like module.exports.foo.bar = 1
for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added!
@jfmengels Thanks for the feedback! I have incorporated all the changes you requested, please have a look. Since this rule now only does anything useful with named exports, I suggest we rename the rule to |
I just pushed a major refactor of the rule's implementation as I discovered some bugs in how CommonJS modules were handled. See 22e103c. Thank you for your review! |
I think "group exports" is still the proper name; since named exports are the only way you could ever have > 1 to group, that was always the case, but |
@ljharb the test suite failed due to coveralls error, looks like something to do with me rebasing the branch. Restarting the suite should fix it. Thanks! |
Closing due to constantly failing tests and an unnecessarily long discussion record and outdated review notes which I fear have been preventing merge for almost 7 months now. Opened a new issue. |
@robertrossmann please don't close PRs and open duplicates; in no way are any of the things you mentioned preventing merges. Please reopen this and close the duplicate - duplicate PRs clutter the repo forever, since github creates undeleteable refs to them. |
@ljharb Reopened. Just let me know what needs to be done in order to have this merged, please. Or tell me you don't like it so I don't waste time coming back to this. Thanks! |
I do like it; @jfmengels has an outstanding review, and I'd prefer them to either approve or dismiss it before this is merged. If @benmosher approves first, we may be able to bypass that, but it's not ideal. |
Any chance to have this merged or reviewed? Thanks. |
Rebased again. Could we get this reviewed and merged in? Regarding tests: They seem to be failing due to an unrelated error in Thank you. |
Rebased again. Could we get this reviewed and merged in? @jfmengels you have requested changes on this PR but your feedback has been addressed, I believe. |
10 months, plus the changes seem addressed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM after a few more changes
CHANGELOG.md
Outdated
@@ -4,20 +4,13 @@ This project adheres to [Semantic Versioning](http://semver.org/). | |||
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). | |||
|
|||
## [Unreleased] | |||
|
|||
|
|||
## [2.8.0] - 2017-10-18 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs rebasing, and some fixing - it appears some info has been lost
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooops, sorry - fixed! If you'd like, I can drop the changes to CHANGELOG completely. Not sure if it's supposed to be part of a PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nah it's good to add it to "unreleased"
```js | ||
// Multiple exports assignments -> not ok! | ||
exports.first = true | ||
exports.second = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has this been addressed?
src/rules/group-exports.js
Outdated
'Program:exit': function onExit() { | ||
// Report multiple `export` declarations (ES2015 modules) | ||
if (nodes.modules.size > 1) { | ||
for (const node of nodes.modules) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we use nodes.modules.forEach
here, instead of a loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
src/rules/group-exports.js
Outdated
|
||
// Report multiple `module.exports` assignments (CommonJS) | ||
if (nodes.commonjs.size > 1) { | ||
for (const node of nodes.commonjs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
@ljharb thanks for feedback - I think I have made all the requested changes. |
Looks like AppVeyor tests pass but uploading coverage data failed on all jobs and marked the build as failed. |
Let's give this a few more days, but then I'm going to merge it. |
Adds a new rule which prefers a particular style in exporting named members from modules.
Looking forward to hearing your feedback. Let me know if I have forgotten anything.