-
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
no-cycle
using strongly connected components
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import calculateScc from '@rtsao/scc'; | ||
Check failure on line 1 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on macos-latest
Check failure on line 1 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on macos-latest
Check failure on line 1 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on ubuntu-latest
|
||
import { resolve } from './resolve'; | ||
Check failure on line 2 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on macos-latest
|
||
import { ExportMap, childContext } from './export-map'; | ||
Check failure on line 3 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on macos-latest
Check failure on line 3 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on macos-latest
Check failure on line 3 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on macos-latest
Check failure on line 3 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on ubuntu-latest
Check failure on line 3 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on ubuntu-latest
|
||
import type { ChildContext, RuleContext } from '../types'; | ||
Check failure on line 4 in src/utils/scc.ts GitHub Actions / Lint and Test with Node.js 20 and ESLint 8 on macos-latest
|
||
|
||
let cache = new Map<string, Record<string, number>>(); | ||
|
||
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<string, Set<string>>(); | ||
// 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<string>[]) { | ||
/** for each key, its SCC's index */ | ||
const obj: Record<string, number> = {}; | ||
sccs.forEach((scc, index) => { | ||
scc.forEach((node) => { | ||
obj[node] = index; | ||
}); | ||
}); | ||
return obj; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |