-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement new rule: no-reaching-inside
- Loading branch information
spalger
committed
Aug 12, 2016
1 parent
90dedd7
commit 8a96faf
Showing
9 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 '' | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
], | ||
}), | ||
], | ||
}) |