Skip to content

Commit 078b6f7

Browse files
Ross Solomonljharb
authored andcommitted
[New] no-restricted-paths: Allow exceptions to zones
- Make exceptions relative to from paths, plus enforcement
1 parent c28fa7c commit 078b6f7

File tree

7 files changed

+147
-6
lines changed

7 files changed

+147
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
77
## [Unreleased]
88
### Added
99
- [`order`]: added `caseInsensitive` as an additional option to `alphabetize` ([#1586], thanks [@dbrewer5])
10+
- [`no-restricted-paths`]: New `except` option per `zone`, allowing exceptions to be defined for a restricted zone ([#1238], thanks [@rsolomon])
1011

1112
### Fixed
1213
- [`no-unused-modules`]: fix usage of `import/extensions` settings ([#1560], thanks [@stekycz])
@@ -685,6 +686,7 @@ for info on changes for earlier releases.
685686
[#1277]: https://github.com/benmosher/eslint-plugin-import/pull/1277
686687
[#1257]: https://github.com/benmosher/eslint-plugin-import/pull/1257
687688
[#1253]: https://github.com/benmosher/eslint-plugin-import/pull/1253
689+
[#1238]: https://github.com/benmosher/eslint-plugin-import/pull/1238
688690
[#1235]: https://github.com/benmosher/eslint-plugin-import/pull/1235
689691
[#1234]: https://github.com/benmosher/eslint-plugin-import/pull/1234
690692
[#1232]: https://github.com/benmosher/eslint-plugin-import/pull/1232
@@ -1051,3 +1053,4 @@ for info on changes for earlier releases.
10511053
[@Pessimistress]: https://github.com/Pessimistress
10521054
[@stekycz]: https://github.com/stekycz
10531055
[@dbrewer5]: https://github.com/dbrewer5
1056+
[@rsolomon]: https://github.com/rsolomon

docs/rules/no-restricted-paths.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ 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.
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.
1313

1414
### Examples
1515

@@ -37,3 +37,43 @@ The following patterns are not considered problems when configuration set to `{
3737
```js
3838
import baz from '../client/baz';
3939
```
40+
41+
---------------
42+
43+
Given the following folder structure:
44+
45+
```
46+
my-project
47+
├── client
48+
│ └── foo.js
49+
│ └── baz.js
50+
└── server
51+
├── one
52+
│ └── a.js
53+
│ └── b.js
54+
└── two
55+
```
56+
57+
and the current file being linted is `my-project/server/one/a.js`.
58+
59+
and the current configuration is set to:
60+
61+
```
62+
{ "zones": [ {
63+
"target": "./tests/files/restricted-paths/server/one",
64+
"from": "./tests/files/restricted-paths/server",
65+
"except": ["./one"]
66+
} ] }
67+
```
68+
69+
The following pattern is considered a problem:
70+
71+
```js
72+
import a from '../two/a'
73+
```
74+
75+
The following pattern is not considered a problem:
76+
77+
```js
78+
import b from './b'
79+
```

src/rules/no-restricted-paths.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'path'
44
import resolve from 'eslint-module-utils/resolve'
55
import isStaticRequire from '../core/staticRequire'
66
import docsUrl from '../docsUrl'
7+
import importType from '../core/importType'
78

89
module.exports = {
910
meta: {
@@ -24,6 +25,13 @@ module.exports = {
2425
properties: {
2526
target: { type: 'string' },
2627
from: { type: 'string' },
28+
except: {
29+
type: 'array',
30+
items: {
31+
type: 'string',
32+
},
33+
uniqueItems: true,
34+
},
2735
},
2836
additionalProperties: false,
2937
},
@@ -46,6 +54,19 @@ module.exports = {
4654
return containsPath(currentFilename, targetPath)
4755
})
4856

57+
function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) {
58+
const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath)
59+
60+
return importType(relativeExceptionPath, context) !== 'parent'
61+
}
62+
63+
function reportInvalidExceptionPath(node) {
64+
context.report({
65+
node,
66+
message: 'Restricted path exceptions must be descendants of the configured `from` path for that zone.',
67+
})
68+
}
69+
4970
function checkForRestrictedImportPath(importPath, node) {
5071
const absoluteImportPath = resolve(importPath, context)
5172

@@ -54,14 +75,36 @@ module.exports = {
5475
}
5576

5677
matchingZones.forEach((zone) => {
78+
const exceptionPaths = zone.except || []
5779
const absoluteFrom = path.resolve(basePath, zone.from)
5880

59-
if (containsPath(absoluteImportPath, absoluteFrom)) {
60-
context.report({
61-
node,
62-
message: `Unexpected path "${importPath}" imported in restricted zone.`,
63-
})
81+
if (!containsPath(absoluteImportPath, absoluteFrom)) {
82+
return
83+
}
84+
85+
const absoluteExceptionPaths = exceptionPaths.map((exceptionPath) =>
86+
path.resolve(absoluteFrom, exceptionPath)
87+
)
88+
const hasValidExceptionPaths = absoluteExceptionPaths
89+
.every((absoluteExceptionPath) => isValidExceptionPath(absoluteFrom, absoluteExceptionPath))
90+
91+
if (!hasValidExceptionPaths) {
92+
reportInvalidExceptionPath(node)
93+
return
94+
}
95+
96+
const pathIsExcepted = absoluteExceptionPaths
97+
.some((absoluteExceptionPath) => containsPath(absoluteImportPath, absoluteExceptionPath))
98+
99+
if (pathIsExcepted) {
100+
return
64101
}
102+
103+
context.report({
104+
node,
105+
message: `Unexpected path "{{importPath}}" imported in restricted zone.`,
106+
data: { importPath },
107+
})
65108
})
66109
}
67110

tests/files/restricted-paths/server/one/a.js

Whitespace-only changes.

tests/files/restricted-paths/server/one/b.js

Whitespace-only changes.

tests/files/restricted-paths/server/two/a.js

Whitespace-only changes.

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ ruleTester.run('no-restricted-paths', rule, {
2828
zones: [ { target: './tests/files/restricted-paths/client', from: './tests/files/restricted-paths/other' } ],
2929
} ],
3030
}),
31+
test({
32+
code: 'import a from "./a.js"',
33+
filename: testFilePath('./restricted-paths/server/one/a.js'),
34+
options: [ {
35+
zones: [ {
36+
target: './tests/files/restricted-paths/server/one',
37+
from: './tests/files/restricted-paths/server',
38+
except: ['./one'],
39+
} ],
40+
} ],
41+
}),
42+
test({
43+
code: 'import a from "../two/a.js"',
44+
filename: testFilePath('./restricted-paths/server/one/a.js'),
45+
options: [ {
46+
zones: [ {
47+
target: './tests/files/restricted-paths/server/one',
48+
from: './tests/files/restricted-paths/server',
49+
except: ['./two'],
50+
} ],
51+
} ],
52+
}),
3153

3254

3355
// irrelevant function calls
@@ -107,5 +129,38 @@ ruleTester.run('no-restricted-paths', rule, {
107129
column: 19,
108130
} ],
109131
}),
132+
test({
133+
code: 'import b from "../two/a.js"',
134+
filename: testFilePath('./restricted-paths/server/one/a.js'),
135+
options: [ {
136+
zones: [ {
137+
target: './tests/files/restricted-paths/server/one',
138+
from: './tests/files/restricted-paths/server',
139+
except: ['./one'],
140+
} ],
141+
} ],
142+
errors: [ {
143+
message: 'Unexpected path "../two/a.js" imported in restricted zone.',
144+
line: 1,
145+
column: 15,
146+
} ],
147+
}),
148+
test({
149+
code: 'import b from "../two/a.js"',
150+
filename: testFilePath('./restricted-paths/server/one/a.js'),
151+
options: [ {
152+
zones: [ {
153+
target: './tests/files/restricted-paths/server/one',
154+
from: './tests/files/restricted-paths/server',
155+
except: ['../client/a'],
156+
} ],
157+
} ],
158+
errors: [ {
159+
message: 'Restricted path exceptions must be descendants of the configured ' +
160+
'`from` path for that zone.',
161+
line: 1,
162+
column: 15,
163+
} ],
164+
}),
110165
],
111166
})

0 commit comments

Comments
 (0)