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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).

## [Unreleased]
### Added
- [`no-anonymous-default-export`] rule: report anonymous default exports ([#712], thanks [@duncanbeevers]).

### Changed
- [`no-extraneous-dependencies`]: use `read-pkg-up` to simplify finding + loading `package.json` ([#680], thanks [@wtgtybhertgeghgtwtg])

Expand Down Expand Up @@ -377,6 +380,7 @@ for info on changes for earlier releases.
[`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md
[`unambiguous`]: ./docs/rules/unambiguous.md

[#712]: https://github.com/benmosher/eslint-plugin-import/pull/712
[#680]: https://github.com/benmosher/eslint-plugin-import/pull/680
[#654]: https://github.com/benmosher/eslint-plugin-import/pull/654
[#639]: https://github.com/benmosher/eslint-plugin-import/pull/639
Expand Down Expand Up @@ -561,3 +565,4 @@ for info on changes for earlier releases.
[@ntdb]: https://github.com/ntdb
[@jakubsta]: https://github.com/jakubsta
[@wtgtybhertgeghgtwtg]: https://github.com/wtgtybhertgeghgtwtg
[@duncanbeevers]: https://github.com/duncanbeevers
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`])
* Forbid anonymous values as default exports ([`no-anonymous-default-export`])

[`first`]: ./docs/rules/first.md
[`no-duplicates`]: ./docs/rules/no-duplicates.md
Expand All @@ -87,6 +88,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
[`max-dependencies`]: ./docs/rules/max-dependencies.md
[`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md
[`no-named-default`]: ./docs/rules/no-named-default.md
[`no-anonymous-default-export`]: ./docs/rules/no-anonymous-default-export.md

## Installation

Expand Down
67 changes: 67 additions & 0 deletions docs/rules/no-anonymous-default-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# no-anonymous-default-export

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

Ensuring that default exports are named helps improve the grepability of the codebase by encouraging the re-use of the same identifier for the module's default export at its declaration site and at its import sites.

## Options

By default, all types of anonymous default exports are forbidden, but any types can be selectively allowed by toggling them on in the options.

The complete default configuration looks like this.

```js
"import/no-anonymous-default-export": ["error", {
"allowArray": false,
"allowArrowFunction": false,
"allowAnonymousClass": false,
"allowAnonymousFunction": false,
"allowLiteral": false,
"allowObject": false
}]
```

## Rule Details

### Fail
```js
export default []

export default () => {}

export default class {}

export default function () {}

export default 123

export default {}
```

### Pass
```js
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 {}
```
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const rules = {
'no-named-default': require('./rules/no-named-default'),
'no-named-as-default': require('./rules/no-named-as-default'),
'no-named-as-default-member': require('./rules/no-named-as-default-member'),
'no-anonymous-default-export': require('./rules/no-anonymous-default-export'),

'no-commonjs': require('./rules/no-commonjs'),
'no-amd': require('./rules/no-amd'),
Expand Down
84 changes: 84 additions & 0 deletions src/rules/no-anonymous-default-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @fileoverview Rule to disallow anonymous default exports.
* @author Duncan Beevers
*/

const defs = {
ArrayExpression: {
option: 'allowArray',
description: 'If `false`, will report default export of an array',
message: 'Assign array to a variable before exporting as module default',
},
ArrowFunctionExpression: {
option: 'allowArrowFunction',
description: 'If `false`, will report default export of an arrow function',
message: 'Assign arrow function to a variable before exporting as module default',
},
ClassDeclaration: {
option: 'allowAnonymousClass',
description: 'If `false`, will report default export of an anonymous class',
message: 'Unexpected default export of anonymous class',
forbid: (node) => !node.declaration.id,
},
FunctionDeclaration: {
option: 'allowAnonymousFunction',
description: 'If `false`, will report default export of an anonymous function',
message: 'Unexpected default export of anonymous function',
forbid: (node) => !node.declaration.id,
},
Literal: {
option: 'allowLiteral',
description: 'If `false`, will report default export of a literal',
message: 'Assign literal to a variable before exporting as module default',
},
ObjectExpression: {
option: 'allowObject',
description: 'If `false`, will report default export of an object expression',
message: 'Assign object to a variable before exporting as module default',
},
TemplateLiteral: {
option: 'allowLiteral',
description: 'If `false`, will report default export of a literal',
message: 'Assign literal to a variable before exporting as module default',
},
}

const schemaProperties = Object.keys(defs).
map((key) => defs[key]).
reduce((acc, def) => {
acc[def.option] = {
description: def.description,
type: 'boolean',
default: false,
}

return acc
}, {})

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.

meta: {
schema: [
{
type: 'object',
properties: schemaProperties,
'additionalProperties': false,
},
],
},

create: function (context) {
const options = Object.assign({}, context.options[0])

return {
'ExportDefaultDeclaration': (node) => {
const def = defs[node.declaration.type]

// Recognized node type and allowed by configuration,
// and has no forbid check, or forbid check return value is truthy
if (def && !options[def.option] && (!def.forbid || def.forbid(node))) {
context.report({ node, message: def.message })
}
},
}
},
}
50 changes: 50 additions & 0 deletions tests/src/rules/no-anonymous-default-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { test, SYNTAX_CASES } from '../utils'

import { RuleTester } from 'eslint'

const ruleTester = new RuleTester()
const rule = require('rules/no-anonymous-default-export')

ruleTester.run('no-anonymous-default-export', rule, {
valid: [
// Exports with identifiers are valid
test({ code: 'const foo = 123\nexport default foo' }),
test({ code: 'export default function foo() {}'}),
test({ code: 'export default class MyClass {}'}),

// Allow each forbidden type with appropriate option
test({ code: 'export default []', options: [{ allowArray: true }] }),
test({ code: 'export default () => {}', options: [{ allowArrowFunction: true }] }),
test({ code: 'export default class {}', options: [{ allowAnonymousClass: true }] }),
test({ code: 'export default function() {}', options: [{ allowAnonymousFunction: true }] }),
test({ code: 'export default 123', options: [{ allowLiteral: true }] }),
test({ code: 'export default \'foo\'', options: [{ allowLiteral: true }] }),
test({ code: 'export default `foo`', options: [{ allowLiteral: true }] }),
test({ code: 'export default {}', options: [{ allowObject: true }] }),

// Allow forbidden types with multiple options
test({ code: 'export default 123', options: [{ allowLiteral: true, allowObject: true }] }),
test({ code: 'export default {}', options: [{ allowLiteral: true, allowObject: true }] }),

// Sanity check unrelated export syntaxes
test({ code: 'export * from \'foo\'' }),
test({ code: 'const foo = 123\nexport { foo }' }),
test({ code: 'const foo = 123\nexport { foo as default }' }),

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

...SYNTAX_CASES,
],

invalid: [
test({ code: 'export default []', errors: [{ message: 'Assign array to a variable before exporting as module default' }] }),
test({ code: 'export default () => {}', errors: [{ message: 'Assign arrow function to a variable before exporting as module default' }] }),
test({ code: 'export default class {}', errors: [{ message: 'Unexpected default export of anonymous class' }] }),
test({ code: 'export default function() {}', errors: [{ message: 'Unexpected default export of anonymous function' }] }),
test({ code: 'export default 123', errors: [{ message: 'Assign literal to a variable before exporting as module default' }] }),
test({ code: 'export default \'foo\'', errors: [{ message: 'Assign literal to a variable before exporting as module default' }] }),
test({ code: 'export default `foo`', errors: [{ message: 'Assign literal to a variable before exporting as module default' }] }),
test({ code: 'export default {}', errors: [{ message: 'Assign object to a variable before exporting as module default' }] }),

// Test failure with non-covering exception
test({ code: 'export default 123', options: [{ allowObject: true }], errors: [{ message: 'Assign literal to a variable before exporting as module default' }] }),
],
})