Skip to content
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

Merged
merged 1 commit into from
Oct 31, 2017
Merged

New: group-exports rule #721

merged 1 commit into from
Oct 31, 2017

Conversation

robertrossmann
Copy link
Contributor

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.

@ljharb
Copy link
Member

ljharb commented Jan 16, 2017

Why would this be preferred over prefer-default-export?

Specifically, why would exporting an object be better than named exports?

@coveralls
Copy link

coveralls commented Jan 16, 2017

Coverage Status

Coverage increased (+0.04%) to 94.907% when pulling c3fa502 on Alaneor:prefer-single-export into c975742 on benmosher:master.

@coveralls
Copy link

coveralls commented Jan 16, 2017

Coverage Status

Coverage increased (+0.04%) to 94.907% when pulling c3fa502 on Alaneor:prefer-single-export into c975742 on benmosher:master.

@robertrossmann
Copy link
Contributor Author

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 export {} declaration simplifies this.

This rule is not preferred over prefer-default-export. Rather, it seems to be a complement to that rule. There are situations where you do not want to provide a single, default export because the things you export do not necessarily relate, ie. a utils module full of various standalone functions. Using named exports for such functions seems more appropriate.

Also, for commonjs modules exporting on a single place also increases performance because you declare the exported object at once, therefore v8 does not have to create new hidden classes for the exported object every time you assign new property to it. Though I have to admit this perf gain is probably marginal.

@ljharb
Copy link
Member

ljharb commented Jan 17, 2017

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.

@robertrossmann
Copy link
Contributor Author

I would like to stress out that you are not forfeiting the benefit of named exports by doing export { thing1, thing2 } - these are named exports. See MDN.

@ljharb
Copy link
Member

ljharb commented Jan 17, 2017

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?

@robertrossmann
Copy link
Contributor Author

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 export statements to be grouped. That would be a nice addition to this rule since it would require both named and default exports to be grouped together, further improving readability. But that's for another issue. 😄

@ljharb
Copy link
Member

ljharb commented Jan 17, 2017

Then in that case, I think this is an excellent rule, but that the rule name needs a bit of work :-)

Maybe group-named-exports?

If we wanted to combine this, with the ability to require grouping all exports, then perhaps group-exports taking an object with named and default booleans?

@robertrossmann
Copy link
Contributor Author

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. default: "before-named|after-named". Alternatively, we can also allow a true value in which case the default export may appear before or after the named exports.

Also, what should happen if named is set to false (no grouping of named exports) and default is enabled? Should the default export appear near the first, last or any named export?

@robertrossmann robertrossmann changed the title New: prefer-single-export rule New: group-exports rule Jan 17, 2017
@coveralls
Copy link

coveralls commented Jan 17, 2017

Coverage Status

Coverage increased (+0.04%) to 94.907% when pulling 14c3150 on Alaneor:prefer-single-export into c975742 on benmosher:master.

@robertrossmann
Copy link
Contributor Author

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!

@coveralls
Copy link

coveralls commented Jan 17, 2017

Coverage Status

Coverage increased (+0.02%) to 94.888% when pulling 57cb453 on Alaneor:prefer-single-export into c975742 on benmosher:master.

@coveralls
Copy link

coveralls commented Jan 17, 2017

Coverage Status

Coverage increased (+0.02%) to 94.888% when pulling 57cb453 on Alaneor:prefer-single-export into c975742 on benmosher:master.

'export const another = true',
].join('\n'),
errors: [
'Multiple named export declarations',
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Collaborator

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 :/

errors: [
'Multiple CommonJS exports',
'Multiple CommonJS exports',
'Multiple CommonJS exports',
Copy link
Member

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?

Copy link
Contributor Author

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.

@robertrossmann
Copy link
Contributor Author

I just pushed another update to this PR, incorporating remaining suggestions and fixing some issues:

  • Assigning to module.exports.exported.* or deeper members is no longer considered as an export
  • Accessing properties on module.exports is no longer mistakenly considered as an export declaration 😱

@coveralls
Copy link

coveralls commented Jan 17, 2017

Coverage Status

Coverage increased (+0.05%) to 94.916% when pulling d4bed97 on Alaneor:prefer-single-export into c975742 on benmosher:master.

ljharb
ljharb previously requested changes Jan 18, 2017
const first = true
const second = true

// A single named export -> ok
Copy link
Member

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"?

### Invalid

```js
// Multiple named exports -> not ok!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Multiple named export statements"

ExportDefaultDeclaration:
'Default export declaration should be adjacent to named export',
MemberExpression:
'Multiple CommonJS exports; consolidate all exports into a single assignment to ' +
Copy link
Member

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.

Copy link
Contributor Author

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.

return
}

return void exports.named.add(node)
Copy link
Member

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;

Copy link
Contributor Author

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.

default:
'Default export declaration should be adjacent to named export',
commonjs:
'Multiple CommonJS exports; consolidate all exports into a single assignment to ' +
Copy link
Member

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

@robertrossmann
Copy link
Contributor Author

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.

@coveralls
Copy link

coveralls commented Jan 18, 2017

Coverage Status

Coverage increased (+0.06%) to 94.922% when pulling 03225eb on Alaneor:prefer-single-export into c975742 on benmosher:master.

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`])
Copy link
Collaborator

@jfmengels jfmengels Jan 21, 2017

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.

Copy link
Contributor Author

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

@@ -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.
Copy link
Collaborator

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).

Copy link
Contributor Author

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.

@@ -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.
Copy link
Collaborator

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.

Copy link
Contributor Author

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.


## 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.
Copy link
Collaborator

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)

'const first = true',
'const second = true',
'export { first,\nsecond }',
].join('\n') }),
Copy link
Collaborator

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.

Copy link
Contributor Author

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
Copy link
Collaborator

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 grouping module.exports = { foo, bar } when possible.

Copy link
Contributor Author

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.

Copy link
Member

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 = {}?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been addressed?

Copy link
Contributor Author

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.

}

function create(context) {
const exports = {
Copy link
Collaborator

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.

Copy link
Contributor Author

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.


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) {
Copy link
Collaborator

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added!

@ljharb ljharb dismissed their stale review January 21, 2017 18:08

Deferring to @jfmengels

@coveralls
Copy link

coveralls commented May 1, 2017

Coverage Status

Coverage increased (+0.05%) to 94.964% when pulling 05aad15 on robertrossmann:prefer-single-export into 106740f on benmosher:master.

@robertrossmann
Copy link
Contributor Author

@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 group-named-exports.

@coveralls
Copy link

coveralls commented May 2, 2017

Coverage Status

Coverage increased (+0.1%) to 95.022% when pulling 22e103c on robertrossmann:prefer-single-export into 106740f on benmosher:master.

@robertrossmann
Copy link
Contributor Author

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!

@ljharb
Copy link
Member

ljharb commented May 2, 2017

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 exports.foo is not a named export - it's a property on the default export.

@robertrossmann
Copy link
Contributor Author

@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!

@coveralls
Copy link

coveralls commented Jul 10, 2017

Coverage Status

Coverage increased (+0.06%) to 96.022% when pulling fd516ad on robertrossmann:prefer-single-export into c9dd91d on benmosher:master.

@robertrossmann robertrossmann deleted the prefer-single-export branch July 19, 2017 18:07
@robertrossmann
Copy link
Contributor Author

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.

@ljharb
Copy link
Member

ljharb commented Jul 19, 2017

@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 ljharb marked this as a duplicate of #898 Jul 19, 2017
@ljharb ljharb requested a review from benmosher July 19, 2017 18:43
@robertrossmann
Copy link
Contributor Author

@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!

@ljharb
Copy link
Member

ljharb commented Jul 19, 2017

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.

@coveralls
Copy link

coveralls commented Jul 19, 2017

Coverage Status

Coverage increased (+0.06%) to 96.022% when pulling fd516ad on robertrossmann:prefer-single-export into c9dd91d on benmosher:master.

@robertrossmann
Copy link
Contributor Author

Any chance to have this merged or reviewed? Thanks.

@robertrossmann
Copy link
Contributor Author

Rebased again. Could we get this reviewed and merged in?

Regarding tests: They seem to be failing due to an unrelated error in exports-last valid test. See the logs.

Thank you.

@coveralls
Copy link

coveralls commented Oct 25, 2017

Coverage Status

Coverage increased (+0.06%) to 96.093% when pulling a5f9d21 on robertrossmann:prefer-single-export into 98acd6a on benmosher:master.

@robertrossmann
Copy link
Contributor Author

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.

@ljharb ljharb dismissed jfmengels’s stale review October 25, 2017 21:01

10 months, plus the changes seem addressed

Copy link
Member

@ljharb ljharb left a 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
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has this been addressed?

'Program:exit': function onExit() {
// Report multiple `export` declarations (ES2015 modules)
if (nodes.modules.size > 1) {
for (const node of nodes.modules) {
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!


// Report multiple `module.exports` assignments (CommonJS)
if (nodes.commonjs.size > 1) {
for (const node of nodes.commonjs) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

@robertrossmann
Copy link
Contributor Author

@ljharb thanks for feedback - I think I have made all the requested changes.

@coveralls
Copy link

coveralls commented Oct 26, 2017

Coverage Status

Coverage increased (+0.06%) to 96.093% when pulling 365dfbb on robertrossmann:prefer-single-export into 98acd6a on benmosher:master.

@robertrossmann
Copy link
Contributor Author

Looks like AppVeyor tests pass but uploading coverage data failed on all jobs and marked the build as failed.

@ljharb
Copy link
Member

ljharb commented Oct 27, 2017

Let's give this a few more days, but then I'm going to merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

4 participants