Home Manual Reference Source Test Repository

src/PluginMetricsModule.js

import HalsteadArray from 'typhonjs-escomplex-commons/src/module/traits/HalsteadArray';
import TraitUtil     from 'typhonjs-escomplex-commons/src/module/traits/TraitUtil';

import ObjectUtil    from 'typhonjs-escomplex-commons/src/utils/ObjectUtil';

/**
 * Provides a typhonjs-escomplex-module / ESComplexModule plugin which gathers and calculates all default metrics.
 *
 * @see https://www.npmjs.com/package/typhonjs-escomplex-commons
 * @see https://www.npmjs.com/package/typhonjs-escomplex-module
 */
export default class PluginMetricsModule
{
   // ESComplexModule plugin callbacks ------------------------------------------------------------------------------

   /**
    * Loads any default settings that are not already provided by any user options.
    *
    * @param {object}   ev - escomplex plugin event data.
    *
    * The following options are:
    * ```
    * (boolean)   newmi - Boolean indicating whether the maintainability index should be rebased on a scale from
    *                     0 to 100; defaults to false.
    * ```
    */
   onConfigure(ev)
   {
      ev.data.settings.newmi = typeof ev.data.options.newmi === 'boolean' ? ev.data.options.newmi : false;
   }

   /**
    * During AST traversal when a node is entered it is processed immediately if the node type corresponds to a
    * loaded trait syntax. Any new report scopes are handled in `onEnterNode`.
    *
    * @param {object}   ev - escomplex plugin event data.
    */
   onEnterNode(ev)
   {
      const report = ev.data.report;
      const node = ev.data.node;
      const parent = ev.data.parent;
      const syntax = this.syntaxes[node.type];

      // Process node syntax.
      if (syntax !== null && typeof syntax === 'object')
      {
         for (const key in syntax)
         {
            switch (syntax[key].metric)
            {
               case 'cyclomatic':
                  report.incrementCyclomatic(syntax[key].valueOf(node, parent));
                  break;

               case 'dependencies':
                  report.addDependencies(syntax[key].valueOf(node, parent));
                  break;

               case 'lloc':
                  report.incrementLogicalSloc(syntax[key].valueOf(node, parent));
                  break;
            }

            // Process operands / operators HalsteadArray entries
            if (syntax[key] instanceof HalsteadArray)
            {
               report.processHalsteadItems(syntax[key].metric, syntax[key].valueOf(node, parent));
            }
         }

         // Handle creating new scope if applicable.
         if (syntax.newScope)
         {
            switch (syntax.newScope.valueOf(node, parent))
            {
               case 'class':
                  report.createScope('class', TraitUtil.safeName(node.id), node.loc.start.line, node.loc.end.line);
                  break;

               case 'method':
                  report.createScope('method', TraitUtil.safeComputedName(node, parent), node.loc.start.line,
                   node.loc.end.line, node.params.length);
                  break;
            }
         }

         // Return any child keys to ignore in AST walking or an empty array.
         ev.data.ignoreKeys = syntax.ignoreKeys ? syntax.ignoreKeys.valueOf(node, parent) : [];
      }
   }

   /**
    * During AST traversal when a node is exited it is processed immediately if the node type corresponds to a
    * loaded trait syntax. If a node has a new report scope it is popped in `onExitNode`.
    *
    * @param {object}   ev - escomplex plugin event data.
    */
   onExitNode(ev)
   {
      const report = ev.data.report;
      const node = ev.data.node;
      const parent = ev.data.parent;
      const syntax = this.syntaxes[node.type];

      if (syntax !== null && typeof syntax === 'object' && syntax.newScope)
      {
         switch (syntax.newScope.valueOf(node, parent))
         {
            case 'class':
               report.popScope('class');
               break;

            case 'method':
               report.popScope('method');
               break;
         }
      }
   }

   /**
    * Performs final calculations based on collected report data.
    *
    * @param {object}   ev - escomplex plugin event data.
    */
   onModuleEnd(ev)
   {
      this._calculateMetrics(ev.data.report);
   }

   /**
    * Stores settings and syntaxes, initializes local variables and creates the initial aggregate report.
    *
    * @param {object}   ev - escomplex plugin event data.
    */
   onModuleStart(ev)
   {
      /**
       * Stores the settings for all ESComplexModule plugins.
       * @type {object}
       */
      this.settings = ev.data.settings;

      /**
       * Stores the trait syntaxes loaded by other ESComplexModule plugins.
       * @type {object}
       */
      this.syntaxes = ev.data.syntaxes;
   }

   // Module metrics calculation ------------------------------------------------------------------------------------

   /**
    * Calculates cyclomatic density - Proposed as a modification to cyclomatic complexity by Geoffrey K. Gill and
    * Chris F. Kemerer in 1991, this metric simply re-expresses it as a percentage of the logical lines of code. Lower
    * is better.
    *
    * @param {MethodReport}   report - A MethodReport to perform calculations on.
    *
    * @private
    */
   _calculateCyclomaticDensity(report)
   {
      report.cyclomaticDensity = report.sloc.logical === 0 ? 0 : (report.cyclomatic / report.sloc.logical) * 100;
   }

   /**
    * Calculates Halstead metrics. In 1977, Maurice Halstead developed a set of metrics which are calculated based on
    * the number of distinct operators, the number of distinct operands, the total number of operators and the total
    * number of operands in each function. This site picks out three Halstead measures in particular: difficulty,
    * volume and effort.
    *
    * @param {HalsteadData}   halstead - A HalsteadData instance to perform calculations on.
    *
    * @see https://en.wikipedia.org/wiki/Halstead_complexity_measures
    *
    * @private
    */
   _calculateHalsteadMetrics(halstead)
   {
      halstead.length = halstead.operators.total + halstead.operands.total;

      /* istanbul ignore if */
      if (halstead.length === 0)
      {
         halstead.reset();
      }
      else
      {
         halstead.vocabulary = halstead.operators.distinct + halstead.operands.distinct;
         halstead.difficulty = (halstead.operators.distinct / 2)
          * (halstead.operands.distinct === 0 ? 1 : halstead.operands.total / halstead.operands.distinct);
         halstead.volume = halstead.length * (Math.log(halstead.vocabulary) / Math.log(2));
         halstead.effort = halstead.difficulty * halstead.volume;
         halstead.bugs = halstead.volume / 3000;
         halstead.time = halstead.effort / 18;
      }
   }

   /**
    * Designed in 1991 by Paul Oman and Jack Hagemeister at the University of Idaho, this metric is calculated at the
    * whole program or module level from averages of the other 3 metrics, using the following formula:
    * ```
    * 171 -
    * (3.42 * ln(mean effort)) -
    * (0.23 * ln(mean cyclomatic complexity)) -
    * (16.2 * ln(mean logical LOC))
    * ```
    * Values are on a logarithmic scale ranging from negative infinity up to 171, with greater numbers indicating a
    * higher level of maintainability. In their original paper, Oman and Hagemeister identified 65 as the threshold
    * value below which a program should be considered difficult to maintain.
    *
    * @param {ClassReport|ModuleReport}   report - A ClassReport or ModuleReport to perform calculations on.
    * @param {number}               averageCyclomatic - Average cyclomatic metric across a ClassReport / ModuleReport.
    * @param {number}               averageEffort - Average Halstead effort across a ClassReport / ModuleReport.
    * @param {number}               averageLoc - Average SLOC metric across a ClassReport / ModuleReport.
    *
    * @private
    */
   _calculateMaintainabilityIndex(report, averageCyclomatic, averageEffort, averageLoc)
   {
      /* istanbul ignore if */
      if (averageCyclomatic === 0) { throw new Error('Encountered function with cyclomatic complexity zero!'); }

      report.maintainability =
       171
       - (3.42 * Math.log(averageEffort))
       - (0.23 * Math.log(averageCyclomatic))
       - (16.2 * Math.log(averageLoc));

      /* istanbul ignore if */
      if (report.maintainability > 171) { report.maintainability = 171; }

      /* istanbul ignore if */
      if (this.settings.newmi) { report.maintainability = Math.max(0, (report.maintainability * 100) / 171); }
   }

   /**
    * Coordinates calculating all metrics. All module and class methods are traversed. If there are no module or class
    * methods respectively the aggregate MethodReport is used for calculations.
    *
    * @param {ModuleReport}   report - The ModuleReport being processed.
    *
    * @private
    */
   _calculateMetrics(report)
   {
      let moduleMethodCount = report.methods.length;
      const moduleMethodAverages = report.methodAverage;
      const moduleMethodAverageKeys = ObjectUtil.getAccessorList(moduleMethodAverages);

      // Handle module methods.
      report.methods.forEach((methodReport) =>
      {
         moduleMethodAverageKeys.forEach((averageKey) =>
         {
            this._calculateCyclomaticDensity(methodReport);
            this._calculateHalsteadMetrics(methodReport.halstead);

            const targetValue = ObjectUtil.safeAccess(methodReport, averageKey, 0);
            ObjectUtil.safeSet(moduleMethodAverages, averageKey, targetValue, 'add');
         });
      });

      // Handle module class reports.
      report.classes.forEach((classReport) =>
      {
         const classMethodAverages = classReport.methodAverage;

         let classMethodCount = classReport.methods.length;
         moduleMethodCount += classMethodCount;

         // Process all class methods.
         classReport.methods.forEach((methodReport) =>
         {
            this._calculateCyclomaticDensity(methodReport);
            this._calculateHalsteadMetrics(methodReport.halstead);

            moduleMethodAverageKeys.forEach((averageKey) =>
            {
               const targetValue = ObjectUtil.safeAccess(methodReport, averageKey, 0);

               ObjectUtil.safeSet(moduleMethodAverages, averageKey, targetValue, 'add');
               ObjectUtil.safeSet(classMethodAverages, averageKey, targetValue, 'add');
            });
         });

         this._calculateCyclomaticDensity(classReport.aggregateMethodReport);
         this._calculateHalsteadMetrics(classReport.aggregateMethodReport.halstead);

         // If there are no class methods use the class aggregate MethodReport.
         if (classMethodCount === 0)
         {
            // Sane handling of classes that contain no methods.
            moduleMethodAverageKeys.forEach((averageKey) =>
            {
               const targetValue = ObjectUtil.safeAccess(classReport.aggregateMethodReport, averageKey, 0);

               ObjectUtil.safeSet(classMethodAverages, averageKey, targetValue, 'add');
            });

            classMethodCount = 1;
         }

         moduleMethodAverageKeys.forEach((averageKey) =>
         {
            ObjectUtil.safeSet(classMethodAverages, averageKey, classMethodCount, 'div');
         });

         this._calculateMaintainabilityIndex(classReport, classMethodAverages.cyclomatic,
          classMethodAverages.halstead.effort, classMethodAverages.sloc.logical);
      });

      this._calculateCyclomaticDensity(report.aggregateMethodReport);
      this._calculateHalsteadMetrics(report.aggregateMethodReport.halstead);

      // If there are no module methods use the module aggregate MethodReport.
      if (moduleMethodCount === 0)
      {
         // Sane handling of classes that contain no methods.
         moduleMethodAverageKeys.forEach((averageKey) =>
         {
            const targetValue = ObjectUtil.safeAccess(report.aggregateMethodReport, averageKey, 0);

            ObjectUtil.safeSet(moduleMethodAverages, averageKey, targetValue, 'add');
         });

         // Sane handling of modules that contain no methods.
         moduleMethodCount = 1;
      }

      moduleMethodAverageKeys.forEach((averageKey) =>
      {
         ObjectUtil.safeSet(moduleMethodAverages, averageKey, moduleMethodCount, 'div');
      });

      this._calculateMaintainabilityIndex(report, moduleMethodAverages.cyclomatic,
       moduleMethodAverages.halstead.effort, moduleMethodAverages.sloc.logical);
   }
}