Skip to content

Commit

Permalink
[New] no-restricted-paths: support arrays for from and target o…
Browse files Browse the repository at this point in the history
…ptions
  • Loading branch information
AdriAt360 authored and ljharb committed Jun 1, 2022
1 parent d3b0618 commit 16324aa
Show file tree
Hide file tree
Showing 6 changed files with 753 additions and 212 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ 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])
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'
```

181 changes: 122 additions & 59 deletions src/rules/no-restricted-paths.js
Expand Up @@ -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,
minLength: 1,
},
],
},
from: {
oneOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
minLength: 1,
},
],
},
except: {
type: 'array',
items: {
Expand All @@ -56,78 +76,138 @@ 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);
return minimatch(filename, targetPath);
}

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);
}

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 +221,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.

0 comments on commit 16324aa

Please sign in to comment.