Skip to content

Commit 20b6bbc

Browse files
committed
Merge pull request #241 from jfmengels/no-extraneous-dependencies
Add `no-extraneous-dependencies` rule
2 parents 60ceb16 + 73ff01e commit 20b6bbc

13 files changed

Lines changed: 349 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
66
## [Unreleased]
77
### Added
88
- [`no-named-as-default-member`] to `warnings` canned config
9+
- add [`no-extraneous-dependencies`] rule
910

1011
## [1.5.0] - 2016-04-18
1112
### Added

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
4848
* Ensure all imports appear before other statements ([`imports-first`])
4949
* Report repeated import of the same module in multiple places ([`no-duplicates`])
5050
* Report namespace imports ([`no-namespace`])
51+
* Forbid the use of extraneous packages ([`no-extraneous-dependencies`])
5152

5253
[`imports-first`]: ./docs/rules/imports-first.md
5354
[`no-duplicates`]: ./docs/rules/no-duplicates.md
5455
[`no-namespace`]: ./docs/rules/no-namespace.md
56+
[`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md
5557

5658

5759
## Installation
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Forbid the use of extraneous packages
2+
3+
Forbid the import of external modules that are not declared in the `package.json`'s `dependencies` or `devDependencies`.
4+
The closest parent `package.json` will be used. If no `package.json` is found, the rule will not lint anything.
5+
6+
### Options
7+
8+
This rule supports the following options:
9+
10+
`devDependencies`: If set to `false`, then the rule will show an error when `devDependencies` are imported. Defaults to `true`.
11+
12+
You can set the options like this:
13+
14+
```js
15+
"import/no-extraneous-dependencies": ["error", {"devDependencies": false}]
16+
```
17+
18+
19+
## Rule Details
20+
21+
Given the following `package.json`:
22+
```json
23+
{
24+
"name": "my-project",
25+
"...": "...",
26+
"dependencies": {
27+
"builtin-modules": "^1.1.1",
28+
"lodash.cond": "^4.2.0",
29+
"lodash.find": "^4.2.0",
30+
"pkg-up": "^1.0.0"
31+
},
32+
"devDependencies": {
33+
"ava": "^0.13.0",
34+
"eslint": "^2.4.0",
35+
"eslint-plugin-ava": "^1.3.0",
36+
"xo": "^0.13.0"
37+
}
38+
}
39+
```
40+
41+
42+
## Fail
43+
44+
```js
45+
var _ = require('lodash');
46+
import _ from 'lodash';
47+
48+
/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": false}] */
49+
import test from 'ava';
50+
var test = require('ava');
51+
```
52+
53+
54+
## Pass
55+
56+
```js
57+
// Builtin and internal modules are fine
58+
var path = require('path');
59+
var foo = require('./foo');
60+
61+
import test from 'ava';
62+
import find from 'lodash.find';
63+
```
64+
65+
66+
## When Not To Use It
67+
68+
If you do not have a `package.json` file in your project.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,14 @@
6565
"eslint": "2.x"
6666
},
6767
"dependencies": {
68+
"builtin-modules": "^1.1.1",
6869
"doctrine": "1.2.0",
6970
"es6-map": "^0.1.3",
7071
"es6-set": "^0.1.4",
7172
"es6-symbol": "*",
7273
"eslint-import-resolver-node": "^0.2.0",
73-
"object-assign": "^4.0.1"
74+
"lodash.cond": "^4.3.0",
75+
"object-assign": "^4.0.1",
76+
"pkg-up": "^1.0.0"
7477
}
7578
}

src/core/importType.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import cond from 'lodash.cond'
2+
import builtinModules from 'builtin-modules'
3+
import { basename, join } from 'path'
4+
5+
import resolve from './resolve'
6+
7+
function constant(value) {
8+
return () => value
9+
}
10+
11+
function isBuiltIn(name) {
12+
return builtinModules.indexOf(name) !== -1
13+
}
14+
15+
const externalModuleRegExp = /^\w/
16+
function isExternalModule(name, path) {
17+
if (!externalModuleRegExp.test(name)) return false
18+
return (!path || -1 < path.indexOf(join('node_modules', name)))
19+
}
20+
21+
function isProjectModule(name, path) {
22+
if (!externalModuleRegExp.test(name)) return false
23+
return (path && -1 === path.indexOf(join('node_modules', name)))
24+
}
25+
26+
function isRelativeToParent(name) {
27+
return name.indexOf('../') === 0
28+
}
29+
30+
const indexFiles = ['.', './', './index', './index.js']
31+
function isIndex(name, path) {
32+
if (path) return basename(path).split('.')[0] === 'index'
33+
return indexFiles.indexOf(name) !== -1
34+
}
35+
36+
function isRelativeToSibling(name) {
37+
return name.indexOf('./') === 0
38+
}
39+
40+
const typeTest = cond([
41+
[isBuiltIn, constant('builtin')],
42+
[isExternalModule, constant('external')],
43+
[isProjectModule, constant('project')],
44+
[isRelativeToParent, constant('parent')],
45+
[isIndex, constant('index')],
46+
[isRelativeToSibling, constant('sibling')],
47+
[constant(true), constant('unknown')],
48+
])
49+
50+
export default function resolveImportType(name, context) {
51+
return typeTest(name, resolve(name, context))
52+
}

src/core/staticRequire.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// todo: merge with module visitor
2+
export default function isStaticRequire(node) {
3+
return node &&
4+
node.callee.type === 'Identifier' &&
5+
node.callee.name === 'require' &&
6+
node.arguments.length === 1 &&
7+
node.arguments[0].type === 'Literal'
8+
}

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const rules = {
1313
'no-amd': require('./rules/no-amd'),
1414
'no-duplicates': require('./rules/no-duplicates'),
1515
'imports-first': require('./rules/imports-first'),
16+
'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'),
1617

1718
// metadata-based
1819
'no-deprecated': require('./rules/no-deprecated'),
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import fs from 'fs'
2+
import pkgUp from 'pkg-up'
3+
import importType from '../core/importType'
4+
import isStaticRequire from '../core/staticRequire'
5+
6+
function getDependencies(context) {
7+
const filepath = pkgUp.sync(context.getFilename())
8+
if (!filepath) {
9+
return null
10+
}
11+
12+
try {
13+
const packageContent = JSON.parse(fs.readFileSync(filepath, 'utf8'))
14+
return {
15+
dependencies: packageContent.dependencies || {},
16+
devDependencies: packageContent.devDependencies || {},
17+
}
18+
} catch (e) {
19+
return null
20+
}
21+
}
22+
23+
function missingErrorMessage(packageName) {
24+
return `'${packageName}' is not listed in the project's dependencies. ` +
25+
`Run 'npm i -S ${packageName}' to add it`
26+
}
27+
28+
function devDepErrorMessage(packageName) {
29+
return `'${packageName}' is not listed in the project's dependencies, not devDependencies.`
30+
}
31+
32+
function reportIfMissing(context, deps, allowDevDeps, node, name) {
33+
if (importType(name, context) !== 'external') {
34+
return
35+
}
36+
const packageName = name.split('/')[0]
37+
38+
if (deps.dependencies[packageName] === undefined) {
39+
if (!allowDevDeps) {
40+
context.report(node, devDepErrorMessage(packageName))
41+
} else if (deps.devDependencies[packageName] === undefined) {
42+
context.report(node, missingErrorMessage(packageName))
43+
}
44+
}
45+
}
46+
47+
module.exports = function (context) {
48+
const options = context.options[0] || {}
49+
const allowDevDeps = options.devDependencies !== false
50+
const deps = getDependencies(context)
51+
52+
if (!deps) {
53+
return {}
54+
}
55+
56+
// todo: use module visitor from module-utils core
57+
return {
58+
ImportDeclaration: function (node) {
59+
reportIfMissing(context, deps, allowDevDeps, node, node.source.value)
60+
},
61+
CallExpression: function handleRequires(node) {
62+
if (isStaticRequire(node)) {
63+
reportIfMissing(context, deps, allowDevDeps, node, node.arguments[0].value)
64+
}
65+
},
66+
}
67+
}
68+
69+
module.exports.schema = [
70+
{
71+
'type': 'object',
72+
'properties': {
73+
'devDependencies': { 'type': 'boolean' },
74+
},
75+
'additionalProperties': false,
76+
},
77+
]

tests/.eslintrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ env:
33
mocha: true
44
rules:
55
no-unused-expressions: 0
6-
quotes: [2, 'single', 'avoid-escape']
76
max-len: 0

tests/files/importType/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* for importType test, just needs to exist */

0 commit comments

Comments
 (0)