...
 
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", () => {
......
......@@ -380,7 +380,7 @@ describe("Engine", () => {
it("should load from plugins", () => {
class MyRule {
public init() {
public init(): void {
/* do nothing */
}
}
......
......@@ -103,7 +103,7 @@ export class Engine<T extends Parser = Parser> {
const dom = parser.parseHtml(source[0]);
const lines: string[] = [];
function decoration(node: HtmlElement) {
function decoration(node: HtmlElement): string {
let output = "";
if (node.hasAttribute("id")) {
output += `#${node.id}`;
......@@ -114,7 +114,11 @@ export class Engine<T extends Parser = Parser> {
return output;
}
function writeNode(node: HtmlElement, level: number, sibling: number) {
function writeNode(
node: HtmlElement,
level: number,
sibling: number
): void {
if (level > 0) {
const indent = " ".repeat(level - 1);
const l = node.childElements.length > 0 ? "" : "";
......@@ -193,9 +197,7 @@ export class Engine<T extends Parser = Parser> {
/* enable rules on node */
parser.on("tag:open", (event: string, data: TagOpenEvent) => {
for (const rule of rules) {
data.target.enableRule(rule.name);
}
data.target.enableRules(rules.map(rule => rule.name));
});
}
......@@ -206,9 +208,7 @@ export class Engine<T extends Parser = Parser> {
/* disable rules on node */
parser.on("tag:open", (event: string, data: TagOpenEvent) => {
for (const rule of rules) {
data.target.disableRule(rule.name);
}
data.target.disableRules(rules.map(rule => rule.name));
});
}
......@@ -228,9 +228,7 @@ export class Engine<T extends Parser = Parser> {
/* disable rules directly on the node so it will be recorded for later,
* more specifically when using the domtree to trigger errors */
for (const rule of rules) {
data.target.disableRule(rule.name);
}
data.target.disableRules(rules.map(rule => rule.name));
}
);
......@@ -267,9 +265,7 @@ export class Engine<T extends Parser = Parser> {
const unregister = parser.on(
"tag:open",
(event: string, data: TagOpenEvent) => {
for (const rule of rules) {
data.target.disableRule(rule.name);
}
data.target.disableRules(rules.map(rule => rule.name));
}
);
......@@ -403,7 +399,7 @@ export class Engine<T extends Parser = Parser> {
private missingRule(name: string): any {
return new (class extends Rule {
public setup() {
public setup(): void {
this.on("dom:load", () => {
this.report(null, `Definition for rule '${name}' was not found`);
});
......
import EventHandler from "./eventhandler";
import { EventHandler } from "./eventhandler";
describe("eventhandler", () => {
let eventhandler: EventHandler;
......
......@@ -9,14 +9,14 @@ const entities: { [key: string]: string } = {
"&": "&amp;",
};
function xmlescape(src: any) {
function xmlescape(src: any): string {
if (src === null || src === undefined) return src;
return src.toString().replace(/[><'"&]/g, (match: string) => {
return entities[match];
});
}
function getMessageType(message: Message) {
function getMessageType(message: Message): "error" | "warning" {
switch (message.severity) {
case 2:
return "error";
......@@ -27,7 +27,7 @@ function getMessageType(message: Message) {
}
}
export default function checkstyleFormatter(results: Result[]) {
export default function checkstyleFormatter(results: Result[]): string {
let output = "";
output += `<?xml version="1.0" encoding="utf-8"?>`;
......
import { codeFrameColumns } from "@babel/code-frame";
import chalk from "chalk";
import path from "path";
import { Message, Result } from "reporter";
import { FormatterModule } from ".";
import { Message, Result } from "../reporter";
interface SourcePoint {
line: number;
......@@ -133,9 +133,7 @@ function formatSummary(errors: number, warnings: number): string {
summary.push(`${warnings} ${pluralize("warning", warnings)}`);
}
const output = chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
return output;
return chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
}
export default function codeframe(results: Result[]): string {
......
import { FormatterModule } from ".";
import { Result } from "../reporter";
export default function jsonFormatter(results: Result[]) {
export default function jsonFormatter(results: Result[]): string {
return JSON.stringify(results);
}
......
import { FormatterModule } from ".";
import { Result } from "../reporter";
export default function textFormatter(results: Result[]) {
export default function textFormatter(results: Result[]): string {
let output = "";
let total = 0;
......
import { Source } from "../context";
import "../matchers";
import { Lexer } from "./lexer";
import { TokenType } from "./token";
function inlineSource(source: string, { line = 1, column = 1 } = {}) {
function inlineSource(source: string, { line = 1, column = 1 } = {}): Source {
return {
data: source,
filename: "inline",
......
......@@ -119,7 +119,7 @@ export class Lexer {
}
/* istanbul ignore next: used to provide a better error when an unhandled state happens */
private unhandled(context: Context) {
private unhandled(context: Context): void {
const truncated = JSON.stringify(
context.string.length > 13
? `${context.string.slice(0, 15)}...`
......@@ -131,7 +131,7 @@ export class Lexer {
}
/* istanbul ignore next: used to provide a better error when lexer is detected to be stuck, no known way to reproduce */
private errorStuck(context: Context) {
private errorStuck(context: Context): void {
const state = State[context.state];
const message = `failed to tokenize ${context.getTruncatedLine()}, state ${state} failed to consume data or change state.`;
throw new InvalidTokenError(context.getLocation(), message);
......@@ -140,7 +140,7 @@ export class Lexer {
private evalNextState(
nextState: State | ((token: Token) => State),
token: Token
) {
): State {
if (typeof nextState === "function") {
return nextState(token);
} else {
......@@ -148,7 +148,11 @@ export class Lexer {
}
}
private *match(context: Context, tests: LexerTest[], error: string) {
private *match(
context: Context,
tests: LexerTest[],
error: string
): Iterable<Token> {
let match;
const n = tests.length;
for (let i = 0; i < n; i++) {
......@@ -174,22 +178,18 @@ export class Lexer {
/**
* Called when entering a new state.
*/
private enter(context: Context, state: State, data: any) {
switch (state) {
case State.TAG:
/* request script tag tokenization */
if (data && data[0][0] === "<") {
if (data[0] === "<script") {
context.contentModel = ContentModel.SCRIPT;
} else {
context.contentModel = ContentModel.TEXT;
}
}
break;
private enter(context: Context, state: State, data: RegExpMatchArray): void {
/* script tags require a different content model */
if (state === State.TAG && data && data[0][0] === "<") {
if (data[0] === "<script") {
context.contentModel = ContentModel.SCRIPT;
} else {
context.contentModel = ContentModel.TEXT;
}
}
}
private *tokenizeInitial(context: Context) {
private *tokenizeInitial(context: Context): Iterable<Token> {
yield* this.match(
context,
[
......@@ -202,7 +202,7 @@ export class Lexer {
);
}
private *tokenizeDoctype(context: Context) {
private *tokenizeDoctype(context: Context): Iterable<Token> {
yield* this.match(
context,
[
......@@ -214,8 +214,8 @@ export class Lexer {
);
}
private *tokenizeTag(context: Context) {
function nextState(token: Token) {
private *tokenizeTag(context: Context): Iterable<Token> {
function nextState(token: Token): State {
switch (context.contentModel) {
case ContentModel.TEXT:
return State.TEXT;
......@@ -244,7 +244,7 @@ export class Lexer {
);
}
private *tokenizeAttr(context: Context) {
private *tokenizeAttr(context: Context): Iterable<Token> {
yield* this.match(
context,
[
......@@ -257,7 +257,7 @@ export class Lexer {
);
}
private *tokenizeText(context: Context) {
private *tokenizeText(context: Context): Iterable<Token> {
yield* this.match(
context,
[
......@@ -274,7 +274,7 @@ export class Lexer {
);
}
private *tokenizeCDATA(context: Context) {
private *tokenizeCDATA(context: Context): Iterable<Token> {
yield* this.match(
context,
[[MATCH_CDATA_END, State.TEXT, false]],
......@@ -282,7 +282,7 @@ export class Lexer {
);
}
private *tokenizeScript(context: Context) {
private *tokenizeScript(context: Context): Iterable<Token> {
yield* this.match(
context,
[
......
/* eslint-disable @typescript-eslint/no-namespace, prefer-template */
/* eslint-disable @typescript-eslint/no-namespace, prefer-template, sonarjs/no-duplicate-string */
import diff from "jest-diff";
import { TokenType } from "./lexer";
......@@ -22,7 +22,7 @@ declare global {
}
}
function toBeValid(report: Report) {
function toBeValid(report: Report): jest.CustomMatcherResult {
if (report.valid) {
return {
pass: true,
......@@ -38,7 +38,7 @@ function toBeValid(report: Report) {
}
}
function toBeInvalid(report: Report) {
function toBeInvalid(report: Report): jest.CustomMatcherResult {
if (report.valid) {
return {
pass: false,
......@@ -52,7 +52,11 @@ function toBeInvalid(report: Report) {
}
}
function toHaveError(report: Report, ruleId: any, message: any) {
function toHaveError(
report: Report,
ruleId: any,
message: any
): jest.CustomMatcherResult {
const actual = report.results.reduce(
(aggregated: Message[], result: Result) => {
return aggregated.concat(result.messages);
......@@ -62,7 +66,7 @@ function toHaveError(report: Report, ruleId: any, message: any) {
const matcher = [expect.objectContaining({ ruleId, message })];
const pass = this.equals(actual, matcher);
const diffString = diff(matcher, actual, { expand: this.expand });
const resultMessage = () =>
const resultMessage = (): string =>
this.utils.matcherHint(".toHaveError") +
"\n\n" +
"Expected token to equal:\n" +
......@@ -74,7 +78,10 @@ function toHaveError(report: Report, ruleId: any, message: any) {
return { pass, message: resultMessage };
}
function toHaveErrors(report: Report, errors: Array<[string, string] | {}>) {
function toHaveErrors(
report: Report,
errors: Array<[string, string] | {}>
): jest.CustomMatcherResult {
const actual = report.results.reduce(
(aggregated: Message[], result: Result) => {
return aggregated.concat(result.messages);
......@@ -91,7 +98,7 @@ function toHaveErrors(report: Report, errors: Array<[string, string] | {}>) {
});
const pass = this.equals(actual, matcher);
const diffString = diff(matcher, actual, { expand: this.expand });
const resultMessage = () =>
const resultMessage = (): string =>
this.utils.matcherHint(".toHaveErrors") +
"\n\n" +
"Expected token to equal:\n" +
......@@ -103,7 +110,7 @@ function toHaveErrors(report: Report, errors: Array<[string, string] | {}>) {
return { pass, message: resultMessage };
}
function toBeToken(actual: any, expected: any) {
function toBeToken(actual: any, expected: any): jest.CustomMatcherResult {
const token = actual.value;
if (token.type) {
......@@ -117,7 +124,7 @@ function toBeToken(actual: any, expected: any) {
const matcher = expect.objectContaining(expected);
const pass = this.equals(token, matcher);
const diffString = diff(matcher, token, { expand: this.expand });
const message = () =>
const message = (): string =>
this.utils.matcherHint(".toBeToken") +
"\n\n" +
"Expected token to equal:\n" +
......
......@@ -4,7 +4,7 @@ interface Validate {
(): boolean;
errors: any[];
}
const validate: Validate = () => {
const validate: Validate = (): boolean => {
return validate.errors.length === 0;
};
validate.errors = [] as any[];
......@@ -54,7 +54,7 @@ describe("MetaTable", () => {
},
];
const table = new MetaTable();
const fn = () =>
const fn = (): void =>
table.loadFromObject({
foo: mockEntry({ invalid: true }),
});
......
......@@ -45,7 +45,7 @@ export class MetaTable {
this.schema = clone(require("../../elements/schema.json"));
}
public init() {
public init(): void {
this.resolveGlobal();
}
......@@ -159,14 +159,14 @@ export class MetaTable {
return deepmerge(a, b);
}
public resolve(node: HtmlElement) {
public resolve(node: HtmlElement): void {
if (node.meta) {
expandProperties(node, node.meta);
}
}
}
function expandProperties(node: HtmlElement, entry: MetaElement) {
function expandProperties(node: HtmlElement, entry: MetaElement): void {
for (const key of dynamicKeys) {
const property = entry[key];
if (property && typeof property !== "boolean") {
......@@ -175,12 +175,13 @@ function expandProperties(node: HtmlElement, entry: MetaElement) {
}
}
function expandRegex(entry: MetaElement) {
function expandRegex(entry: MetaElement): RegExp {
if (!entry.attributes) return;
for (const [name, values] of Object.entries(entry.attributes)) {
entry.attributes[name] = values.map((value: string | RegExp) => {
const match = typeof value === "string" && value.match(/^\/(.*)\/$/);
if (match) {
// eslint-disable-next-line security/detect-non-literal-regexp
return new RegExp(match[1]);
} else {
return value;
......
......@@ -8,7 +8,7 @@ import "../matchers";
import { AttributeData } from "./attribute-data";
import { Parser } from "./parser";
function mergeEvent(event: string, data: any) {
function mergeEvent(event: string, data: any): any {
const merged = Object.assign({}, { event }, data);
/* not useful for these tests */
......@@ -25,7 +25,7 @@ function mergeEvent(event: string, data: any) {
}
class ExposedParser extends Parser {
public consumeDirective(token: Token) {
public consumeDirective(token: Token): void {
super.consumeDirective(token);
}
......@@ -36,7 +36,7 @@ class ExposedParser extends Parser {
yield* super.consumeUntil(tokenStream, search);
}
public trigger(event: any, data: any) {
public trigger(event: any, data: any): void {
super.trigger(event, data);
}
}
......
import { ProcessAttributeCallback } from "context/source";
import { Config } from "../config";
import { Location, sliceLocation, Source } from "../context";
import { ProcessAttributeCallback } from "../context/source";
import { DOMTree, HtmlElement, NodeClosed } from "../dom";
import { ElementReadyEvent, EventCallback, EventHandler } from "../event";
import {
AttributeEvent,
ConditionalEvent,
DirectiveEvent,
DoctypeEvent,
DOMReadyEvent,
ElementReadyEvent,
Event,
EventCallback,
EventHandler,
TagCloseEvent,
TagOpenEvent,
WhitespaceEvent,
......@@ -245,7 +247,7 @@ export class Parser {
node: HtmlElement,
active: HtmlElement,
location: Location
) {
): void {
/* call processElement hook */
if (source.hooks && source.hooks.processElement) {
const processElement = source.hooks.processElement;
......@@ -328,7 +330,7 @@ export class Parser {
node: HtmlElement,
token: Token,
next?: Token
) {
): void {
const keyLocation = token.location;
const valueLocation = this.getAttributeValueLocation(next);
const haveValue = next && next.type === TokenType.ATTR_VALUE;
......@@ -344,9 +346,9 @@ export class Parser {
/* get callback to process attributes, default is to just return attribute
* data right away but a transformer may override it to allow aliasing
* attributes, e.g ng-attr-foo or v-bind:foo */
let processAttribute: ProcessAttributeCallback = (attr: AttributeData) => [
attr,
];
let processAttribute: ProcessAttributeCallback = (
attr: AttributeData
): Iterable<AttributeData> => [attr];
if (source.hooks && source.hooks.processAttribute) {
processAttribute = source.hooks.processAttribute;
}
......@@ -403,7 +405,7 @@ export class Parser {
}
}
protected consumeDirective(token: Token) {
protected consumeDirective(token: Token): void {
const directive = token.data[1];
const match = directive.match(/^([a-zA-Z0-9-]+)\s*(.*?)(?:\s*:\s*(.*))?$/);
if (!match) {
......@@ -422,7 +424,7 @@ export class Parser {
/**
* Consumes doctype tokens. Emits doctype event.
*/
protected consumeDoctype(startToken: Token, tokenStream: TokenStream) {
protected consumeDoctype(startToken: Token, tokenStream: TokenStream): void {
const tokens = Array.from(
this.consumeUntil(tokenStream, TokenType.DOCTYPE_CLOSE)
);
......
......@@ -10,6 +10,7 @@ export function parsePattern(pattern: string): RegExp {
return /^[a-z0-9_]+$/;
default:
// eslint-disable-next-line security/detect-non-literal-regexp
return new RegExp(pattern);
}
}
......
......@@ -206,7 +206,7 @@ describe("Plugin", () => {
it("Engine should call rule init callback", () => {
const mockRule: Rule = new (class extends Rule {
public setup() {
public setup(): void {
/* do nothing */
}
})({});
......
import { Source } from "./context";
import { Message, Reporter } from "./reporter";
import { Message, Reporter, Result } from "./reporter";
describe("Reporter", () => {
describe("merge()", () => {
......@@ -186,7 +186,7 @@ describe("Reporter", () => {
});
});
function createResult(filename: string, messages: string[]) {
function createResult(filename: string, messages: string[]): Result {
return {
filePath: filename,
messages: messages.map(cur => createMessage(cur)),
......
......@@ -89,7 +89,7 @@ export class Reporter {
severity: number,
location: Location,
context?: any
) {
): void {
if (!(location.filename in this.result)) {
this.result[location.filename] = [];
}
......@@ -139,11 +139,11 @@ export class Reporter {
}
}
function countErrors(messages: Message[]) {
function countErrors(messages: Message[]): number {
return messages.filter(m => m.severity === Severity.ERROR).length;
}
function countWarnings(messages: Message[]) {
function countWarnings(messages: Message[]): number {
return messages.filter(m => m.severity === Severity.WARN).length;
}
......
......@@ -7,7 +7,7 @@ import { Reporter } from "./reporter";
import { Rule, ruleDocumentationUrl } from "./rule";
class MockRule extends Rule {
public setup() {
public setup(): void {
/* do nothing */
}
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule no-missing-references should contain contextual documentation 1`] = `
Object {
"description": "The element ID \\"my-id\\" referenced by the my-attribute attribute must point to an existing element.",
"url": "https://html-validate.org/rules/no-missing-references.html",
}
`;
exports[`rule no-missing-references should contain documentation 1`] = `
Object {
"description": "The element ID referenced by the attribute must point to an existing element.",
"url": "https://html-validate.org/rules/no-missing-references.html",
}
`;
......@@ -23,7 +23,7 @@ class AttrCase extends Rule {
};
}
public setup() {
public setup():