-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[New] [Refactor]
no-cycle
: use scc algorithm to optimize; add `skip…
…ErrorMessagePath` for faster error messages
- Loading branch information
Showing
6 changed files
with
301 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import calculateScc from '@rtsao/scc'; | ||
import { hashObject } from 'eslint-module-utils/hash'; | ||
import resolve from 'eslint-module-utils/resolve'; | ||
import ExportMapBuilder from './exportMap/builder'; | ||
import childContext from './exportMap/childContext'; | ||
|
||
let cache = new Map(); | ||
|
||
export default class StronglyConnectedComponentsBuilder { | ||
static clearCache() { | ||
cache = new Map(); | ||
} | ||
|
||
static get(source, context) { | ||
const path = resolve(source, context); | ||
if (path == null) { return null; } | ||
return StronglyConnectedComponentsBuilder.for(childContext(path, context)); | ||
} | ||
|
||
static for(context) { | ||
const cacheKey = context.cacheKey || hashObject(context).digest('hex'); | ||
if (cache.has(cacheKey)) { | ||
return cache.get(cacheKey); | ||
} | ||
const scc = StronglyConnectedComponentsBuilder.calculate(context); | ||
cache.set(cacheKey, scc); | ||
return scc; | ||
} | ||
|
||
static calculate(context) { | ||
const exportMap = ExportMapBuilder.for(context); | ||
const adjacencyList = this.exportMapToAdjacencyList(exportMap); | ||
const calculatedScc = calculateScc(adjacencyList); | ||
return StronglyConnectedComponentsBuilder.calculatedSccToPlainObject(calculatedScc); | ||
} | ||
|
||
/** @returns {Map<string, Set<string>>} for each dep, what are its direct deps */ | ||
static exportMapToAdjacencyList(initialExportMap) { | ||
const adjacencyList = new Map(); | ||
// BFS | ||
function visitNode(exportMap) { | ||
if (!exportMap) { | ||
return; | ||
} | ||
exportMap.imports.forEach((v, importedPath) => { | ||
const from = exportMap.path; | ||
const to = importedPath; | ||
|
||
if (!adjacencyList.has(from)) { | ||
adjacencyList.set(from, new Set()); | ||
} | ||
|
||
if (adjacencyList.get(from).has(to)) { | ||
return; // prevent endless loop | ||
} | ||
adjacencyList.get(from).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; | ||
} | ||
|
||
/** @returns {Record<string, number>} for each key, its SCC's index */ | ||
static calculatedSccToPlainObject(sccs) { | ||
const obj = {}; | ||
sccs.forEach((scc, index) => { | ||
scc.forEach((node) => { | ||
obj[node] = index; | ||
}); | ||
}); | ||
return obj; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import sinon from 'sinon'; | ||
import { expect } from 'chai'; | ||
import StronglyConnectedComponentsBuilder from '../../src/scc'; | ||
import ExportMapBuilder from '../../src/exportMap/builder'; | ||
|
||
function exportMapFixtureBuilder(path, imports) { | ||
return { | ||
path, | ||
imports: new Map(imports.map((imp) => [imp.path, { getter: () => imp }])), | ||
}; | ||
} | ||
|
||
describe('Strongly Connected Components Builder', () => { | ||
afterEach(() => ExportMapBuilder.for.restore()); | ||
afterEach(() => StronglyConnectedComponentsBuilder.clearCache()); | ||
|
||
describe('When getting an SCC', () => { | ||
const source = ''; | ||
const context = { | ||
settings: {}, | ||
parserOptions: {}, | ||
parserPath: '', | ||
}; | ||
|
||
describe('Given two files', () => { | ||
describe('When they don\'t cycle', () => { | ||
it('Should return foreign SCCs', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [exportMapFixtureBuilder('bar.js', [])]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 1, 'bar.js': 0 }); | ||
}); | ||
}); | ||
|
||
describe('When they do cycle', () => { | ||
it('Should return same SCC', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', [ | ||
exportMapFixtureBuilder('foo.js', []), | ||
]), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ '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', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', [ | ||
exportMapFixtureBuilder('buzz.js', []), | ||
]), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 2, 'bar.js': 1, 'buzz.js': 0 }); | ||
}); | ||
}); | ||
|
||
describe('When A -> B <-> C', () => { | ||
it('Should return 2 SCCs, A on its own', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', [ | ||
exportMapFixtureBuilder('buzz.js', [ | ||
exportMapFixtureBuilder('bar.js', []), | ||
]), | ||
]), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 1, 'bar.js': 0, 'buzz.js': 0 }); | ||
}); | ||
}); | ||
|
||
describe('When A <-> B -> C', () => { | ||
it('Should return 2 SCCs, C on its own', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', [ | ||
exportMapFixtureBuilder('buzz.js', []), | ||
exportMapFixtureBuilder('foo.js', []), | ||
]), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 1, 'bar.js': 1, 'buzz.js': 0 }); | ||
}); | ||
}); | ||
|
||
describe('When A <-> B <-> C', () => { | ||
it('Should return same SCC', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', [ | ||
exportMapFixtureBuilder('foo.js', []), | ||
exportMapFixtureBuilder('buzz.js', [ | ||
exportMapFixtureBuilder('bar.js', []), | ||
]), | ||
]), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('When they form a loop', () => { | ||
it('Should return same SCC', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', [ | ||
exportMapFixtureBuilder('buzz.js', [ | ||
exportMapFixtureBuilder('foo.js', []), | ||
]), | ||
]), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); | ||
}); | ||
}); | ||
|
||
describe('When they form a Y', () => { | ||
it('Should return 3 distinct SCCs', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', []), | ||
exportMapFixtureBuilder('buzz.js', []), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 2, 'bar.js': 0, 'buzz.js': 1 }); | ||
}); | ||
}); | ||
|
||
describe('When they form a Mercedes', () => { | ||
it('Should return 1 SCC', () => { | ||
sinon.stub(ExportMapBuilder, 'for').returns( | ||
exportMapFixtureBuilder('foo.js', [ | ||
exportMapFixtureBuilder('bar.js', [ | ||
exportMapFixtureBuilder('foo.js', []), | ||
exportMapFixtureBuilder('buzz.js', []), | ||
]), | ||
exportMapFixtureBuilder('buzz.js', [ | ||
exportMapFixtureBuilder('foo.js', []), | ||
exportMapFixtureBuilder('bar.js', []), | ||
]), | ||
]), | ||
); | ||
const actual = StronglyConnectedComponentsBuilder.for(source, context); | ||
expect(actual).to.deep.equal({ 'foo.js': 0, 'bar.js': 0, 'buzz.js': 0 }); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |