UNPKG

escomplex-plugin-metrics-project

Version:

Provides the core project metric / report generation plugin for typhonjs-escomplex project processing.

289 lines (233 loc) 11.3 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); var _createClass2 = require('babel-runtime/helpers/createClass'); var _createClass3 = _interopRequireDefault(_createClass2); var _MathUtil = require('typhonjs-escomplex-commons/dist/utils/MathUtil'); var _MathUtil2 = _interopRequireDefault(_MathUtil); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * ProjectMetricCalculate */ var ProjectMetricCalculate = function () { function ProjectMetricCalculate() { (0, _classCallCheck3.default)(this, ProjectMetricCalculate); } (0, _createClass3.default)(ProjectMetricCalculate, null, [{ key: 'calculate', /** * Calculates project report. * * @param {ProjectReport} projectReport - Project report to calculate * @param {object} pathModule - * @param {object} settings - */ value: function calculate(projectReport, pathModule, settings) { var adjacencyMatrix = ProjectMetricCalculate.calculateAdjacencyMatrix(projectReport, pathModule); if (!settings.noCoreSize) { var visibilityMatrix = ProjectMetricCalculate.calculateVisibilityMatrix(projectReport, adjacencyMatrix); ProjectMetricCalculate.calculateCoreSize(projectReport, visibilityMatrix); } } /** * Calculates an adjacency matrix for all modules based on ES Module and CommonJS dependencies also storing a * compacted while returning the matrix for further calculation. Each row index corresponds to the same module index. * Each row entry corresponds to a module index. These relationships dictate the dependencies between all * module ModuleReports given the source paths. * * @param {object} projectReport - The project report being processed. * @param {object} pathModule - A module that conforms to the Node path API. * * @returns {Array<Array<number>>} * @private */ }, { key: 'calculateAdjacencyMatrix', value: function calculateAdjacencyMatrix(projectReport, pathModule) { var modules = projectReport.modules; var length = modules.length; var adjacencyMatrix = _MathUtil2.default.create2DArray(length, 0); var density = 0; for (var x = 0; x < length; x++) { for (var y = 0; y < length; y++) { adjacencyMatrix[x][y] = x !== y && ProjectMetricCalculate.doesDependencyExist(pathModule, modules[x], modules[y]) ? 1 : 0; if (adjacencyMatrix[x][y] === 1) { density += 1; } } } projectReport.adjacencyList = _MathUtil2.default.compactMatrix(adjacencyMatrix); projectReport.firstOrderDensity = _MathUtil2.default.getPercent(density, length * length); return adjacencyMatrix; } /** * Calculates core size which is the percentage of modules / files that are both widely depended on and themselves * depend on other modules. Lower is better. * * @param {object} projectReport - The project report being processed. * @param {Array<Array<number>>} visibilityMatrix - The calculated visibilityMatrix. * * @private */ }, { key: 'calculateCoreSize', value: function calculateCoreSize(projectReport, visibilityMatrix) { if (projectReport.firstOrderDensity === 0) { projectReport.coreSize = 0; return; } var length = visibilityMatrix.length; var fanIn = new Array(length); var fanOut = new Array(length); var coreSize = 0; var _loop = function _loop(rowIndex) { fanIn[rowIndex] = visibilityMatrix[rowIndex].reduce(function (sum, value, valueIndex) { fanOut[valueIndex] = rowIndex === 0 ? value : fanOut[valueIndex] + value; return sum + value; }, 0); }; for (var rowIndex = 0; rowIndex < length; rowIndex++) { _loop(rowIndex); } // Boundary values can also be chosen by looking for discontinuity in the // distribution of values, but to keep it simple the median is used. var boundaries = { fanIn: _MathUtil2.default.getMedian(fanIn.slice()), fanOut: _MathUtil2.default.getMedian(fanOut.slice()) }; for (var rowIndex = 0; rowIndex < length; rowIndex++) { if (fanIn[rowIndex] >= boundaries.fanIn && fanOut[rowIndex] >= boundaries.fanOut) { coreSize += 1; } } projectReport.coreSize = _MathUtil2.default.getPercent(coreSize, length); } /** * Stores a compacted form of the visibility matrix. Each row index corresponds to the same module index. * Each row entry corresponds to a module index. These relationships dictate the reverse visibility between all * module ModuleReports which may indirectly impact the given module / file. The full matrix is returned for further * calculation. * * Implementation of Floyd Warshall algorithm for calculating visibility matrix in O(n^3) instead of O(n^4) with * successive raising of powers. * * @param {object} projectReport - The project report being processed. * @param {Array<Array<number>>} adjacencyMatrix - The calculated adjacencyMatrix. * * @return {Array<Array<number>>} * @private */ }, { key: 'calculateVisibilityMatrix', value: function calculateVisibilityMatrix(projectReport, adjacencyMatrix) { var changeCost = 0; var length = adjacencyMatrix.length; var visibilityMatrix = _MathUtil2.default.create2DArray(length, 0); // Convert adjacency matrix to a distance matrix suitable for the Floyd Warshall algorithm. // if i !== j and adjacency matrix value is 0 set distance to Infinity. for (var x = 0; x < length; x++) { for (var y = 0; y < length; y++) { visibilityMatrix[x][y] = x === y ? 1 : adjacencyMatrix[x][y] || Infinity; } } // Floyd Warshall core algorithm for (var k = 0; k < length; k++) { for (var _x = 0; _x < length; _x++) { for (var _y = 0; _y < length; _y++) { if (visibilityMatrix[_x][_y] > visibilityMatrix[_x][k] + visibilityMatrix[k][_y]) { visibilityMatrix[_x][_y] = visibilityMatrix[_x][k] + visibilityMatrix[k][_y]; } } } } // Convert back from a distance matrix to adjacency matrix while also calculating change cost. for (var _x2 = 0; _x2 < length; _x2++) { for (var _y2 = 0; _y2 < length; _y2++) { if (visibilityMatrix[_x2][_y2] < Infinity) { changeCost++; if (_x2 !== _y2) { visibilityMatrix[_x2][_y2] = 1; } } else { visibilityMatrix[_x2][_y2] = 0; } } } projectReport.visibilityList = _MathUtil2.default.compactMatrix(visibilityMatrix); projectReport.changeCost = _MathUtil2.default.getPercent(changeCost, length * length); return visibilityMatrix; } /** * Determines if there is at least one dependency that matches `toModuleReport.srcPath` from all the dependencies * stored in `fromModuleReport`. * * @param {object} pathModule - A module that conforms to the Node path API. * @param {ModuleReport} fromModuleReport - A ModuleReport to match to the srcPath of `toModuleReport`. * @param {ModuleReport} toModuleReport - A ModuleReport providing the `srcPath` to match. * * @returns {boolean} * @private */ }, { key: 'doesDependencyExist', value: function doesDependencyExist(pathModule, fromModuleReport, toModuleReport) { var matchedDependency = false; var fromModuleReport_dirname = pathModule.dirname(fromModuleReport.srcPath); // First test for srcPathAlias which is the case when an NPM or JSPM module has a main entry and is mapped to a // given name or alias. for (var cntr = 0; cntr < fromModuleReport.dependencies.length; cntr++) { var depPath = fromModuleReport.dependencies[cntr].path; if (typeof toModuleReport.srcPathAlias === 'string' && depPath === toModuleReport.srcPathAlias) { matchedDependency = true; break; } } // Exit early if alias match was found above. if (matchedDependency) { return true; } // Now test for srcPath matches. for (var _cntr = 0; _cntr < fromModuleReport.dependencies.length; _cntr++) { var _depPath = fromModuleReport.dependencies[_cntr].path; // If there is no extension provided in the dependency then add the extension of the `to srcPath`. if (pathModule.extname(_depPath) === '') { _depPath += pathModule.extname(toModuleReport.srcPath); } // Best case match scenario when dependency matches toModuleReportPath.srcPath. if (_depPath === toModuleReport.srcPath) { matchedDependency = true; break; } // Make sure that fromModuleReport dirname has the path separator prepended. This is necessary to make sure // pathModule (Node.js path) treats `fromModuleReport_dirname` as the absolute root. if (!fromModuleReport_dirname.startsWith(pathModule.sep)) { fromModuleReport_dirname = '' + pathModule.sep + fromModuleReport_dirname; } if (pathModule.resolve(fromModuleReport_dirname, _depPath) === toModuleReport.srcPath) { matchedDependency = true; break; } var toModuleReport_modpath = toModuleReport.srcPath; // Remove any local directory (`.`) leading character from `toModuleReport_modpath`. if (toModuleReport_modpath.startsWith('.')) { toModuleReport_modpath = toModuleReport_modpath.replace(/^\./, ''); } // Ensure `toModuleReport_modpath` starts with the path separator. if (!toModuleReport_modpath.startsWith(pathModule.sep)) { toModuleReport_modpath = '' + pathModule.sep + toModuleReport_modpath; } if (pathModule.resolve(fromModuleReport_dirname, _depPath) === toModuleReport_modpath) { matchedDependency = true; break; } } return matchedDependency; } }]); return ProjectMetricCalculate; }(); exports.default = ProjectMetricCalculate; module.exports = exports['default'];