From a05d02b32dd381e37fd025ee484db925e48a068b Mon Sep 17 00:00:00 2001 From: Shaopeng Li Date: Wed, 2 Nov 2022 01:44:53 -0700 Subject: [PATCH] Add SARIF support --- CHANGELOG.md | 4 + dist/cli/formatters/sarif.js | 59 ++++ docs/user-guide/usage/options.md | 1 + package-lock.json | 63 ++++ package.json | 1 + src/cli/formatters/sarif.ts | 76 +++++ test/cli/formatters/sarif.sarif | 535 ++++++++++++++++++++++++++++++ test/cli/formatters/sarif.spec.js | 70 ++++ 8 files changed, 809 insertions(+) create mode 100644 dist/cli/formatters/sarif.js create mode 100644 src/cli/formatters/sarif.ts create mode 100644 test/cli/formatters/sarif.sarif create mode 100644 test/cli/formatters/sarif.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8227e03b9..10b68b3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### Features + +* add support format: sarif ([#1045](https://github.com/htmlhint/HTMLHint/issues/1045)) + ## [1.1.4](https://github.com/htmlhint/HTMLHint/compare/v1.1.3...v1.1.4) (2022-04-11) diff --git a/dist/cli/formatters/sarif.js b/dist/cli/formatters/sarif.js new file mode 100644 index 000000000..a1f59b059 --- /dev/null +++ b/dist/cli/formatters/sarif.js @@ -0,0 +1,59 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_sarif_builder_1 = require("node-sarif-builder"); +const path = require("path"); +const url_1 = require("url"); +const pkg = require('../../../package.json'); +const sarifFormatter = function (formatter) { + formatter.on('end', (event) => { + const arrAllMessages = event.arrAllMessages; + const sarifBuilder = new node_sarif_builder_1.SarifBuilder(); + const sarifRunBuilder = new node_sarif_builder_1.SarifRunBuilder().initSimple({ + toolDriverName: 'HTMLHint', + toolDriverVersion: pkg.version, + url: 'https://htmlhint.com/', + }); + const addedRuleSet = new Set(); + arrAllMessages.forEach((result) => { + result.messages.forEach((message) => { + const rule = message.rule; + if (addedRuleSet.has(rule.id)) { + return; + } + addedRuleSet.add(rule.id); + const sarifRuleBuiler = new node_sarif_builder_1.SarifRuleBuilder().initSimple({ + ruleId: rule.id, + shortDescriptionText: rule.description, + helpUri: rule.link, + }); + sarifRunBuilder.addRule(sarifRuleBuiler); + }); + }); + arrAllMessages.forEach((result) => { + result.messages.forEach((message) => { + const sarifResultBuilder = new node_sarif_builder_1.SarifResultBuilder(); + const ruleId = message.rule.id; + const sarifResultInit = { + level: message.type === 'info' + ? 'note' + : message.type.toString(), + messageText: message.message, + ruleId: ruleId, + fileUri: process.env.SARIF_URI_ABSOLUTE + ? (0, url_1.pathToFileURL)(result.file).toString() + : path.relative(process.cwd(), result.file).replace(/\\/g, '/'), + startLine: message.line, + startColumn: message.col, + endLine: message.line, + endColumn: message.col, + }; + sarifResultBuilder.initSimple(sarifResultInit); + sarifRunBuilder.addResult(sarifResultBuilder); + }); + }); + sarifBuilder.addRun(sarifRunBuilder); + console.log(sarifBuilder.buildSarifJsonString({ indent: true })); + }); +}; +module.exports = sarifFormatter; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2FyaWYuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY2xpL2Zvcm1hdHRlcnMvc2FyaWYudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFDQSwyREFLMkI7QUFDM0IsNkJBQTRCO0FBQzVCLDZCQUFtQztBQUduQyxNQUFNLEdBQUcsR0FBRyxPQUFPLENBQUMsdUJBQXVCLENBQUMsQ0FBQTtBQUU1QyxNQUFNLGNBQWMsR0FBc0IsVUFBVSxTQUFTO0lBQzNELFNBQVMsQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFLENBQUMsS0FBSyxFQUFFLEVBQUU7UUFDNUIsTUFBTSxjQUFjLEdBQUcsS0FBSyxDQUFDLGNBQWMsQ0FBQTtRQUczQyxNQUFNLFlBQVksR0FBRyxJQUFJLGlDQUFZLEVBQUUsQ0FBQTtRQUd2QyxNQUFNLGVBQWUsR0FBRyxJQUFJLG9DQUFlLEVBQUUsQ0FBQyxVQUFVLENBQUM7WUFDdkQsY0FBYyxFQUFFLFVBQVU7WUFDMUIsaUJBQWlCLEVBQUUsR0FBRyxDQUFDLE9BQU87WUFDOUIsR0FBRyxFQUFFLHVCQUF1QjtTQUM3QixDQUFDLENBQUE7UUFHRixNQUFNLFlBQVksR0FBRyxJQUFJLEdBQUcsRUFBVSxDQUFBO1FBQ3RDLGNBQWMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxNQUFNLEVBQUUsRUFBRTtZQUNoQyxNQUFNLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO2dCQUNsQyxNQUFNLElBQUksR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFBO2dCQUN6QixJQUFJLFlBQVksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxFQUFFO29CQUM3QixPQUFNO2lCQUNQO2dCQUNELFlBQVksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFBO2dCQUN6QixNQUFNLGVBQWUsR0FBRyxJQUFJLHFDQUFnQixFQUFFLENBQUMsVUFBVSxDQUFDO29CQUN4RCxNQUFNLEVBQUUsSUFBSSxDQUFDLEVBQUU7b0JBQ2Ysb0JBQW9CLEVBQUUsSUFBSSxDQUFDLFdBQVc7b0JBQ3RDLE9BQU8sRUFBRSxJQUFJLENBQUMsSUFBSTtpQkFDbkIsQ0FBQyxDQUFBO2dCQUNGLGVBQWUsQ0FBQyxPQUFPLENBQUMsZUFBZSxDQUFDLENBQUE7WUFDMUMsQ0FBQyxDQUFDLENBQUE7UUFDSixDQUFDLENBQUMsQ0FBQTtRQUdGLGNBQWMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxNQUFNLEVBQUUsRUFBRTtZQUNoQyxNQUFNLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxFQUFFO2dCQUNsQyxNQUFNLGtCQUFrQixHQUFHLElBQUksdUNBQWtCLEVBQUUsQ0FBQTtnQkFDbkQsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUE7Z0JBQzlCLE1BQU0sZUFBZSxHQUFHO29CQUN0QixLQUFLLEVBQ0gsT0FBTyxDQUFDLElBQUksS0FBSyxNQUFNO3dCQUNyQixDQUFDLENBQUMsTUFBTTt3QkFDUixDQUFDLENBQUUsT0FBTyxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQW1CO29CQUMvQyxXQUFXLEVBQUUsT0FBTyxDQUFDLE9BQU87b0JBQzVCLE1BQU0sRUFBRSxNQUFNO29CQUNkLE9BQU8sRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQjt3QkFDckMsQ0FBQyxDQUFDLElBQUEsbUJBQWEsRUFBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsUUFBUSxFQUFFO3dCQUN2QyxDQUFDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsR0FBRyxDQUFDO29CQUNqRSxTQUFTLEVBQUUsT0FBTyxDQUFDLElBQUk7b0JBQ3ZCLFdBQVcsRUFBRSxPQUFPLENBQUMsR0FBRztvQkFDeEIsT0FBTyxFQUFFLE9BQU8sQ0FBQyxJQUFJO29CQUNyQixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUc7aUJBQ2QsQ0FBQTtnQkFDVixrQkFBa0IsQ0FBQyxVQUFVLENBQUMsZUFBZSxDQUFDLENBQUE7Z0JBQzlDLGVBQWUsQ0FBQyxTQUFTLENBQUMsa0JBQWtCLENBQUMsQ0FBQTtZQUMvQyxDQUFDLENBQUMsQ0FBQTtRQUNKLENBQUMsQ0FBQyxDQUFBO1FBRUYsWUFBWSxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQTtRQUNwQyxPQUFPLENBQUMsR0FBRyxDQUFDLFlBQVksQ0FBQyxvQkFBb0IsQ0FBQyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUE7SUFDbEUsQ0FBQyxDQUFDLENBQUE7QUFDSixDQUFDLENBQUE7QUFFRCxNQUFNLENBQUMsT0FBTyxHQUFHLGNBQWMsQ0FBQSJ9 \ No newline at end of file diff --git a/docs/user-guide/usage/options.md b/docs/user-guide/usage/options.md index 614077103..612896648 100644 --- a/docs/user-guide/usage/options.md +++ b/docs/user-guide/usage/options.md @@ -28,6 +28,7 @@ Options are: - `html` - `junit` - `markdown` +- `sarif` ## `ignore` diff --git a/package-lock.json b/package-lock.json index 0e52234d7..ba03702f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "glob": "^8.0.3", "is-glob": "^4.0.3", "node-fetch": "^2.6.2", + "node-sarif-builder": "^2.0.3", "strip-json-comments": "3.1.0", "xml": "1.0.1" }, @@ -1816,6 +1817,12 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@types/sarif": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.4.tgz", + "integrity": "sha512-4xKHMdg3foh3Va1fxTzY1qt8QVqmaJpGWsVvtjQrJBn+/bkig2pWFKJ4FPI2yLI4PAj0SUKiPO4Vd7ggYIMZjQ==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -6421,6 +6428,33 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "node_modules/node-sarif-builder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-2.0.3.tgz", + "integrity": "sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==", + "dev": true, + "dependencies": { + "@types/sarif": "^2.1.4", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/node-sarif-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", @@ -9552,6 +9586,12 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "@types/sarif": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.4.tgz", + "integrity": "sha512-4xKHMdg3foh3Va1fxTzY1qt8QVqmaJpGWsVvtjQrJBn+/bkig2pWFKJ4FPI2yLI4PAj0SUKiPO4Vd7ggYIMZjQ==", + "dev": true + }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -12900,6 +12940,29 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "node-sarif-builder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-2.0.3.tgz", + "integrity": "sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg==", + "dev": true, + "requires": { + "@types/sarif": "^2.1.4", + "fs-extra": "^10.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, "normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", diff --git a/package.json b/package.json index d6df746b4..4d6bbc04c 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "glob": "^8.0.3", "is-glob": "^4.0.3", "node-fetch": "^2.6.2", + "node-sarif-builder": "^2.0.3", "strip-json-comments": "3.1.0", "xml": "1.0.1" }, diff --git a/src/cli/formatters/sarif.ts b/src/cli/formatters/sarif.ts new file mode 100644 index 000000000..ab136683a --- /dev/null +++ b/src/cli/formatters/sarif.ts @@ -0,0 +1,76 @@ +import { FormatterCallback } from '../formatter' +import { + SarifBuilder, + SarifResultBuilder, + SarifRuleBuilder, + SarifRunBuilder, +} from 'node-sarif-builder' +import * as path from 'path' +import { pathToFileURL } from 'url' +import { Result } from 'sarif' + +const pkg = require('../../../package.json') + +const sarifFormatter: FormatterCallback = function (formatter) { + formatter.on('end', (event) => { + const arrAllMessages = event.arrAllMessages + + // SARIF builder + const sarifBuilder = new SarifBuilder() + + // SARIF Run builder + const sarifRunBuilder = new SarifRunBuilder().initSimple({ + toolDriverName: 'HTMLHint', + toolDriverVersion: pkg.version, + url: 'https://htmlhint.com/', + }) + + // SARIF rules + const addedRuleSet = new Set() + arrAllMessages.forEach((result) => { + result.messages.forEach((message) => { + const rule = message.rule + if (addedRuleSet.has(rule.id)) { + return + } + addedRuleSet.add(rule.id) + const sarifRuleBuiler = new SarifRuleBuilder().initSimple({ + ruleId: rule.id, + shortDescriptionText: rule.description, + helpUri: rule.link, + }) + sarifRunBuilder.addRule(sarifRuleBuiler) + }) + }) + + // Add SARIF results (individual errors) + arrAllMessages.forEach((result) => { + result.messages.forEach((message) => { + const sarifResultBuilder = new SarifResultBuilder() + const ruleId = message.rule.id + const sarifResultInit = { + level: + message.type === 'info' + ? 'note' + : (message.type.toString() as Result.level), + messageText: message.message, + ruleId: ruleId, + fileUri: process.env.SARIF_URI_ABSOLUTE + ? pathToFileURL(result.file).toString() + : path.relative(process.cwd(), result.file).replace(/\\/g, '/'), + startLine: message.line, + startColumn: message.col, + endLine: message.line, + endColumn: message.col, + } as const + sarifResultBuilder.initSimple(sarifResultInit) + sarifRunBuilder.addResult(sarifResultBuilder) + }) + }) + + sarifBuilder.addRun(sarifRunBuilder) + console.log(sarifBuilder.buildSarifJsonString({ indent: true })) + }) +} + +module.exports = sarifFormatter diff --git a/test/cli/formatters/sarif.sarif b/test/cli/formatters/sarif.sarif new file mode 100644 index 000000000..6fb9c6c51 --- /dev/null +++ b/test/cli/formatters/sarif.sarif @@ -0,0 +1,535 @@ +{ + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "HTMLHint", + "rules": [ + { + "id": "attr-value-double-quotes", + "shortDescription": { + "text": "Attribute values must be in double quotes." + }, + "helpUri": "https://htmlhint.com/docs/user-guide/rules/attr-value-double-quotes" + }, + { + "id": "attr-no-duplication", + "shortDescription": { + "text": "Elements cannot have duplicate attributes." + }, + "helpUri": "https://htmlhint.com/docs/user-guide/rules/attr-no-duplication" + }, + { + "id": "tag-pair", + "shortDescription": { + "text": "Tag must be paired." + }, + "helpUri": "https://htmlhint.com/docs/user-guide/rules/tag-pair" + }, + { + "id": "spec-char-escape", + "shortDescription": { + "text": "Special characters must be escaped." + }, + "helpUri": "https://htmlhint.com/docs/user-guide/rules/spec-char-escape" + } + ], + "version": "1.1.4", + "informationUri": "https://htmlhint.com/" + } + }, + "results": [ + { + "level": "error", + "message": { + "text": "The value of attribute [ bad ] must be in double quotes." + }, + "ruleId": "attr-value-double-quotes", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 8, + "startColumn": 7, + "endLine": 8, + "endColumn": 7 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "error", + "message": { + "text": "The value of attribute [ bad ] must be in double quotes." + }, + "ruleId": "attr-value-double-quotes", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 8, + "startColumn": 14, + "endLine": 8, + "endColumn": 14 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "error", + "message": { + "text": "Duplicate of attribute name [ bad ] was found." + }, + "ruleId": "attr-no-duplication", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 8, + "startColumn": 14, + "endLine": 8, + "endColumn": 14 + } + } + } + ], + "ruleIndex": 1 + }, + { + "level": "error", + "message": { + "text": "The value of attribute [ bad ] must be in double quotes." + }, + "ruleId": "attr-value-double-quotes", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 9, + "startColumn": 7, + "endLine": 9, + "endColumn": 7 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "error", + "message": { + "text": "The value of attribute [ bad ] must be in double quotes." + }, + "ruleId": "attr-value-double-quotes", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 9, + "startColumn": 14, + "endLine": 9, + "endColumn": 14 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "error", + "message": { + "text": "Duplicate of attribute name [ bad ] was found." + }, + "ruleId": "attr-no-duplication", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 9, + "startColumn": 14, + "endLine": 9, + "endColumn": 14 + } + } + } + ], + "ruleIndex": 1 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 10, + "startColumn": 22, + "endLine": 10, + "endColumn": 22 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "Special characters must be escaped : [ < ]." + }, + "ruleId": "spec-char-escape", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 11, + "startColumn": 3, + "endLine": 11, + "endColumn": 3 + } + } + } + ], + "ruleIndex": 3 + }, + { + "level": "error", + "message": { + "text": "Special characters must be escaped : [ > ]." + }, + "ruleId": "spec-char-escape", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 11, + "startColumn": 18, + "endLine": 11, + "endColumn": 18 + } + } + } + ], + "ruleIndex": 3 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 13, + "startColumn": 11, + "endLine": 13, + "endColumn": 11 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 14, + "startColumn": 9, + "endLine": 14, + "endColumn": 9 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 15, + "startColumn": 7, + "endLine": 15, + "endColumn": 7 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 16, + "startColumn": 5, + "endLine": 16, + "endColumn": 5 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 17, + "startColumn": 3, + "endLine": 17, + "endColumn": 3 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "The value of attribute [ class ] must be in double quotes." + }, + "ruleId": "attr-value-double-quotes", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 21, + "startColumn": 15, + "endLine": 21, + "endColumn": 15 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "error", + "message": { + "text": "The value of attribute [ what ] must be in double quotes." + }, + "ruleId": "attr-value-double-quotes", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 21, + "startColumn": 24, + "endLine": 21, + "endColumn": 24 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "error", + "message": { + "text": "The value of attribute [ something ] must be in double quotes." + }, + "ruleId": "attr-value-double-quotes", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 21, + "startColumn": 32, + "endLine": 21, + "endColumn": 32 + } + } + } + ], + "ruleIndex": 0 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 25, + "startColumn": 3, + "endLine": 25, + "endColumn": 3 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 26, + "startColumn": 1, + "endLine": 26, + "endColumn": 1 + } + } + } + ], + "ruleIndex": 2 + }, + { + "level": "error", + "message": { + "text": "Tag must be paired, no start tag: [ ]" + }, + "ruleId": "tag-pair", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test/cli/formatters/example.html", + "index": 0 + }, + "region": { + "startLine": 27, + "startColumn": 1, + "endLine": 27, + "endColumn": 1 + } + } + } + ], + "ruleIndex": 2 + } + ], + "artifacts": [ + { + "sourceLanguage": "HTML", + "location": { + "uri": "test/cli/formatters/example.html" + } + } + ] + } + ] +} diff --git a/test/cli/formatters/sarif.spec.js b/test/cli/formatters/sarif.spec.js new file mode 100644 index 000000000..b28fcc95c --- /dev/null +++ b/test/cli/formatters/sarif.spec.js @@ -0,0 +1,70 @@ +const ChildProcess = require('child_process') +const fs = require('fs') +const path = require('path') +const os = require('os') + +describe('CLI', () => { + describe('Formatter: sarif', () => { + it('should have stdout output with formatter sarif', (done) => { + const expectedFileContent = fs + .readFileSync(path.resolve(__dirname, 'sarif.sarif'), 'utf8') + .replace( + /\{\{path\}\}/g, + path.resolve(__dirname, 'example.html').replace(/\\/g, '\\\\') + ) + + const expected = JSON.parse(expectedFileContent) + + var child = ChildProcess.spawn('node', [ + path.resolve(__dirname, '../../../bin/htmlhint'), + path.resolve(__dirname, 'example.html'), + '--format', + 'sarif', + ]) + + child.stdout.on('data', function (stdout) { + expect(stdout).not.toBe('') + + if (os.platform() !== 'darwin') { + const jsonStdout = JSON.parse(stdout) + expect(typeof jsonStdout).toBe('object') + expect( + jsonStdout['runs'][0]['artifacts'][0]['location']['uri'] + ).toContain('example.html') + + const stdoutResults = jsonStdout['runs'][0]['results'] + const stdoutRules = jsonStdout['runs'][0]['tool']['driver']['rules'] + + expect(stdoutResults).toBeInstanceOf(Array) + expect(stdoutResults.length).toBe( + expected['runs'][0]['results'].length + ) + + expect(stdoutRules).toBeInstanceOf(Array) + expect(stdoutRules.length).toBe( + expected['runs'][0]['tool']['driver']['rules'].length + ) + + for (let i = 0; i < stdoutResults.length; i++) { + expect(stdoutResults[i]).toEqual(expected['runs'][0]['results'][i]) + } + + for (let i = 0; i < stdoutRules.length; i++) { + expect(stdoutRules[i]).toEqual( + expected['runs'][0]['tool']['driver']['rules'][i] + ) + } + } + }) + + child.stderr.on('data', function (stderr) { + expect(stderr).toBe('') + }) + + child.on('close', function (code) { + expect(code).toBe(1) + done() + }) + }) + }) +})