Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] no-restricted-paths: support arrays for from and target options #2466

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,9 +9,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
### Added
- [`newline-after-import`]: add `considerComments` option ([#2399], thanks [@pri1311])
- [`no-cycle`]: add `allowUnsafeDynamicCyclicDependency` option ([#2387], thanks [@GerkinDev])
- [`no-restricted-paths`]: support arrays for `from` and `target` options ([#2466], thanks [@AdriAt360])

### Fixed
- [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])
- [`no-restricted-paths`]: fix an error message ([#2466], thanks [@AdriAt360])

### Changed
- [Tests] `named`: Run all TypeScript test ([#2427], thanks [@ProdigySim])
Expand Down Expand Up @@ -990,6 +992,7 @@ for info on changes for earlier releases.

[`memo-parser`]: ./memo-parser/README.md

[#2466]: https://github.com/import-js/eslint-plugin-import/pull/2466
[#2440]: https://github.com/import-js/eslint-plugin-import/pull/2440
[#2427]: https://github.com/import-js/eslint-plugin-import/pull/2427
[#2417]: https://github.com/import-js/eslint-plugin-import/pull/2417
Expand Down Expand Up @@ -1493,6 +1496,7 @@ for info on changes for earlier releases.
[@aberezkin]: https://github.com/aberezkin
[@adamborowski]: https://github.com/adamborowski
[@adjerbetian]: https://github.com/adjerbetian
[@AdriAt360]: https://github.com/AdriAt360
[@ai]: https://github.com/ai
[@aladdin-add]: https://github.com/aladdin-add
[@alex-page]: https://github.com/alex-page
Expand Down
80 changes: 75 additions & 5 deletions docs/rules/no-restricted-paths.md
Expand Up @@ -10,16 +10,19 @@ 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, a `from` path, and an optional `except` and `message` attribute.
- `target` is the path where the restricted imports should be applied. It can be expressed by
Each zone consists of the `target` paths, a `from` paths, and an optional `except` and `message` attribute.
- `target` contains the paths where the restricted imports should be applied. It can be expressed by
- directory string path that matches all its containing files
- glob pattern matching all the targeted files
- `from` path defines the folder that is not allowed to be used in an import. It can be expressed by
- an array of multiple of the two types above
- `from` paths define the folders that are not allowed to be used in an import. It can be expressed by
- directory string path that matches all its containing files
- glob pattern matching all the files restricted to be imported
- an array of multiple directory string path
- an array of multiple glob patterns
- `except` may be defined for a zone, allowing exception paths that would otherwise violate the related `from`. Note that it does not alter the behaviour of `target` in any way.
- in case `from` is a glob pattern, `except` must be an array of glob patterns as well
- in case `from` is a directory path, `except` is relative to `from` and cannot backtrack to a parent directory.
- in case `from` contains only glob patterns, `except` must be an array of glob patterns as well
- in case `from` contains only directory path, `except` is relative to `from` and cannot backtrack to a parent directory
- `message` - will be displayed in case of the rule violation.

### Examples
Expand Down Expand Up @@ -124,3 +127,70 @@ The following import is not considered a problem in `my-project/client/sub-modul
```js
import b from './baz'
```

---------------

Given the following folder structure:

```
my-project
└── one
└── a.js
└── b.js
└── two
└── a.js
└── b.js
└── three
└── a.js
└── b.js
```

and the current configuration is set to:

```
{
"zones": [
{
"target": ["./tests/files/restricted-paths/two/*", "./tests/files/restricted-paths/three/*"],
"from": ["./tests/files/restricted-paths/one", "./tests/files/restricted-paths/three"],
}
]
}
```

The following patterns are not considered a problem in `my-project/one/b.js`:

```js
import a from '../three/a'
```

```js
import a from './a'
```

The following pattern is not considered a problem in `my-project/two/b.js`:

```js
import a from './a'
```

The following patterns are considered a problem in `my-project/two/a.js`:

```js
import a from '../one/a'
```

```js
import a from '../three/a'
```

The following patterns are considered a problem in `my-project/three/b.js`:

```js
import a from '../one/a'
```

```js
import a from './a'
```

184 changes: 124 additions & 60 deletions src/rules/no-restricted-paths.js
Expand Up @@ -3,7 +3,7 @@ import path from 'path';
import resolve from 'eslint-module-utils/resolve';
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
import isGlob from 'is-glob';
import { Minimatch, default as minimatch } from 'minimatch';
import { Minimatch } from 'minimatch';
import docsUrl from '../docsUrl';
import importType from '../core/importType';

Expand All @@ -29,8 +29,28 @@ module.exports = {
items: {
type: 'object',
properties: {
target: { type: 'string' },
from: { type: 'string' },
target: {
oneOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
ljharb marked this conversation as resolved.
Show resolved Hide resolved
minLength: 1,
},
],
},
from: {
oneOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
ljharb marked this conversation as resolved.
Show resolved Hide resolved
minLength: 1,
},
],
},
except: {
type: 'array',
items: {
Expand All @@ -56,78 +76,139 @@ module.exports = {
const basePath = options.basePath || process.cwd();
const currentFilename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
const matchingZones = restrictedPaths.filter((zone) => {
const targetPath = path.resolve(basePath, zone.target);
return [].concat(zone.target)
.map(target => path.resolve(basePath, target))
.some(targetPath => isMatchingTargetPath(currentFilename, targetPath));
});

function isMatchingTargetPath(filename, targetPath) {
if (isGlob(targetPath)) {
return minimatch(currentFilename, targetPath);
const mm = new Minimatch(targetPath);
return mm.match(filename);
}

return containsPath(currentFilename, targetPath);
});
return containsPath(filename, targetPath);
}

function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) {
const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath);

return importType(relativeExceptionPath, context) !== 'parent';
}

function areBothGlobPatternAndAbsolutePath(areGlobPatterns) {
return areGlobPatterns.some((isGlob) => isGlob) && areGlobPatterns.some((isGlob) => !isGlob);
ljharb marked this conversation as resolved.
Show resolved Hide resolved
}

function reportInvalidExceptionPath(node) {
context.report({
node,
message: 'Restricted path exceptions must be descendants of the configured `from` path for that zone.',
});
}

function reportInvalidExceptionMixedGlobAndNonGlob(node) {
context.report({
node,
message: 'Restricted path `from` must contain either only glob patterns or none',
});
}

function reportInvalidExceptionGlob(node) {
context.report({
node,
message: 'Restricted path exceptions must be glob patterns when`from` is a glob pattern',
message: 'Restricted path exceptions must be glob patterns when `from` contains glob patterns',
});
}

const makePathValidator = (zoneFrom, zoneExcept = []) => {
const absoluteFrom = path.resolve(basePath, zoneFrom);
const isGlobPattern = isGlob(zoneFrom);
let isPathRestricted;
let hasValidExceptions;
function computeMixedGlobAndAbsolutePathValidator() {
return {
isPathRestricted: () => true,
hasValidExceptions: false,
reportInvalidException: reportInvalidExceptionMixedGlobAndNonGlob,
};
}

function computeGlobPatternPathValidator(absoluteFrom, zoneExcept) {
let isPathException;
let reportInvalidException;

if (isGlobPattern) {
const mm = new Minimatch(absoluteFrom);
isPathRestricted = (absoluteImportPath) => mm.match(absoluteImportPath);
const mm = new Minimatch(absoluteFrom);
const isPathRestricted = (absoluteImportPath) => mm.match(absoluteImportPath);
const hasValidExceptions = zoneExcept.every(isGlob);

hasValidExceptions = zoneExcept.every(isGlob);
if (hasValidExceptions) {
const exceptionsMm = zoneExcept.map((except) => new Minimatch(except));
isPathException = (absoluteImportPath) => exceptionsMm.some((mm) => mm.match(absoluteImportPath));
}

if (hasValidExceptions) {
const exceptionsMm = zoneExcept.map((except) => new Minimatch(except));
isPathException = (absoluteImportPath) => exceptionsMm.some((mm) => mm.match(absoluteImportPath));
}
const reportInvalidException = reportInvalidExceptionGlob;

return {
isPathRestricted,
hasValidExceptions,
isPathException,
reportInvalidException,
};
}

reportInvalidException = reportInvalidExceptionGlob;
} else {
isPathRestricted = (absoluteImportPath) => containsPath(absoluteImportPath, absoluteFrom);
function computeAbsolutePathValidator(absoluteFrom, zoneExcept) {
let isPathException;

const absoluteExceptionPaths = zoneExcept
.map((exceptionPath) => path.resolve(absoluteFrom, exceptionPath));
hasValidExceptions = absoluteExceptionPaths
.every((absoluteExceptionPath) => isValidExceptionPath(absoluteFrom, absoluteExceptionPath));
const isPathRestricted = (absoluteImportPath) => containsPath(absoluteImportPath, absoluteFrom);

if (hasValidExceptions) {
isPathException = (absoluteImportPath) => absoluteExceptionPaths.some(
(absoluteExceptionPath) => containsPath(absoluteImportPath, absoluteExceptionPath),
);
}
const absoluteExceptionPaths = zoneExcept
.map((exceptionPath) => path.resolve(absoluteFrom, exceptionPath));
const hasValidExceptions = absoluteExceptionPaths
.every((absoluteExceptionPath) => isValidExceptionPath(absoluteFrom, absoluteExceptionPath));

reportInvalidException = reportInvalidExceptionPath;
if (hasValidExceptions) {
isPathException = (absoluteImportPath) => absoluteExceptionPaths.some(
(absoluteExceptionPath) => containsPath(absoluteImportPath, absoluteExceptionPath),
);
}

const reportInvalidException = reportInvalidExceptionPath;

return {
isPathRestricted,
hasValidExceptions,
isPathException,
reportInvalidException,
};
}

function reportInvalidExceptions(validators, node) {
validators.forEach(validator => validator.reportInvalidException(node));
}

function reportImportsInRestrictedZone(validators, node, importPath, customMessage) {
validators.forEach(() => {
context.report({
node,
message: `Unexpected path "{{importPath}}" imported in restricted zone.${customMessage ? ` ${customMessage}` : ''}`,
data: { importPath },
});
});
}

const makePathValidators = (zoneFrom, zoneExcept = []) => {
const allZoneFrom = [].concat(zoneFrom);
const areGlobPatterns = allZoneFrom.map(isGlob);

if (areBothGlobPatternAndAbsolutePath(areGlobPatterns)) {
return [computeMixedGlobAndAbsolutePathValidator()];
}

const isGlobPattern = areGlobPatterns.every((isGlob) => isGlob);

return allZoneFrom.map(singleZoneFrom => {
const absoluteFrom = path.resolve(basePath, singleZoneFrom);

if (isGlobPattern) {
return computeGlobPatternPathValidator(absoluteFrom, zoneExcept);
}
return computeAbsolutePathValidator(absoluteFrom, zoneExcept);
});
};

const validators = [];
Expand All @@ -141,35 +222,18 @@ module.exports = {

matchingZones.forEach((zone, index) => {
if (!validators[index]) {
validators[index] = makePathValidator(zone.from, zone.except);
}

const {
isPathRestricted,
hasValidExceptions,
isPathException,
reportInvalidException,
} = validators[index];

if (!isPathRestricted(absoluteImportPath)) {
return;
validators[index] = makePathValidators(zone.from, zone.except);
}

if (!hasValidExceptions) {
reportInvalidException(node);
return;
}
const applicableValidatorsForImportPath = validators[index].filter(validator => validator.isPathRestricted(absoluteImportPath));

const pathIsExcepted = isPathException(absoluteImportPath);
if (pathIsExcepted) {
return;
}
const validatorsWithInvalidExceptions = applicableValidatorsForImportPath.filter(validator => !validator.hasValidExceptions);
reportInvalidExceptions(validatorsWithInvalidExceptions, node);

context.report({
node,
message: `Unexpected path "{{importPath}}" imported in restricted zone.${zone.message ? ` ${zone.message}` : ''}`,
data: { importPath },
});
const applicableValidatorsForImportPathExcludingExceptions = applicableValidatorsForImportPath
.filter(validator => validator.hasValidExceptions)
.filter(validator => !validator.isPathException(absoluteImportPath));
reportImportsInRestrictedZone(applicableValidatorsForImportPathExcludingExceptions, node, importPath, zone.message);
});
}

Expand Down
Empty file.
Empty file.