Skip to content

Commit

Permalink
feat(formatters): add code climate (GitLab) formatter
Browse files Browse the repository at this point in the history
Add code climate formatter

Update documentation
  • Loading branch information
smsalisbury committed Aug 7, 2024
1 parent 9e906ea commit 3ecda64
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 18 deletions.
35 changes: 17 additions & 18 deletions docs/guides/2-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,25 @@ 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"]
[default: "stylish"]
-o, --output where to output results, can be a single file name, multiple "output.<format>" 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]
-r, --ruleset path/URL to a ruleset file [string]
[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", "code-climate", "gitlab"]
[default: "stylish"]
-o, --output where to output results, can be a single file name, multiple "output.<format>" 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]
-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.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export enum OutputFormat {
PRETTY = 'pretty',
GITHUB_ACTIONS = 'github-actions',
SARIF = 'sarif',
CODE_CLIMATE = 'code-climate',
GITLAB = 'gitlab',
}

export interface ILintConfig {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/services/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
pretty,
githubActions,
sarif,
codeClimate,
} from '@stoplight/spectral-formatters';
import type { Formatter, FormatterOptions } from '@stoplight/spectral-formatters';
import type { OutputFormat } from './config';
Expand All @@ -26,6 +27,8 @@ const formatters: Record<OutputFormat, Formatter> = {
teamcity,
'github-actions': githubActions,
sarif,
'code-climate': codeClimate,
gitlab: codeClimate,
};

export function formatOutput(
Expand Down
2 changes: 2 additions & 0 deletions packages/formatters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ console.error(output);
- pretty
- github-actions
- sarif
- gitlab
- code-climate
89 changes: 89 additions & 0 deletions packages/formatters/src/__tests__/code-climate.jest.test.ts
Original file line number Diff line number Diff line change
@@ -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',
}),
]);
});
});
69 changes: 69 additions & 0 deletions packages/formatters/src/code-climate.ts
Original file line number Diff line number Diff line change
@@ -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, CodeClimateIssueSeverity> = {
[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');
};
1 change: 1 addition & 0 deletions packages/formatters/src/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 4 additions & 0 deletions packages/formatters/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,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');
};

0 comments on commit 3ecda64

Please sign in to comment.