Skip to content

Commit f8e663e

Browse files
committed
[feat] no-restricted-paths support glob patterns
1 parent a032b83 commit f8e663e

File tree

5 files changed

+194
-21
lines changed

5 files changed

+194
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
88

99
### Added
1010
- [`no-unused-modules`]: add eslint v8 support ([#2194], thanks [@coderaiser])
11+
- [`no-restricted-paths`]: support for glob patterns
1112

1213
## [2.24.2] - 2021-08-24
1314

docs/rules/no-restricted-paths.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,18 @@ In order to prevent such scenarios this rule allows you to define restricted zon
99

1010
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.
1111
The default value for `basePath` is the current working directory.
12-
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.
13-
You may also specify an optional `message` for a zone, which will be displayed in case of the rule violation.
12+
13+
Each zone consists of the `target` path, a `from` path, and an optional `except` and `message` attribute.
14+
- `target` is the path where the restricted imports should be applied. It can be expressed by
15+
- directory string path that matches all its containing files
16+
- glob pattern matching all the targeted files
17+
- `from` path defines the folder that is not allowed to be used in an import. It can be expressed by
18+
- directory string path that matches all its containing files
19+
- glob pattern matching all the files restricted to be imported
20+
- `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.
21+
- in case `from` is a glob pattern, `except` must be an array of glob patterns as well
22+
- in case `from` is a directory path, `except` is relative to `from` and cannot backtrack to a parent directory.
23+
- `message` - will be displayed in case of the rule violation.
1424

1525
### Examples
1626

@@ -77,4 +87,40 @@ The following pattern is not considered a problem:
7787

7888
```js
7989
import b from './b'
90+
91+
```
92+
93+
---------------
94+
95+
Given the following folder structure:
96+
97+
```
98+
my-project
99+
├── client
100+
└── foo.js
101+
└── sub-module
102+
└── bar.js
103+
└── baz.js
104+
105+
```
106+
107+
and the current configuration is set to:
108+
109+
```
110+
{ "zones": [ {
111+
"target": "./tests/files/restricted-paths/client/!(sub-module)/**/*",
112+
"from": "./tests/files/restricted-paths/client/sub-module/**/*",
113+
} ] }
114+
```
115+
116+
The following import is considered a problem in `my-project/client/foo.js`:
117+
118+
```js
119+
import a from './sub-module/baz'
120+
```
121+
122+
The following import is not considered a problem in `my-project/client/sub-module/bar.js`:
123+
124+
```js
125+
import b from './baz'
80126
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"find-up": "^2.0.0",
108108
"has": "^1.0.3",
109109
"is-core-module": "^2.6.0",
110+
"is-glob": "^4.0.1",
110111
"minimatch": "^3.0.4",
111112
"object.values": "^1.1.4",
112113
"pkg-up": "^2.0.0",

src/rules/no-restricted-paths.js

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import path from 'path';
22

33
import resolve from 'eslint-module-utils/resolve';
44
import moduleVisitor from 'eslint-module-utils/moduleVisitor';
5+
import isGlob from 'is-glob';
6+
import { Minimatch, default as minimatch } from 'minimatch';
57
import docsUrl from '../docsUrl';
68
import importType from '../core/importType';
79

@@ -56,6 +58,10 @@ module.exports = {
5658
const matchingZones = restrictedPaths.filter((zone) => {
5759
const targetPath = path.resolve(basePath, zone.target);
5860

61+
if (isGlob(targetPath)) {
62+
return minimatch(currentFilename, targetPath);
63+
}
64+
5965
return containsPath(currentFilename, targetPath);
6066
});
6167

@@ -72,18 +78,62 @@ module.exports = {
7278
});
7379
}
7480

75-
const zoneExceptions = matchingZones.map((zone) => {
76-
const exceptionPaths = zone.except || [];
77-
const absoluteFrom = path.resolve(basePath, zone.from);
78-
const absoluteExceptionPaths = exceptionPaths.map((exceptionPath) => path.resolve(absoluteFrom, exceptionPath));
79-
const hasValidExceptionPaths = absoluteExceptionPaths
80-
.every((absoluteExceptionPath) => isValidExceptionPath(absoluteFrom, absoluteExceptionPath));
81+
function reportInvalidExceptionGlob(node) {
82+
context.report({
83+
node,
84+
message: 'Restricted path exceptions must be glob patterns when`from` is a glob pattern',
85+
});
86+
}
87+
88+
const makePathValidator = (zoneFrom, zoneExcept = []) => {
89+
const absoluteFrom = path.resolve(basePath, zoneFrom);
90+
const isGlobPattern = isGlob(zoneFrom);
91+
let isPathRestricted;
92+
let hasValidExceptions;
93+
let isPathException;
94+
let reportInvalidException;
95+
96+
if(isGlobPattern) {
97+
const mm = new Minimatch(absoluteFrom);
98+
isPathRestricted = (absoluteImportPath) => mm.match(absoluteImportPath);
99+
100+
hasValidExceptions = zoneExcept.every((p) => isGlob(p));
101+
102+
if(hasValidExceptions) {
103+
const exceptionsMm = zoneExcept.map((except) => new Minimatch(except));
104+
isPathException = (absoluteImportPath) =>
105+
exceptionsMm.some((mm) => mm.match(absoluteImportPath));
106+
}
107+
108+
reportInvalidException = reportInvalidExceptionGlob;
109+
} else {
110+
isPathRestricted = (absoluteImportPath) => containsPath(absoluteImportPath, absoluteFrom);
111+
112+
const absoluteExceptionPaths = zoneExcept
113+
.map((exceptionPath) => path.resolve(absoluteFrom, exceptionPath));
114+
hasValidExceptions = absoluteExceptionPaths
115+
.every((absoluteExceptionPath) =>
116+
isValidExceptionPath(absoluteFrom, absoluteExceptionPath),
117+
);
118+
119+
if(hasValidExceptions) {
120+
isPathException = (absoluteImportPath) => absoluteExceptionPaths.some(
121+
(absoluteExceptionPath) => containsPath(absoluteImportPath, absoluteExceptionPath),
122+
);
123+
}
124+
125+
reportInvalidException = reportInvalidExceptionPath;
126+
}
81127

82128
return {
83-
absoluteExceptionPaths,
84-
hasValidExceptionPaths,
129+
isPathRestricted,
130+
hasValidExceptions,
131+
isPathException,
132+
reportInvalidException,
85133
};
86-
});
134+
};
135+
136+
const validators = [];
87137

88138
function checkForRestrictedImportPath(importPath, node) {
89139
const absoluteImportPath = resolve(importPath, context);
@@ -93,22 +143,27 @@ module.exports = {
93143
}
94144

95145
matchingZones.forEach((zone, index) => {
96-
const absoluteFrom = path.resolve(basePath, zone.from);
97-
98-
if (!containsPath(absoluteImportPath, absoluteFrom)) {
99-
return;
146+
if(!validators[index]) {
147+
validators[index] = makePathValidator(zone.from, zone.except);
100148
}
101149

102-
const { hasValidExceptionPaths, absoluteExceptionPaths } = zoneExceptions[index];
150+
const {
151+
isPathRestricted,
152+
hasValidExceptions,
153+
isPathException,
154+
reportInvalidException,
155+
} = validators[index];
103156

104-
if (!hasValidExceptionPaths) {
105-
reportInvalidExceptionPath(node);
157+
if(!isPathRestricted(absoluteImportPath)) {
106158
return;
107159
}
108160

109-
const pathIsExcepted = absoluteExceptionPaths
110-
.some((absoluteExceptionPath) => containsPath(absoluteImportPath, absoluteExceptionPath));
161+
if (!hasValidExceptions) {
162+
reportInvalidException(node);
163+
return;
164+
}
111165

166+
const pathIsExcepted = isPathException(absoluteImportPath);
112167
if (pathIsExcepted) {
113168
return;
114169
}

tests/src/rules/no-restricted-paths.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ ruleTester.run('no-restricted-paths', rule, {
1414
zones: [ { target: './tests/files/restricted-paths/server', from: './tests/files/restricted-paths/other' } ],
1515
} ],
1616
}),
17+
test({
18+
code: 'import a from "../client/a.js"',
19+
filename: testFilePath('./restricted-paths/server/b.js'),
20+
options: [ {
21+
zones: [ { target: '**/*', from: './tests/files/restricted-paths/other' } ],
22+
} ],
23+
}),
24+
test({
25+
code: 'import a from "../client/a.js"',
26+
filename: testFilePath('./restricted-paths/client/b.js'),
27+
options: [ {
28+
zones: [ {
29+
target: './tests/files/restricted-paths/!(client)/**/*',
30+
from: './tests/files/restricted-paths/client/**/*',
31+
} ],
32+
} ],
33+
}),
1734
test({
1835
code: 'const a = require("../client/a.js")',
1936
filename: testFilePath('./restricted-paths/server/b.js'),
@@ -61,7 +78,17 @@ ruleTester.run('no-restricted-paths', rule, {
6178
} ],
6279
} ],
6380
}),
64-
81+
test({
82+
code: 'import A from "../two/a.js"',
83+
filename: testFilePath('./restricted-paths/server/one/a.js'),
84+
options: [ {
85+
zones: [ {
86+
target: '**/*',
87+
from: './tests/files/restricted-paths/server/**/*',
88+
except: ['**/a.js'],
89+
} ],
90+
} ],
91+
}),
6592

6693
// irrelevant function calls
6794
test({ code: 'notrequire("../server/b.js")' }),
@@ -93,6 +120,18 @@ ruleTester.run('no-restricted-paths', rule, {
93120
column: 15,
94121
} ],
95122
}),
123+
test({
124+
code: 'import b from "../server/b.js"',
125+
filename: testFilePath('./restricted-paths/client/a.js'),
126+
options: [ {
127+
zones: [ { target: './tests/files/restricted-paths/client/**/*', from: './tests/files/restricted-paths/server' } ],
128+
} ],
129+
errors: [ {
130+
message: 'Unexpected path "../server/b.js" imported in restricted zone.',
131+
line: 1,
132+
column: 15,
133+
} ],
134+
}),
96135
test({
97136
code: 'import a from "../client/a"\nimport c from "./c"',
98137
filename: testFilePath('./restricted-paths/server/b.js'),
@@ -190,5 +229,36 @@ ruleTester.run('no-restricted-paths', rule, {
190229
column: 15,
191230
} ],
192231
}),
232+
test({
233+
code: 'import A from "../two/a.js"',
234+
filename: testFilePath('./restricted-paths/server/one/a.js'),
235+
options: [ {
236+
zones: [ {
237+
target: '**/*',
238+
from: './tests/files/restricted-paths/server/**/*',
239+
} ],
240+
} ],
241+
errors: [ {
242+
message: 'Unexpected path "../two/a.js" imported in restricted zone.',
243+
line: 1,
244+
column: 15,
245+
} ],
246+
}),
247+
test({
248+
code: 'import A from "../two/a.js"',
249+
filename: testFilePath('./restricted-paths/server/one/a.js'),
250+
options: [ {
251+
zones: [ {
252+
target: '**/*',
253+
from: './tests/files/restricted-paths/server/**/*',
254+
except: ['a.js'],
255+
} ],
256+
} ],
257+
errors: [ {
258+
message: 'Restricted path exceptions must be glob patterns when`from` is a glob pattern',
259+
line: 1,
260+
column: 15,
261+
} ],
262+
}),
193263
],
194264
});

0 commit comments

Comments
 (0)