diff --git a/CHANGELOG.md b/CHANGELOG.md index e1ff0c4e7..613a7608e 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 519e707d8..c8c3789aa 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 000000000..205cb5ea8 --- /dev/null +++ b/docs/rules/no-anonymous-default-export.md @@ -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 {} +``` diff --git a/src/index.js b/src/index.js index 67fdb1326..69cbc2f5e 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 000000000..f333e6db7 --- /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', + 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 }) + } + }, + } + }, +} 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 000000000..9ea18a341 --- /dev/null +++ b/tests/src/rules/no-anonymous-default-export.js @@ -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' }] }), + ], +})