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 8a96faf
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 0 deletions.
58 changes: 58 additions & 0 deletions docs/rules/no-reaching-inside.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# no-reaching-inside - Prevent importing internal modules of a module

Use this rule to ensure that import statements include as many `../` segments as they want, but only one named segment. This way modules can define all of the code they want to export in an index.js file and only the exported values will be accessible outside of the module.

## Rule Details

This rule has one option, `allow` which is an array of minimatch-patterns that will be used to allow addressing the direct children of that directory.

### 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:

***in `my-project/entry.js`***
```js
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:

***in `my-project/entry.js`***
```js
import 'source-map-support/register';
import { settings } from '../app';
import getUser from '../actions/getUser';
import ''
```
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 8a96faf

Please sign in to comment.