Skip to content

Commit

Permalink
Add no-anonymous-default-export rule (#712)
Browse files Browse the repository at this point in the history
* Add `no-anonymous-default-export` rule
  • Loading branch information
duncanbeevers authored and jfmengels committed Jan 21, 2017
1 parent 29a870f commit f43cf95
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 0 deletions.
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 = {
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 }' }),

...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' }] }),
],
})

0 comments on commit f43cf95

Please sign in to comment.