UNPKG

dependency-cruiser

Version:

Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.

257 lines (233 loc) 7.58 kB
/* eslint-disable security/detect-object-injection */ /** * @import { IFolderDependency, IDependency, IFolder, IModule } from "../../types/cruise-result.mjs" * @import { DependencyType, IMiniDependency } from "../../types/shared-types.mjs" * @typedef {(IDependency|IFolderDependency) & {name:string; dependencyTypes?: DependencyType[]}} IEdge * @typedef {IModule|IFolder} IModuleOrFolder * @typedef {IModuleOrFolder & {dependencies: IEdge[]}} IVertex */ export default class IndexedModuleGraph { #indexedGraph; /** * @param {IModuleOrFolder} pModule * @returns {IVertex} */ #normalizeModule(pModule) { return { ...pModule, dependencies: (pModule?.dependencies ?? []).map((pDependency) => ({ ...pDependency, name: pDependency.name ? pDependency.name : pDependency.resolved, })), }; } #init(pModules, pIndexAttribute) { this.#indexedGraph = new Map( pModules.map((pModule) => [ pModule[pIndexAttribute], this.#normalizeModule(pModule), ]), ); } /** * @param {import("../../types/dependency-cruiser.mjs").IModule[]} pModules * @param {string} pIndexAttribute */ constructor(pModules, pIndexAttribute = "source") { this.#init(pModules, pIndexAttribute); } /** * @param {string} pName * @return {IVertex} */ findVertexByName(pName) { return this.#indexedGraph.get(pName); } /** * @param {string} pName - the name of the module to find transitive dependents of. * @param {number} pMaxDepth - the maximum depth to search for dependents. * Defaults to 0 (no maximum). To only get direct dependents. * specify 1. To get dependents of these dependents as well * specify 2 - etc. * @param {number} pDepth - technical parameter - best to leave out of direct calls * @param {Set<string>} pVisited - technical parameter - best to leave out of direct calls * @returns {Array<string>} */ // eslint-disable-next-line complexity findTransitiveDependents( pName, pMaxDepth = 0, pDepth = 0, pVisited = new Set(), ) { /** @type {string[]} */ let lReturnValue = []; const lModule = this.#indexedGraph.get(pName); if (lModule && (!pMaxDepth || pDepth <= pMaxDepth)) { let lDependents = lModule.dependents || []; const lVisited = pVisited.add(pName); // @TODO this works for modules, but not for folders yet if (lDependents.length > 0) { for (const lDependent of lDependents) { // eslint-disable-next-line max-depth if (!lVisited.has(lDependent)) { this.findTransitiveDependents( lDependent, pMaxDepth, pDepth + 1, lVisited, ); } } } lReturnValue = Array.from(lVisited); } return lReturnValue; } /** * @param {string} pName - the name of the module to find transitive dependencies of. * @param {number} pMaxDepth - the maximum depth to search for dependencies * Defaults to 0 (no maximum). To only get direct dependencies. * specify 1. To get dependencies of these dependencies as well * specify 2 - etc. * @param {number} pDepth - technical parameter - best to leave out of direct calls * @param {Set<string>} pVisited - technical parameter - best to leave out of direct calls * @returns {Array<string>} */ findTransitiveDependencies( pName, pMaxDepth = 0, pDepth = 0, pVisited = new Set(), ) { /** @type {string[]} */ let lReturnValue = []; /** @type {IVertex} */ const lModule = this.#indexedGraph.get(pName); if (lModule && (!pMaxDepth || pDepth <= pMaxDepth)) { let lDependencies = lModule.dependencies; const lVisited = pVisited.add(pName); if (lDependencies.length > 0) { // eslint-disable-next-line budapestian/local-variable-pattern for (const { name } of lDependencies) { // eslint-disable-next-line max-depth if (!lVisited.has(name)) { this.findTransitiveDependencies( name, pMaxDepth, pDepth + 1, lVisited, ); } } } lReturnValue = Array.from(lVisited); } return lReturnValue; } /** * * @param {IEdge} pEdge * @returns {IMiniDependency} */ #geldEdge(pEdge) { return { name: pEdge.name, dependencyTypes: pEdge.dependencyTypes ?? [], }; } /** * @param {string} pFrom * @param {string} pTo * @param {Set<string>} pVisited * @returns {Array<IMiniDependency>} */ getPath(pFrom, pTo, pVisited = new Set()) { /** @type {IVertex} */ const lFromNode = this.#indexedGraph.get(pFrom); pVisited.add(pFrom); if (!lFromNode) { return []; } for (const lDependency of lFromNode.dependencies) { if (!pVisited.has(lDependency.name)) { if (lDependency.name === pTo) { return [this.#geldEdge(lDependency)]; } let lCandidatePath = this.getPath(lDependency.name, pTo, pVisited); if (lCandidatePath.length > 0) { return [this.#geldEdge(lDependency)].concat(lCandidatePath); } } } return []; } /** * Returns the first non-zero length path from pInitialSource to pInitialSource * Returns the empty array if there is no such path * * @param {string} pInitialSource The 'source' attribute of the node to be tested (source uniquely identifying a node) * @param {IEdge} pCurrentDependency The 'to' node to be traversed as a dependency object of the previous 'from' traversed * @param {Set<string>=} pVisited Technical parameter - best to leave out of direct calls * @return {Array<IMiniDependency>} see description above */ #getCycle(pInitialSource, pCurrentDependency, pVisited) { const lVisited = pVisited || new Set(); /** @type {IVertex} */ const lCurrentVertex = this.#indexedGraph.get(pCurrentDependency.name); const lEdges = lCurrentVertex.dependencies.filter( (pDependency) => !lVisited.has(pDependency.name), ); const lInitialAsDependency = lEdges.find( (pDependency) => pDependency.name === pInitialSource, ); if (lInitialAsDependency) { return pInitialSource === pCurrentDependency.name ? [this.#geldEdge(lInitialAsDependency)] : [ this.#geldEdge(pCurrentDependency), this.#geldEdge(lInitialAsDependency), ]; } /** @type {Array<IMiniDependency>} */ const lResult = []; for (const lDependency of lEdges) { if (!lResult.some((pSome) => pSome.name === pCurrentDependency.name)) { const lCycle = this.#getCycle( pInitialSource, lDependency, lVisited.add(lDependency.name), ); if ( lCycle.length > 0 && !lCycle.some((pSome) => pSome.name === pCurrentDependency.name) ) { lResult.push(this.#geldEdge(pCurrentDependency), ...lCycle); return lResult; } } } return lResult; } /** * Returns the first non-zero length path from pInitialSource to pInitialSource * Returns the empty array if there is no such path * * @param {string} pInitialSource The 'source' attribute of the node to be tested * (source uniquely identifying a node) * @param {string} pCurrentSource The 'source' attribute of the 'to' node to * be traversed * @return {Array<IMiniDependency>} see description above */ getCycle(pInitialSource, pCurrentSource) { /** @type {IVertex} */ const lInitialNode = this.#indexedGraph.get(pInitialSource); const lCurrentDependency = lInitialNode.dependencies.find( (pDependency) => pDependency.name === pCurrentSource, ); if (!lCurrentDependency) { return []; } return this.#getCycle(pInitialSource, lCurrentDependency); } }