Skip to content

Commit ae173f8

Browse files
authored
Merge pull request #7 from snyk/feat/count-paths
feat: method to count all paths from pkg to root
2 parents 930c5d3 + 5564304 commit ae173f8

File tree

4 files changed

+64
-8
lines changed

4 files changed

+64
-8
lines changed

src/core/dep-graph.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class DepGraphImpl implements types.DepGraphInternal {
2424
private _rootNodeId: string;
2525
private _rootPkgId: string;
2626

27+
private _countNodePathsToRootCache: Map<string, number> = new Map();
28+
2729
public constructor(
2830
graph: graphlib.Graph,
2931
rootNodeId: string,
@@ -104,18 +106,28 @@ class DepGraphImpl implements types.DepGraphInternal {
104106
}
105107

106108
const pathsToRoot: types.PkgInfo[][] = [];
107-
108-
const nodeIds = this.getPkgNodeIds(pkg);
109-
if (nodeIds) {
110-
for (const id of nodeIds) {
111-
pathsToRoot.push(...this.pathsFromNodeToRoot(id));
112-
}
109+
for (const id of this.getPkgNodeIds(pkg)) {
110+
pathsToRoot.push(...this.pathsFromNodeToRoot(id));
113111
}
114112
// note: sorting to get shorter paths first -
115113
// it's nicer - and better resembles older behaviour
116114
return pathsToRoot.sort((a, b) => a.length - b.length);
117115
}
118116

117+
public countPathsToRoot(pkg: types.Pkg): number {
118+
// TODO: implement cycles support
119+
if (this.hasCycles()) {
120+
throw new Error('countPathsToRoot does not support cyclic graphs yet');
121+
}
122+
123+
let count = 0;
124+
for (const nodeId of this.getPkgNodeIds(pkg)) {
125+
count += this.countNodePathsToRoot(nodeId);
126+
}
127+
128+
return count;
129+
}
130+
119131
public toJSON(): types.DepGraphData {
120132
const nodeIds = this._graph.nodes();
121133

@@ -153,14 +165,34 @@ class DepGraphImpl implements types.DepGraphInternal {
153165
if (parentNodesIds.length === 0) {
154166
return [[this.getNodePkg(nodeId)]];
155167
}
168+
156169
const allPaths: types.PkgInfo[][] = [];
157170
parentNodesIds.map((id) => {
158171
const out = this.pathsFromNodeToRoot(id).map((path) => {
159172
return [this.getNodePkg(nodeId)].concat(path);
160173
});
161174
allPaths.push(...out);
162175
});
176+
163177
return allPaths;
164178
}
165179

180+
private countNodePathsToRoot(nodeId: string): number {
181+
if (this._countNodePathsToRootCache.has(nodeId)) {
182+
return this._countNodePathsToRootCache.get(nodeId);
183+
}
184+
185+
const parentNodesIds = this.getNodeParentsNodeIds(nodeId);
186+
if (parentNodesIds.length === 0) {
187+
this._countNodePathsToRootCache.set(nodeId, 1);
188+
return 1;
189+
}
190+
191+
const count = parentNodesIds.reduce((acc, parentNodeId) => {
192+
return acc + this.countNodePathsToRoot(parentNodeId);
193+
}, 0);
194+
195+
this._countNodePathsToRootCache.set(nodeId, count);
196+
return count;
197+
}
166198
}

src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface DepGraph {
7272
getPkgs(): PkgInfo[];
7373
toJSON(): DepGraphData;
7474
pkgPathsToRoot(pkg: Pkg): PkgInfo[][];
75+
countPathsToRoot(pkg: Pkg): number;
7576
}
7677

7778
// NOTE/TODO(shaun): deferring any/all design decisions here

test/core/create-from-json.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ describe('fromJSON simple', () => {
3131

3232
test('getPathsToRoot', async () => {
3333
expect(graph.pkgPathsToRoot({ name: 'd', version: '0.0.1' })).toHaveLength(1);
34+
expect(graph.countPathsToRoot({ name: 'd', version: '0.0.1' })).toBe(1);
3435

3536
expect(graph.pkgPathsToRoot({ name: 'd', version: '0.0.2' })).toHaveLength(1);
37+
expect(graph.countPathsToRoot({ name: 'd', version: '0.0.2' })).toBe(1);
3638

3739
expect(graph.pkgPathsToRoot({ name: 'c', version: '1.0.0' })).toHaveLength(2);
40+
expect(graph.countPathsToRoot({ name: 'c', version: '1.0.0' })).toBe(2);
3841

3942
expect(graph.pkgPathsToRoot({ name: 'e', version: '5.0.0' })).toHaveLength(2);
43+
expect(graph.countPathsToRoot({ name: 'e', version: '5.0.0' })).toBe(2);
4044

4145
expect(graph.pkgPathsToRoot({ name: 'e', version: '5.0.0' })).toEqual([
4246
[
@@ -135,6 +139,7 @@ test('fromJSON a pkg and a node share same id', async () => {
135139
{ name: 'foo', version: '2' },
136140
{ name: 'toor', version: '1.0.0' },
137141
]]);
142+
expect(depGraph.countPathsToRoot({ name: 'foo', version: '2' })).toBe(1);
138143
});
139144

140145
test('fromJSON no deps', async () => {

test/legacy/from-dep-tree.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,16 @@ describe('createFromDepTree simple dysmorphic', () => {
6767

6868
test('getPathsToRoot', async () => {
6969
expect(depGraph.pkgPathsToRoot({ name: 'd', version: '0.0.1' })).toHaveLength(1);
70+
expect(depGraph.countPathsToRoot({ name: 'd', version: '0.0.1' })).toBe(1);
7071

7172
expect(depGraph.pkgPathsToRoot({ name: 'd', version: '0.0.2' })).toHaveLength(1);
73+
expect(depGraph.countPathsToRoot({ name: 'd', version: '0.0.2' })).toBe(1);
7274

7375
expect(depGraph.pkgPathsToRoot({ name: 'c', version: '1.0.0' })).toHaveLength(2);
76+
expect(depGraph.countPathsToRoot({ name: 'c', version: '1.0.0' })).toBe(2);
7477

7578
expect(depGraph.pkgPathsToRoot({ name: 'e', version: '5.0.0' })).toHaveLength(2);
79+
expect(depGraph.countPathsToRoot({ name: 'e', version: '5.0.0' })).toBe(2);
7680

7781
expect(depGraph.pkgPathsToRoot({ name: 'e', version: '5.0.0' })).toEqual([
7882
[
@@ -141,7 +145,7 @@ describe('createFromDepTree goof', () => {
141145
const depTree = helpers.loadFixture('goof-dep-tree.json');
142146
const expectedGraph = helpers.loadFixture('goof-graph.json');
143147

144-
let depGraph;
148+
let depGraph: types.DepGraph;
145149
test('create', async () => {
146150
depGraph = await depGraphLib.legacy.depTreeToGraph(depTree, 'npm');
147151

@@ -152,19 +156,33 @@ describe('createFromDepTree goof', () => {
152156
const depGraphInternal = depGraph as types.DepGraphInternal;
153157

154158
const stripAnsiPkg = {name: 'strip-ansi', version: '3.0.1'};
155-
const stripAnsiNodes = depGraph.getPkgNodeIds(stripAnsiPkg);
159+
const stripAnsiNodes = depGraphInternal.getPkgNodeIds(stripAnsiPkg);
156160
expect(stripAnsiNodes).toHaveLength(2);
157161

158162
const stripAnsiPaths = depGraph.pkgPathsToRoot(stripAnsiPkg);
159163
expect(stripAnsiPaths).toHaveLength(6);
164+
expect(depGraph.countPathsToRoot(stripAnsiPkg)).toBe(6);
160165

161166
expect(depGraph.pkgPathsToRoot({name: 'ansi-regex', version: '2.0.0'})).toHaveLength(4);
167+
expect(depGraph.countPathsToRoot({name: 'ansi-regex', version: '2.0.0'})).toBe(4);
162168
expect(depGraph.pkgPathsToRoot({name: 'ansi-regex', version: '2.1.1'})).toHaveLength(3);
169+
expect(depGraph.countPathsToRoot({name: 'ansi-regex', version: '2.1.1'})).toBe(3);
170+
expect(depGraph.pkgPathsToRoot({name: 'wrappy', version: '1.0.2'})).toHaveLength(22);
171+
expect(depGraph.countPathsToRoot({name: 'wrappy', version: '1.0.2'})).toBe(22);
163172

164173
const expressNodes = depGraphInternal.getPkgNodeIds({name: 'express', version: '4.12.4'});
165174
expect(expressNodes).toHaveLength(1);
166175
});
167176

177+
test('count paths to root', async () => {
178+
for (const pkg of depGraph.getPkgs()) {
179+
const firstResult = depGraph.countPathsToRoot(pkg);
180+
const secondResult = depGraph.countPathsToRoot(pkg);
181+
expect(secondResult).toEqual(firstResult);
182+
expect(secondResult).toEqual(depGraph.pkgPathsToRoot(pkg).length);
183+
}
184+
});
185+
168186
test('compare to expected fixture', async () => {
169187
expect(depGraph.toJSON()).toEqual(expectedGraph);
170188
});

0 commit comments

Comments
 (0)