Skip to content

Commit

Permalink
Implement new rule: no-reaching-inside
Browse files Browse the repository at this point in the history
  • Loading branch information
spalger committed Aug 12, 2016
1 parent 90dedd7 commit f072e8a
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 0 deletions.
64 changes: 64 additions & 0 deletions docs/rules/no-reaching-inside.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# no-reaching-inside

Use this rule to prevent importing the submodules of other modules.

## Rule Details

This rule has one option, `allow` which is an array of minimatch patterns to identify directories whose children can be imported explicitly.

### Examples

Given the following folder structure:

```
my-project
├── actions
│ └── getUser.js
│ └── updateUser.js
├── reducer
│ └── index.js
│ └── user.js
├── redux
│ └── index.js
│ └── configureStore.js
└── app
│ └── index.js
│ └── settings.js
└── entry.js
```

And the .eslintrc file:
```
{
...
"rules": {
"import/no-reaching-inside": [ "error", {
"allow": [ "**/actions", "source-map-support/*" ]
} ]
}
}
```

The following patterns are considered problems:

```js
/**
* in my-project/entry.jz
*/

import { settings } from './app/index'; // Reaching into "./app" is not allowed
import userReducer from './reducer/user'; // Reaching into "./reducer" is not allowed
import configureStore from './redux/configureStore'; // Reaching into "./redux" is not allowed
```

The following patterns are NOT considered problems:

```js
/**
* in my-project/entry.jz
*/

import 'source-map-support/register';
import { settings } from '../app';
import getUser from '../actions/getUser';
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"lodash.endswith": "^4.0.1",
"lodash.find": "^4.3.0",
"lodash.findindex": "^4.3.0",
"minimatch": "^3.0.3",
"object-assign": "^4.0.1",
"pkg-dir": "^1.0.0",
"pkg-up": "^1.0.0"
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const rules = {
'no-mutable-exports': require('./rules/no-mutable-exports'),
'extensions': require('./rules/extensions'),
'no-restricted-paths': require('./rules/no-restricted-paths'),
'no-reaching-inside': require('./rules/no-reaching-inside'),

'no-named-as-default': require('./rules/no-named-as-default'),
'no-named-as-default-member': require('./rules/no-named-as-default-member'),
Expand Down
99 changes: 99 additions & 0 deletions src/rules/no-reaching-inside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import path from 'path'
import find from 'lodash.find'
import minimatch from 'minimatch'

import importType from '../core/importType'
import isStaticRequire from '../core/staticRequire'

module.exports = function noReachingInside(context) {
const options = context.options[0] || {}
const dirname = path.dirname(context.getFilename())
const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p))

// test if reaching into this directory is allowed by the
// config, path.sep is automatically added so that globs like
// "lodash/**" will match both "lodash" (which requires the trailing /) and "lodash/get"
function reachingAllowed(someDir) {
return !!find(allowRegexps, re => re.test(someDir) || re.test(someDir + path.sep))
}

function isRelativeStep (step) {
return step === '' || step === '.' || step === '..'
}

function report(reachedTo, node) {
context.report({
node,
message: `Reaching into "${reachedTo}" is not allowed.`,
})
}

function findNotAllowedReach(importPath, startingBase, join, ignoreStep) {
const steps = importPath.split('/').filter(Boolean)
let parentDir = startingBase
while (steps.length) {
const step = steps.shift()
parentDir = join(parentDir, step)

if (ignoreStep && ignoreStep(step)) continue

if (steps.length) {
if (!reachingAllowed(parentDir)) {
return parentDir
}
}
}
}

function checkRelativeImportForReaching(importPath, node) {
const reachedInto = findNotAllowedReach(importPath, dirname, path.resolve, isRelativeStep)
if (reachedInto) report(path.relative(dirname, reachedInto), node)
}

function checkAbsoluteImportForReaching(importPath, node) {
const reachedInto = findNotAllowedReach(importPath, '', path.join)
if (reachedInto) report(reachedInto, node)
}

function checkImportForReaching(importPath, node) {
switch (importType(importPath, context)) {
case 'parent':
case 'index':
case 'sibling':
return checkRelativeImportForReaching(importPath, node)

case 'external':
case 'internal':
return checkAbsoluteImportForReaching(importPath, node)
default:
return
}
}

return {
ImportDeclaration(node) {
checkImportForReaching(node.source.value, node.source)
},
CallExpression(node) {
if (isStaticRequire(node)) {
const [ firstArgument ] = node.arguments
checkImportForReaching(firstArgument.value, firstArgument)
}
},
}
}

module.exports.schema = [
{
type: 'object',
properties: {
allow: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
]
Empty file.
Empty file.
Empty file.
Empty file.
94 changes: 94 additions & 0 deletions tests/src/rules/no-reaching-inside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { RuleTester } from 'eslint'
import rule from 'rules/no-reaching-inside'

import { test, testFilePath } from '../utils'

const ruleTester = new RuleTester()

ruleTester.run('no-reaching-inside', rule, {
valid: [
test({
code: 'import a from "./plugin2"',
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
options: [],
}),
test({
code: 'const a = require("./plugin2")',
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
}),
test({
code: 'const a = require("./plugin2/")',
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
}),
test({
code: 'const dynamic = "./plugin2/"; const a = require(dynamic)',
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
}),
test({
code: 'import b from "./internal.js"',
filename: testFilePath('./reaching-inside/plugins/plugin2/index.js'),
}),
test({
code: 'import get from "lodash.get"',
filename: testFilePath('./reaching-inside/plugins/plugin2/index.js'),
}),
test({
code: 'import b from "../../api/service"',
filename: testFilePath('./reaching-inside/plugins/plugin2/internal.js'),
options: [ {
allow: [ '**/api' ],
} ],
}),
test({
code: 'import "jquery/dist/jquery"',
filename: testFilePath('./reaching-inside/plugins/plugin2/internal.js'),
options: [ {
allow: [ 'jquery/**' ],
} ],
}),
test({
code: 'import "/app/index.js"',
filename: testFilePath('./reaching-inside/plugins/plugin2/internal.js'),
options: [ {
allow: [ '/app' ],
} ],
}),
],

invalid: [
test({
code: 'import b from "./plugin2/internal"',
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
errors: [ {
message: 'Reaching into "plugin2" is not allowed.',
line: 1,
column: 15,
} ],
}),
test({
code: 'import a from "../api/service/index"',
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
options: [ {
allow: [ '**/reaching-inside/*' ],
} ],
errors: [
{
message: 'Reaching into "../api/service" is not allowed.',
line: 1,
column: 15,
},
],
}),
test({
code: 'import get from "lodash/get"',
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
errors: [
{
message: 'Reaching into "lodash" is not allowed.',
line: 1,
column: 17,
},
],
}),
],
})

0 comments on commit f072e8a

Please sign in to comment.