diff --git a/docs/rules/no-reaching-inside.md b/docs/rules/no-reaching-inside.md new file mode 100644 index 000000000..29305dc31 --- /dev/null +++ b/docs/rules/no-reaching-inside.md @@ -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'; +``` diff --git a/package.json b/package.json index 165bdf1d4..722f7511d 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/index.js b/src/index.js index 52d5668c5..85ebf104d 100644 --- a/src/index.js +++ b/src/index.js @@ -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'), diff --git a/src/rules/no-reaching-inside.js b/src/rules/no-reaching-inside.js new file mode 100644 index 000000000..369a62390 --- /dev/null +++ b/src/rules/no-reaching-inside.js @@ -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, + }, +] diff --git a/tests/files/reaching-inside/api/service/index.js b/tests/files/reaching-inside/api/service/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/reaching-inside/plugins/plugin.js b/tests/files/reaching-inside/plugins/plugin.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/reaching-inside/plugins/plugin2/index.js b/tests/files/reaching-inside/plugins/plugin2/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/reaching-inside/plugins/plugin2/internal.js b/tests/files/reaching-inside/plugins/plugin2/internal.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/src/rules/no-reaching-inside.js b/tests/src/rules/no-reaching-inside.js new file mode 100644 index 000000000..826665422 --- /dev/null +++ b/tests/src/rules/no-reaching-inside.js @@ -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, + }, + ], + }), + ], +})