diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a5a615e..8d0349328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### Fixed - [`namespace`] exception for get property from `namespace` import, which are re-export from commonjs module ([#499] fixes [#416], thanks [@wKich]) +## [1.14.0] - 2016-08-21 +### Added +- [`max-dependencies`] for specifying the maximum number of dependencies (both `import` and `require`) a module can have. (see [#489], thanks [@tizmagik]) + ## [1.13.0] - 2016-08-11 ### Added - `allowComputed` option for [`namespace`] rule. If set to `true`, won't report @@ -293,11 +297,13 @@ for info on changes for earlier releases. [`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md [`prefer-default-export`]: ./docs/rules/prefer-default-export.md [`no-restricted-paths`]: ./docs/rules/no-restricted-paths.md +[`max-dependencies`]: ./docs/rules/max-dependencies.md [#509]: https://github.com/benmosher/eslint-plugin-import/pull/509 [#508]: https://github.com/benmosher/eslint-plugin-import/pull/508 [#503]: https://github.com/benmosher/eslint-plugin-import/pull/503 [#499]: https://github.com/benmosher/eslint-plugin-import/pull/499 +[#489]: https://github.com/benmosher/eslint-plugin-import/pull/489 [#461]: https://github.com/benmosher/eslint-plugin-import/pull/461 [#444]: https://github.com/benmosher/eslint-plugin-import/pull/444 [#428]: https://github.com/benmosher/eslint-plugin-import/pull/428 @@ -430,3 +436,4 @@ for info on changes for earlier releases. [@zloirock]: https://github.com/zloirock [@rhys-vdw]: https://github.com/rhys-vdw [@wKich]: https://github.com/wKich +[@tizmagik]: https://github.com/tizmagik diff --git a/README.md b/README.md index b396daf3b..249ba36ee 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-amd`]: ./docs/rules/no-amd.md [`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md + **Style guide:** * Ensure all imports appear before other statements ([`imports-first`]) @@ -61,6 +62,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Enforce a convention in module import order ([`order`]) * Enforce a newline after import statements ([`newline-after-import`]) * Prefer a default export if module exports a single name ([`prefer-default-export`]) +* Limit the maximum number of dependencies a module can have. ([`max-dependencies`]) [`imports-first`]: ./docs/rules/imports-first.md [`no-duplicates`]: ./docs/rules/no-duplicates.md @@ -69,7 +71,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`order`]: ./docs/rules/order.md [`newline-after-import`]: ./docs/rules/newline-after-import.md [`prefer-default-export`]: ./docs/rules/prefer-default-export.md - +[`max-dependencies`]: ./docs/rules/max-dependencies.md ## Installation diff --git a/docs/rules/max-dependencies.md b/docs/rules/max-dependencies.md new file mode 100644 index 000000000..f4aa2a57a --- /dev/null +++ b/docs/rules/max-dependencies.md @@ -0,0 +1,44 @@ +# max-dependencies + +Forbid modules to have too many dependencies (`import` or `require` statements). + +This is a useful rule because a module with too many dependencies is a code smell, and usually indicates the module is doing too much and/or should be broken up into smaller modules. + +Importing multiple named exports from a single module will only count once (e.g. `import {x, y, z} from './foo'` will only count as a single dependency). + +### Options + +This rule takes the following option: + +`max`: The maximum number of dependencies allowed. Anything over will trigger the rule. **Default is 10** if the rule is enabled and no `max` is specified. + +You can set the option like this: + +```js +"import/max-dependencies": ["error", {"max": 10}] +``` + + +## Example + +Given a max value of `{"max": 2}`: + +### Fail + +```js +import a from './a'; // 1 +const b = require('./b'); // 2 +import c from './c'; // 3 - exceeds max! +``` + +### Pass + +```js +import a from './a'; // 1 +const anotherA = require('./a'); // still 1 +import {x, y, z} from './foo'; // 2 +``` + +## When Not To Use It + +If you don't care how many dependencies a module has. diff --git a/src/index.js b/src/index.js index 52d5668c5..16d23da7e 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,7 @@ export const rules = { 'no-amd': require('./rules/no-amd'), 'no-duplicates': require('./rules/no-duplicates'), 'imports-first': require('./rules/imports-first'), + 'max-dependencies': require('./rules/max-dependencies'), 'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'), 'no-nodejs-modules': require('./rules/no-nodejs-modules'), 'order': require('./rules/order'), diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js new file mode 100644 index 000000000..396e21e54 --- /dev/null +++ b/src/rules/max-dependencies.js @@ -0,0 +1,49 @@ +import Set from 'es6-set' +import isStaticRequire from '../core/staticRequire' + +const DEFAULT_MAX = 10 + +const countDependencies = (dependencies, lastNode, context) => { + const {max} = context.options[0] || { max: DEFAULT_MAX } + + if (dependencies.size > max) { + context.report( + lastNode, + `Maximum number of dependencies (${max}) exceeded.` + ) + } +} + +module.exports = context => { + const dependencies = new Set() // keep track of dependencies + let lastNode // keep track of the last node to report on + + return { + ImportDeclaration(node) { + dependencies.add(node.source.value) + lastNode = node.source + }, + + CallExpression(node) { + if (isStaticRequire(node)) { + const [ requirePath ] = node.arguments + dependencies.add(requirePath.value) + lastNode = node + } + }, + + 'Program:exit': function () { + countDependencies(dependencies, lastNode, context) + }, + } +} + +module.exports.schema = [ + { + 'type': 'object', + 'properties': { + 'max': { 'type': 'number' }, + }, + 'additionalProperties': false, + }, +] diff --git a/tests/src/rules/max-dependencies.js b/tests/src/rules/max-dependencies.js new file mode 100644 index 000000000..7377c1451 --- /dev/null +++ b/tests/src/rules/max-dependencies.js @@ -0,0 +1,78 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() + , rule = require('rules/max-dependencies') + +ruleTester.run('max-dependencies', rule, { + valid: [ + test({ code: 'import "./foo.js"' }), + + test({ code: 'import "./foo.js"; import "./bar.js";', + options: [{ + max: 2, + }], + }), + + test({ code: 'import "./foo.js"; import "./bar.js"; const a = require("./foo.js"); const b = require("./bar.js");', + options: [{ + max: 2, + }], + }), + + test({ code: 'import {x, y, z} from "./foo"'}), + ], + invalid: [ + test({ + code: 'import { x } from \'./foo\'; import { y } from \'./foo\'; import {z} from \'./bar\';', + options: [{ + max: 1, + }], + errors: [ + 'Maximum number of dependencies (1) exceeded.', + ], + }), + + test({ + code: 'import { x } from \'./foo\'; import { y } from \'./bar\'; import { z } from \'./baz\';', + options: [{ + max: 2, + }], + errors: [ + 'Maximum number of dependencies (2) exceeded.', + ], + }), + + test({ + code: 'import { x } from \'./foo\'; require("./bar"); import { z } from \'./baz\';', + options: [{ + max: 2, + }], + errors: [ + 'Maximum number of dependencies (2) exceeded.', + ], + }), + + test({ + code: 'import { x } from \'./foo\'; import { z } from \'./foo\'; require("./bar"); const path = require("path");', + options: [{ + max: 2, + }], + errors: [ + 'Maximum number of dependencies (2) exceeded.', + ], + }), + + test({ + code: 'import type { x } from \'./foo\'; import type { y } from \'./bar\'', + parser: 'babel-eslint', + options: [{ + max: 1, + }], + errors: [ + 'Maximum number of dependencies (1) exceeded.', + ], + }), + ], +})