From 41eca612d292520142ace3bd97cde630c33366f1 Mon Sep 17 00:00:00 2001 From: Spencer Salisbury Date: Fri, 13 Sep 2024 02:49:15 -0600 Subject: [PATCH] feat(formatters): add code climate (GitLab) formatter (#2648) Add code climate formatter Update documentation Co-authored-by: Nauman --- docs/guides/2-cli.md | 18 ++-- packages/cli/src/services/config.ts | 2 + packages/cli/src/services/output.ts | 3 + packages/formatters/README.md | 2 + .../src/__tests__/code-climate.jest.test.ts | 89 +++++++++++++++++++ packages/formatters/src/code-climate.ts | 69 ++++++++++++++ packages/formatters/src/index.node.ts | 1 + packages/formatters/src/index.ts | 4 + .../scenarios/help-no-document.scenario | 2 +- .../scenarios/strict-options.scenario | 2 +- 10 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 packages/formatters/src/__tests__/code-climate.jest.test.ts create mode 100644 packages/formatters/src/code-climate.ts diff --git a/docs/guides/2-cli.md b/docs/guides/2-cli.md index a68a7d4a2..f0edd3e7c 100644 --- a/docs/guides/2-cli.md +++ b/docs/guides/2-cli.md @@ -27,13 +27,13 @@ spectral lint ./reference/**/*.oas*.{json,yml,yaml} Other options include: ``` - --version Show version number [boolean] - --help Show help [boolean] + --version Show version number [boolean] + --help Show help [boolean] -e, --encoding text encoding to use [string] [choices: "utf8", "ascii", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1"] [default: "utf8"] -f, --format formatters to use for outputting results, more than one can be provided by using multiple flags - [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif", "markdown"] + [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif", "markdown","gitlab"] [default: "stylish"] -o, --output where to output results, can be a single file name, multiple "output." or missing to print to stdout [string] @@ -41,12 +41,12 @@ Other options include: --resolver path to custom json-ref-resolver instance [string] -r, --ruleset path/URL to a ruleset file [string] -F, --fail-severity results of this level or above will trigger a failure exit code - [string] [choices: "error", "warn", "info", "hint"] [default: "error"] - -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] - --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] - --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] - -v, --verbose increase verbosity [boolean] - -q, --quiet no logging - output only [boolean] + [string] [choices: "error", "warn", "info", "hint"] [default: "error"] + -D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false] + --ignore-unknown-format do not warn about unmatched formats [boolean] [default: false] + --fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false] + -v, --verbose increase verbosity [boolean] + -q, --quiet no logging - output only [boolean] ``` The Spectral CLI supports loading documents as YAML or JSON, and validation of OpenAPI v2/v3 documents via the built-in ruleset. diff --git a/packages/cli/src/services/config.ts b/packages/cli/src/services/config.ts index 6c1f7db21..03047950e 100644 --- a/packages/cli/src/services/config.ts +++ b/packages/cli/src/services/config.ts @@ -13,6 +13,8 @@ export enum OutputFormat { PRETTY = 'pretty', GITHUB_ACTIONS = 'github-actions', SARIF = 'sarif', + CODE_CLIMATE = 'code-climate', + GITLAB = 'gitlab', MARKDOWN = 'markdown', } diff --git a/packages/cli/src/services/output.ts b/packages/cli/src/services/output.ts index 996d03b1d..23015090a 100644 --- a/packages/cli/src/services/output.ts +++ b/packages/cli/src/services/output.ts @@ -11,6 +11,7 @@ import { pretty, githubActions, sarif, + codeClimate, markdown, } from '@stoplight/spectral-formatters'; import type { Formatter, FormatterOptions } from '@stoplight/spectral-formatters'; @@ -27,6 +28,8 @@ const formatters: Record = { teamcity, 'github-actions': githubActions, sarif, + 'code-climate': codeClimate, + gitlab: codeClimate, markdown, }; diff --git a/packages/formatters/README.md b/packages/formatters/README.md index d15726790..06debf878 100644 --- a/packages/formatters/README.md +++ b/packages/formatters/README.md @@ -35,3 +35,5 @@ console.error(output); - pretty - github-actions - sarif +- gitlab +- code-climate diff --git a/packages/formatters/src/__tests__/code-climate.jest.test.ts b/packages/formatters/src/__tests__/code-climate.jest.test.ts new file mode 100644 index 000000000..92f3ad935 --- /dev/null +++ b/packages/formatters/src/__tests__/code-climate.jest.test.ts @@ -0,0 +1,89 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import type { IRuleResult } from '@stoplight/spectral-core'; +import { codeClimate } from '../code-climate'; + +const cwd = process.cwd(); +const results: IRuleResult[] = [ + { + code: 'operation-description', + message: 'paths./pets.get.description is not truthy', + path: ['paths', '/pets', 'get', 'description'], + severity: 1, + source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`, + range: { + start: { + line: 60, + character: 8, + }, + end: { + line: 71, + character: 60, + }, + }, + }, + { + code: 'operation-tags', + message: 'paths./pets.get.tags is not truthy', + path: ['paths', '/pets', 'get', 'tags'], + severity: 1, + source: `${cwd}/__tests__/fixtures/petstore.oas2.yaml`, + range: { + start: { + line: 60, + character: 8, + }, + end: { + line: 71, + character: 60, + }, + }, + }, +]; + +describe('Code climate formatter', () => { + test('should include ranges', () => { + expect(JSON.parse(codeClimate(results, { failSeverity: DiagnosticSeverity.Error }))).toEqual([ + expect.objectContaining({ + location: { + path: '__tests__/fixtures/petstore.oas2.yaml', + positions: { + begin: { + line: 60, + column: 8, + }, + end: { + line: 71, + column: 60, + }, + }, + }, + }), + expect.objectContaining({ + location: { + path: '__tests__/fixtures/petstore.oas2.yaml', + positions: { + begin: { + line: 60, + column: 8, + }, + end: { + line: 71, + column: 60, + }, + }, + }, + }), + ]); + }); + + test('should include description', () => { + expect(JSON.parse(codeClimate(results, { failSeverity: DiagnosticSeverity.Error }))).toEqual([ + expect.objectContaining({ + description: 'paths./pets.get.description is not truthy', + }), + expect.objectContaining({ + description: 'paths./pets.get.tags is not truthy', + }), + ]); + }); +}); diff --git a/packages/formatters/src/code-climate.ts b/packages/formatters/src/code-climate.ts new file mode 100644 index 000000000..04f4a2af0 --- /dev/null +++ b/packages/formatters/src/code-climate.ts @@ -0,0 +1,69 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import { Formatter } from './types'; +import { relative } from '@stoplight/path'; + +/** + * @see https://github.com/codeclimate/platform/blob/690633cb2a08839a5bfa350ed925ddb6de55bbdc/spec/analyzers/SPEC.md#data-types + */ +interface CodeClimateIssue { + type: 'issue'; + check_name: string; + description: string; + categories: CodeClimateIssueCategory[]; + location: CodeClimateIssueLocation; + content?: { body: string }; + trace?: CodeClimateIssueTrace; + remediation_points?: number; + severity?: CodeClimateIssueSeverity; + fingerprint?: string; +} +type CodeClimateIssueCategory = + | 'Bug Risk' + | 'Clarity' + | 'Compatibility' + | 'Complexity' + | 'Duplication' + | 'Performance' + | 'Security' + | 'Style'; +interface CodeClimateIssueLocation { + path: string; + positions: { + begin: { line: number; column: number }; + end: { line: number; column: number }; + }; +} +interface CodeClimateIssueTrace { + locations: CodeClimateIssueLocation[]; + stackTrace: boolean; +} +type CodeClimateIssueSeverity = 'info' | 'minor' | 'major' | 'critical' | 'blocker'; +const severityMap: Record = { + [DiagnosticSeverity.Error]: 'critical', + [DiagnosticSeverity.Warning]: 'major', + [DiagnosticSeverity.Information]: 'minor', + [DiagnosticSeverity.Hint]: 'info', +}; + +export const codeClimate: Formatter = results => { + const outputJson: CodeClimateIssue[] = results.map(result => { + const relPath = relative(process.cwd(), result.source ?? '').replace(/\\/g, '/'); + const fingerprint = `${relPath}:${result.path.join('.')}:${result.code}`; + return { + type: 'issue' as const, + check_name: result.code.toString(), + description: result.message, + categories: ['Style'], + location: { + path: relPath, + positions: { + begin: { line: result.range.start.line, column: result.range.start.character }, + end: { line: result.range.end.line, column: result.range.end.character }, + }, + }, + severity: severityMap[result.severity], + fingerprint, + }; + }); + return JSON.stringify(outputJson, null, '\t'); +}; diff --git a/packages/formatters/src/index.node.ts b/packages/formatters/src/index.node.ts index 94a9c8d74..a33aacbe4 100644 --- a/packages/formatters/src/index.node.ts +++ b/packages/formatters/src/index.node.ts @@ -3,3 +3,4 @@ export type { Formatter, FormatterOptions } from './index'; export { pretty } from './pretty'; export { githubActions } from './github-actions'; export { sarif } from './sarif'; +export { codeClimate } from './code-climate'; diff --git a/packages/formatters/src/index.ts b/packages/formatters/src/index.ts index 14567521a..ad4c68913 100644 --- a/packages/formatters/src/index.ts +++ b/packages/formatters/src/index.ts @@ -19,3 +19,7 @@ export const githubActions: Formatter = () => { export const sarif: Formatter = () => { throw Error('sarif formatter is available only in Node.js'); }; + +export const codeClimate: Formatter = () => { + throw Error('sarif formatter is available only in Node.js'); +}; diff --git a/test-harness/scenarios/help-no-document.scenario b/test-harness/scenarios/help-no-document.scenario index 76f28fc3c..ffe7d9879 100644 --- a/test-harness/scenarios/help-no-document.scenario +++ b/test-harness/scenarios/help-no-document.scenario @@ -20,7 +20,7 @@ Options: --version Show version number [boolean] --help Show help [boolean] -e, --encoding text encoding to use [string] [choices: "utf8", "ascii", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1"] [default: "utf8"] - -f, --format formatters to use for outputting results, more than one can be provided by using multiple flags [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif", "markdown"] [default: "stylish"] + -f, --format formatters to use for outputting results, more than one can be provided by using multiple flags [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif", "code-climate", "gitlab", "markdown"] [default: "stylish"] -o, --output where to output results, can be a single file name, multiple "output." or missing to print to stdout [string] --stdin-filepath path to a file to pretend that stdin comes from [string] --resolver path to custom json-ref-resolver instance [string] diff --git a/test-harness/scenarios/strict-options.scenario b/test-harness/scenarios/strict-options.scenario index badde74ba..06f345cce 100644 --- a/test-harness/scenarios/strict-options.scenario +++ b/test-harness/scenarios/strict-options.scenario @@ -20,7 +20,7 @@ Options: --version Show version number [boolean] --help Show help [boolean] -e, --encoding text encoding to use [string] [choices: "utf8", "ascii", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1"] [default: "utf8"] - -f, --format formatters to use for outputting results, more than one can be provided by using multiple flags [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif", "markdown"] [default: "stylish"] + -f, --format formatters to use for outputting results, more than one can be provided by using multiple flags [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif", "code-climate", "gitlab", "markdown"] [default: "stylish"] -o, --output where to output results, can be a single file name, multiple "output." or missing to print to stdout [string] --stdin-filepath path to a file to pretend that stdin comes from [string] --resolver path to custom json-ref-resolver instance [string]