diff --git a/CHANGELOG.md b/CHANGELOG.md index ef04278d62..102c6fd355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] ### Added - Autofixer for [`no-duplicates`] rule ([#1312], thanks [@lydell]) +- [`no-restricted-paths`]: New `except` option per `zone`, allowing exceptions to be defined for a restricted zone. ### Fixed - [`order`]: Fix interpreting some external modules being interpreted as internal modules ([#793], [#794] thanks [@ephys]) diff --git a/docs/rules/no-restricted-paths.md b/docs/rules/no-restricted-paths.md index bad65ab8e1..3776699836 100644 --- a/docs/rules/no-restricted-paths.md +++ b/docs/rules/no-restricted-paths.md @@ -9,7 +9,7 @@ In order to prevent such scenarios this rule allows you to define restricted zon This rule has one option. The option is an object containing the definition of all restricted `zones` and the optional `basePath` which is used to resolve relative paths within. The default value for `basePath` is the current working directory. -Each zone consists of the `target` path and a `from` path. The `target` is the path where the restricted imports should be applied. The `from` path defines the folder that is not allowed to be used in an import. +Each zone consists of the `target` path and a `from` path. The `target` is the path where the restricted imports should be applied. The `from` path defines the folder that is not allowed to be used in an import. An optional `except` may be defined for a zone, allowing exception paths that would otherwise violate the related `from`. Note that `except` is relative to `from` and cannot backtrack to a parent directory. ### Examples @@ -37,3 +37,43 @@ The following patterns are not considered problems when configuration set to `{ ```js import baz from '../client/baz'; ``` + +--------------- + +Given the following folder structure: + +``` +my-project +├── client +│ └── foo.js +│ └── baz.js +└── server + ├── one + │ └── a.js + │ └── b.js + └── two +``` + +and the current file being linted is `my-project/server/one/a.js`. + +and the current configuration is set to: + +``` +{ "zones": [ { + "target": "./tests/files/restricted-paths/server/one", + "from": "./tests/files/restricted-paths/server", + "except": ["./one"] +} ] } +``` + +The following pattern is considered a problem: + +```js +import a from '../two/a' +``` + +The following pattern is not considered a problem: + +```js +import b from './b' +``` diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 0d906f6318..dd7f0cb954 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -4,6 +4,7 @@ import path from 'path' import resolve from 'eslint-module-utils/resolve' import isStaticRequire from '../core/staticRequire' import docsUrl from '../docsUrl' +import importType from '../core/importType' module.exports = { meta: { @@ -24,6 +25,13 @@ module.exports = { properties: { target: { type: 'string' }, from: { type: 'string' }, + except: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + }, }, additionalProperties: false, }, @@ -46,6 +54,20 @@ module.exports = { return containsPath(currentFilename, targetPath) }) + function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) { + const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath) + + return importType(relativeExceptionPath, context) !== 'parent' + } + + function reportInvalidExceptionPath(node) { + context.report({ + node, + message: 'Restricted path exceptions must be descendants of the configured ' + + '`from` path for that zone.', + }) + } + function checkForRestrictedImportPath(importPath, node) { const absoluteImportPath = resolve(importPath, context) @@ -54,14 +76,37 @@ module.exports = { } matchingZones.forEach((zone) => { + const exceptionPaths = zone.except || [] const absoluteFrom = path.resolve(basePath, zone.from) - if (containsPath(absoluteImportPath, absoluteFrom)) { - context.report({ - node, - message: `Unexpected path "${importPath}" imported in restricted zone.`, - }) + if (!containsPath(absoluteImportPath, absoluteFrom)) { + return + } + + const absoluteExceptionPaths = exceptionPaths.map((exceptionPath) => + path.resolve(absoluteFrom, exceptionPath) + ) + const hasValidExceptionPaths = absoluteExceptionPaths.every((absoluteExceptionPath) => + isValidExceptionPath(absoluteFrom, absoluteExceptionPath) + ) + + if (!hasValidExceptionPaths) { + reportInvalidExceptionPath(node) + return + } + + const pathIsExcepted = absoluteExceptionPaths.some((absoluteExceptionPath) => + containsPath(absoluteImportPath, absoluteExceptionPath) + ) + + if (pathIsExcepted) { + return } + + context.report({ + node, + message: `Unexpected path "${importPath}" imported in restricted zone.`, + }) }) } diff --git a/tests/files/restricted-paths/server/one/a.js b/tests/files/restricted-paths/server/one/a.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/restricted-paths/server/one/b.js b/tests/files/restricted-paths/server/one/b.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/restricted-paths/server/two/a.js b/tests/files/restricted-paths/server/two/a.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/src/rules/no-restricted-paths.js b/tests/src/rules/no-restricted-paths.js index 13f8472cb1..12dc049896 100644 --- a/tests/src/rules/no-restricted-paths.js +++ b/tests/src/rules/no-restricted-paths.js @@ -28,6 +28,28 @@ ruleTester.run('no-restricted-paths', rule, { zones: [ { target: './tests/files/restricted-paths/client', from: './tests/files/restricted-paths/other' } ], } ], }), + test({ + code: 'import a from "./a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['./one'], + } ], + } ], + }), + test({ + code: 'import a from "../two/a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['./two'], + } ], + } ], + }), // irrelevant function calls @@ -107,5 +129,38 @@ ruleTester.run('no-restricted-paths', rule, { column: 19, } ], }), + test({ + code: 'import b from "../two/a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['./one'], + } ], + } ], + errors: [ { + message: 'Unexpected path "../two/a.js" imported in restricted zone.', + line: 1, + column: 15, + } ], + }), + test({ + code: 'import b from "../two/a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['../client/a'], + } ], + } ], + errors: [ { + message: 'Restricted path exceptions must be descendants of the configured ' + + '`from` path for that zone.', + line: 1, + column: 15, + } ], + }), ], })