Skip to content

Commit 20b6bbc

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

13 files changed

+349
-3
lines changed
 

‎CHANGELOG.md

+1
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

+2
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
+68
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

+4-1
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

+52
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

+8
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

+1
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'),
+77
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

-1
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

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* for importType test, just needs to exist */

‎tests/files/package.json

+13-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-
{ "dummy": true }
1+
{
2+
"dummy": true,
3+
"devDependencies": {
4+
"eslint": "2.x"
5+
},
6+
"peerDependencies": {
7+
"eslint": "2.x"
8+
},
9+
"dependencies": {
10+
"lodash.cond": "^4.3.0",
11+
"pkg-up": "^1.0.0"
12+
}
13+
}

‎tests/src/core/importType.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { expect } from 'chai'
2+
import * as path from 'path'
3+
4+
import importType from 'core/importType'
5+
6+
import { testContext } from '../utils'
7+
8+
describe('importType(name)', function () {
9+
const context = testContext()
10+
11+
it("should return 'builtin' for node.js modules", function() {
12+
expect(importType('fs', context)).to.equal('builtin')
13+
expect(importType('path', context)).to.equal('builtin')
14+
})
15+
16+
it("should return 'external' for non-builtin modules without a relative path", function() {
17+
expect(importType('lodash', context)).to.equal('external')
18+
expect(importType('async', context)).to.equal('external')
19+
expect(importType('chalk', context)).to.equal('external')
20+
expect(importType('foo', context)).to.equal('external')
21+
expect(importType('lodash.find', context)).to.equal('external')
22+
expect(importType('lodash/fp', context)).to.equal('external')
23+
})
24+
25+
it("should return 'project' for non-builtins resolved outside of node_modules", function () {
26+
const pathContext = testContext({ "import/resolver": { node: { paths: [ path.join(__dirname, '..', '..', 'files') ] } } })
27+
expect(importType('importType', pathContext)).to.equal('project')
28+
})
29+
30+
it("should return 'parent' for internal modules that go through the parent", function() {
31+
expect(importType('../foo', context)).to.equal('parent')
32+
expect(importType('../../foo', context)).to.equal('parent')
33+
expect(importType('../bar/foo', context)).to.equal('parent')
34+
})
35+
36+
it("should return 'sibling' for internal modules that are connected to one of the siblings", function() {
37+
expect(importType('./foo', context)).to.equal('sibling')
38+
expect(importType('./foo/bar', context)).to.equal('sibling')
39+
})
40+
41+
describe("should return 'index' for sibling index file when", function() {
42+
it("resolves", function() {
43+
expect(importType('./importType', context)).to.equal('index')
44+
expect(importType('./importType/', context)).to.equal('index')
45+
expect(importType('./importType/index', context)).to.equal('index')
46+
expect(importType('./importType/index.js', context)).to.equal('index')
47+
})
48+
it("doesn't resolve", function() {
49+
expect(importType('.', context)).to.equal('index')
50+
expect(importType('./', context)).to.equal('index')
51+
expect(importType('./index', context)).to.equal('index')
52+
expect(importType('./index.js', context)).to.equal('index')
53+
})
54+
})
55+
56+
it("should return 'unknown' for any unhandled cases", function() {
57+
expect(importType('/malformed', context)).to.equal('unknown')
58+
expect(importType(' foo', context)).to.equal('unknown')
59+
})
60+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test } from '../utils'
2+
import * as path from 'path'
3+
4+
import { RuleTester } from 'eslint'
5+
6+
const ruleTester = new RuleTester()
7+
, rule = require('rules/no-extraneous-dependencies')
8+
9+
ruleTester.run('no-extraneous-dependencies', rule, {
10+
valid: [
11+
test({ code: 'import "lodash.cond"'}),
12+
test({ code: 'import "pkg-up"'}),
13+
test({ code: 'import foo, { bar } from "lodash.cond"'}),
14+
test({ code: 'import foo, { bar } from "pkg-up"'}),
15+
test({ code: 'import "eslint"'}),
16+
test({ code: 'import "eslint/lib/api"'}),
17+
test({ code: 'require("lodash.cond")'}),
18+
test({ code: 'require("pkg-up")'}),
19+
test({ code: 'var foo = require("lodash.cond")'}),
20+
test({ code: 'var foo = require("pkg-up")'}),
21+
test({ code: 'import "fs"'}),
22+
test({ code: 'import "./foo"'}),
23+
24+
// 'project' type
25+
test({
26+
code: 'import "importType"',
27+
settings: { "import/resolver": { node: { paths: [ path.join(__dirname, '../../files') ] } } },
28+
}),
29+
],
30+
invalid: [
31+
test({
32+
code: 'import "not-a-dependency"',
33+
errors: [{
34+
ruleId: 'no-extraneous-dependencies',
35+
message: '\'not-a-dependency\' is not listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it',
36+
}],
37+
}),
38+
test({
39+
code: 'import "eslint"',
40+
options: [{devDependencies: false}],
41+
errors: [{
42+
ruleId: 'no-extraneous-dependencies',
43+
message: '\'eslint\' is not listed in the project\'s dependencies, not devDependencies.',
44+
}],
45+
}),
46+
test({
47+
code: 'var foo = require("not-a-dependency");',
48+
errors: [{
49+
ruleId: 'no-extraneous-dependencies',
50+
message: '\'not-a-dependency\' is not listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it',
51+
}],
52+
}),
53+
test({
54+
code: 'var eslint = require("eslint");',
55+
options: [{devDependencies: false}],
56+
errors: [{
57+
ruleId: 'no-extraneous-dependencies',
58+
message: '\'eslint\' is not listed in the project\'s dependencies, not devDependencies.',
59+
}],
60+
}),
61+
],
62+
})

0 commit comments

Comments
 (0)
Please sign in to comment.