diff --git a/docs/rules/no-reaching-inside.md b/docs/rules/no-reaching-inside.md new file mode 100644 index 0000000000..340f75152c --- /dev/null +++ b/docs/rules/no-reaching-inside.md @@ -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 '' +``` diff --git a/package.json b/package.json index 165bdf1d4a..722f7511dd 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 52d5668c53..85ebf104dc 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 0000000000..369a623903 --- /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 0000000000..e69de29bb2 diff --git a/tests/files/reaching-inside/plugins/plugin.js b/tests/files/reaching-inside/plugins/plugin.js new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..e69de29bb2 diff --git a/tests/src/rules/no-reaching-inside.js b/tests/src/rules/no-reaching-inside.js new file mode 100644 index 0000000000..8266654221 --- /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, + }, + ], + }), + ], +})