Commit 50e1d67c authored by David Sveningsson's avatar David Sveningsson

fix(parser): report parser-error when stream ends before required token

parent 59a1f0d6
Pipeline #101437898 passed with stages
in 13 minutes and 51 seconds
......@@ -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> {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment