Skip to content

Commit c57beae

Browse files
spalgerbenmosher
authored andcommitted
Implement new rule: no-internal-modules (#485)
* Implement new rule: no-reaching-inside * normalize path segments all path segments to '/' * point to minimatch/glob docs to define glob syntax * refactor no-reaching-inside, allow now points to importable modules/files * add test to verify that "no-reaching/allow" can limit the depth of matches * code style improvements * rename to no-internal-modules * remove extra test executor
1 parent 32b5494 commit c57beae

File tree

11 files changed

+274
-0
lines changed

11 files changed

+274
-0
lines changed

docs/rules/no-internal-modules.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# no-internal-modules
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/glob patterns](https://github.com/isaacs/node-glob#glob-primer) patterns that whitelist paths and import statements that can be imported with reaching.
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-internal-modules": [ "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.js
47+
*/
48+
49+
import { settings } from './app/index'; // Reaching to "./app/index" is not allowed
50+
import userReducer from './reducer/user'; // Reaching to "./reducer/user" is not allowed
51+
import configureStore from './redux/configureStore'; // Reaching to "./redux/configureStore" is not allowed
52+
```
53+
54+
The following patterns are NOT considered problems:
55+
56+
```js
57+
/**
58+
* in my-project/entry.js
59+
*/
60+
61+
import 'source-map-support/register';
62+
import { settings } from '../app';
63+
import getUser from '../actions/getUser';
64+
```

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-internal-modules': require('./rules/no-internal-modules'),
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'),

src/rules/no-internal-modules.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import find from 'lodash.find'
2+
import minimatch from 'minimatch'
3+
4+
import resolve from '../core/resolve'
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 allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p))
11+
12+
// test if reaching to this destination is allowed
13+
function reachingAllowed(importPath) {
14+
return !!find(allowRegexps, re => re.test(importPath))
15+
}
16+
17+
// minimatch patterns are expected to use / path separators, like import
18+
// statements, so normalize paths to use the same
19+
function normalizeSep(somePath) {
20+
return somePath.split('\\').join('/')
21+
}
22+
23+
// find a directory that is being reached into, but which shouldn't be
24+
function isReachViolation(importPath) {
25+
const steps = normalizeSep(importPath)
26+
.split('/')
27+
.reduce((acc, step) => {
28+
if (!step || step === '.') {
29+
return acc
30+
} else if (step === '..') {
31+
return acc.slice(0, -1)
32+
} else {
33+
return acc.concat(step)
34+
}
35+
}, [])
36+
37+
if (steps.length <= 1) return false
38+
39+
// before trying to resolve, see if the raw import (with relative
40+
// segments resolved) matches an allowed pattern
41+
const justSteps = steps.join('/')
42+
if (reachingAllowed(justSteps) || reachingAllowed(`/${justSteps}`)) return false
43+
44+
// if the import statement doesn't match directly, try to match the
45+
// resolved path if the import is resolvable
46+
const resolved = resolve(importPath, context)
47+
if (!resolved || reachingAllowed(normalizeSep(resolved))) return false
48+
49+
// this import was not allowed by the allowed paths, and reaches
50+
// so it is a violation
51+
return true
52+
}
53+
54+
function checkImportForReaching(importPath, node) {
55+
const potentialViolationTypes = ['parent', 'index', 'sibling', 'external', 'internal']
56+
if (potentialViolationTypes.indexOf(importType(importPath, context)) !== -1 &&
57+
isReachViolation(importPath)
58+
) {
59+
context.report({
60+
node,
61+
message: `Reaching to "${importPath}" is not allowed.`,
62+
})
63+
}
64+
}
65+
66+
return {
67+
ImportDeclaration(node) {
68+
checkImportForReaching(node.source.value, node.source)
69+
},
70+
CallExpression(node) {
71+
if (isStaticRequire(node)) {
72+
const [ firstArgument ] = node.arguments
73+
checkImportForReaching(firstArgument.value, firstArgument)
74+
}
75+
},
76+
}
77+
}
78+
79+
module.exports.schema = [
80+
{
81+
type: 'object',
82+
properties: {
83+
allow: {
84+
type: 'array',
85+
items: {
86+
type: 'string',
87+
},
88+
},
89+
},
90+
additionalProperties: false,
91+
},
92+
]

tests/files/internal-modules/api/service/index.js

Whitespace-only changes.

tests/files/internal-modules/plugins/plugin.js

Whitespace-only changes.

tests/files/internal-modules/plugins/plugin2/app/index.js

Whitespace-only changes.

tests/files/internal-modules/plugins/plugin2/index.js

Whitespace-only changes.

tests/files/internal-modules/plugins/plugin2/internal.js

Whitespace-only changes.

tests/files/node_modules/jquery/dist/jquery.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/files/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"dependencies": {
1111
"@scope/core": "^1.0.0",
12+
"jquery": "^3.1.0",
1213
"lodash.cond": "^4.3.0",
1314
"pkg-up": "^1.0.0"
1415
},
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { RuleTester } from 'eslint'
2+
import rule from 'rules/no-internal-modules'
3+
4+
import { test, testFilePath } from '../utils'
5+
6+
const ruleTester = new RuleTester()
7+
8+
ruleTester.run('no-internal-modules', rule, {
9+
valid: [
10+
test({
11+
code: 'import a from "./plugin2"',
12+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
13+
options: [],
14+
}),
15+
test({
16+
code: 'const a = require("./plugin2")',
17+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
18+
}),
19+
test({
20+
code: 'const a = require("./plugin2/")',
21+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
22+
}),
23+
test({
24+
code: 'const dynamic = "./plugin2/"; const a = require(dynamic)',
25+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
26+
}),
27+
test({
28+
code: 'import b from "./internal.js"',
29+
filename: testFilePath('./internal-modules/plugins/plugin2/index.js'),
30+
}),
31+
test({
32+
code: 'import get from "lodash.get"',
33+
filename: testFilePath('./internal-modules/plugins/plugin2/index.js'),
34+
}),
35+
test({
36+
code: 'import b from "../../api/service"',
37+
filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'),
38+
options: [ {
39+
allow: [ '**/api/*' ],
40+
} ],
41+
}),
42+
test({
43+
code: 'import "jquery/dist/jquery"',
44+
filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'),
45+
options: [ {
46+
allow: [ 'jquery/dist/*' ],
47+
} ],
48+
}),
49+
test({
50+
code: 'import "./app/index.js";\nimport "./app/index"',
51+
filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'),
52+
options: [ {
53+
allow: [ '**/index{.js,}' ],
54+
} ],
55+
}),
56+
],
57+
58+
invalid: [
59+
test({
60+
code: 'import "./plugin2/index.js";\nimport "./plugin2/app/index"',
61+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
62+
options: [ {
63+
allow: [ '*/index.js' ],
64+
} ],
65+
errors: [ {
66+
message: 'Reaching to "./plugin2/app/index" is not allowed.',
67+
line: 2,
68+
column: 8,
69+
} ],
70+
}),
71+
test({
72+
code: 'import "./app/index.js"',
73+
filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'),
74+
errors: [ {
75+
message: 'Reaching to "./app/index.js" is not allowed.',
76+
line: 1,
77+
column: 8,
78+
} ],
79+
}),
80+
test({
81+
code: 'import b from "./plugin2/internal"',
82+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
83+
errors: [ {
84+
message: 'Reaching to "./plugin2/internal" is not allowed.',
85+
line: 1,
86+
column: 15,
87+
} ],
88+
}),
89+
test({
90+
code: 'import a from "../api/service/index"',
91+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
92+
options: [ {
93+
allow: [ '**/internal-modules/*' ],
94+
} ],
95+
errors: [
96+
{
97+
message: 'Reaching to "../api/service/index" is not allowed.',
98+
line: 1,
99+
column: 15,
100+
},
101+
],
102+
}),
103+
test({
104+
code: 'import get from "debug/node"',
105+
filename: testFilePath('./internal-modules/plugins/plugin.js'),
106+
errors: [
107+
{
108+
message: 'Reaching to "debug/node" is not allowed.',
109+
line: 1,
110+
column: 17,
111+
},
112+
],
113+
}),
114+
],
115+
})

0 commit comments

Comments
 (0)