@snyk/dep-graph
Version:
Snyk dependency graph library
289 lines • 10.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DepGraphImpl = void 0;
const _isEqual = require("lodash.isequal");
const graphlib = require("../graphlib");
const create_from_json_1 = require("./create-from-json");
class DepGraphImpl {
constructor(_graph, _rootNodeId, _pkgs, _pkgNodes, _pkgManager) {
this._graph = _graph;
this._rootNodeId = _rootNodeId;
this._pkgs = _pkgs;
this._pkgNodes = _pkgNodes;
this._pkgManager = _pkgManager;
this._countNodePathsToRootCache = new Map();
this._rootPkgId = _graph.node(_rootNodeId).pkgId;
this._pkgList = Object.values(_pkgs);
this._depPkgsList = this._pkgList.filter((pkg) => pkg !== this.rootPkg);
}
static getPkgId(pkg) {
return `${pkg.name}@${pkg.version || ''}`;
}
get pkgManager() {
return this._pkgManager;
}
get rootPkg() {
return this._pkgs[this._rootPkgId];
}
get rootNodeId() {
return this._rootNodeId;
}
/**
* Get all unique packages in the graph (including the root package)
*/
getPkgs() {
return this._pkgList;
}
/**
* Get all unique packages in the graph (excluding the root package)
*/
getDepPkgs() {
return this._depPkgsList;
}
getPkgNodes(pkg) {
const pkgId = DepGraphImpl.getPkgId(pkg);
const nodes = [];
for (const nodeId of Array.from(this._pkgNodes[pkgId])) {
const graphNode = this.getGraphNode(nodeId);
nodes.push({
info: graphNode.info || {},
});
}
return nodes;
}
getNode(nodeId) {
return this.getGraphNode(nodeId).info || {};
}
getNodePkg(nodeId) {
return this._pkgs[this.getGraphNode(nodeId).pkgId];
}
getPkgNodeIds(pkg) {
const pkgId = DepGraphImpl.getPkgId(pkg);
if (!this._pkgs[pkgId]) {
throw new Error(`no such pkg: ${pkgId}`);
}
return Array.from(this._pkgNodes[pkgId]);
}
getNodeDepsNodeIds(nodeId) {
const deps = this._graph.successors(nodeId);
if (!deps) {
throw new Error(`no such node: ${nodeId}`);
}
return deps;
}
getNodeParentsNodeIds(nodeId) {
const parents = this._graph.predecessors(nodeId);
if (!parents) {
throw new Error(`no such node: ${nodeId}`);
}
return parents;
}
hasCycles() {
// `isAcyclic` is expensive, so memoize
if (this._hasCycles === undefined) {
this._hasCycles = !graphlib.alg.isAcyclic(this._graph);
}
return this._hasCycles;
}
pkgPathsToRoot(pkg, opts) {
const pathsToRoot = [];
const limit = opts === null || opts === void 0 ? void 0 : opts.limit;
for (const nodeId of this.getPkgNodeIds(pkg)) {
const pathsFromNodeToRoot = this.pathsFromNodeToRoot(nodeId, [], {
limit,
});
for (const path of pathsFromNodeToRoot) {
pathsToRoot.push(path);
}
if (limit && pathsToRoot.length >= limit) {
break;
}
}
// note: sorting to get shorter paths first -
// it's nicer - and better resembles older behaviour
return pathsToRoot.sort((a, b) => a.length - b.length);
}
countPathsToRoot(pkg, opts) {
let count = 0;
const limit = opts === null || opts === void 0 ? void 0 : opts.limit;
for (const nodeId of this.getPkgNodeIds(pkg)) {
if (this._countNodePathsToRootCache.has(nodeId)) {
count += this._countNodePathsToRootCache.get(nodeId);
}
else {
const c = this.countNodePathsToRoot(nodeId, limit);
// don't cache if a limit was supplied
if (!limit) {
this._countNodePathsToRootCache.set(nodeId, c);
}
count += c;
}
if (limit && count >= limit) {
return limit;
}
}
return count;
}
isTransitive(pkg) {
const checking = new Set(this.getPkgNodeIds(pkg));
for (const directDep of this.getNodeDepsNodeIds(this.rootNodeId)) {
if (checking.has(directDep))
return false;
}
return true;
}
equals(other, { compareRoot = true } = {}) {
let otherDepGraph;
if (other instanceof DepGraphImpl) {
otherDepGraph = other;
}
else {
// At runtime theoretically we can have multiple versions of
// @snyk/dep-graph. If "other" is not an instance of the same class it is
// safer to rebuild it from JSON.
otherDepGraph = (0, create_from_json_1.createFromJSON)(other.toJSON());
}
// In theory, for the graphs created by standard means, `_.isEquals(this._data, otherDepGraph._data)`
// should suffice, since node IDs will be generated in a predictable way.
// However, there might be different versions of graph and inconsistencies
// in the ordering of the arrays, so we perform a deep comparison.
return this.nodeEquals(this, this.rootNodeId, otherDepGraph, otherDepGraph.rootNodeId, compareRoot);
}
directDepsLeadingTo(pkg) {
const pkgNodes = this.getPkgNodeIds(pkg);
const directDeps = this.getNodeDepsNodeIds(this.rootNodeId);
const nodes = directDeps.filter((directDep) => {
const reachableNodes = graphlib.alg.postorder(this._graph, [directDep]);
return reachableNodes.filter((node) => pkgNodes.includes(node)).length;
});
return nodes.map((node) => this.getNodePkg(node));
}
/**
* Create a JSON representation of a dep graph. This is typically used to
* send the dep graph over the wire
*/
toJSON() {
const nodeIds = this._graph.nodes();
const nodes = nodeIds.reduce((acc, nodeId) => {
const deps = (this._graph.successors(nodeId) || []).map((depNodeId) => ({
nodeId: depNodeId,
}));
const node = this._graph.node(nodeId);
const elem = {
nodeId,
pkgId: node.pkgId,
deps,
};
if (node.info && Object.keys(node.info).length > 0) {
elem.info = node.info;
}
acc.push(elem);
return acc;
}, []);
const pkgs = Object.keys(this._pkgs).map((pkgId) => ({
id: pkgId,
info: this._pkgs[pkgId],
}));
return {
schemaVersion: DepGraphImpl.SCHEMA_VERSION,
pkgManager: this._pkgManager,
pkgs,
graph: {
rootNodeId: this._rootNodeId,
nodes,
},
};
}
nodeEquals(graphA, nodeIdA, graphB, nodeIdB, compareRoot, traversedPairs = new Set()) {
// Skip root nodes comparison if needed.
if (compareRoot ||
(nodeIdA !== graphA.rootNodeId && nodeIdB !== graphB.rootNodeId)) {
const pkgA = graphA.getNodePkg(nodeIdA);
const pkgB = graphB.getNodePkg(nodeIdB);
// Compare PkgInfo (name and version).
if (!_isEqual(pkgA, pkgB)) {
return false;
}
const infoA = graphA.getNode(nodeIdA);
const infoB = graphB.getNode(nodeIdB);
// Compare NodeInfo (VersionProvenance and labels).
if (!_isEqual(infoA, infoB)) {
return false;
}
}
let depsA = graphA.getNodeDepsNodeIds(nodeIdA);
let depsB = graphB.getNodeDepsNodeIds(nodeIdB);
// Number of dependencies should be the same.
if (depsA.length !== depsB.length) {
return false;
}
// Sort dependencies by name@version string.
const sortFn = (graph) => (idA, idB) => {
const pkgA = graph.getNodePkg(idA);
const pkgB = graph.getNodePkg(idB);
return DepGraphImpl.getPkgId(pkgA).localeCompare(DepGraphImpl.getPkgId(pkgB));
};
depsA = depsA.sort(sortFn(graphA));
depsB = depsB.sort(sortFn(graphB));
// Compare Each dependency recursively.
for (let i = 0; i < depsA.length; i++) {
const pairKey = `${depsA[i]}_${depsB[i]}`;
// Prevent cycles.
if (traversedPairs.has(pairKey)) {
continue;
}
traversedPairs.add(pairKey);
if (!this.nodeEquals(graphA, depsA[i], graphB, depsB[i], compareRoot, traversedPairs)) {
return false;
}
}
return true;
}
getGraphNode(nodeId) {
const node = this._graph.node(nodeId);
if (!node) {
throw new Error(`no such node: ${nodeId}`);
}
return node;
}
pathsFromNodeToRoot(nodeId, ancestors = [], opts) {
const parentNodesIds = this.getNodeParentsNodeIds(nodeId);
const pkgInfo = this.getNodePkg(nodeId);
if (parentNodesIds.length === 0) {
return [[pkgInfo]];
}
const allPaths = [];
ancestors = ancestors.concat(nodeId);
const limit = opts.limit;
for (const id of parentNodesIds) {
if (ancestors.includes(id))
continue;
const pathsFromNodeToRoot = this.pathsFromNodeToRoot(id, ancestors, opts);
for (const path of pathsFromNodeToRoot) {
allPaths.push([pkgInfo].concat(path));
}
if (limit && allPaths.length >= limit) {
break;
}
}
return allPaths;
}
countNodePathsToRoot(nodeId, limit = 0, count = 0, visited = []) {
if (nodeId === this._rootNodeId) {
return count + 1;
}
visited = visited.concat(nodeId);
for (const parentNodeId of this.getNodeParentsNodeIds(nodeId)) {
if (!visited.includes(parentNodeId)) {
count = this.countNodePathsToRoot(parentNodeId, limit, count, visited);
if (limit && count >= limit) {
return limit;
}
}
}
return count;
}
}
exports.DepGraphImpl = DepGraphImpl;
DepGraphImpl.SCHEMA_VERSION = '1.3.0';
//# sourceMappingURL=dep-graph.js.map