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 }
}
};