Home Manual Reference Source Test Repository

src/transform/formats/json/FormatJSONCheckstyle.js

import ObjectUtil from '../../../utils/ObjectUtil';
import ReportType from '../../../types/ReportType';

/**
 * Provides a format transform for ESComplex ModuleReport / ProjectReport instances converting them to JSON that
 * corresponds to the XML checkstyle format.
 *
 * The checkstyle XML format outputs error elements for each file / module. This format depends on the output of
 * `FormatJSONCheckstyle`. The implementation below outputs a `file` array that contains an `error` array entries.
 *
 * There is a corresponding `FormatXMLCheckstyle` format loaded when `escomplex-plugin-formats-xml` during plugin
 * loading which converts the JSON output of this format transform to the official XML checkstyle format.
 *
 * @see http://checkstyle.sourceforge.net/
 * @see https://github.com/checkstyle/checkstyle
 * @see https://github.com/checkstyle/checkstyle/blob/master/src/main/java/com/puppycrawl/tools/checkstyle/XMLLogger.java
 */
export default class FormatJSONCheckstyle
{
   constructor(thresholds = s_DEFAULT_THRESHOLDS)
   {
      this._thresholds = thresholds;
   }

   /**
    * Formats a module report as JSON / checkstyle.
    *
    * @param {ModuleReport|ProjectReport} report - A module or project report to format.
    *
    * @param {object}         options - (Optional) One or more optional parameters passed to the formatter.
    * @property {number}      spacing - (Optional) An integer defining the JSON output spacing.
    *
    * @returns {string}
    */
   formatReport(report, options = {})
   {
      let reports, reportsAvailable;

      switch (report.type)
      {
         case ReportType.MODULE:
            reports = [report];
            reportsAvailable = true;
            break;

         case ReportType.PROJECT:
            reports = report.modules;
            reportsAvailable = report.getSetting('serializeModules', false);
            break;

         default:
            console.warn(`formatReport '${this.name}' warning: unsupported report type '${report.type}'.`);
            return '';
      }

      let localOptions = Object.assign({}, this._thresholds);
      localOptions = Object.assign(localOptions, options);

      const output = { version: '7.0', file: [] };

      reports.forEach((report) => { output.file.push(this._formatModule(report, reportsAvailable, localOptions)); });

      return typeof localOptions === 'object' && Number.isInteger(localOptions.spacing) ?
       JSON.stringify(output, void 0, localOptions.spacing) : JSON.stringify(output);
   }

   /**
    * Gets the file extension.
    *
    * @returns {string}
    */
   get extension()
   {
      return 'json';
   }

   /**
    * Gets the format name.
    *
    * @returns {string}
    */
   get name()
   {
      return 'json-checkstyle';
   }

   /**
    * Gets the format type.
    *
    * @returns {string}
    */
   get type()
   {
      return 'checkstyle';
   }

   /**
    * Returns whether a given ReportType is supported by this format transform.
    *
    * @param {ReportType}  reportType - A given report type.
    *
    * @returns {boolean}
    */
   isSupported(reportType)
   {
      switch (reportType)
      {
         case ReportType.MODULE:
         case ReportType.PROJECT:
            return true;

         default:
            return false;
      }
   }

   /**
    * Formats a module report.
    *
    * @param {ModuleReport}   report - A module report.
    *
    * @param {boolean}        reportsAvailable - Indicates that the report metric data is available.
    *
    * @param {object}         options - (Optional) One or more optional entries defining threshold parameters.
    *
    * @returns {object}
    */
   _formatModule(report, reportsAvailable, options)
   {
      const output = {};

      output.name = report.filePath ? report.filePath : '<unknown>';

      output.error = [];

      if (reportsAvailable)
      {
         if (typeof options.moduleReport === 'object')
         {
            this._parseErrors(report, options.moduleReport, output.error);
         }

         for (let cntr = 0; cntr < report.methods.length; cntr++)
         {
            if (typeof options.methodReport === 'object')
            {
               this._parseErrors(report.methods[cntr], options.methodReport, output.error);
            }
         }

         for (let cntr = 0; cntr < report.classes.length; cntr++)
         {
            const classReport = report.classes[cntr];

            if (typeof options.classReport === 'object')
            {
               this._parseErrors(classReport, options.classReport, output.error);
            }

            for (let cntr2 = 0; cntr2 < classReport.methods.length; cntr2++)
            {
               if (typeof options.methodReport === 'object')
               {
                  this._parseErrors(classReport.methods[cntr2], options.methodReport, output.error);
               }
            }
         }
      }

      return output;
   }

   _parseErrors(sourceObject, options, errors)
   {
      for (const key in options)
      {
         if (!options.hasOwnProperty(key)) { continue; }

         const sourceObjectValue = ObjectUtil.safeAccess(sourceObject, key);

         if (typeof sourceObjectValue === 'number')
         {
            let severity = undefined;
            let mapEntryValue;
            let testSign;

            const map = options[key];

            for (const entryKey in map)
            {
               if (!map.hasOwnProperty(entryKey)) { continue; }

               // Skip `_test` entry.
               if (entryKey === '_test') { continue; }

               switch (map._test)
               {
                  case '<':
                     if (sourceObjectValue < map[entryKey])
                     {
                        severity = entryKey;
                        mapEntryValue = map[entryKey];
                        testSign = ' < ';
                     }
                     break;

                  case '<=':
                     if (sourceObjectValue <= map[entryKey])
                     {
                        severity = entryKey;
                        mapEntryValue = map[entryKey];
                        testSign = ' <= ';
                     }
                     break;

                  case '>=':
                     if (sourceObjectValue >= map[entryKey])
                     {
                        severity = entryKey;
                        mapEntryValue = map[entryKey];
                        testSign = ' >= ';
                     }
                     break;

                  default:
                     if (sourceObjectValue > map[entryKey])
                     {
                        severity = entryKey;
                        mapEntryValue = map[entryKey];
                        testSign = ' > ';
                     }
                     break;
               }
            }

            if (typeof severity === 'string')
            {
               const sourceName = sourceObject.getName();

               errors.push(
               {
                  line: sourceObject.lineStart,
                  severity,
                  message: `${key}: ${sourceObjectValue}${testSign}${mapEntryValue}`,
                  source: `${sourceObject.type.description} ${sourceName !== '' ? `- ${sourceName}` : ''}`
               });
            }
         }
      }
   }
}

// Module private ---------------------------------------------------------------------------------------------------

/**
 * Defines default thresholds for severity levels matching the XML checkstyle format.
 * error levels: info, warning, error
 *
 * Entries may include `classReport`, `methodReport`, `moduleReport` that each define an object hash of threshold
 * object hashes. The key of each threshold hash is the entry key to compare against the `info, warning, error` values.
 * By default the order flows left to right using greater than comparisons. An optional entry is `_test` which is a
 * string defining the comparison operations with the following supported options, `<`, `<=`, `>`, and `>=`.
 *
 * @type {{classReport: {maintainability: {_test: string, info: number, warning: number, error: number}}, methodReport: {cyclomatic: {info: number, warning: number, error: number}, [halstead.difficulty]: {info: number, warning: number, error: number}}, moduleReport: {maintainability: {_test: string, info: number, warning: number, error: number}}}}
 * @ignore
 */
const s_DEFAULT_THRESHOLDS =
{
   classReport:
   {
      maintainability: { _test: '<', info: 115, warning: 100, error: 90 }
   },
   methodReport:
   {
      'cyclomatic': { info: 3, warning: 7, error: 12 },
      'halstead.difficulty': { info: 8, warning: 13, error: 20 }
   },
   moduleReport:
   {
      maintainability: { _test: '<', info: 115, warning: 100, error: 90 }
   }
};