From 82115119d72d162fb8e7e2650101122722c9dea5 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 11 Jul 2024 22:55:35 +0800 Subject: [PATCH 1/2] perf: improve rule `no-cycle` using strongly connected components --- package.json | 1 + src/rules/no-cycle.ts | 15 ++++ src/utils/export-map.ts | 2 +- src/utils/index.ts | 1 + src/utils/scc.ts | 83 +++++++++++++++++++++ test/utils/scc.spec.ts | 160 ++++++++++++++++++++++++++++++++++++++++ yarn.lock | 5 ++ 7 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 src/utils/scc.ts create mode 100644 test/utils/scc.spec.ts diff --git a/package.json b/package.json index ea5921865..cad54b543 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint": "^8.56.0 || ^9.0.0-0" }, "dependencies": { + "@rtsao/scc": "^1.1.0", "@typescript-eslint/utils": "^7.4.0", "debug": "^4.3.4", "doctrine": "^3.0.0", diff --git a/src/rules/no-cycle.ts b/src/rules/no-cycle.ts index 58a0a20e1..da94205b6 100644 --- a/src/rules/no-cycle.ts +++ b/src/rules/no-cycle.ts @@ -5,6 +5,7 @@ import type { DeclarationMetadata, ModuleOptions } from '../utils' import { ExportMap, + StronglyConnectedComponents, isExternalModule, createRule, moduleVisitor, @@ -87,6 +88,8 @@ export = createRule<[Options?], MessageId>({ options.ignoreExternal && isExternalModule(name, resolve(name, context)!, context) + const scc = StronglyConnectedComponents.get(filename, context); + return { ...moduleVisitor(function checkSourceValue(sourceNode, importer) { if (ignoreModule(sourceNode.value)) { @@ -126,6 +129,18 @@ export = createRule<[Options?], MessageId>({ return // no-self-import territory } + /* If we're in the same Strongly Connected Component, + * Then there exists a path from each node in the SCC to every other node in the SCC, + * Then there exists at least one path from them to us and from us to them, + * Then we have a cycle between us. + */ + if (scc) { + const hasDependencyCycle = scc[filename] === scc[imported.path]; + if (!hasDependencyCycle) { + return; + } + } + const untraversed: Traverser[] = [{ mget: () => imported, route: [] }] function detectCycle({ mget, route }: Traverser) { diff --git a/src/utils/export-map.ts b/src/utils/export-map.ts index 58f42b6ba..48827b7ba 100644 --- a/src/utils/export-map.ts +++ b/src/utils/export-map.ts @@ -1083,7 +1083,7 @@ export function recursivePatternCapture( * don't hold full context object in memory, just grab what we need. * also calculate a cacheKey, where parts of the cacheKey hash are memoized */ -function childContext( +export function childContext( path: string, context: RuleContext | ChildContext, ): ChildContext { diff --git a/src/utils/index.ts b/src/utils/index.ts index 6e0b8c986..cbf25f257 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -17,6 +17,7 @@ export * from './pkg-dir' export * from './pkg-up' export * from './read-pkg-up' export * from './resolve' +export * from './scc' export * from './static-require' export * from './unambiguous' export * from './visit' diff --git a/src/utils/scc.ts b/src/utils/scc.ts new file mode 100644 index 000000000..87873946b --- /dev/null +++ b/src/utils/scc.ts @@ -0,0 +1,83 @@ +import calculateScc from '@rtsao/scc'; +import { resolve } from './resolve'; +import { ExportMap, childContext } from './export-map'; +import type { ChildContext, RuleContext } from '../types'; + +let cache = new Map>(); + +export class StronglyConnectedComponents { + static clearCache() { + cache.clear() + } + + static get(source: string, context: RuleContext) { + const path = resolve(source, context); + if (path == null) { return null; } + return StronglyConnectedComponents.for(childContext(path, context)); + } + + static for(context: ChildContext) { + const cacheKey = context.cacheKey + if (cache.has(cacheKey)) { + return cache.get(cacheKey)!; + } + const scc = StronglyConnectedComponents.calculate(context); + cache.set(cacheKey, scc); + return scc; + } + + static calculate(context: ChildContext) { + const exportMap = ExportMap.for(context); + const adjacencyList = StronglyConnectedComponents.exportMapToAdjacencyList(exportMap); + const calculatedScc = calculateScc(adjacencyList); + return StronglyConnectedComponents.calculatedSccToPlainObject(calculatedScc); + } + + static exportMapToAdjacencyList(initialExportMap: ExportMap | null) { + /** for each dep, what are its direct deps */ + const adjacencyList = new Map>(); + // BFS + function visitNode(exportMap: ExportMap | null) { + if (!exportMap) { + return; + } + exportMap.imports.forEach((v, importedPath) => { + const from = exportMap.path; + const to = importedPath; + + if (!adjacencyList.has(from)) { + adjacencyList.set(from, new Set()); + } + + const set = adjacencyList.get(from)!; + + if (set.has(to)) { + return; // prevent endless loop + } + set.add(to); + visitNode(v.getter()); + }); + } + visitNode(initialExportMap); + // Fill gaps + adjacencyList.forEach((values) => { + values.forEach((value) => { + if (!adjacencyList.has(value)) { + adjacencyList.set(value, new Set()); + } + }); + }); + return adjacencyList; + } + + static calculatedSccToPlainObject(sccs: Set[]) { + /** for each key, its SCC's index */ + const obj: Record = {}; + sccs.forEach((scc, index) => { + scc.forEach((node) => { + obj[node] = index; + }); + }); + return obj; + } +} diff --git a/test/utils/scc.spec.ts b/test/utils/scc.spec.ts new file mode 100644 index 000000000..56f9f0833 --- /dev/null +++ b/test/utils/scc.spec.ts @@ -0,0 +1,160 @@ +// import sinon from 'sinon'; +import { StronglyConnectedComponents, ExportMap, childContext as buildChildContext } from 'eslint-plugin-import-x/utils'; +import { testContext } from '../utils'; + +function exportMapFixtureBuilder(path: string, imports: ExportMap[]): ExportMap { + return { + path, + imports: new Map(imports.map((imp) => [imp.path, { getter: () => imp, declarations: new Set() }])), + } as ExportMap; +} + +describe('Strongly Connected Components Builder', () => { + afterEach(() => StronglyConnectedComponents.clearCache()); + + describe('When getting an SCC', () => { + const source = ''; + const ruleContext = testContext({}); + const childContext = buildChildContext(source, ruleContext); + + describe('Given two files', () => { + describe('When they don\'t cycle', () => { + it('Should return foreign SCCs', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [exportMapFixtureBuilder('bar.js', [])]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 0 }); + }); + }); + + describe.skip('When they do cycle', () => { + it('Should return same SCC', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + ]), + ]), + ); + const actual = StronglyConnectedComponents.get(source, ruleContext); + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0 }); + }); + }); + }); + + describe('Given three files', () => { + describe('When they form a line', () => { + describe('When A -> B -> C', () => { + it('Should return foreign SCCs', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', []), + ]), + ]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 2, 'bar.js': 1, 'buzz.js': 0 }); + }); + }); + + describe('When A -> B <-> C', () => { + it('Should return 2 SCCs, A on its own', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('bar.js', []), + ]), + ]), + ]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + + describe('When A <-> B -> C', () => { + it('Should return 2 SCCs, C on its own', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', []), + exportMapFixtureBuilder('foo.js', []), + ]), + ]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 1, 'buzz.js': 0 }); + }); + }); + + describe('When A <-> B <-> C', () => { + it('Should return same SCC', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('bar.js', []), + ]), + ]), + ]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + }); + + describe('When they form a loop', () => { + it('Should return same SCC', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('foo.js', []), + ]), + ]), + ]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + + describe('When they form a Y', () => { + it('Should return 3 distinct SCCs', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', []), + exportMapFixtureBuilder('buzz.js', []), + ]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 2, 'bar.js': 0, 'buzz.js': 1 }); + }); + }); + + describe('When they form a Mercedes', () => { + it('Should return 1 SCC', () => { + jest.spyOn(ExportMap, 'for').mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('buzz.js', []), + ]), + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('bar.js', []), + ]), + ]), + ); + const actual = StronglyConnectedComponents.for(childContext); + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); + }); + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 760e281b4..2a185b9c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1886,6 +1886,11 @@ dependencies: "@xml-tools/parser" "^1.0.11" +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" From f21b2438a2a37eb44800e903c9c9ecaa3f961e8e Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 11 Jul 2024 23:00:44 +0800 Subject: [PATCH 2/2] chore: make eslint happy --- .changeset/silent-pumas-sell.md | 5 + src/rules/no-cycle.ts | 6 +- src/utils/scc.ts | 118 ++++++++------- test/utils/scc.spec.ts | 257 ++++++++++++++++++-------------- 4 files changed, 216 insertions(+), 170 deletions(-) create mode 100644 .changeset/silent-pumas-sell.md diff --git a/.changeset/silent-pumas-sell.md b/.changeset/silent-pumas-sell.md new file mode 100644 index 000000000..728dba057 --- /dev/null +++ b/.changeset/silent-pumas-sell.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-import-x": patch +--- + +Drastically improve `no-cycle`'s performance by skipping unnecessary BFSes using [Tarjan's SCC](https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm). diff --git a/src/rules/no-cycle.ts b/src/rules/no-cycle.ts index da94205b6..f06b120c8 100644 --- a/src/rules/no-cycle.ts +++ b/src/rules/no-cycle.ts @@ -88,7 +88,7 @@ export = createRule<[Options?], MessageId>({ options.ignoreExternal && isExternalModule(name, resolve(name, context)!, context) - const scc = StronglyConnectedComponents.get(filename, context); + const scc = StronglyConnectedComponents.get(filename, context) return { ...moduleVisitor(function checkSourceValue(sourceNode, importer) { @@ -135,9 +135,9 @@ export = createRule<[Options?], MessageId>({ * Then we have a cycle between us. */ if (scc) { - const hasDependencyCycle = scc[filename] === scc[imported.path]; + const hasDependencyCycle = scc[filename] === scc[imported.path] if (!hasDependencyCycle) { - return; + return } } diff --git a/src/utils/scc.ts b/src/utils/scc.ts index 87873946b..2d4b71149 100644 --- a/src/utils/scc.ts +++ b/src/utils/scc.ts @@ -1,83 +1,91 @@ -import calculateScc from '@rtsao/scc'; -import { resolve } from './resolve'; -import { ExportMap, childContext } from './export-map'; -import type { ChildContext, RuleContext } from '../types'; +import calculateScc from '@rtsao/scc' -let cache = new Map>(); +import type { ChildContext, RuleContext } from '../types' -export class StronglyConnectedComponents { - static clearCache() { +import { ExportMap, childContext } from './export-map' +import { resolve } from './resolve' + +const cache = new Map>() + +export const StronglyConnectedComponents = { + clearCache() { cache.clear() - } + }, - static get(source: string, context: RuleContext) { - const path = resolve(source, context); - if (path == null) { return null; } - return StronglyConnectedComponents.for(childContext(path, context)); - } + get(source: string, context: RuleContext) { + const path = resolve(source, context) + if (path == null) { + return null + } + return StronglyConnectedComponents.for(childContext(path, context)) + }, - static for(context: ChildContext) { + for(context: ChildContext) { const cacheKey = context.cacheKey if (cache.has(cacheKey)) { - return cache.get(cacheKey)!; + return cache.get(cacheKey)! } - const scc = StronglyConnectedComponents.calculate(context); - cache.set(cacheKey, scc); - return scc; - } + const scc = StronglyConnectedComponents.calculate(context) + cache.set(cacheKey, scc) + return scc + }, - static calculate(context: ChildContext) { - const exportMap = ExportMap.for(context); - const adjacencyList = StronglyConnectedComponents.exportMapToAdjacencyList(exportMap); - const calculatedScc = calculateScc(adjacencyList); - return StronglyConnectedComponents.calculatedSccToPlainObject(calculatedScc); - } + calculate(context: ChildContext) { + const exportMap = ExportMap.for(context) + const adjacencyList = + StronglyConnectedComponents.exportMapToAdjacencyList(exportMap) + const calculatedScc = calculateScc(adjacencyList) + return StronglyConnectedComponents.calculatedSccToPlainObject(calculatedScc) + }, - static exportMapToAdjacencyList(initialExportMap: ExportMap | null) { + exportMapToAdjacencyList(initialExportMap: ExportMap | null) { /** for each dep, what are its direct deps */ - const adjacencyList = new Map>(); + const adjacencyList = new Map>() // BFS function visitNode(exportMap: ExportMap | null) { if (!exportMap) { - return; + return } - exportMap.imports.forEach((v, importedPath) => { - const from = exportMap.path; - const to = importedPath; + for (const [importedPath, v] of exportMap.imports.entries()) { + const from = exportMap.path + const to = importedPath if (!adjacencyList.has(from)) { - adjacencyList.set(from, new Set()); + adjacencyList.set(from, new Set()) } - const set = adjacencyList.get(from)!; + const set = adjacencyList.get(from)! if (set.has(to)) { - return; // prevent endless loop + continue // prevent endless loop } - set.add(to); - visitNode(v.getter()); - }); + set.add(to) + visitNode(v.getter()) + } } - visitNode(initialExportMap); + visitNode(initialExportMap) // Fill gaps - adjacencyList.forEach((values) => { - values.forEach((value) => { + // eslint-disable-next-line unicorn/no-array-for-each -- Map.forEach, and it is way faster + adjacencyList.forEach(values => { + // eslint-disable-next-line unicorn/no-array-for-each -- Set.forEach + values.forEach(value => { if (!adjacencyList.has(value)) { - adjacencyList.set(value, new Set()); + adjacencyList.set(value, new Set()) } - }); - }); - return adjacencyList; - } + }) + }) - static calculatedSccToPlainObject(sccs: Set[]) { - /** for each key, its SCC's index */ - const obj: Record = {}; - sccs.forEach((scc, index) => { - scc.forEach((node) => { - obj[node] = index; - }); - }); - return obj; - } + return adjacencyList + }, + + calculatedSccToPlainObject(sccs: Array>) { + /** for each key, its SCC's index */ + const obj: Record = {} + for (const [index, scc] of sccs.entries()) { + for (const node of scc) { + obj[node] = index + } + } + return obj + }, } diff --git a/test/utils/scc.spec.ts b/test/utils/scc.spec.ts index 56f9f0833..150575ae6 100644 --- a/test/utils/scc.spec.ts +++ b/test/utils/scc.spec.ts @@ -1,160 +1,193 @@ // import sinon from 'sinon'; -import { StronglyConnectedComponents, ExportMap, childContext as buildChildContext } from 'eslint-plugin-import-x/utils'; -import { testContext } from '../utils'; +import { testContext } from '../utils' -function exportMapFixtureBuilder(path: string, imports: ExportMap[]): ExportMap { +import { + StronglyConnectedComponents, + ExportMap, + childContext as buildChildContext, +} from 'eslint-plugin-import-x/utils' + +function exportMapFixtureBuilder( + path: string, + imports: ExportMap[], +): ExportMap { return { path, - imports: new Map(imports.map((imp) => [imp.path, { getter: () => imp, declarations: new Set() }])), - } as ExportMap; + imports: new Map( + imports.map(imp => [ + imp.path, + { getter: () => imp, declarations: new Set() }, + ]), + ), + } as ExportMap } describe('Strongly Connected Components Builder', () => { - afterEach(() => StronglyConnectedComponents.clearCache()); + afterEach(() => StronglyConnectedComponents.clearCache()) describe('When getting an SCC', () => { - const source = ''; - const ruleContext = testContext({}); - const childContext = buildChildContext(source, ruleContext); + const source = '' + const ruleContext = testContext({}) + const childContext = buildChildContext(source, ruleContext) describe('Given two files', () => { - describe('When they don\'t cycle', () => { + describe("When they don't cycle", () => { it('Should return foreign SCCs', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [exportMapFixtureBuilder('bar.js', [])]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 0 }); - }); - }); + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', []), + ]), + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 0 }) + }) + }) describe.skip('When they do cycle', () => { it('Should return same SCC', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', [ - exportMapFixtureBuilder('foo.js', []), + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + ]), ]), - ]), - ); - const actual = StronglyConnectedComponents.get(source, ruleContext); - expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0 }); - }); - }); - }); + ) + const actual = StronglyConnectedComponents.get(source, ruleContext) + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0 }) + }) + }) + }) describe('Given three files', () => { describe('When they form a line', () => { describe('When A -> B -> C', () => { it('Should return foreign SCCs', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', [ - exportMapFixtureBuilder('buzz.js', []), + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', []), + ]), ]), - ]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 2, 'bar.js': 1, 'buzz.js': 0 }); - }); - }); + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 2, 'bar.js': 1, 'buzz.js': 0 }) + }) + }) describe('When A -> B <-> C', () => { it('Should return 2 SCCs, A on its own', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', [ - exportMapFixtureBuilder('buzz.js', [ - exportMapFixtureBuilder('bar.js', []), + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('bar.js', []), + ]), ]), ]), - ]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 0, 'buzz.js': 0 }); - }); - }); + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 0, 'buzz.js': 0 }) + }) + }) describe('When A <-> B -> C', () => { it('Should return 2 SCCs, C on its own', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', [ - exportMapFixtureBuilder('buzz.js', []), - exportMapFixtureBuilder('foo.js', []), + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', []), + exportMapFixtureBuilder('foo.js', []), + ]), ]), - ]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 1, 'buzz.js': 0 }); - }); - }); + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 1, 'bar.js': 1, 'buzz.js': 0 }) + }) + }) describe('When A <-> B <-> C', () => { it('Should return same SCC', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', [ - exportMapFixtureBuilder('foo.js', []), - exportMapFixtureBuilder('buzz.js', [ - exportMapFixtureBuilder('bar.js', []), + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('bar.js', []), + ]), ]), ]), - ]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); - }); - }); - }); + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }) + }) + }) + }) describe('When they form a loop', () => { it('Should return same SCC', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', [ - exportMapFixtureBuilder('buzz.js', [ - exportMapFixtureBuilder('foo.js', []), + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('foo.js', []), + ]), ]), ]), - ]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); - }); - }); + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }) + }) + }) describe('When they form a Y', () => { it('Should return 3 distinct SCCs', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', []), - exportMapFixtureBuilder('buzz.js', []), - ]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 2, 'bar.js': 0, 'buzz.js': 1 }); - }); - }); + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', []), + exportMapFixtureBuilder('buzz.js', []), + ]), + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 2, 'bar.js': 0, 'buzz.js': 1 }) + }) + }) describe('When they form a Mercedes', () => { it('Should return 1 SCC', () => { - jest.spyOn(ExportMap, 'for').mockReturnValue( - exportMapFixtureBuilder('foo.js', [ - exportMapFixtureBuilder('bar.js', [ - exportMapFixtureBuilder('foo.js', []), - exportMapFixtureBuilder('buzz.js', []), - ]), - exportMapFixtureBuilder('buzz.js', [ - exportMapFixtureBuilder('foo.js', []), - exportMapFixtureBuilder('bar.js', []), + jest + .spyOn(ExportMap, 'for') + .mockReturnValue( + exportMapFixtureBuilder('foo.js', [ + exportMapFixtureBuilder('bar.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('buzz.js', []), + ]), + exportMapFixtureBuilder('buzz.js', [ + exportMapFixtureBuilder('foo.js', []), + exportMapFixtureBuilder('bar.js', []), + ]), ]), - ]), - ); - const actual = StronglyConnectedComponents.for(childContext); - expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); - }); - }); - }); - }); -}); + ) + const actual = StronglyConnectedComponents.for(childContext) + expect(actual).toEqual({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }) + }) + }) + }) + }) +})