...
 
Commits (6)
# html-validate changelog
## [2.4.3](https://gitlab.com/html-validate/html-validate/compare/v2.4.2...v2.4.3) (2019-12-08)
### Bug Fixes
- **parser:** report parser-error when stream ends before required token ([50e1d67](https://gitlab.com/html-validate/html-validate/commit/50e1d67c5c79b44d53fe3889ee76ed9577c04865))
## [2.4.2](https://gitlab.com/html-validate/html-validate/compare/v2.4.1...v2.4.2) (2019-12-05)
### Bug Fixes
......
This diff is collapsed.
{
"name": "html-validate",
"version": "2.4.2",
"version": "2.4.3",
"description": "html linter",
"keywords": [
"html",
......@@ -143,8 +143,8 @@
"minimist": "^1.2.0"
},
"devDependencies": {
"@babel/core": "7.7.4",
"@babel/preset-env": "7.7.4",
"@babel/core": "7.7.5",
"@babel/preset-env": "7.7.5",
"@commitlint/cli": "8.2.0",
"@commitlint/config-conventional": "8.2.0",
"@semantic-release/changelog": "3.0.6",
......@@ -191,7 +191,7 @@
"husky": "3.1.0",
"jest": "24.9.0",
"jest-diff": "24.9.0",
"jest-junit": "9.0.0",
"jest-junit": "10.0.0",
"jquery": "3.4.1",
"lint-staged": "9.5.0",
"load-grunt-tasks": "5.1.0",
......
......@@ -4,7 +4,7 @@ import { DOMTree } from "../dom";
import { InvalidTokenError } from "../lexer";
import "../matchers";
import { MetaTable } from "../meta";
import { Parser } from "../parser";
import { Parser, ParserError } from "../parser";
import { Reporter } from "../reporter";
import { Rule, RuleOptions } from "../rule";
import { Engine } from "./engine";
......@@ -23,15 +23,25 @@ class MockParser extends Parser {
public parseHtml(source: string | Source): DOMTree {
if (typeof source === "string") return null;
switch (source.data) {
case "parse-error":
case "invalid-token-error":
throw new InvalidTokenError(
{
filename: source.filename,
line: 1,
column: 1,
offset: 0,
},
"invalid token error"
);
case "parser-error":
throw new ParserError(
{
filename: source.filename,
line: 1,
column: 1,
offset: 0,
},
"parse error"
"parser error"
);
case "exception":
throw new Error("exception");
......@@ -82,21 +92,55 @@ describe("Engine", () => {
});
it("should report lexing errors", () => {
const source: Source[] = [inline("parse-error")]; // see MockParser, will raise InvalidTokenError
const source: Source[] = [inline("invalid-token-error")]; // see MockParser, will raise InvalidTokenError
const report = engine.lint(source);
expect(report.valid).toBeFalsy();
expect(report.results).toHaveLength(1);
expect(report.results[0].messages).toEqual([
{
offset: 0,
line: 1,
column: 1,
size: 0,
severity: 2,
ruleId: "parser-error",
message: "parse error",
},
]);
expect(report.results[0]).toMatchInlineSnapshot(`
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 1,
"line": 1,
"message": "invalid token error",
"offset": 0,
"ruleId": "parser-error",
"severity": 2,
"size": 0,
},
],
"source": "invalid-token-error",
"warningCount": 0,
}
`);
});
it("should report parser errors", () => {
const source: Source[] = [inline("parser-error")]; // see MockParser, will raise ParserError
const report = engine.lint(source);
expect(report.valid).toBeFalsy();
expect(report.results).toHaveLength(1);
expect(report.results[0]).toMatchInlineSnapshot(`
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 1,
"line": 1,
"message": "parser error",
"offset": 0,
"ruleId": "parser-error",
"severity": 2,
"size": 0,
},
],
"source": "parser-error",
"warningCount": 0,
}
`);
});
it("should pass exceptions", () => {
......
......@@ -8,7 +8,7 @@ import {
TagOpenEvent,
} from "../event";
import { InvalidTokenError, Lexer, TokenType } from "../lexer";
import { Parser } from "../parser";
import { Parser, ParserError } from "../parser";
import { Report, Reporter } from "../reporter";
import { Rule, RuleConstructor, RuleDocumentation, RuleOptions } from "../rule";
......@@ -72,7 +72,7 @@ export class Engine<T extends Parser = Parser> {
try {
parser.parseHtml(source);
} catch (e) {
if (e instanceof InvalidTokenError) {
if (e instanceof InvalidTokenError || e instanceof ParserError) {
this.reportError("parser-error", e.message, e.location);
} else {
throw e;
......
......@@ -142,3 +142,31 @@ it("should handle source missing properties", () => {
}
`);
});
it("should report parser-error when last tag is left unopened", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
root: true,
});
const report = htmlvalidate.validateString("<div");
expect(report).toBeInvalid();
expect(report.results[0]).toMatchInlineSnapshot(`
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 1,
"line": 1,
"message": "stream ended before TAG_CLOSE token was found",
"offset": 0,
"ruleId": "parser-error",
"severity": 2,
"size": 4,
},
],
"source": "<div",
"warningCount": 0,
}
`);
});
export { Parser } from "./parser";
export { AttributeData } from "./attribute-data";
export { ParserError } from "./parser-error";
import { Location } from "../context";
export class ParserError extends Error {
public location: Location;
public constructor(location: Location, message: string) {
super(message);
this.location = location;
}
}
import { Config } from "../config";
import { Source } from "../context";
import { Location, Source } from "../context";
import { DOMTree, HtmlElement, TextNode } from "../dom";
import { EventCallback } from "../event";
import HtmlValidate from "../htmlvalidate";
......@@ -31,9 +31,10 @@ class ExposedParser extends Parser {
public *consumeUntil(
tokenStream: TokenStream,
search: TokenType
search: TokenType,
errorLocation: Location
): IterableIterator<Token> {
yield* super.consumeUntil(tokenStream, search);
yield* super.consumeUntil(tokenStream, search, errorLocation);
}
public trigger(event: any, data: any): void {
......@@ -1055,7 +1056,15 @@ describe("parser", () => {
data: null,
},
][Symbol.iterator]();
const result = Array.from(parser.consumeUntil(src, TokenType.TAG_CLOSE));
const location: Location = {
filename: "inline",
line: 1,
column: 1,
offset: 0,
};
const result = Array.from(
parser.consumeUntil(src, TokenType.TAG_CLOSE, location)
);
expect(result).toEqual([
{
type: TokenType.TAG_OPEN,
......@@ -1083,9 +1092,15 @@ describe("parser", () => {
data: null,
},
][Symbol.iterator]();
const location: Location = {
filename: "inline",
line: 1,
column: 1,
offset: 0,
};
expect(() =>
Array.from(parser.consumeUntil(src, TokenType.TAG_CLOSE))
).toThrow("stream ended before consumeUntil finished");
Array.from(parser.consumeUntil(src, TokenType.TAG_CLOSE, location))
).toThrow("stream ended before TAG_CLOSE token was found");
});
});
......
......@@ -21,6 +21,7 @@ import { Lexer, Token, TokenStream, TokenType } from "../lexer";
import { MetaTable } from "../meta";
import { AttributeData } from "./attribute-data";
import { parseConditionalComment } from "./conditional-comment";
import { ParserError } from "./parser-error";
/**
* Parse HTML document into a DOM tree.
......@@ -181,7 +182,7 @@ export class Parser {
tokenStream: TokenStream
): void {
const tokens = Array.from(
this.consumeUntil(tokenStream, TokenType.TAG_CLOSE)
this.consumeUntil(tokenStream, TokenType.TAG_CLOSE, startToken.location)
);
const endToken = tokens.slice(-1)[0];
const closeOptional = this.closeOptional(startToken);
......@@ -246,7 +247,12 @@ export class Parser {
} else if (foreign) {
/* consume the body of the foreign element so it won't be part of the
* document (only the root foreign element is). */
this.discardForeignBody(source, node.tagName, tokenStream);
this.discardForeignBody(
source,
node.tagName,
tokenStream,
startToken.location
);
}
}
......@@ -285,7 +291,8 @@ export class Parser {
protected discardForeignBody(
source: Source,
foreignTagName: string,
tokenStream: TokenStream
tokenStream: TokenStream,
errorLocation: Location
): void {
/* consume elements until the end tag for this foreign element is found */
let nested = 1;
......@@ -294,7 +301,7 @@ export class Parser {
do {
/* search for tags */
const tokens = Array.from(
this.consumeUntil(tokenStream, TokenType.TAG_OPEN)
this.consumeUntil(tokenStream, TokenType.TAG_OPEN, errorLocation)
);
const [last] = tokens.slice(-1);
const [, tagClosed, tagName] = last.data;
......@@ -306,7 +313,7 @@ export class Parser {
/* locate end token and determine if this is a self-closed tag */
const endTokens = Array.from(
this.consumeUntil(tokenStream, TokenType.TAG_CLOSE)
this.consumeUntil(tokenStream, TokenType.TAG_CLOSE, last.location)
);
endToken = endTokens.slice(-1)[0];
const selfClosed = endToken.data[0] === "/>";
......@@ -452,7 +459,11 @@ export class Parser {
*/
protected consumeDoctype(startToken: Token, tokenStream: TokenStream): void {
const tokens = Array.from(
this.consumeUntil(tokenStream, TokenType.DOCTYPE_CLOSE)
this.consumeUntil(
tokenStream,
TokenType.DOCTYPE_CLOSE,
startToken.location
)
);
const doctype =
tokens[0]; /* first token is the doctype, second is the closing ">" */
......@@ -467,10 +478,13 @@ export class Parser {
/**
* Return a list of tokens found until the expected token was found.
*
* @param errorLocation - What location to use if an error occurs
*/
protected *consumeUntil(
tokenStream: TokenStream,
search: TokenType
search: TokenType,
errorLocation: Location
): IterableIterator<Token> {
let it = this.next(tokenStream);
while (!it.done) {
......@@ -479,7 +493,10 @@ export class Parser {
if (token.type === search) return;
it = this.next(tokenStream);
}
throw Error("stream ended before consumeUntil finished");
throw new ParserError(
errorLocation,
`stream ended before ${TokenType[search]} token was found`
);
}
private next(tokenStream: TokenStream): IteratorResult<Token> {
......