UNPKG

dependency-cruiser

Version:

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

289 lines (272 loc) 7.45 kB
import { EOL } from "node:os"; import { styleText } from "node:util"; import { formatNumber, formatPercentage } from "./utl/index.mjs"; /** * @typedef {"string"|"integer"|"percent"} ColumnFormatType * @typedef {{title: string; format: ColumnFormatType; width: string}} IColumnMetaData * @typedef {Map<string,IColumnMetaData>} IMeta * @typedef {{name: string; moduleCount: number; afferentCouplings:number; efferentCouplings:number; instability:number; size:number; topLevelStatementCount:number;}} IData * @typedef {{meta: IMeta; data: IData[]}} ITableObject */ const METRIC_WIDTH = 5; function getMetricsFromFolder({ name, moduleCount, afferentCouplings, efferentCouplings, instability, experimentalStats, }) { const lReturnValue = { type: "folder", name, moduleCount, afferentCouplings, efferentCouplings, instability, }; if (experimentalStats) { lReturnValue.size = experimentalStats.size; lReturnValue.topLevelStatementCount = experimentalStats.topLevelStatementCount; } return lReturnValue; } /** * * @param {import("../../types/cruise-result.mjs").IModule} param0 * @returns */ function getMetricsFromModule({ source, dependents, dependencies, instability, experimentalStats, }) { const lReturnValue = { type: "module", name: source, moduleCount: 1, afferentCouplings: dependents.length, efferentCouplings: dependencies.length, instability, }; if (experimentalStats) { lReturnValue.size = experimentalStats.size; lReturnValue.topLevelStatementCount = experimentalStats.topLevelStatementCount; } return lReturnValue; } function metricsAreCalculable(pModule) { return Object.hasOwn(pModule, "instability"); } function componentIsCalculable(pComponent) { return ( Number.isInteger(pComponent.moduleCount) && pComponent.moduleCount > -1 ); } /** * * @param {{modules: import("../../types/cruise-result.mjs").IModule[]; folders: import("../../types/cruise-result.mjs").IFolder[];}} param0 * @param {smallIntegerWidth:number; largeIntegerWidth: number} param1 * @returns {ITableObject} */ // eslint-disable-next-line max-lines-per-function function transformToTableObject( { modules, folders }, { smallIntegerWidth, largeIntegerWidth }, ) { const lReturnValue = { meta: new Map([ ["type", { width: 6, title: "type", format: "string" }], ["name", { width: 0, title: "name", format: "string" }], [ "moduleCount", { width: smallIntegerWidth, title: "N", format: "integer", }, ], [ "afferentCouplings", { width: smallIntegerWidth, title: "Ca", format: "integer", }, ], [ "efferentCouplings", { width: smallIntegerWidth, title: "Ce", format: "integer", }, ], [ "instability", { width: smallIntegerWidth, title: "I (%)", format: "percent", }, ], [ "size", { width: largeIntegerWidth, title: "size", format: "integer", }, ], [ "topLevelStatementCount", { width: smallIntegerWidth, title: "#tls", format: "integer", }, ], ]), data: [], }; lReturnValue.data = folders .map(getMetricsFromFolder) .concat(modules.filter(metricsAreCalculable).map(getMetricsFromModule)) .filter(componentIsCalculable); const lMaxNameWidth = lReturnValue.data .map(({ name }) => name.length) .concat(lReturnValue.meta.get("name").title.length) .sort((pLeft, pRight) => pLeft - pRight) .pop(); lReturnValue.meta.get("name").width = lMaxNameWidth; return lReturnValue; } function orderByNumber(pAttributeName) { return (pLeft, pRight) => // eslint-disable-next-line security/detect-object-injection (pRight[pAttributeName] || 0) - (pLeft[pAttributeName] || 0); } function orderByString(pAttributeName) { return (pLeft, pRight) => // eslint-disable-next-line security/detect-object-injection pLeft[pAttributeName].localeCompare(pRight[pAttributeName]); } /** * @param {IMeta} pMetaData * @returns {string} */ function formatToTextHeader(pMetaData) { return Array.from(pMetaData.values()) .map(({ title, width, format }) => format === "string" ? title.padEnd(width + 1) : title.padStart(width + 1), ) .join(" "); } /** * @param {IMeta} pMetaData * @returns {string} */ function formatToTextDemarcationLine(pMetaData) { return Array.from(pMetaData.values()) .map(({ width }) => "-".repeat(width + 1)) .join(" "); } /** * @param {IData[]} pData * @param {IMeta} pMetaData * @returns {string} */ function formatToTextData(pData, pMetaData) { return pData .map((pRow) => { return Object.keys(pRow) .map((pKey) => { const lMeta = pMetaData.get(pKey); if (lMeta.format === "string") { // eslint-disable-next-line security/detect-object-injection return pRow[pKey].padEnd(lMeta.width + 1); } if (lMeta.format === "percent") { // eslint-disable-next-line security/detect-object-injection return formatPercentage(pRow[pKey]).padStart(lMeta.width + 1); } // eslint-disable-next-line security/detect-object-injection return Number.isInteger(pRow[pKey]) ? // eslint-disable-next-line security/detect-object-injection formatNumber(pRow[pKey]).padStart(lMeta.width + 1) : "".padStart(lMeta.width + 1); }) .join(" "); }) .join(EOL); } /** * @param {IData[]} pData * @param {IMeta} pMetaData * @returns {string} */ function formatToTextTable(pData, pMetaData) { return [styleText("bold", formatToTextHeader(pMetaData))] .concat(formatToTextDemarcationLine(pMetaData)) .concat(formatToTextData(pData, pMetaData)) .join(EOL) .concat(EOL); } /** * * @param {{modules: import("../../types/cruise-result.mjs").IModule[]; folders: import("../../types/cruise-result.mjs").IFolder[];}} param0 * @param {{orderBy: string; hideFolders: boolean; hideModules: boolean; experimentalStats: boolean;}} param1 * @returns {string} */ function transformMetricsToTable( { modules, folders }, { orderBy, hideFolders, hideModules }, ) { /** @type {ITableObject} */ const lObject = transformToTableObject( { modules, folders }, { smallIntegerWidth: METRIC_WIDTH, largeIntegerWidth: METRIC_WIDTH + METRIC_WIDTH, }, ); let lComponents = lObject.data .filter( (pComponent) => (!hideModules && pComponent.type === "module") || (!hideFolders && pComponent.type === "folder"), ) .sort(orderByString("name")) .sort(orderByNumber(orderBy || "instability")); return formatToTextTable(lComponents, lObject.meta); } /** * returns stability metrics of modules & folders in an ascii table * * Potential future features: * - additional output formats (csv?, html?) * * @param {import('../../types/dependency-cruiser.js').ICruiseResult} pCruiseResult - * @param {import("../../types/reporter-options.js").IMetricsReporterOptions} pReporterOptions * @return {import('../../types/dependency-cruiser.js').IReporterOutput} - */ export default function metrics(pCruiseResult, pReporterOptions) { const lReporterOptions = pReporterOptions || {}; if (pCruiseResult.folders) { return { output: transformMetricsToTable(pCruiseResult, lReporterOptions), exitCode: 0, }; } else { return { output: `${EOL}ERROR: The cruise result didn't contain any metrics - re-running the cruise with${EOL}` + ` the '--metrics' command line option should fix that.${EOL}${EOL}`, exitCode: 1, }; } }