From 2a12e671d8d51fbcb4bf3af03b3c8a2e9033a6b4 Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Thu, 5 Jan 2017 11:22:22 -0800 Subject: [PATCH] Add `no-anonymous-default-export` rule --- CHANGELOG.md | 3 + README.md | 2 + docs/rules/no-anonymous-default-export.md | 65 ++++++++++++++ src/index.js | 1 + src/rules/no-anonymous-default-export.js | 84 +++++++++++++++++++ .../src/rules/no-anonymous-default-export.js | 47 +++++++++++ 6 files changed, 202 insertions(+) create mode 100644 docs/rules/no-anonymous-default-export.md create mode 100644 src/rules/no-anonymous-default-export.js create mode 100644 tests/src/rules/no-anonymous-default-export.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e1ff0c4e70..613a7608ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. + ### Changed - [`no-extraneous-dependencies`]: use `read-pkg-up` to simplify finding + loading `package.json` ([#680], thanks [@wtgtybhertgeghgtwtg]) diff --git a/README.md b/README.md index 519e707d81..c8c3789aa8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docs/rules/no-anonymous-default-export.md b/docs/rules/no-anonymous-default-export.md new file mode 100644 index 0000000000..9620fb376e --- /dev/null +++ b/docs/rules/no-anonymous-default-export.md @@ -0,0 +1,65 @@ +# 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 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 {} +``` diff --git a/src/index.js b/src/index.js index 67fdb13261..69cbc2f5e6 100644 --- a/src/index.js +++ b/src/index.js @@ -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'), diff --git a/src/rules/no-anonymous-default-export.js b/src/rules/no-anonymous-default-export.js new file mode 100644 index 0000000000..1447dd7248 --- /dev/null +++ b/src/rules/no-anonymous-default-export.js @@ -0,0 +1,84 @@ +/** + * @fileoverview Rule to disallow anonymous default exports. + * @author Duncan Beevers + */ + +const defs = { + ArrayExpression: { + option: 'allowArray', + message: 'an array', + }, + ArrowFunctionExpression: { + option: 'allowArrowFunction', + message: 'an arrow function', + }, + ClassDeclaration: { + option: 'allowAnonymousClass', + message: 'an anonymous class', + forbid: (node) => !node.declaration.id, + }, + FunctionDeclaration: { + option: 'allowAnonymousFunction', + message: 'an anonymous function', + forbid: (node) => !node.declaration.id, + }, + Literal: { + option: 'allowLiteral', + message: 'a literal', + }, + ObjectExpression: { + option: 'allowObject', + message: 'an object expression', + }, +} + +const defaultOptions = Object.keys(defs). + map((key) => defs[key]). + reduce((acc, def) => { + acc[def.options] = false + + return acc + }) + +const schemaProperties = Object.keys(defs). + map((key) => defs[key]). + reduce((acc, def) => { + acc[def.option] = { + description: 'If `false`, will not report default export of ' + def.message, + type: 'boolean', + default: true, + } + + return acc + }, {}) + +module.exports = { + meta: { + schema: [ + { + type: 'object', + properties: schemaProperties, + 'additionalProperties': false, + }, + ], + }, + + create: function (context) { + const options = Object.assign({}, defaultOptions, context.options[0]) + + return { + 'ExportDefaultDeclaration': (node) => { + const def = defs[node.declaration.type] + + // Unrecognized node type or else allowed by configuration + if (!def || options[def.option]) { + return + } + + if (!def.forbid || def.forbid(node)) { + context.report({ node, message: 'Unexpected default export of ' + def.message }) + } + }, + } + }, +} diff --git a/tests/src/rules/no-anonymous-default-export.js b/tests/src/rules/no-anonymous-default-export.js new file mode 100644 index 0000000000..a2ffc2be7b --- /dev/null +++ b/tests/src/rules/no-anonymous-default-export.js @@ -0,0 +1,47 @@ +import { test, SYNTAX_CASES } from '../utils' + +import { RuleTester } from 'eslint' + +var ruleTester = new RuleTester() +var 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 {}', 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: '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' }] }), + test({ code: 'export default {}', errors: [{ message: 'Unexpected default export of an object expression' }] }), + test({ code: 'export default function() {}', errors: [{ message: 'Unexpected default export of an anonymous function' }] }), + + // Test failure with non-covering exception + test({ code: 'export default 123', options: [{ allowObject: true }], errors: [{ message: 'Unexpected default export of a literal' }] }), + ], +})