Skip to content

Commit f072e8a

Browse files
author
spalger
committed
Implement new rule: no-reaching-inside
1 parent 90dedd7 commit f072e8a

File tree

9 files changed

+259
-0
lines changed

9 files changed

+259
-0
lines changed

Diff for: docs/rules/no-reaching-inside.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# no-reaching-inside
2+
3+
Use this rule to prevent importing the submodules of other modules.
4+
5+
## Rule Details
6+
7+
This rule has one option, `allow` which is an array of minimatch patterns to identify directories whose children can be imported explicitly.
8+
9+
### Examples
10+
11+
Given the following folder structure:
12+
13+
```
14+
my-project
15+
├── actions
16+
│ └── getUser.js
17+
│ └── updateUser.js
18+
├── reducer
19+
│ └── index.js
20+
│ └── user.js
21+
├── redux
22+
│ └── index.js
23+
│ └── configureStore.js
24+
└── app
25+
│ └── index.js
26+
│ └── settings.js
27+
└── entry.js
28+
```
29+
30+
And the .eslintrc file:
31+
```
32+
{
33+
...
34+
"rules": {
35+
"import/no-reaching-inside": [ "error", {
36+
"allow": [ "**/actions", "source-map-support/*" ]
37+
} ]
38+
}
39+
}
40+
```
41+
42+
The following patterns are considered problems:
43+
44+
```js
45+
/**
46+
* in my-project/entry.jz
47+
*/
48+
49+
import { settings } from './app/index'; // Reaching into "./app" is not allowed
50+
import userReducer from './reducer/user'; // Reaching into "./reducer" is not allowed
51+
import configureStore from './redux/configureStore'; // Reaching into "./redux" is not allowed
52+
```
53+
54+
The following patterns are NOT considered problems:
55+
56+
```js
57+
/**
58+
* in my-project/entry.jz
59+
*/
60+
61+
import 'source-map-support/register';
62+
import { settings } from '../app';
63+
import getUser from '../actions/getUser';
64+
```

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"lodash.endswith": "^4.0.1",
8080
"lodash.find": "^4.3.0",
8181
"lodash.findindex": "^4.3.0",
82+
"minimatch": "^3.0.3",
8283
"object-assign": "^4.0.1",
8384
"pkg-dir": "^1.0.0",
8485
"pkg-up": "^1.0.0"

Diff for: src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const rules = {
88
'no-mutable-exports': require('./rules/no-mutable-exports'),
99
'extensions': require('./rules/extensions'),
1010
'no-restricted-paths': require('./rules/no-restricted-paths'),
11+
'no-reaching-inside': require('./rules/no-reaching-inside'),
1112

1213
'no-named-as-default': require('./rules/no-named-as-default'),
1314
'no-named-as-default-member': require('./rules/no-named-as-default-member'),

Diff for: src/rules/no-reaching-inside.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import path from 'path'
2+
import find from 'lodash.find'
3+
import minimatch from 'minimatch'
4+
5+
import importType from '../core/importType'
6+
import isStaticRequire from '../core/staticRequire'
7+
8+
module.exports = function noReachingInside(context) {
9+
const options = context.options[0] || {}
10+
const dirname = path.dirname(context.getFilename())
11+
const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p))
12+
13+
// test if reaching into this directory is allowed by the
14+
// config, path.sep is automatically added so that globs like
15+
// "lodash/**" will match both "lodash" (which requires the trailing /) and "lodash/get"
16+
function reachingAllowed(someDir) {
17+
return !!find(allowRegexps, re => re.test(someDir) || re.test(someDir + path.sep))
18+
}
19+
20+
function isRelativeStep (step) {
21+
return step === '' || step === '.' || step === '..'
22+
}
23+
24+
function report(reachedTo, node) {
25+
context.report({
26+
node,
27+
message: `Reaching into "${reachedTo}" is not allowed.`,
28+
})
29+
}
30+
31+
function findNotAllowedReach(importPath, startingBase, join, ignoreStep) {
32+
const steps = importPath.split('/').filter(Boolean)
33+
let parentDir = startingBase
34+
while (steps.length) {
35+
const step = steps.shift()
36+
parentDir = join(parentDir, step)
37+
38+
if (ignoreStep && ignoreStep(step)) continue
39+
40+
if (steps.length) {
41+
if (!reachingAllowed(parentDir)) {
42+
return parentDir
43+
}
44+
}
45+
}
46+
}
47+
48+
function checkRelativeImportForReaching(importPath, node) {
49+
const reachedInto = findNotAllowedReach(importPath, dirname, path.resolve, isRelativeStep)
50+
if (reachedInto) report(path.relative(dirname, reachedInto), node)
51+
}
52+
53+
function checkAbsoluteImportForReaching(importPath, node) {
54+
const reachedInto = findNotAllowedReach(importPath, '', path.join)
55+
if (reachedInto) report(reachedInto, node)
56+
}
57+
58+
function checkImportForReaching(importPath, node) {
59+
switch (importType(importPath, context)) {
60+
case 'parent':
61+
case 'index':
62+
case 'sibling':
63+
return checkRelativeImportForReaching(importPath, node)
64+
65+
case 'external':
66+
case 'internal':
67+
return checkAbsoluteImportForReaching(importPath, node)
68+
default:
69+
return
70+
}
71+
}
72+
73+
return {
74+
ImportDeclaration(node) {
75+
checkImportForReaching(node.source.value, node.source)
76+
},
77+
CallExpression(node) {
78+
if (isStaticRequire(node)) {
79+
const [ firstArgument ] = node.arguments
80+
checkImportForReaching(firstArgument.value, firstArgument)
81+
}
82+
},
83+
}
84+
}
85+
86+
module.exports.schema = [
87+
{
88+
type: 'object',
89+
properties: {
90+
allow: {
91+
type: 'array',
92+
items: {
93+
type: 'string',
94+
},
95+
},
96+
},
97+
additionalProperties: false,
98+
},
99+
]

Diff for: tests/files/reaching-inside/api/service/index.js

Whitespace-only changes.

Diff for: tests/files/reaching-inside/plugins/plugin.js

Whitespace-only changes.

Diff for: tests/files/reaching-inside/plugins/plugin2/index.js

Whitespace-only changes.

Diff for: tests/files/reaching-inside/plugins/plugin2/internal.js

Whitespace-only changes.

Diff for: tests/src/rules/no-reaching-inside.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { RuleTester } from 'eslint'
2+
import rule from 'rules/no-reaching-inside'
3+
4+
import { test, testFilePath } from '../utils'
5+
6+
const ruleTester = new RuleTester()
7+
8+
ruleTester.run('no-reaching-inside', rule, {
9+
valid: [
10+
test({
11+
code: 'import a from "./plugin2"',
12+
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
13+
options: [],
14+
}),
15+
test({
16+
code: 'const a = require("./plugin2")',
17+
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
18+
}),
19+
test({
20+
code: 'const a = require("./plugin2/")',
21+
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
22+
}),
23+
test({
24+
code: 'const dynamic = "./plugin2/"; const a = require(dynamic)',
25+
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
26+
}),
27+
test({
28+
code: 'import b from "./internal.js"',
29+
filename: testFilePath('./reaching-inside/plugins/plugin2/index.js'),
30+
}),
31+
test({
32+
code: 'import get from "lodash.get"',
33+
filename: testFilePath('./reaching-inside/plugins/plugin2/index.js'),
34+
}),
35+
test({
36+
code: 'import b from "../../api/service"',
37+
filename: testFilePath('./reaching-inside/plugins/plugin2/internal.js'),
38+
options: [ {
39+
allow: [ '**/api' ],
40+
} ],
41+
}),
42+
test({
43+
code: 'import "jquery/dist/jquery"',
44+
filename: testFilePath('./reaching-inside/plugins/plugin2/internal.js'),
45+
options: [ {
46+
allow: [ 'jquery/**' ],
47+
} ],
48+
}),
49+
test({
50+
code: 'import "/app/index.js"',
51+
filename: testFilePath('./reaching-inside/plugins/plugin2/internal.js'),
52+
options: [ {
53+
allow: [ '/app' ],
54+
} ],
55+
}),
56+
],
57+
58+
invalid: [
59+
test({
60+
code: 'import b from "./plugin2/internal"',
61+
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
62+
errors: [ {
63+
message: 'Reaching into "plugin2" is not allowed.',
64+
line: 1,
65+
column: 15,
66+
} ],
67+
}),
68+
test({
69+
code: 'import a from "../api/service/index"',
70+
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
71+
options: [ {
72+
allow: [ '**/reaching-inside/*' ],
73+
} ],
74+
errors: [
75+
{
76+
message: 'Reaching into "../api/service" is not allowed.',
77+
line: 1,
78+
column: 15,
79+
},
80+
],
81+
}),
82+
test({
83+
code: 'import get from "lodash/get"',
84+
filename: testFilePath('./reaching-inside/plugins/plugin.js'),
85+
errors: [
86+
{
87+
message: 'Reaching into "lodash" is not allowed.',
88+
line: 1,
89+
column: 17,
90+
},
91+
],
92+
}),
93+
],
94+
})

0 commit comments

Comments
 (0)