Skip to content

Commit

Permalink
Add no-extraneous-dependencies rule
Browse files Browse the repository at this point in the history
  • Loading branch information
jfmengels committed Apr 13, 2016
1 parent de93a27 commit b3ac0f8
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 1 deletion.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@
"eslint": "2.x"
},
"dependencies": {
"builtin-modules": "^1.1.1",
"doctrine": "1.2.0",
"es6-map": "^0.1.3",
"es6-set": "^0.1.4",
"es6-symbol": "*",
"eslint-import-resolver-node": "^0.2.0",
"object-assign": "^4.0.1"
"lodash.cond": "^4.3.0",
"object-assign": "^4.0.1",
"pkg-up": "^1.0.0"
}
}
39 changes: 39 additions & 0 deletions src/core/importType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict'

import cond from 'lodash.cond'
import builtinModules from 'builtin-modules'

function constant(value) {
return () => value
}

function isBuiltIn(name) {
return builtinModules.indexOf(name) !== -1
}

const externalModuleRegExp = /^\w/
function isExternalModule(name) {
return externalModuleRegExp.test(name)
}

function isRelativeToParent(name) {
return name.indexOf('../') === 0
}

const indexFiles = ['.', './', './index', './index.js']
function isIndex(name) {
return indexFiles.indexOf(name) !== -1
}

function isRelativeToSibling(name) {
return name.indexOf('./') === 0
}

export default cond([
[isBuiltIn, constant('builtin')],
[isExternalModule, constant('external')],
[isRelativeToParent, constant('parent')],
[isIndex, constant('index')],
[isRelativeToSibling, constant('sibling')],
[constant(true), constant('unknown')],
])
7 changes: 7 additions & 0 deletions src/core/staticRequire.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function isStaticRequire(node) {
return node &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'Literal'
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const rules = {
'no-amd': require('./rules/no-amd'),
'no-duplicates': require('./rules/no-duplicates'),
'imports-first': require('./rules/imports-first'),
'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'),

// metadata-based
'no-deprecated': require('./rules/no-deprecated'),
Expand Down
76 changes: 76 additions & 0 deletions src/rules/no-extraneous-dependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import fs from 'fs'
import pkgUp from 'pkg-up'
import importType from '../core/importType'
import isStaticRequire from '../core/staticRequire'

function getDependencies() {
const filepath = pkgUp.sync()
if (!filepath) {
return null
}

try {
const packageContent = JSON.parse(fs.readFileSync(filepath, 'utf8'))
return {
dependencies: packageContent.dependencies || {},
devDependencies: packageContent.devDependencies || {},
}
} catch (e) {
return null
}
}

function missingErrorMessage(packageName) {
return `'${packageName}' is not listed in the project's dependencies. ` +
`Run 'npm i -S ${packageName}' to add it`
}

function devDepErrorMessage(packageName) {
return `'${packageName}' is not listed in the project's dependencies, not devDependencies.`
}

function reportIfMissing(context, deps, allowDevDeps, node, name) {
if (importType(name) !== 'external') {
return
}
const packageName = name.split('/')[0]

if (deps.dependencies[packageName] === undefined) {
if (!allowDevDeps) {
context.report(node, devDepErrorMessage(packageName))
} else if (deps.devDependencies[packageName] === undefined) {
context.report(node, missingErrorMessage(packageName))
}
}
}

module.exports = function (context) {
const options = context.options[0] || {}
const allowDevDeps = options.devDependencies !== false
const deps = getDependencies()

if (!deps) {
return {}
}

return {
ImportDeclaration: function (node) {
reportIfMissing(context, deps, allowDevDeps, node, node.source.value)
},
CallExpression: function handleRequires(node) {
if (isStaticRequire(node)) {
reportIfMissing(context, deps, allowDevDeps, node, node.arguments[0].value)
}
},
}
}

module.exports.schema = [
{
'type': 'object',
'properties': {
'devDependencies': { 'type': 'boolean' },
},
'additionalProperties': false,
},
]
41 changes: 41 additions & 0 deletions tests/src/core/importType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect } from 'chai'
import importType from 'core/importType'

describe('importType(name)', function () {
it("should return 'builtin' for node.js modules", function() {
expect(importType('fs')).to.equal('builtin')
expect(importType('path')).to.equal('builtin')
})

it("should return 'external' for non-builtin modules without a relative path", function() {
expect(importType('lodash')).to.equal('external')
expect(importType('async')).to.equal('external')
expect(importType('chalk')).to.equal('external')
expect(importType('foo')).to.equal('external')
expect(importType('lodash.find')).to.equal('external')
expect(importType('lodash/fp')).to.equal('external')
})

it("should return 'parent' for internal modules that go through the parent", function() {
expect(importType('../foo')).to.equal('parent')
expect(importType('../../foo')).to.equal('parent')
expect(importType('../bar/foo')).to.equal('parent')
})

it("should return 'sibling' for internal modules that are connected to one of the siblings", function() {
expect(importType('./foo')).to.equal('sibling')
expect(importType('./foo/bar')).to.equal('sibling')
})

it("should return 'index' for sibling index file", function() {
expect(importType('.')).to.equal('index')
expect(importType('./')).to.equal('index')
expect(importType('./index')).to.equal('index')
expect(importType('./index.js')).to.equal('index')
})

it("should return 'unknown' for any unhandled cases", function() {
expect(importType('/malformed')).to.equal('unknown')
expect(importType(' foo')).to.equal('unknown')
})
})
55 changes: 55 additions & 0 deletions tests/src/rules/no-extraneous-dependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { test } from '../utils'

import { RuleTester } from 'eslint'

const ruleTester = new RuleTester()
, rule = require('rules/no-extraneous-dependencies')

ruleTester.run('no-extraneous-dependencies', rule, {
valid: [
test({ code: 'import "lodash.cond"'}),
test({ code: 'import "pkg-up"'}),
test({ code: 'import foo, { bar } from "lodash.cond"'}),
test({ code: 'import foo, { bar } from "pkg-up"'}),
test({ code: 'import "eslint"'}),
test({ code: 'import "eslint/lib/api"'}),
test({ code: 'require("lodash.cond")'}),
test({ code: 'require("pkg-up")'}),
test({ code: 'var foo = require("lodash.cond")'}),
test({ code: 'var foo = require("pkg-up")'}),
test({ code: 'import "fs"'}),
test({ code: 'import "./foo"'}),
],
invalid: [
test({
code: 'import "not-a-dependency"',
errors: [{
ruleId: 'no-extraneous-dependencies',
message: '\'not-a-dependency\' is not listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it',
}],
}),
test({
code: 'import "eslint"',
options: [{devDependencies: false}],
errors: [{
ruleId: 'no-extraneous-dependencies',
message: '\'eslint\' is not listed in the project\'s dependencies, not devDependencies.',
}],
}),
test({
code: 'var foo = require("not-a-dependency");',
errors: [{
ruleId: 'no-extraneous-dependencies',
message: '\'not-a-dependency\' is not listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it',
}],
}),
test({
code: 'var eslint = require("eslint");',
options: [{devDependencies: false}],
errors: [{
ruleId: 'no-extraneous-dependencies',
message: '\'eslint\' is not listed in the project\'s dependencies, not devDependencies.',
}],
}),
],
})

0 comments on commit b3ac0f8

Please sign in to comment.