Commits (27)
......@@ -8,14 +8,33 @@
"extends": [
"sidvind/es2017",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
"plugin:array-func/recommended",
"plugin:prettier/recommended",
"plugin:node/recommended-module",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:security/recommended",
"plugin:sonarjs/recommended"
],
"plugins": ["@typescript-eslint", "prettier"],
"plugins": [
"@typescript-eslint",
"array-func",
"node",
"prettier",
"security",
"sonarjs"
],
"rules": {
"@typescript-eslint/array-type": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
],
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/member-delimiter-style": "error",
......@@ -27,10 +46,17 @@
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/type-annotation-spacing": "error",
"consistent-this": "off",
"import/named": "off",
"no-console": "warn",
"no-dupe-class-members": "off",
"no-undef": "off",
"node/no-missing-import": "off",
"node/no-unsupported-features/es-syntax": "off",
"prettier/prettier": "warn",
"security/detect-non-literal-fs-filename": "off",
"security/detect-non-literal-require": "off",
"security/detect-object-injection": "off",
"security/detect-unsafe-regex": "off",
"strict": "off"
},
......@@ -45,7 +71,9 @@
"rules": {
"jest/no-disabled-tests": "warn",
"jest/no-focused-tests": "warn",
"jest/no-test-prefixes": "warn"
"jest/no-test-prefixes": "warn",
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-identical-functions": "off"
}
}
]
......
......@@ -108,17 +108,17 @@ Node 12.x (current):
<<: *compat
image: node:12
Publish to NPM:
Release:
stage: release
only:
- tags
- master
variables:
GIT_AUTHOR_NAME: "David Sveningsson"
GIT_AUTHOR_EMAIL: "ext@sidvind.com"
GIT_COMMITTER_NAME: "David Sveningsson"
GIT_COMMITTER_EMAIL: "ext@sidvind.com"
script:
- echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc
- export PKG_VERSION=v$(node -p "require('./package.json').version")
- echo ${CI_COMMIT_TAG}
- echo ${PKG_VERSION}
- test ${CI_COMMIT_TAG} = ${PKG_VERSION}
- npm publish
- npm run semantic-release
.downstream: &downstream
stage: postrelease
......
# 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))
## Upcoming release
# html-validate changelog
## 1.2.1 (2019-07-30)
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-missing-references.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-missing-references.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 3,
"filePath": "inline",
"messages": Array [
Object {
"column": 13,
"context": Object {
"key": "for",
"value": "missing-input",
},
"line": 1,
"message": "Element references missing id \\"missing-input\\"",
"offset": 12,
"ruleId": "no-missing-references",
"severity": 2,
"size": 13,
},
Object {
"column": 23,
"context": Object {
"key": "aria-labelledby",
"value": "missing-text",
},
"line": 2,
"message": "Element references missing id \\"missing-text\\"",
"offset": 58,
"ruleId": "no-missing-references",
"severity": 2,
"size": 12,
},
Object {
"column": 24,
"context": Object {
"key": "aria-describedby",
"value": "missing-text",
},
"line": 3,
"message": "Element references missing id \\"missing-text\\"",
"offset": 102,
"ruleId": "no-missing-references",
"severity": 2,
"size": 12,
},
],
"source": "<label for=\\"missing-input\\"></label>
<div aria-labelledby=\\"missing-text\\"></div>
<div aria-describedby=\\"missing-text\\"></div>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<label for="missing-input"></label>
<div aria-labelledby="missing-text"></div>
<div aria-describedby="missing-text"></div>`;
markup["correct"] = `<label for="my-input"></label>
<div id="verbose-text"></div>
<div aria-labelledby="verbose-text"></div>
<div aria-describedby="verbose-text"></div>
<input id="my-input">`;
describe("docs/rules/no-missing-references.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-missing-references":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-missing-references":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
@ngdoc rule
@module rules
@name no-missing-references
@category document
@summary Require all element references to exist
@description
# no missing references (`no-missing-references`)
Require all elements referenced by attributes such as `for` to exist in the
current document.
Checked attributes:
- `for`
- `aria-labelledby`
- `aria-describedby`
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-missing-references">
<label for="missing-input"></label>
<div aria-labelledby="missing-text"></div>
<div aria-describedby="missing-text"></div>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="no-missing-references">
<label for="my-input"></label>
<div id="verbose-text"></div>
<div aria-labelledby="verbose-text"></div>
<div aria-describedby="verbose-text"></div>
<input id="my-input">
</validate>
......@@ -179,7 +179,7 @@ describe("HTML elements", () => {
});
for (const tagName of tagNames) {
const filename = (variant: string) =>
const filename = (variant: string): string =>
`${fileDirectory}/${tagName}-${variant}.html`;
describe(`<${tagName}>`, () => {
......
This diff is collapsed.
{
"name": "html-validate",
"version": "1.2.1",
"version": "1.3.0",
"description": "html linter",
"keywords": [
"html",
......@@ -20,7 +20,7 @@
},
"main": "build/shim.js",
"engines": {
"node": ">=8.0"
"node": ">= 8.5"
},
"bin": {
"html-validate": "bin/html-validate.js"
......@@ -32,15 +32,14 @@
"eslint": "eslint *.js '{docs,elements,src}/**/*.{js,ts}'",
"eslint:fix": "eslint --fix *.js '{docs,elements,src}/**/*.{js,ts}'",
"lint": "npm run eslint && npm run tslint",
"postversion": "git push --follow-tags",
"prettier:check": "prettier '**/*.{ts,js,json,md,scss}' --list-different",
"prettier:write": "prettier '**/*.{ts,js,json,md,scss}' --write",
"preversion": "./scripts/validate-version",
"semantic-release": "semantic-release",
"start": "grunt connect",
"test": "jest --ci",
"tslint": "tslint -t verbose *.ts src/**/*.ts",
"tslint:fix": "tslint -t verbose --fix *.ts src/**/*.ts",
"version": "sv-update-changelog -i && git add CHANGELOG.md"
"tslint:fix": "tslint -t verbose --fix *.ts src/**/*.ts"
},
"husky": {
"hooks": {
......@@ -66,6 +65,16 @@
"automerge": false
}
},
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/gitlab",
"@semantic-release/changelog",
"@semantic-release/git"
]
},
"dependencies": {
"@babel/code-frame": "^7.0.0",
"acorn-walk": "^6.1.1",
......@@ -82,14 +91,18 @@
"devDependencies": {
"@babel/core": "7.5.5",
"@babel/preset-env": "7.5.5",
"@sidvind/build-scripts": "1.0.0",
"@semantic-release/changelog": "3.0.4",
"@semantic-release/git": "7.0.16",
"@semantic-release/gitlab": "3.1.7",
"@semantic-release/npm": "5.1.13",
"@semantic-release/release-notes-generator": "7.3.0",
"@types/babel__code-frame": "7.0.1",
"@types/estree": "0.0.39",
"@types/glob": "7.1.1",
"@types/jest": "24.0.15",
"@types/jest": "24.0.17",
"@types/json-merge-patch": "0.0.4",
"@types/minimist": "1.2.0",
"@types/node": "11.13.18",
"@types/node": "11.13.19",
"@typescript-eslint/eslint-plugin": "1.13.0",
"@typescript-eslint/parser": "1.13.0",
"autoprefixer": "9.6.1",
......@@ -101,8 +114,13 @@
"dgeni-packages": "0.28.1",
"eslint-config-prettier": "6.0.0",
"eslint-config-sidvind": "1.3.2",
"eslint-plugin-jest": "22.14.0",
"eslint-plugin-array-func": "3.1.3",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "22.15.0",
"eslint-plugin-node": "9.1.0",
"eslint-plugin-prettier": "3.1.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.4.0",
"font-awesome": "4.7.0",
"grunt": "1.0.4",
"grunt-browserify": "5.3.0",
......@@ -111,15 +129,17 @@
"grunt-contrib-copy": "1.0.0",
"grunt-postcss": "0.9.0",
"grunt-sass": "3.0.2",
"highlight.js": "9.15.8",
"husky": "3.0.1",
"highlight.js": "9.15.9",
"husky": "3.0.3",
"jest": "24.8.0",
"jest-diff": "24.8.0",
"jest-junit": "7.0.0",
"jquery": "3.4.1",
"lint-staged": "9.2.1",
"load-grunt-tasks": "5.0.0",
"load-grunt-tasks": "5.1.0",
"prettier": "1.18.2",
"sass": "1.22.7",
"sass": "1.22.9",
"semantic-release": "15.13.19",
"serve-static": "1.14.1",
"strip-ansi": "5.2.0",
"ts-jest": "24.0.2",
......
......@@ -4,7 +4,10 @@ import { Report, Result } from "../reporter";
type WrappedFormatter = (results: Result[]) => string;
function wrap(formatter: Formatter, dst: string) {
function wrap(
formatter: Formatter,
dst: string
): (results: Result[]) => string {
return (results: Result[]) => {
const output = formatter(results);
if (dst) {
......
/* eslint-disable no-console */
/* eslint-disable no-console, no-process-exit */
import { ConfigData } from "../config";
import defaultConfig from "../config/default";
import { TokenDump } from "../engine";
import { UserError } from "../error/user-error";
......@@ -6,33 +7,42 @@ import HtmlValidate from "../htmlvalidate";
import { Report, Reporter, Result } from "../reporter";
import { getFormatter } from "./formatter";
import { eventFormatter } from "./json";
const pkg = require("../../package.json");
import chalk from "chalk";
import glob from "glob";
import minimist from "minimist";
function getMode(argv: { [key: string]: any }) {
enum Mode {
LINT,
DUMP_EVENTS,
DUMP_TOKENS,
DUMP_TREE,
PRINT_CONFIG,
}
function getMode(argv: { [key: string]: any }): Mode {
if (argv["dump-events"]) {
return "dump-events";
return Mode.DUMP_EVENTS;
}
if (argv["dump-tokens"]) {
return "dump-tokens";
return Mode.DUMP_TOKENS;
}
if (argv["dump-tree"]) {
return "dump-tree";
return Mode.DUMP_TREE;
}
if (argv["print-config"]) {
return "print-config";
return Mode.PRINT_CONFIG;
}
return "lint";
return Mode.LINT;
}
function getGlobalConfig(rules?: string | string[]) {
function getGlobalConfig(rules?: string | string[]): ConfigData {
const config: any = Object.assign({}, defaultConfig);
if (rules) {
if (Array.isArray(rules)) {
......@@ -67,15 +77,15 @@ function lint(files: string[]): Report {
return Reporter.merge(reports);
}
function dump(files: string[], mode: string) {
function dump(files: string[], mode: Mode): string {
let lines: string[][] = [];
switch (mode) {
case "dump-events":
case Mode.DUMP_EVENTS:
lines = files.map((filename: string) =>
htmlvalidate.dumpEvents(filename).map(eventFormatter)
);
break;
case "dump-tokens":
case Mode.DUMP_TOKENS:
lines = files.map((filename: string) =>
htmlvalidate.dumpTokens(filename).map((entry: TokenDump) => {
const data = JSON.stringify(entry.data);
......@@ -83,7 +93,7 @@ function dump(files: string[], mode: string) {
})
);
break;
case "dump-tree":
case Mode.DUMP_TREE:
lines = files.map((filename: string) => htmlvalidate.dumpTree(filename));
break;
default:
......@@ -103,6 +113,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"],
boolean: ["dump-events", "dump-tokens", "dump-tree", "print-config", "stdin"],
alias: {
......@@ -113,7 +124,7 @@ const argv: minimist.ParsedArgs = minimist(process.argv.slice(2), {
},
});
function showUsage() {
function showUsage(): void {
const pkg = require("../../package.json");
process.stdout.write(`${pkg.name}-${pkg.version}
Usage: html-validate [OPTIONS] [FILENAME..] [DIR..]
......@@ -164,7 +175,7 @@ const files = argv._.reduce((files: string[], pattern: string) => {
}
return files.concat(glob.sync(pattern));
}, []);
const unique = [...new Set(files)];
const unique = Array.from(new Set(files));
if (unique.length === 0) {
console.error("No files matching patterns", argv._);
......@@ -172,7 +183,7 @@ if (unique.length === 0) {
}
try {
if (mode === "lint") {
if (mode === Mode.LINT) {
const result = lint(unique);
/* rename stdin if an explicit filename was passed */
......@@ -182,7 +193,7 @@ try {
process.stdout.write(formatter(result));
process.exit(result.valid ? 0 : 1);
} else if (mode === "print-config") {
} else if (mode === Mode.PRINT_CONFIG) {
const config = htmlvalidate.getConfigFor(files[0]);
const json = JSON.stringify(config.get(), null, 2);
console.log(json);
......
......@@ -60,7 +60,8 @@ export class ConfigLoader {
let current = path.resolve(path.dirname(filename));
let config = this.configClass.empty();
for (;;) {
// eslint-disable-next-line no-constant-condition
while (true) {
const search = path.join(current, ".htmlvalidate.json");
if (fs.existsSync(search)) {
......
......@@ -417,7 +417,7 @@ describe("config", () => {
it("should find rootDir", () => {
const config = new (class extends Config {
public findRootDir() {
public findRootDir(): string {
return super.findRootDir();
}
})();
......
......@@ -40,6 +40,7 @@ function mergeInternal(base: ConfigData, rhs: ConfigData): ConfigData {
function loadFromFile(filename: string): ConfigData {
let json;
try {
// eslint-disable-next-line security/detect-non-literal-require
json = require(filename);
} catch (err) {
throw new UserError(`Failed to read configuration from "${filename}"`, err);
......@@ -141,7 +142,7 @@ export class Config {
*
* Must be called before trying to use config.
*/
public init() {
public init(): void {
/* precompile transform patterns */
this.transformers = this.precompileTransformers(
this.config.transform || {}
......@@ -210,6 +211,7 @@ export class Config {
}
/* assume it is loadable with require() */
// eslint-disable-next-line security/detect-non-literal-require
metaTable.loadFromObject(require(entry));
}
......@@ -269,6 +271,7 @@ export class Config {
private loadPlugins(plugins: string[]): Plugin[] {
return plugins.map((moduleName: string) => {
// eslint-disable-next-line security/detect-non-literal-require
const plugin = require(moduleName.replace(
"<rootDir>",
this.rootDir
......@@ -335,20 +338,24 @@ export class Config {
private precompileTransformers(transform: TransformMap): Transformer[] {
return Object.entries(transform).map(([pattern, module]) => {
return {
// eslint-disable-next-line security/detect-non-literal-regexp
pattern: new RegExp(pattern),
// eslint-disable-next-line security/detect-non-literal-require
fn: require(module.replace("<rootDir>", this.rootDir)),
};
});
}
protected findRootDir() {
protected findRootDir(): string {
if (rootDirCache !== null) {
return rootDirCache;
}
/* try to locate package.json */
let current = process.cwd();
for (;;) {
// eslint-disable-next-line no-constant-condition
while (true) {
const search = path.join(current, "package.json");
if (fs.existsSync(search)) {
return (rootDirCache = current);
......
......@@ -3,6 +3,7 @@ module.exports = {
"input-missing-label": "error",
"heading-level": "error",
"missing-doctype": "error",
"no-missing-references": "error",
"require-sri": "error",
},
};
......@@ -31,7 +31,7 @@ export class Context {
);
}
public consume(n: number | string[], state: number) {
public consume(n: number | string[], state: number): void {
/* if "n" is an regex match the first value is the full matched
* string so consume that many characters. */
if (typeof n !== "number") {
......
......@@ -46,7 +46,7 @@ export class Attribute {
/**
* Flag set to true if the attribute value is static.
*/
public get isStatic() {
public get isStatic(): boolean {
return !this.isDynamic;
}
......
......@@ -6,7 +6,7 @@ const DOCUMENT_NODE_NAME = "#document";
let counter = 0;
/* istanbul ignore next: only for testing */
export function reset() {
export function reset(): void {
counter = 0;
}
......@@ -80,6 +80,15 @@ export class DOMNode {
this.disabledRules.add(ruleId);
}
/**
* Disables multiple rules.
*/
public disableRules(rules: string[]): void {
for (const rule of rules) {
this.disableRule(rule);
}
}
/**
* Enable a previously disabled rule for this node.
*/
......@@ -87,6 +96,15 @@ export class DOMNode {
this.disabledRules.delete(ruleId);
}
/**
* Enables multiple rules.
*/
public enableRules(rules: string[]): void {
for (const rule of rules) {
this.enableRule(rule);
}
}
/**
* Test if a rule is enabled for this node.
*/
......
......@@ -56,7 +56,7 @@ export class HtmlElement extends DOMNode {
}
}
public static rootNode(location: Location) {
public static rootNode(location: Location): HtmlElement {
return new HtmlElement(
undefined,
undefined,
......@@ -71,7 +71,7 @@ export class HtmlElement extends DOMNode {
endToken: Token,
parent: HtmlElement,
metaTable: MetaTable
) {
): HtmlElement {
const tagName = startToken.data[2];
if (!tagName) {
throw new Error("tagName cannot be empty");
......@@ -238,7 +238,7 @@ export class HtmlElement extends DOMNode {
* @param text - Text to add.
* @param location - Source code location of this text.
*/
public appendText(text: string | DynamicValue, location?: Location) {
public appendText(text: string | DynamicValue, location?: Location): void {
this.childNodes.push(new TextNode(text, location));
}
......@@ -257,7 +257,7 @@ export class HtmlElement extends DOMNode {
return new DOMTokenList(classes);
}
get id() {
get id(): string {
return this.getAttributeValue("id");
}
......@@ -321,7 +321,7 @@ export class HtmlElement extends DOMNode {
/**
* Evaluates callbackk on all descendants, returning true if any are true.
*/
public someChildren(callback: (node: HtmlElement) => boolean) {
public someChildren(callback: (node: HtmlElement) => boolean): boolean {
return this.childElements.some(visit);
function visit(node: HtmlElement): boolean {
......@@ -336,7 +336,7 @@ export class HtmlElement extends DOMNode {
/**
* Evaluates callbackk on all descendants, returning true if all are true.
*/
public everyChildren(callback: (node: HtmlElement) => boolean) {
public everyChildren(callback: (node: HtmlElement) => boolean): boolean {
return this.childElements.every(visit);
function visit(node: HtmlElement): boolean {
......
......@@ -17,7 +17,7 @@ function stripHtmlElement(node: HtmlElement): object {
}
function fetch(it: IterableIterator<HtmlElement>): object[] {
return Array.from(it).map(stripHtmlElement);
return Array.from(it, stripHtmlElement);
}
describe("Selector", () => {
......