...
 
Commits (14)
image: node:10
image: node:latest
include:
- template: Security/License-Management.gitlab-ci.yml
......
# [1.3.0](https://gitlab.com/html-validate/html-validate/compare/v1.2.1...v1.3.0) (2019-08-12)
# html-validate changelog
# [1.4.0](https://gitlab.com/html-validate/html-validate/compare/v1.3.0...v1.4.0) (2019-08-15)
### Bug Fixes
* **deps:** update dependency acorn-walk to v7 ([1fe89e0](https://gitlab.com/html-validate/html-validate/commit/1fe89e0))
* **reporter:** fix {error,warning}Count after merging reports ([bc657d0](https://gitlab.com/html-validate/html-validate/commit/bc657d0))
* **reporter:** require {error,warning}Count to be present in Result ([b1306a4](https://gitlab.com/html-validate/html-validate/commit/b1306a4))
### Features
* **rules:** new rule no-missing-references ([4653384](https://gitlab.com/html-validate/html-validate/commit/4653384))
* **cli:** add new --max-warnings flag ([e78a1dc](https://gitlab.com/html-validate/html-validate/commit/e78a1dc))
* **reporter:** add {error,warning}Count summary to Report object ([2bae1d0](https://gitlab.com/html-validate/html-validate/commit/2bae1d0))
# html-validate changelog
# [1.3.0](https://gitlab.com/html-validate/html-validate/compare/v1.2.1...v1.3.0) (2019-08-12)
### Features
- **rules:** new rule no-missing-references ([4653384](https://gitlab.com/html-validate/html-validate/commit/4653384))
## 1.2.1 (2019-07-30)
......
{
"name": "html-validate",
"version": "1.3.0",
"version": "1.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -2528,6 +2528,14 @@
"requires": {
"acorn": "^6.0.1",
"acorn-walk": "^6.0.1"
},
"dependencies": {
"acorn-walk": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==",
"dev": true
}
}
},
"acorn-jsx": {
......@@ -2545,12 +2553,20 @@
"acorn-dynamic-import": "^4.0.0",
"acorn-walk": "^6.1.0",
"xtend": "^4.0.1"
},
"dependencies": {
"acorn-walk": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==",
"dev": true
}
}
},
"acorn-walk": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz",
"integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw=="
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.0.0.tgz",
"integrity": "sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg=="
},
"agent-base": {
"version": "4.3.0",
......@@ -6519,9 +6535,9 @@
}
},
"eslint-plugin-jest": {
"version": "22.15.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.15.0.tgz",
"integrity": "sha512-hgnPbSqAIcLLS9ePb12hNHTRkXnkVaCfOwCt2pzQ8KpOKPWGA4HhLMaFN38NBa/0uvLfrZpcIRjT+6tMAfr58Q==",
"version": "22.15.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.15.1.tgz",
"integrity": "sha512-CWq/RR/3tLaKFB+FZcCJwU9hH5q/bKeO3rFP8G07+q7hcDCFNqpvdphVbEbGE6o6qo1UbciEev4ejUWv7brUhw==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "^1.13.0"
......@@ -8246,9 +8262,9 @@
}
},
"grunt-sass": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-3.0.2.tgz",
"integrity": "sha512-Ogq4cWqBre71gZIkgxIxevgzZHSIIsrKu/5yvPDl4Mvib0A4TRTJEQUdpQ0YV1iai0DPjayz02vDJE6KUVHQ2w==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-3.1.0.tgz",
"integrity": "sha512-90s27H7FoCDcA8C8+R0GwC+ntYD3lG6S/jqcavWm3bn9RiJTmSfOvfbFa1PXx4NbBWuiGQMLfQTj/JvvqT5w6A==",
"dev": true
},
"handlebars": {
......@@ -12118,6 +12134,15 @@
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
"dev": true
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
"yallist": "^3.0.2"
}
},
"macos-release": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
......@@ -19152,9 +19177,9 @@
"dev": true
},
"semantic-release": {
"version": "15.13.19",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-15.13.19.tgz",
"integrity": "sha512-6eqqAmzGaJWgP5R5IkWIQK9is+cWUp/A+pwzxf/YaG1hJv1eD25klUP7Y0fedsPOxxI8eLuDUVlEs7U8SOlK0Q==",
"version": "15.13.21",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-15.13.21.tgz",
"integrity": "sha512-3S9thQas28iv3NeHUqQVsDnxMcBGQICdxabeNnJ8BnbRBvCkgqCg3v9zo/+O5a8GCyxrgjtwJ2iWozL8SiIq1w==",
"dev": true,
"requires": {
"@semantic-release/commit-analyzer": "^6.1.0",
......@@ -19172,8 +19197,8 @@
"get-stream": "^5.0.0",
"git-log-parser": "^1.2.0",
"hook-std": "^2.0.0",
"hosted-git-info": "^2.7.1",
"lodash": "^4.17.4",
"hosted-git-info": "^3.0.0",
"lodash": "^4.17.15",
"marked": "^0.7.0",
"marked-terminal": "^3.2.0",
"p-locate": "^4.0.0",
......@@ -19254,6 +19279,15 @@
"pump": "^3.0.0"
}
},
"hosted-git-info": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.0.tgz",
"integrity": "sha512-zYSx1cP4MLsvKtTg8DF/PI6e6FHZ3wcawcTGsrLU2TM+UfD4jmSrn2wdQT16TFbH3lO4PIdjLG0E+cuYDgFD9g==",
"dev": true,
"requires": {
"lru-cache": "^5.1.1"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
......@@ -19287,6 +19321,12 @@
"validate-npm-package-license": "^3.0.1"
},
"dependencies": {
"hosted-git-info": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz",
"integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==",
"dev": true
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
......@@ -21641,6 +21681,12 @@
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"dev": true
},
"yargs": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
......
{
"name": "html-validate",
"version": "1.3.0",
"version": "1.4.0",
"description": "html linter",
"keywords": [
"html",
......@@ -71,13 +71,23 @@
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/gitlab",
"@semantic-release/changelog",
"@semantic-release/git"
[
"@semantic-release/changelog",
{
"changelogTitle": "# html-validate changelog\n\n"
}
],
[
"@semantic-release/git",
{
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}
]
]
},
"dependencies": {
"@babel/code-frame": "^7.0.0",
"acorn-walk": "^6.1.1",
"acorn-walk": "^7.0.0",
"ajv": "^6.10.0",
"better-ajv-errors": "^0.6.2",
"chalk": "^2.4.2",
......@@ -116,7 +126,7 @@
"eslint-config-sidvind": "1.3.2",
"eslint-plugin-array-func": "3.1.3",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "22.15.0",
"eslint-plugin-jest": "22.15.1",
"eslint-plugin-node": "9.1.0",
"eslint-plugin-prettier": "3.1.0",
"eslint-plugin-security": "1.4.0",
......@@ -128,7 +138,7 @@
"grunt-contrib-connect": "2.0.0",
"grunt-contrib-copy": "1.0.0",
"grunt-postcss": "0.9.0",
"grunt-sass": "3.0.2",
"grunt-sass": "3.1.0",
"highlight.js": "9.15.9",
"husky": "3.0.3",
"jest": "24.8.0",
......@@ -139,7 +149,7 @@
"load-grunt-tasks": "5.1.0",
"prettier": "1.18.2",
"sass": "1.22.9",
"semantic-release": "15.13.19",
"semantic-release": "15.13.21",
"serve-static": "1.14.1",
"strip-ansi": "5.2.0",
"ts-jest": "24.0.2",
......
......@@ -40,6 +40,8 @@ const report: Report = {
warningCount: 0,
},
],
errorCount: 1,
warningCount: 0,
};
describe("cli/formatters", () => {
......
......@@ -114,7 +114,7 @@ function renameStdin(report: Report, filename: string): void {
const argv: minimist.ParsedArgs = minimist(process.argv.slice(2), {
// eslint-disable-next-line sonarjs/no-duplicate-string
string: ["f", "formatter", "rule", "stdin-filename"],
string: ["f", "formatter", "max-warnings", "rule", "stdin-filename"],
boolean: ["dump-events", "dump-tokens", "dump-tree", "print-config", "stdin"],
alias: {
f: "formatter",
......@@ -131,6 +131,7 @@ Usage: html-validate [OPTIONS] [FILENAME..] [DIR..]
Common options:
-f, --formatter=FORMATTER specify the formatter to use.
--max-warnings=INT number of warnings to trigger nonzero exit code
--rule=RULE:SEVERITY set additional rule, use comma separator for
multiple.
--stdin process markup from stdin.
......@@ -166,8 +167,17 @@ if (argv.h || argv.help || argv._.length === 0) {
const mode = getMode(argv);
const config = getGlobalConfig(argv.rule);
const formatter = getFormatter(argv.formatter);
const maxWarnings = parseInt(argv["max-warnings"] || "-1", 10);
const htmlvalidate = new HtmlValidate(config);
/* sanity check: ensure maxWarnings has a valid value */
if (isNaN(maxWarnings)) {
console.log(
`Invalid value "${argv["max-warnings"]}" given to --max-warnings`
);
process.exit(1);
}
const files = argv._.reduce((files: string[], pattern: string) => {
/* process - as standard input */
if (pattern === "-") {
......@@ -192,6 +202,14 @@ try {
}
process.stdout.write(formatter(result));
if (maxWarnings >= 0 && result.warningCount > maxWarnings) {
console.log(
`\nhtml-validate found too many warnings (maxiumum: ${maxWarnings}).`
);
result.valid = false;
}
process.exit(result.valid ? 0 : 1);
} else if (mode === Mode.PRINT_CONFIG) {
const config = htmlvalidate.getConfigFor(files[0]);
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`json formatter should empty messages 1`] = `"[{\\"filePath\\":\\"empty.html\\",\\"messages\\":[]}]"`;
exports[`json formatter should empty messages 1`] = `"[{\\"filePath\\":\\"empty.html\\",\\"messages\\":[],\\"errorCount\\":0,\\"warningCount\\":0}]"`;
exports[`json formatter should empty result 1`] = `"[]"`;
exports[`json formatter should generate json 1`] = `"[{\\"filePath\\":\\"regular.html\\",\\"messages\\":[{\\"ruleId\\":\\"foo\\",\\"severity\\":2,\\"message\\":\\"An error\\",\\"offset\\":4,\\"line\\":1,\\"column\\":5,\\"size\\":1},{\\"ruleId\\":\\"bar\\",\\"severity\\":1,\\"message\\":\\"A warning\\",\\"offset\\":12,\\"line\\":2,\\"column\\":4,\\"size\\":1}]},{\\"filePath\\":\\"edge-cases.html\\",\\"messages\\":[{\\"ruleId\\":\\"baz\\",\\"severity\\":2,\\"message\\":\\"Another error\\",\\"offset\\":14,\\"line\\":3,\\"column\\":3,\\"size\\":1}]}]"`;
exports[`json formatter should generate json 1`] = `"[{\\"filePath\\":\\"regular.html\\",\\"errorCount\\":1,\\"warningCount\\":1,\\"messages\\":[{\\"ruleId\\":\\"foo\\",\\"severity\\":2,\\"message\\":\\"An error\\",\\"offset\\":4,\\"line\\":1,\\"column\\":5,\\"size\\":1},{\\"ruleId\\":\\"bar\\",\\"severity\\":1,\\"message\\":\\"A warning\\",\\"offset\\":12,\\"line\\":2,\\"column\\":4,\\"size\\":1}]},{\\"filePath\\":\\"edge-cases.html\\",\\"errorCount\\":1,\\"warningCount\\":0,\\"messages\\":[{\\"ruleId\\":\\"baz\\",\\"severity\\":2,\\"message\\":\\"Another error\\",\\"offset\\":14,\\"line\\":3,\\"column\\":3,\\"size\\":1}]}]"`;
......@@ -6,6 +6,8 @@ describe("checkstyle formatter", () => {
const results: Result[] = [
{
filePath: "regular.html",
errorCount: 1,
warningCount: 0,
messages: [
{
ruleId: "foo",
......@@ -29,6 +31,8 @@ describe("checkstyle formatter", () => {
},
{
filePath: "edge-cases.html",
errorCount: 2,
warningCount: 0,
messages: [
{
ruleId: "foo",
......@@ -69,7 +73,9 @@ describe("checkstyle formatter", () => {
});
it("should empty messages", () => {
const results: Result[] = [{ filePath: "empty.html", messages: [] }];
const results: Result[] = [
{ filePath: "empty.html", messages: [], errorCount: 0, warningCount: 0 },
];
expect(formatter(results)).toMatchSnapshot();
});
});
......@@ -70,7 +70,9 @@ describe("codeframe formatter", () => {
});
it("should empty messages", () => {
const results: Result[] = [{ filePath: "empty.html", messages: [] }];
const results: Result[] = [
{ filePath: "empty.html", messages: [], errorCount: 0, warningCount: 0 },
];
expect(formatter(results)).toMatchSnapshot();
});
});
......@@ -6,6 +6,8 @@ describe("json formatter", () => {
const results: Result[] = [
{
filePath: "regular.html",
errorCount: 1,
warningCount: 1,
messages: [
{
ruleId: "foo",
......@@ -29,6 +31,8 @@ describe("json formatter", () => {
},
{
filePath: "edge-cases.html",
errorCount: 1,
warningCount: 0,
messages: [
{
ruleId: "baz",
......@@ -51,7 +55,9 @@ describe("json formatter", () => {
});
it("should empty messages", () => {
const results: Result[] = [{ filePath: "empty.html", messages: [] }];
const results: Result[] = [
{ filePath: "empty.html", messages: [], errorCount: 0, warningCount: 0 },
];
expect(formatter(results)).toMatchSnapshot();
});
});
......@@ -63,7 +63,9 @@ describe("stylish formatter", () => {
});
it("should empty messages", () => {
const results: Result[] = [{ filePath: "empty.html", messages: [] }];
const results: Result[] = [
{ filePath: "empty.html", messages: [], errorCount: 0, warningCount: 0 },
];
expect(formatter(results)).toMatchSnapshot();
});
});
......@@ -6,6 +6,8 @@ describe("text formatter", () => {
const results: Result[] = [
{
filePath: "regular.html",
errorCount: 1,
warningCount: 1,
messages: [
{
ruleId: "foo",
......@@ -29,6 +31,8 @@ describe("text formatter", () => {
},
{
filePath: "edge-cases.html",
errorCount: 1,
warningCount: 0,
messages: [
{
ruleId: "baz",
......@@ -51,7 +55,9 @@ describe("text formatter", () => {
});
it("should empty messages", () => {
const results: Result[] = [{ filePath: "empty.html", messages: [] }];
const results: Result[] = [
{ filePath: "empty.html", messages: [], errorCount: 0, warningCount: 0 },
];
expect(formatter(results)).toMatchSnapshot();
});
});
......@@ -5,16 +5,16 @@ describe("Reporter", () => {
describe("merge()", () => {
it("should set valid only if all reports are valid", () => {
const none = Reporter.merge([
{ valid: false, results: [] },
{ valid: false, results: [] },
{ valid: false, results: [], errorCount: 1, warningCount: 2 },
{ valid: false, results: [], errorCount: 3, warningCount: 4 },
]);
const one = Reporter.merge([
{ valid: true, results: [] },
{ valid: false, results: [] },
{ valid: true, results: [], errorCount: 0, warningCount: 0 },
{ valid: false, results: [], errorCount: 1, warningCount: 0 },
]);
const all = Reporter.merge([
{ valid: true, results: [] },
{ valid: true, results: [] },
{ valid: true, results: [], errorCount: 0, warningCount: 0 },
{ valid: true, results: [], errorCount: 0, warningCount: 0 },
]);
expect(none.valid).toBeFalsy();
expect(one.valid).toBeFalsy();
......@@ -29,10 +29,14 @@ describe("Reporter", () => {
createResult("foo", ["fred", "barney"]),
createResult("bar", ["spam"]),
],
errorCount: 3,
warningCount: 0,
},
{
valid: false,
results: [createResult("foo", ["wilma"])],
errorCount: 1,
warningCount: 0,
},
]);
expect(merged.results).toHaveLength(2);
......@@ -42,8 +46,12 @@ describe("Reporter", () => {
"barney",
"wilma",
]);
expect(merged.results[0].errorCount).toEqual(3);
expect(merged.results[1].filePath).toEqual("bar");
expect(merged.results[1].messages.map(x => x.message)).toEqual(["spam"]);
expect(merged.results[1].errorCount).toEqual(1);
expect(merged.errorCount).toEqual(4);
expect(merged.warningCount).toEqual(0);
});
});
......@@ -190,6 +198,8 @@ function createResult(filename: string, messages: string[]): Result {
return {
filePath: filename,
messages: messages.map(cur => createMessage(cur)),
errorCount: messages.length,
warningCount: 0,
};
}
......
......@@ -38,8 +38,8 @@ export interface Message {
export interface Result {
messages: Message[];
filePath: string;
errorCount?: number;
warningCount?: number;
errorCount: number;
warningCount: number;
source?: string;
}
......@@ -52,6 +52,12 @@ export interface Report {
/** Detailed results per validated source */
results: Result[];
/** Total number of errors across all sources */
errorCount: number;
/** Total warnings of errors across all sources */
warningCount: number;
}
export class Reporter {
......@@ -80,7 +86,18 @@ export class Reporter {
}
});
});
return { valid, results: Object.keys(merged).map(key => merged[key]) };
const results: Result[] = Object.values(merged).map((result: Result) => {
/* recalculate error- and warning-count */
result.errorCount = countErrors(result.messages);
result.warningCount = countWarnings(result.messages);
return result;
});
return {
valid,
results,
errorCount: sumErrors(results),
warningCount: sumWarnings(results),
};
}
public add(
......@@ -113,7 +130,7 @@ export class Reporter {
}
public save(sources?: Source[]): Report {
return {
const report: Report = {
valid: this.isValid(),
results: Object.keys(this.result).map(filePath => {
const messages = [].concat(this.result[filePath]).sort(messageSort);
......@@ -128,7 +145,12 @@ export class Reporter {
source: source ? source.originalData || source.data : null,
};
}),
errorCount: 0,
warningCount: 0,
};
report.errorCount = sumErrors(report.results);
report.warningCount = sumWarnings(report.results);
return report;
}
protected isValid(): boolean {
......@@ -147,6 +169,18 @@ function countWarnings(messages: Message[]): number {
return messages.filter(m => m.severity === Severity.WARN).length;
}
function sumErrors(results: Result[]): number {
return results.reduce((sum: number, result: Result) => {
return sum + result.errorCount;
}, 0);
}
function sumWarnings(results: Result[]): number {
return results.reduce((sum: number, result: Result) => {
return sum + result.warningCount;
}, 0);
}
function messageSort(a: Message, b: Message): number {
if (a.line < b.line) {
return -1;
......