From 033363930bb45ab42828da131709f743d184389d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Matthias=20Bl=C3=BCmel?= <blaimi@blaimi.de>
Date: Sat, 6 May 2023 07:38:12 +0200
Subject: [PATCH] Draft: feat(codeclimate): outputFormat for CodeClimate (#512)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Matthias Blümel <blaimi@blaimi.de>
---
 README.md                                     | 12 +++--
 .../src/cli-validator/run-validator.js        | 13 +++--
 .../src/cli-validator/utils/cli-options.js    |  4 ++
 .../utils/codeclimate-results.js              | 47 +++++++++++++++++++
 .../utils/configuration-manager.js            |  4 ++
 .../validator/src/schemas/config-file.yaml    |  1 +
 6 files changed, 73 insertions(+), 8 deletions(-)
 create mode 100644 packages/validator/src/cli-validator/utils/codeclimate-results.js

diff --git a/README.md b/README.md
index 9e49ec3e5..70b2aa617 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ The IBM OpenAPI Validator lets you validate OpenAPI 3.x documents according to t
 - [Validator Output](#validator-output)
   * [Text](#text)
   * [JSON](#json)
+  * [CodeClimate](#codeclimate)
 - [Logging](#logging)
 - [Contributing](#contributing)
 - [License](#license)
@@ -115,6 +116,7 @@ Options:
   -e, --errors-only              include only errors in the output and skip warnings (default is false)
   -i, --ignore <file>            avoid validating <file> (e.g. -i /dir1/ignore-file1.json --ignore /dir2/ignore-file2.yaml ...) (default is []) (default: [])
   -j, --json                     produce JSON output (default is text)
+  --codeclimate                  produce JSON output according to CodeClimate spec
   -l, --log-level <loglevel>     set the log level for one or more loggers (e.g. -l root=info -l ibm-schema-description-exists=debug ...)  (default: [])
   -n, --no-colors                disable colorizing of the output (default is false)
   -r, --ruleset <file>           use Spectral ruleset contained in `<file>` ("default" forces use of default IBM Cloud Validation Ruleset)
@@ -482,9 +484,9 @@ module.exports = {
 <td width=25%><b>Default</b></td>
 </tr>
 <tr>
-<td>You can set the <code>outputFormat</code> configuration property to either <code>text</code> or <code>json</code>
+<td>You can set the <code>outputFormat</code> configuration property to either <code>text</code>, <code>json</code> or <code>codeclimate</code>
 to indicate the type of output you want the validator to produce.
-This property corresponds to the <code>-j</code>/<code>--json</code> command-line option.</td>
+This property corresponds to the <code>-j</code>/<code>--json</code>/<code>--codeclimate</code> command-line option.</td>
 <td><code>text</code></td>
 </tr>
 </table>
@@ -621,7 +623,7 @@ module.exports = {
 
 ## Validator Output
 The validator can produce output in either text or JSON format.  The default is `text` output, and this can be
-controlled with the `-j`/`--json` command-line option or `outputFormat` configuration property.
+controlled with the `-j`/`--json`/`--codeclimate` command-line option or `outputFormat` configuration property.
 
 ### Text
 Here is an example of text output:
@@ -753,6 +755,10 @@ Here is an example of JSON output:
 The JSON output is also affected by the `-s`/`--summary-only` and `-e`/`--errors-only` options as well as the `summaryOnly` and `errorsOnly`
 configuration properties.
 
+### CodeClimate
+When displaying CodeClimate JSON output, the validator will produce a null-byte separated stream of JSON objects
+which complies with [the CodeClimate Output format](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#output).
+
 ## Logging
 The validator uses a *logger* for displaying messages on the console.
 The core validator uses a single logger named `root`, while each of the rules contained in the
diff --git a/packages/validator/src/cli-validator/run-validator.js b/packages/validator/src/cli-validator/run-validator.js
index a62d7c9bb..93cacfc98 100644
--- a/packages/validator/src/cli-validator/run-validator.js
+++ b/packages/validator/src/cli-validator/run-validator.js
@@ -18,6 +18,7 @@ const ext = require('./utils/file-extension-validator');
 const preprocessFile = require('./utils/preprocess-file');
 const print = require('./utils/print-results');
 const { printJson } = require('./utils/json-results');
+const { printCCJson } = require('./utils/codeclimate-results');
 const { runSpectral } = require('../spectral/spectral-validator');
 const getCopyrightString = require('./utils/get-copyright-string');
 
@@ -72,7 +73,7 @@ async function runValidator(cliArgs, parseOptions = {}) {
 
   context.chalk = chalk;
 
-  if (context.config.outputFormat !== 'json') {
+  if (context.config.outputFormat === 'text') {
     console.log(getCopyrightString());
   }
 
@@ -157,7 +158,7 @@ async function runValidator(cliArgs, parseOptions = {}) {
     let originalFile;
     let input;
 
-    if (context.config.outputFormat != 'json') {
+    if (context.config.outputFormat === 'text') {
       console.log('');
       console.log(chalk.underline(`Validation Results for ${validFile}:\n`));
     }
@@ -202,15 +203,15 @@ async function runValidator(cliArgs, parseOptions = {}) {
 
     // Check to see if we should be passing back a non-zero exit code.
     if (results.error.summary.total) {
-      // If we have any errors, then exit code 1 is returned.
-      exitCode = 1;
+      // If we have any errors, then exit code 1 is returned, except when running for codeclimate.
+      exitCode = context.config.outputFormat === 'codeclimate' ? 0 : 1;
     }
 
     // If the # of warnings exceeded the warnings limit, then this is an error.
     const numWarnings = results.warning.summary.total;
     const warningsLimit = context.config.limits.warnings;
     if (warningsLimit >= 0 && numWarnings > warningsLimit) {
-      exitCode = 1;
+      exitCode = context.config.outputFormat === 'codeclimate' ? 0 : 1;
       logger.error(
         `Number of warnings (${numWarnings}) exceeds warnings limit (${warningsLimit}).`
       );
@@ -219,6 +220,8 @@ async function runValidator(cliArgs, parseOptions = {}) {
     // Now print the results, either JSON or text.
     if (context.config.outputFormat === 'json') {
       printJson(context, results);
+    } else if (context.config.outputFormat === 'codeclimate') {
+      printCCJson(validFile, results);
     } else {
       if (results.hasResults) {
         print(context, results);
diff --git a/packages/validator/src/cli-validator/utils/cli-options.js b/packages/validator/src/cli-validator/utils/cli-options.js
index 328dd88fb..ad895d75c 100644
--- a/packages/validator/src/cli-validator/utils/cli-options.js
+++ b/packages/validator/src/cli-validator/utils/cli-options.js
@@ -58,6 +58,10 @@ function createCLIOptions() {
       []
     )
     .option('-j, --json', 'produce JSON output (default is text)')
+    .option(
+      '--codeclimate',
+      'produce JSON output according to CodeClimate spec'
+    )
     .option(
       '-l, --log-level <loglevel>',
       'set the log level for one or more loggers (e.g. -l root=info -l ibm-schema-description-exists=debug ...) ',
diff --git a/packages/validator/src/cli-validator/utils/codeclimate-results.js b/packages/validator/src/cli-validator/utils/codeclimate-results.js
new file mode 100644
index 000000000..7a16846ff
--- /dev/null
+++ b/packages/validator/src/cli-validator/utils/codeclimate-results.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2023 IBM Corporation, Matthias Blümel.
+ * SPDX-License-Identifier: Apache2.0
+ */
+
+const each = require('lodash/each');
+
+function printCCJson(validFile, results) {
+  const types = ['error', 'warning', 'info', 'hint'];
+  const ccTypeMap = {
+    error: 'critical',
+    warning: 'major',
+    info: 'minor',
+    hint: 'info',
+  };
+
+  types.forEach(type => {
+    each(results[type].results, result => {
+      let content;
+      if (result.path.length !== 0) {
+        let markdown = '';
+        each(result.path, pathItem => {
+          markdown += '* ' + pathItem + '\n';
+        });
+        content = { body: markdown };
+      }
+      const ccResult = {
+        type: 'issue',
+        check_name: result.rule,
+        description: result.message,
+        content: content,
+        categories: ['Style'], // required by codeclimate, ignored by gitlab; has to be defined by the rule.
+        location: {
+          path: validFile,
+          lines: {
+            begin: result.line,
+            end: result.line,
+          },
+        },
+        severity: ccTypeMap[type],
+      };
+      console.log(JSON.stringify(ccResult) + '\0\n');
+    });
+  });
+}
+
+module.exports.printCCJson = printCCJson;
diff --git a/packages/validator/src/cli-validator/utils/configuration-manager.js b/packages/validator/src/cli-validator/utils/configuration-manager.js
index 8be68630a..6a5d58102 100644
--- a/packages/validator/src/cli-validator/utils/configuration-manager.js
+++ b/packages/validator/src/cli-validator/utils/configuration-manager.js
@@ -224,6 +224,10 @@ async function processArgs(args, cliParseOptions) {
     configObj.outputFormat = 'json';
   }
 
+  if ('codeclimate' in opts) {
+    configObj.outputFormat = 'codeclimate';
+  }
+
   if ('ruleset' in opts) {
     configObj.ruleset = opts.ruleset;
   }
diff --git a/packages/validator/src/schemas/config-file.yaml b/packages/validator/src/schemas/config-file.yaml
index fa37fb835..eb2e2bfb9 100644
--- a/packages/validator/src/schemas/config-file.yaml
+++ b/packages/validator/src/schemas/config-file.yaml
@@ -59,6 +59,7 @@ properties:
     type: string
     enum:
       - json
+      - codeclimate
       - text
     default: text
   ruleset: