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

Add no-anonymous-default-export rule #712

Merged
merged 2 commits into from
Jan 21, 2017

Conversation

duncanbeevers
Copy link
Contributor

@duncanbeevers duncanbeevers commented Jan 5, 2017

Forbids exporting anonymous functions, literals, class expressions, or arrow functions as module default.

This is intended to improve the grepability of codebases by encouraging the re-use of identifiers at their declaration site and at their import sites.

Rule Details

Fail

export default []

export default () => {}

export default class {}

export default function () {}

export default 123

export default {}

Pass

const foo = 123
export default foo

export default class MyClass() {}

export default function foo() {}

/* eslint import/no-anonymous-default-export: [2, {"allowArray": true}] */
export default []

/* eslint import/no-anonymous-default-export: [2, {"allowArrowFunction": true}] */
export default () => {}

/* eslint import/no-anonymous-default-export: [2, {"allowAnonymousClass": true}] */
export default class {}

/* eslint import/no-anonymous-default-export: [2, {"allowAnonymousFunction": true}] */
export default function () {}

/* eslint import/no-anonymous-default-export: [2, {"allowLiteral": true}] */
export default 123

/* eslint import/no-anonymous-default-export: [2, {"allowObject": true}] */
export default {}

@coveralls
Copy link

coveralls commented Jan 5, 2017

Coverage Status

Coverage decreased (-0.02%) to 94.844% when pulling 3b25d5b on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@coveralls
Copy link

coveralls commented Jan 5, 2017

Coverage Status

Coverage decreased (-0.02%) to 94.844% when pulling 6a47f2d on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@coveralls
Copy link

coveralls commented Jan 5, 2017

Coverage Status

Coverage increased (+0.03%) to 94.895% when pulling 949ac38 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@ljharb
Copy link
Member

ljharb commented Jan 5, 2017

Should this then warn if you default-export an arrow function? what about an anonymous class expression?

Why are literals forbidden? The identifier name of their import would usually match the filename, by convention.

@duncanbeevers
Copy link
Contributor Author

The idea is to improve grepability of the codebase. When grepping around for an identifier, filenames aren't matched on, only the contents of the file.

I agree that it should warn if you default-export an arrow function or an anonymous class expression. I'll add specs for those cases.

@duncanbeevers
Copy link
Contributor Author

@ljharb Now forbids default exports of anonymous class expressions and arrow functions. Added functionality, specs and documentation to cover these cases.

@duncanbeevers duncanbeevers force-pushed the no-anonymous-default-export branch 2 times, most recently from c7efe8e to 74af3e3 Compare January 6, 2017 03:37
@coveralls
Copy link

coveralls commented Jan 6, 2017

Coverage Status

Coverage increased (+0.05%) to 94.912% when pulling 72d4d31 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@coveralls
Copy link

coveralls commented Jan 6, 2017

Coverage Status

Coverage increased (+0.05%) to 94.912% when pulling 74af3e3 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@coveralls
Copy link

coveralls commented Jan 6, 2017

Coverage Status

Coverage increased (+0.05%) to 94.912% when pulling 74af3e3 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

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.

Currently, this checks for primitive literals, object literals, anon classes, anon functions, and arrows. What would you think about an option for each of these items?

My personal use cases would only enable the functions and classes, but object literals in particular I'd need to be able to turn off.

@duncanbeevers
Copy link
Contributor Author

@ljharb Each forbidden unnamed default export type can now be selectively allowed with an option.

@coveralls
Copy link

coveralls commented Jan 6, 2017

Coverage Status

Coverage increased (+0.07%) to 94.93% when pulling 5a8d6ef on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

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.

Looking much better, thanks!

Couple comments, and could we add at least one example or note with multiple options passed?


create: function (context) {

const forbid = (option) => !~context.options.indexOf('allow-' + option)
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 use > -1 here, !~ is a bit too clever ;-)

* @author Duncan Beevers
*/

module.exports = {
Copy link
Member

Choose a reason for hiding this comment

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

please add a schema to the rule, so that invalid options are rejected.

@coveralls
Copy link

coveralls commented Jan 6, 2017

Coverage Status

Coverage increased (+0.07%) to 94.93% when pulling 7c07aa0 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@duncanbeevers
Copy link
Contributor Author

@ljharb

  • Added specs demonstrating use of multiple options
  • Added schema
  • Removed ~+indexOf gymnastics

@coveralls
Copy link

coveralls commented Jan 6, 2017

Coverage Status

Coverage increased (+0.08%) to 94.941% when pulling 818bee2 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@duncanbeevers duncanbeevers force-pushed the no-anonymous-default-export branch 2 times, most recently from babb7cd to b1dafb3 Compare January 6, 2017 10:24
Copy link
Collaborator

@jfmengels jfmengels left a comment

Choose a reason for hiding this comment

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

Hi @duncanbeevers! Thanks for the interesting rule and making a PR for it :)

I have written some ways this could be simplified, feel free to disagree if you feel I'm mistaken somewhere.

// Allow forbidden types with multiple options
test({ code: 'export default 123', options: ['allow-literal', 'allow-object-expression'] }),
test({ code: 'export default {}', options: ['allow-literal', 'allow-object-expression'] }),

Copy link
Collaborator

@jfmengels jfmengels Jan 6, 2017

Choose a reason for hiding this comment

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

Could you add few valid tests for other kind of exports (named exports, export * from 'foo').
They are properly ignored at the moment, but it's mostly for documentation's sake.
(Might just be me, but I like to add additional tests slightly out of bounds of what is being treated to prevent surprises)

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 a few specs for a couple of flavors of exports and labeled the section

// Sanity check unrelated export syntaxes


ruleTester.run('no-anonymous-default-export', rule, {
valid: [
// Named exports are valid
Copy link
Collaborator

@jfmengels jfmengels Jan 6, 2017

Choose a reason for hiding this comment

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

This is a confusing comment, as named exports are a thing of their own

export const foo = 2;

Instead, "not anonymous", "with a name"? Or whatever you feel like naming it.

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 this comment to

// Exports with identifiers are valid

const type = node.declaration.type
const noID = !node.declaration.id

if (type === 'Literal' && forbid(MAP[type])) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can simplify the conditions by having this pretty much at the start of the function:

if (!forbid(MAP[type])) {
  return;
}

if (type === 'FunctionDeclaration' && noID && forbid(MAP[type])) {
context.report({
node: node,
message: 'Unexpected default export of anonymous function',
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not a native speaker, but doesn't of an anonymous function sound better?

Copy link
Member

Choose a reason for hiding this comment

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

it does.

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've added articles are all the default export types for consistency.

a literal
an array
an object

etc...

return
}

if (type === 'ObjectExpression' && forbid(MAP[type])) {
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 you can make all this less repetitive:

const types = { // or whatever you wish to name it
  ObjectExpression: {
    validation: () => true,
    description: 'object expression',
  },
  FunctionDeclaration: {
    validation: id => !id,
    description: 'anonymous function',
  },
  // ...
}

// ....


'ExportDefaultDeclaration': (node) => {
  if (forbid(MAP[type]) && types[type] && types[type].validation(node.declaration.id)) {
    context.report({
      node: node,
      message: 'Unexpected default export of ' + types[type].description,
    })
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The architecture of the rule now roughly matches this suggested layout.

@@ -0,0 +1,51 @@
# no-anonymous-default-export

Reports if a module's default export is unnamed. This includes several types of unnamed data types; literals, object expressions, anonymous functions, arrow functions, and anonymous class declarations.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would also enforce this on arrays, which I for instance sometimes use to create a fixtures file.
Also, I think it would make sense to also handle template literals if we intend to handle pretty much any value ?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed - it should handle arrays, but template literals i'd expect to be covered under "literals".

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I was thinking the same thing, no real reason to handle them separately from normal strings and numbers.

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 enforcement for arrays, and a spec for the handling of template literals.

module.exports = {
meta: {
schema: {
type: 'array',
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 the options should better be passed as an object, with a true/false values to each key. I found that having one object with settings instead of multiple values in a given order makes it much more extendable.

My gut feeling is to make it so that a field set to true would make it forbidden, and false would make it ignored, and that you'd have it true by default.

/*eslint import/no-anonymous-default-export: [2, "allow-arrow-function-expression"]*/
-->
/*eslint import/no-anonymous-default-export: [2, {ArrowFunctionExpression: false}]*/

The schema would look something like this (IIRC) (adapted from what is done in https://github.com/benmosher/eslint-plugin-import/blob/master/src/rules/no-extraneous-dependencies.js)

 schema: [
      {
        'type': 'object',
        'properties': {
          'Literal': { 'type': 'boolean' },
          'ObjectExpression': { 'type': 'boolean' },
          'FunctionDeclaration': { 'type': 'boolean' },
          'ArrowFunctionExpression': { 'type': 'boolean' },
          'ClassDeclaration': { 'type': 'boolean' },
        },
        'additionalProperties': false,
      },
    ],

And you'd get options like

  create: function (context) {
    const options = context.options[0] || {}
  }

Copy link
Collaborator

@jfmengels jfmengels Jan 7, 2017

Choose a reason for hiding this comment

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

I would be fine with simplifying the name of the properties (like ArrowFunctionExpression --> ArrowFunction), you'd just need to account for the difference in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Options are now handled as a single object. Details are spelled out in the rule description file.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The names are explicit, I like it 👍

@coveralls
Copy link

coveralls commented Jan 7, 2017

Coverage Status

Coverage increased (+0.06%) to 94.927% when pulling 2a12e67 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

import { RuleTester } from 'eslint'

var ruleTester = new RuleTester()
var rule = require('rules/no-anonymous-default-export')
Copy link
Collaborator

Choose a reason for hiding this comment

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

var -> const

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

],

invalid: [
test({ code: 'export default []', errors: [{ message: 'Unexpected default export of an array' }] }),
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 the error messages are confusing.
Unexpected default export of an anonymous class/function are fine, because they mention that what is wrong is the anonymousness.

For the others, it feels like this rule says that arrays/literals/objects are not allowed to be the default export for some reason.
I think it would be clearer if we recommended giving it a name or assigning it to a variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error messages and schema description strings have all been made explicit. The messages about anonymous functions and classes remain the same, while the rest of the messages are updated to use a more active voice.

'Assign literal to a variable before exporting as module default'

test({ code: 'export default []', errors: [{ message: 'Unexpected default export of an array' }] }),
test({ code: 'export default () => {}', errors: [{ message: 'Unexpected default export of an arrow function' }] }),
test({ code: 'export default class {}', errors: [{ message: 'Unexpected default export of an anonymous class' }] }),
test({ code: 'export default 123', errors: [{ message: 'Unexpected default export of a literal' }] }),
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 a test with a string and one with a template literal ?

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 added tests with string and template literal cases to both the success and failure corpuses, and found that template literals were not yet forbidden.

I corrected this by adding a TemplateLiteral clause to the defs structure whose value is intentionally identical to Literal's

map((key) => defs[key]).
reduce((acc, def) => {
acc[def.option] = {
description: 'If `false`, will not report default export of ' + def.message,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This sounds like the opposite of what the name suggests.
From what I understand, if allowLiteral is set to true, then Literals will be reported. 🤔

This should be reversed and the default value should be false.

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, this fell out of date as I reworked the implementation. Corrected.

const defaultOptions = Object.keys(defs).
map((key) => defs[key]).
reduce((acc, def) => {
acc[def.options] = false
Copy link
Collaborator

Choose a reason for hiding this comment

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

def.option ? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This defaultOptions object doesn't strictly need to be constructed. I created it (poorly) for parity with the documentation. Since it doesn't affect behavior, I'm rewinding my decision here and removing this.

Copy link
Collaborator

@jfmengels jfmengels left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for all your work @duncanbeevers! :)

I'm letting the others some time to have a final look, and will merge soon if they don't.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.05%) to 94.91% when pulling ad9e908 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

3 similar comments
@coveralls
Copy link

Coverage Status

Coverage increased (+0.05%) to 94.91% when pulling ad9e908 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.05%) to 94.91% when pulling ad9e908 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.05%) to 94.91% when pulling ad9e908 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@duncanbeevers
Copy link
Contributor Author

I added an example case to the documentation of exporting an identified rather than anonymous class.

@coveralls
Copy link

coveralls commented Jan 7, 2017

Coverage Status

Coverage increased (+0.05%) to 94.91% when pulling 2a9a7a1 on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

@duncanbeevers
Copy link
Contributor Author

@benmosher Let me know if there's anything I can do to shepherd this along.

@jfmengels jfmengels merged commit f43cf95 into import-js:master Jan 21, 2017
@jfmengels
Copy link
Collaborator

Thanks a lot for this @duncanbeevers! (and sorry for the delay in merging this)

@coveralls
Copy link

coveralls commented Jan 21, 2017

Coverage Status

Coverage decreased (-1.3%) to 93.609% when pulling ba3277c on duncanbeevers:no-anonymous-default-export into c975742 on benmosher:master.

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