Skip to content

Commit b2f6ac8

Browse files
GerkinDevljharb
authored andcommitted
[New] no-cycle: add option to allow cycle via dynamic import
1 parent be30a34 commit b2f6ac8

File tree

6 files changed

+114
-10
lines changed

6 files changed

+114
-10
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
2424
- [`no-named-default`, `no-default-export`, `prefer-default-export`, `no-named-export`, `export`, `named`, `namespace`, `no-unused-modules`]: support arbitrary module namespace names ([#2358], thanks [@sosukesuzuki])
2525
- [`no-dynamic-require`]: support dynamic import with espree ([#2371], thanks [@sosukesuzuki])
2626
- [`no-relative-packages`]: add fixer ([#2381], thanks [@forivall])
27+
- [`no-cycle`]: add `allowUnsafeDynamicCyclicDependency` option ([#2387], thanks [@GerkinDev])
2728

2829
### Fixed
2930
- [`default`]: `typescript-eslint-parser`: avoid a crash on exporting as namespace (thanks [@ljharb])

Diff for: docs/rules/no-cycle.md

+16
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ import { a } from './dep-a.js' // not reported as this module is external
7575

7676
Its value is `false` by default, but can be set to `true` for reducing total project lint time, if needed.
7777

78+
#### `allowUnsafeDynamicCyclicDependency`
79+
80+
This option disable reporting of errors if a cycle is detected with at least one dynamic import.
81+
82+
```js
83+
// bar.js
84+
import { foo } from './foo';
85+
export const bar = foo;
86+
87+
// foo.js
88+
export const foo = 'Foo';
89+
export function getBar() { return import('./bar'); }
90+
```
91+
92+
> Cyclic dependency are **always** a dangerous anti-pattern as discussed extensively in [#2265](https://github.com/import-js/eslint-plugin-import/issues/2265). Please be extra careful about using this option.
93+
7894
## When Not To Use It
7995

8096
This rule is comparatively computationally expensive. If you are pressed for lint

Diff for: src/ExportMap.js

+1
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ ExportMap.parse = function (path, content, context) {
395395
loc: source.loc,
396396
},
397397
importedSpecifiers,
398+
dynamic: true,
398399
}]),
399400
});
400401
}

Diff for: src/rules/no-cycle.js

+18
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ module.exports = {
3333
type: 'boolean',
3434
default: false,
3535
},
36+
allowUnsafeDynamicCyclicDependency: {
37+
description: 'Allow cyclic dependency if there is at least one dynamic import in the chain',
38+
type: 'boolean',
39+
default: false,
40+
},
3641
})],
3742
},
3843

@@ -52,6 +57,13 @@ module.exports = {
5257
if (ignoreModule(sourceNode.value)) {
5358
return; // ignore external modules
5459
}
60+
if (options.allowUnsafeDynamicCyclicDependency && (
61+
// Ignore `import()`
62+
importer.type === 'ImportExpression' ||
63+
// `require()` calls are always checked (if possible)
64+
(importer.type === 'CallExpression' && importer.callee.name !== 'require'))) {
65+
return; // cycle via dynamic import allowed by config
66+
}
5567

5668
if (
5769
importer.type === 'ImportDeclaration' && (
@@ -89,6 +101,12 @@ module.exports = {
89101
// Ignore only type imports
90102
!isOnlyImportingTypes,
91103
);
104+
105+
/*
106+
If cyclic dependency is allowed via dynamic import, skip checking if any module is imported dynamically
107+
*/
108+
if (options.allowUnsafeDynamicCyclicDependency && toTraverse.some(d => d.dynamic)) return;
109+
92110
/*
93111
Only report as a cycle if there are any import declarations that are considered by
94112
the rule. For example:

Diff for: tests/files/cycles/es6/depth-one-dynamic.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const bar = () => import("../depth-zero").then(({foo}) => foo);

Diff for: tests/src/rules/no-cycle.js

+77-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parsers, test as _test, testFilePath } from '../utils';
1+
import { parsers, test as _test, testFilePath, testVersion as _testVersion } from '../utils';
22

33
import { RuleTester } from 'eslint';
44
import flatMap from 'array.prototype.flatmap';
@@ -11,6 +11,9 @@ const error = message => ({ message });
1111
const test = def => _test(Object.assign(def, {
1212
filename: testFilePath('./cycles/depth-zero.js'),
1313
}));
14+
const testVersion = (specifier, t) => _testVersion(specifier, () => Object.assign(t(), {
15+
filename: testFilePath('./cycles/depth-zero.js'),
16+
}));
1417

1518
const testDialects = ['es6'];
1619

@@ -73,7 +76,28 @@ ruleTester.run('no-cycle', rule, {
7376
code: `import type { FooType, BarType } from "./${testDialect}/depth-one"`,
7477
parser: parsers.BABEL_OLD,
7578
}),
76-
]),
79+
test({
80+
code: `function bar(){ return import("./${testDialect}/depth-one"); } // #2265 1`,
81+
options: [{ allowUnsafeDynamicCyclicDependency: true }],
82+
parser: parsers.BABEL_OLD,
83+
}),
84+
test({
85+
code: `import { foo } from "./${testDialect}/depth-one-dynamic"; // #2265 2`,
86+
options: [{ allowUnsafeDynamicCyclicDependency: true }],
87+
parser: parsers.BABEL_OLD,
88+
}),
89+
].concat(parsers.TS_NEW ? [
90+
test({
91+
code: `function bar(){ return import("./${testDialect}/depth-one"); } // #2265 3`,
92+
options: [{ allowUnsafeDynamicCyclicDependency: true }],
93+
parser: parsers.TS_NEW,
94+
}),
95+
test({
96+
code: `import { foo } from "./${testDialect}/depth-one-dynamic"; // #2265 4`,
97+
options: [{ allowUnsafeDynamicCyclicDependency: true }],
98+
parser: parsers.TS_NEW,
99+
}),
100+
] : [])),
77101

78102
test({
79103
code: 'import { bar } from "./flow-types"',
@@ -112,62 +136,83 @@ ruleTester.run('no-cycle', rule, {
112136
},
113137
}),
114138

115-
flatMap(testDialects, (testDialect) => [
139+
// Ensure behavior does not change for those tests, with or without `
140+
flatMap(testDialects, (testDialect) => flatMap([
141+
{},
142+
{ allowUnsafeDynamicCyclicDependency: true },
143+
], (opts) => [
116144
test({
117145
code: `import { foo } from "./${testDialect}/depth-one"`,
146+
options: [{ ...opts }],
118147
errors: [error(`Dependency cycle detected.`)],
119148
}),
120149
test({
121150
code: `import { foo } from "./${testDialect}/depth-one"`,
122-
options: [{ maxDepth: 1 }],
151+
options: [{ ...opts, maxDepth: 1 }],
123152
errors: [error(`Dependency cycle detected.`)],
124153
}),
125154
test({
126155
code: `const { foo } = require("./${testDialect}/depth-one")`,
127156
errors: [error(`Dependency cycle detected.`)],
128-
options: [{ commonjs: true }],
157+
options: [{ ...opts, commonjs: true }],
129158
}),
130159
test({
131160
code: `require(["./${testDialect}/depth-one"], d1 => {})`,
132161
errors: [error(`Dependency cycle detected.`)],
133-
options: [{ amd: true }],
162+
options: [{ ...opts, amd: true }],
134163
}),
135164
test({
136165
code: `define(["./${testDialect}/depth-one"], d1 => {})`,
137166
errors: [error(`Dependency cycle detected.`)],
138-
options: [{ amd: true }],
167+
options: [{ ...opts, amd: true }],
139168
}),
140169
test({
141170
code: `import { foo } from "./${testDialect}/depth-two"`,
171+
options: [{ ...opts }],
142172
errors: [error(`Dependency cycle via ./depth-one:1`)],
143173
}),
144174
test({
145175
code: `import { foo } from "./${testDialect}/depth-two"`,
146-
options: [{ maxDepth: 2 }],
176+
options: [{ ...opts, maxDepth: 2 }],
147177
errors: [error(`Dependency cycle via ./depth-one:1`)],
148178
}),
149179
test({
150180
code: `const { foo } = require("./${testDialect}/depth-two")`,
151181
errors: [error(`Dependency cycle via ./depth-one:1`)],
152-
options: [{ commonjs: true }],
182+
options: [{ ...opts, commonjs: true }],
153183
}),
154184
test({
155185
code: `import { two } from "./${testDialect}/depth-three-star"`,
186+
options: [{ ...opts }],
156187
errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)],
157188
}),
158189
test({
159190
code: `import one, { two, three } from "./${testDialect}/depth-three-star"`,
191+
options: [{ ...opts }],
160192
errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)],
161193
}),
162194
test({
163195
code: `import { bar } from "./${testDialect}/depth-three-indirect"`,
196+
options: [{ ...opts }],
164197
errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)],
165198
}),
166199
test({
167200
code: `import { bar } from "./${testDialect}/depth-three-indirect"`,
201+
options: [{ ...opts }],
168202
errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)],
169203
parser: parsers.BABEL_OLD,
170204
}),
205+
test({
206+
code: `import { foo } from "./${testDialect}/depth-two"`,
207+
options: [{ ...opts, maxDepth: Infinity }],
208+
errors: [error(`Dependency cycle via ./depth-one:1`)],
209+
}),
210+
test({
211+
code: `import { foo } from "./${testDialect}/depth-two"`,
212+
options: [{ ...opts, maxDepth: '∞' }],
213+
errors: [error(`Dependency cycle via ./depth-one:1`)],
214+
}),
215+
]).concat([
171216
test({
172217
code: `import("./${testDialect}/depth-three-star")`,
173218
errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)],
@@ -188,7 +233,29 @@ ruleTester.run('no-cycle', rule, {
188233
options: [{ maxDepth: '∞' }],
189234
errors: [error(`Dependency cycle via ./depth-one:1`)],
190235
}),
191-
]),
236+
test({
237+
code: `function bar(){ return import("./${testDialect}/depth-one"); } // #2265 5`,
238+
errors: [error(`Dependency cycle detected.`)],
239+
parser: parsers.BABEL_OLD,
240+
}),
241+
]).concat(
242+
testVersion('> 3', () => ({ // Dynamic import is not properly caracterized with eslint < 4
243+
code: `import { foo } from "./${testDialect}/depth-one-dynamic"; // #2265 6`,
244+
errors: [error(`Dependency cycle detected.`)],
245+
parser: parsers.BABEL_OLD,
246+
})),
247+
).concat(parsers.TS_NEW ? [
248+
test({
249+
code: `function bar(){ return import("./${testDialect}/depth-one"); } // #2265 7`,
250+
errors: [error(`Dependency cycle detected.`)],
251+
parser: parsers.TS_NEW,
252+
}),
253+
test({
254+
code: `import { foo } from "./${testDialect}/depth-one-dynamic"; // #2265 8`,
255+
errors: [error(`Dependency cycle detected.`)],
256+
parser: parsers.TS_NEW,
257+
}),
258+
] : [])),
192259

193260
test({
194261
code: 'import { bar } from "./flow-types-depth-one"',

0 commit comments

Comments
 (0)