Skip to content

Commit 815b43e

Browse files
committed
Add no-extraneous-dependencies rule
1 parent 60ceb16 commit 815b43e

File tree

7 files changed

+223
-1
lines changed

7 files changed

+223
-1
lines changed

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

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict'
2+
3+
import cond from 'lodash.cond'
4+
import builtinModules from 'builtin-modules'
5+
6+
function constant(value) {
7+
return () => value
8+
}
9+
10+
function isBuiltIn(name) {
11+
return builtinModules.indexOf(name) !== -1
12+
}
13+
14+
const externalModuleRegExp = /^\w/
15+
function isExternalModule(name) {
16+
return externalModuleRegExp.test(name)
17+
}
18+
19+
function isRelativeToParent(name) {
20+
return name.indexOf('../') === 0
21+
}
22+
23+
const indexFiles = ['.', './', './index', './index.js']
24+
function isIndex(name) {
25+
return indexFiles.indexOf(name) !== -1
26+
}
27+
28+
function isRelativeToSibling(name) {
29+
return name.indexOf('./') === 0
30+
}
31+
32+
export default cond([
33+
[isBuiltIn, constant('builtin')],
34+
[isExternalModule, constant('external')],
35+
[isRelativeToParent, constant('parent')],
36+
[isIndex, constant('index')],
37+
[isRelativeToSibling, constant('sibling')],
38+
[constant(true), constant('unknown')],
39+
])

src/core/staticRequire.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function isStaticRequire(node) {
2+
return node &&
3+
node.callee.type === 'Identifier' &&
4+
node.callee.name === 'require' &&
5+
node.arguments.length === 1 &&
6+
node.arguments[0].type === 'Literal'
7+
}

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'),
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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() {
7+
const filepath = pkgUp.sync()
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) !== '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()
51+
52+
if (!deps) {
53+
return {}
54+
}
55+
56+
return {
57+
ImportDeclaration: function (node) {
58+
reportIfMissing(context, deps, allowDevDeps, node, node.source.value)
59+
},
60+
CallExpression: function handleRequires(node) {
61+
if (isStaticRequire(node)) {
62+
reportIfMissing(context, deps, allowDevDeps, node, node.arguments[0].value)
63+
}
64+
},
65+
}
66+
}
67+
68+
module.exports.schema = [
69+
{
70+
'type': 'object',
71+
'properties': {
72+
'devDependencies': { 'type': 'boolean' },
73+
},
74+
'additionalProperties': false,
75+
},
76+
]

tests/src/core/importType.js

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

0 commit comments

Comments
 (0)