Skip to content

Commit f4eb390

Browse files
committed
feat: Adding CodeClimate Output
resolves #7
1 parent ebde745 commit f4eb390

File tree

3 files changed

+108
-6
lines changed

3 files changed

+108
-6
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
Command line interface for [ajv](https://github.com/epoberezkin/ajv), one of the [fastest json schema validators](https://github.com/ebdrup/json-schema-benchmark).
44
Supports [JSON](http://json.org/), [JSON5](http://json5.org/), and [YAML](http://yaml.org/).
55

6-
[![build](https://github.com/ajv-validator/ajv-cli/workflows/build/badge.svg)](https://github.com/ajv-validator/ajv-cli/actions?query=workflow%3Abuild)
7-
[![npm](https://img.shields.io/npm/v/ajv-cli.svg)](https://www.npmjs.com/package/ajv-cli)
8-
[![coverage](https://coveralls.io/repos/github/ajv-validator/ajv-cli/badge.svg?branch=master)](https://coveralls.io/github/ajv-validator/ajv-cli?branch=master)
9-
[![gitter](https://img.shields.io/gitter/room/ajv-validator/ajv.svg)](https://gitter.im/ajv-validator/ajv)
6+
[![build](https://github.com/ContinuousSecurityTooling/ajv-cli/actions/workflows/build.yml/badge.svg)](https://github.com/ContinuousSecurityTooling/ajv-cli/actions/workflows/build.yml)
7+
[![NPM Version](https://img.shields.io/npm/v/:packageName)](https://www.npmjs.com/package/@continuoussecuritytooling/ajv-cli)
8+
[![Coverage Status](https://coveralls.io/repos/github/ContinuousSecurityTooling/ajv-cli/badge.svg?branch=develop)](https://coveralls.io/github/ContinuousSecurityTooling/ajv-cli?branch=develop)
109

1110
- [ajv-cli](#ajv-cli)
1211
- [Installation](#installation)
@@ -153,6 +152,7 @@ For example, you can use `-c ajv-keywords` to add all keywords from [ajv-keyword
153152

154153
- `js` (default): JavaScript object
155154
- `json`: JSON with indentation and line-breaks
155+
- `code-climate` emits a JSON array of CodeClimate issues to **stdout** (for easy pipe/redirect to a file). Stderr still receives the `<file> invalid` message.
156156
- `line`: JSON without indentation/line-breaks (for easy parsing)
157157
- `text`: human readable error messages with data paths
158158

src/commands/validate.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@ import type {ParsedArgs} from "minimist"
33
import {compile, getFiles, openFile, logJSON} from "./util"
44
import getAjv from "./ajv"
55
import * as jsonPatch from "fast-json-patch"
6+
import {createHash} from "crypto"
7+
import type {ErrorObject} from "ajv"
8+
9+
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
10+
// https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
11+
interface CodeClimateIssue {
12+
description: string
13+
check_name: string
14+
fingerprint: string
15+
severity: "info" | "minor" | "major" | "critical" | "blocker"
16+
location: {
17+
path: string
18+
lines: {begin: number}
19+
}
20+
}
621

722
const cmd: Command = {
823
execute,
@@ -18,7 +33,7 @@ const cmd: Command = {
1833
r: {$ref: "#/$defs/stringOrArray"},
1934
m: {$ref: "#/$defs/stringOrArray"},
2035
c: {$ref: "#/$defs/stringOrArray"},
21-
errors: {enum: ["json", "line", "text", "js", "no"]},
36+
errors: {enum: ["json", "line", "text", "js", "no", "code-climate"]},
2237
changes: {enum: [true, "json", "line", "js"]},
2338
spec: {enum: ["draft7", "draft2019", "draft2020", "jtd"]},
2439
},
@@ -28,6 +43,27 @@ const cmd: Command = {
2843

2944
export default cmd
3045

46+
function formatCodeClimate(file: string, errors: ErrorObject[]): string {
47+
const issues: CodeClimateIssue[] = errors.map((err) => {
48+
const instancePath = err.instancePath || "/"
49+
const message = err.message || "validation error"
50+
const fingerprint = createHash("sha1")
51+
.update(`${file}\0${instancePath}\0${message}`)
52+
.digest("hex")
53+
return {
54+
description: `[schema] #${instancePath} ${message}`,
55+
check_name: "json-schema",
56+
fingerprint,
57+
severity: "major",
58+
location: {
59+
path: file,
60+
lines: {begin: 1},
61+
},
62+
}
63+
})
64+
return JSON.stringify(issues, null, " ")
65+
}
66+
3167
function execute(argv: ParsedArgs): boolean {
3268
const ajv = getAjv(argv)
3369
const validate = compile(ajv, argv.s)
@@ -54,7 +90,11 @@ function execute(argv: ParsedArgs): boolean {
5490
}
5591
} else {
5692
console.error(file, "invalid")
57-
console.error(logJSON(argv.errors, validate.errors, ajv))
93+
if (argv.errors === "code-climate") {
94+
console.log(formatCodeClimate(file, validate.errors as ErrorObject[]))
95+
} else {
96+
console.error(logJSON(argv.errors, validate.errors, ajv))
97+
}
5898
}
5999
return validData
60100
}

test/validate.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,68 @@ describe("validate", function () {
242242
})
243243
})
244244

245+
describe('option "errors" code-climate', () => {
246+
it("should output valid CodeClimate JSON for invalid data", (done) => {
247+
cli(
248+
"-s test/schema.json -d test/invalid_data.json --errors=code-climate",
249+
(error, stdout, stderr) => {
250+
assert(error instanceof Error)
251+
assert(stderr.includes("invalid"))
252+
const issues = JSON.parse(stdout)
253+
assert(Array.isArray(issues))
254+
assert(issues.length > 0)
255+
const issue = issues[0]
256+
assert.strictEqual(typeof issue.description, "string")
257+
assert.strictEqual(issue.check_name, "json-schema")
258+
assert.strictEqual(typeof issue.fingerprint, "string")
259+
assert.strictEqual(issue.fingerprint.length, 40)
260+
assert.strictEqual(issue.severity, "major")
261+
assert.strictEqual(typeof issue.location.path, "string")
262+
assert.strictEqual(typeof issue.location.lines.begin, "number")
263+
done()
264+
}
265+
)
266+
})
267+
268+
it("should produce no issues for valid data", (done) => {
269+
cli(
270+
"-s test/schema.json -d test/valid_data.json --errors=code-climate",
271+
(error, stdout, stderr) => {
272+
assert.strictEqual(error, null)
273+
assert(stdout.includes("valid"))
274+
assert.strictEqual(stderr, "")
275+
done()
276+
}
277+
)
278+
})
279+
280+
it("should include the file path in the location", (done) => {
281+
cli(
282+
"-s test/schema.json -d test/invalid_data.json --errors=code-climate",
283+
(error, stdout, _stderr) => {
284+
assert(error instanceof Error)
285+
const issues = JSON.parse(stdout)
286+
assert(issues[0].location.path.includes("invalid_data"))
287+
done()
288+
}
289+
)
290+
})
291+
292+
it("should produce unique fingerprints per error", (done) => {
293+
cli(
294+
"-s test/schema.json -d test/invalid_data.json --errors=code-climate --all-errors",
295+
(error, stdout, _stderr) => {
296+
assert(error instanceof Error)
297+
const issues = JSON.parse(stdout)
298+
const fingerprints = issues.map((i: {fingerprint: string}) => i.fingerprint)
299+
const unique = new Set(fingerprints)
300+
assert.strictEqual(fingerprints.length, unique.size)
301+
done()
302+
}
303+
)
304+
})
305+
})
306+
245307
describe('option "changes"', () => {
246308
it("should log changes in the object after validation", (done) => {
247309
cli(

0 commit comments

Comments
 (0)