storing full sourcemap in token, refactored error reporting

This commit is contained in:
Ilya Puchka
2017-12-27 02:31:47 +01:00
parent cb124319ec
commit ac2fd56e8e
15 changed files with 149 additions and 188 deletions

View File

@@ -45,30 +45,7 @@ public struct Environment {
func render(template: Template, context: [String: Any]?) throws -> String {
// update temaplte environment as it cen be created from string literal with default environment
template.environment = self
errorReporter.context = ErrorReporterContext(template: template)
do {
return try template.render(context)
} catch {
throw errorReporter.reportError(error)
}
}
var template: Template? {
return errorReporter.context?.template
}
public func pushTemplate<Result>(_ template: Template, token: Token?, closure: (() throws -> Result)) rethrows -> Result {
let errorReporterContext = errorReporter.context
defer { errorReporter.context = errorReporterContext }
errorReporter.context = ErrorReporterContext(
template: template,
parent: errorReporterContext != nil ? (errorReporterContext!, token) : nil
)
do {
return try closure()
} catch {
throw errorReporter.reportError(error)
}
return try template.render(context)
}
}

View File

@@ -22,13 +22,15 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
public let reason: String
public var description: String { return reason }
public internal(set) var token: Token?
public internal(set) var template: Template?
public internal(set) var parentError: Error?
public internal(set) var stackTrace: [Token]
public var templateName: String? { return token?.sourceMap.filename }
var allTokens: [Token] {
return stackTrace + (token.map({ [$0] }) ?? [])
}
public init(reason: String, token: Token? = nil, template: Template? = nil, parentError: Error? = nil) {
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
self.reason = reason
self.parentError = parentError
self.template = template
self.stackTrace = stackTrace
self.token = token
}
@@ -37,77 +39,34 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
}
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
guard lhs.description == rhs.description else { return false }
switch (lhs.parentError, rhs.parentError) {
case let (lhsParent? as TemplateSyntaxError?, rhsParent? as TemplateSyntaxError?):
return lhsParent == rhsParent
case let (lhsParent?, rhsParent?):
return String(describing: lhsParent) == String(describing: rhsParent)
default:
return lhs.parentError == nil && rhs.parentError == nil
}
return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace
}
}
public class ErrorReporterContext {
public let template: Template
public typealias ParentContext = (context: ErrorReporterContext, token: Token?)
public let parent: ParentContext?
public init(template: Template, parent: ParentContext? = nil) {
self.template = template
self.parent = parent
}
}
public protocol ErrorReporter: class {
var context: ErrorReporterContext! { get set }
func reportError(_ error: Error) -> Error
func renderError(_ error: Error) -> String
}
open class SimpleErrorReporter: ErrorReporter {
public var context: ErrorReporterContext!
open func reportError(_ error: Error) -> Error {
guard let context = context else { return error }
return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)",
token: (error as? TemplateSyntaxError)?.token,
template: (error as? TemplateSyntaxError)?.template ?? context.template,
parentError: (error as? TemplateSyntaxError)?.parentError
)
}
open func renderError(_ error: Error) -> String {
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
let description: String
if let template = templateError.template, let token = templateError.token {
let templateName = template.name.map({ "\($0):" }) ?? ""
let range = template.templateString.range(of: token.contents, range: token.range) ?? token.range
let line = template.templateString.rangeLine(range)
func describe(token: Token) -> String {
let templateName = token.sourceMap.filename ?? ""
let line = token.sourceMap.line
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))"
description = "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n"
return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n"
+ "\(line.content)\n"
+ "\(highlight)\n"
} else {
description = templateError.reason
}
var descriptions = [description]
var currentError: TemplateSyntaxError? = templateError
while let parentError = currentError?.parentError {
descriptions.append(renderError(parentError))
currentError = parentError as? TemplateSyntaxError
}
return descriptions.reversed().joined(separator: "\n")
var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] }
let description = templateError.token.map(describe(token:)) ?? templateError.reason
descriptions.append(description)
return descriptions.joined(separator: "\n")
}
}

View File

@@ -28,14 +28,12 @@ class IncludeNode : NodeType {
let template = try context.environment.loadTemplate(name: templateName)
do {
return try context.environment.pushTemplate(template, token: token) {
try context.push {
return try template.render(context)
}
return try context.push {
return try template.render(context)
}
} catch {
if let parentError = error as? TemplateSyntaxError {
throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError)
if let error = error as? TemplateSyntaxError {
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
} else {
throw error
}

View File

@@ -2,22 +2,22 @@ class BlockContext {
class var contextKey: String { return "block_context" }
// contains mapping of block names to their nodes and templates where they are defined
var blocks: [String: [(BlockNode, Template?)]]
var blocks: [String: [BlockNode]]
init(blocks: [String: [(BlockNode, Template?)]]) {
init(blocks: [String: [BlockNode]]) {
self.blocks = blocks
}
func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) {
func pushBlock(_ block: BlockNode, named blockName: String) {
if var blocks = blocks[blockName] {
blocks.append((block, template))
blocks.append(block)
self.blocks[blockName] = blocks
} else {
self.blocks[blockName] = [(block, template)]
self.blocks[blockName] = [block]
}
}
func popBlock(named blockName: String) -> (node: BlockNode, template: Template?)? {
func popBlock(named blockName: String) -> BlockNode? {
if var blocks = blocks[blockName] {
let block = blocks.removeFirst()
if blocks.isEmpty {
@@ -86,34 +86,31 @@ class ExtendsNode : NodeType {
}
let baseTemplate = try context.environment.loadTemplate(name: templateName)
let template = context.environment.template
let blockContext: BlockContext
if let _blockContext = context[BlockContext.contextKey] as? BlockContext {
blockContext = _blockContext
for (name, block) in blocks {
blockContext.pushBlock(block, named: name, definedIn: template)
blockContext.pushBlock(block, named: name)
}
} else {
var blocks = [String: [(BlockNode, Template?)]]()
self.blocks.forEach { blocks[$0.key] = [($0.value, template)] }
var blocks = [String: [BlockNode]]()
self.blocks.forEach { blocks[$0.key] = [$0.value] }
blockContext = BlockContext(blocks: blocks)
}
do {
// pushes base template and renders it's content
// block_context contains all blocks from child templates
return try context.environment.pushTemplate(baseTemplate, token: token) {
try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
return try baseTemplate.render(context)
}
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
return try baseTemplate.render(context)
}
} catch {
// if error template is already set (see catch in BlockNode)
// and it happend in the same template as current template
// there is no need to wrap it in another error
if let error = error as? TemplateSyntaxError, error.template !== context.environment.template {
throw TemplateSyntaxError(reason: error.reason, parentError: error)
if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename {
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
} else {
throw error
}
@@ -152,7 +149,7 @@ class BlockNode : NodeType {
var newContext: [String: Any] = [BlockContext.contextKey: blockContext]
if let blockSuperNode = child.node.nodes.first(where: {
if let blockSuperNode = child.nodes.first(where: {
if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true }
else { return false}
}) {
@@ -160,27 +157,28 @@ class BlockNode : NodeType {
// render current (base) node so that its content can be used as part of node that extends it
newContext["block"] = ["super": try self.render(context)]
} catch {
let baseError = context.errorReporter.reportError(error)
throw TemplateSyntaxError(
reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)",
token: blockSuperNode.token,
template: child.template,
parentError: baseError)
if let error = error as? TemplateSyntaxError {
throw TemplateSyntaxError(
reason: error.reason,
token: blockSuperNode.token,
stackTrace: error.allTokens)
} else {
throw TemplateSyntaxError(
reason: "\(error)",
token: blockSuperNode.token,
stackTrace: [])
}
}
}
// render extension node
do {
return try context.push(dictionary: newContext) {
return try child.node.render(context)
return try child.render(context)
}
} catch {
// child node belongs to child template, which is currently not on top of stack
// so we need to use node's template to report errors, not current template
// unless it's already set
if var error = error as? TemplateSyntaxError {
error.template = error.template ?? child.template
error.token = error.token ?? child.node.token
error.token = error.token ?? child.token
throw error
} else {
throw error

View File

@@ -1,9 +1,11 @@
import Foundation
struct Lexer {
let templateName: String?
let templateString: String
init(templateString: String) {
init(templateName: String? = nil, templateString: String) {
self.templateName = templateName
self.templateString = templateString
}
@@ -16,14 +18,28 @@ struct Lexer {
}
if string.hasPrefix("{{") {
return .variable(value: strip(), at: range)
let value = strip()
let range = templateString.range(of: value, range: range) ?? range
let line = templateString.rangeLine(range)
let sourceMap = SourceMap(filename: templateName, line: line)
return .variable(value: value, at: sourceMap)
} else if string.hasPrefix("{%") {
return .block(value: strip(), at: range)
let value = strip()
let range = templateString.range(of: value, range: range) ?? range
let line = templateString.rangeLine(range)
let sourceMap = SourceMap(filename: templateName, line: line)
return .block(value: value, at: sourceMap)
} else if string.hasPrefix("{#") {
return .comment(value: strip(), at: range)
let value = strip()
let range = templateString.range(of: value, range: range) ?? range
let line = templateString.rangeLine(range)
let sourceMap = SourceMap(filename: templateName, line: line)
return .comment(value: value, at: sourceMap)
}
return .text(value: string, at: range)
let line = templateString.rangeLine(range)
let sourceMap = SourceMap(filename: templateName, line: line)
return .text(value: string, at: sourceMap)
}
/// Returns an array of tokens from a given template string.
@@ -41,6 +57,7 @@ struct Lexer {
while !scanner.isEmpty {
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
if !text.1.isEmpty {
let line = templateString.rangeLine(scanner.range)
tokens.append(createToken(string: text.1, at: scanner.range))
}
@@ -48,6 +65,7 @@ struct Lexer {
let result = scanner.scan(until: end, returnUntil: true)
tokens.append(createToken(string: result, at: scanner.range))
} else {
let line = templateString.rangeLine(scanner.range)
tokens.append(createToken(string: scanner.content, at: scanner.range))
scanner.content = ""
}
@@ -165,7 +183,7 @@ extension String {
return String(self[first..<last])
}
public func rangeLine(_ range: Range<String.Index>) -> (content: String, number: UInt, offset: String.IndexDistance) {
public func rangeLine(_ range: Range<String.Index>) -> RangeLine {
var lineNumber: UInt = 0
var offset: Int = 0
var lineContent = ""
@@ -183,3 +201,5 @@ extension String {
return (lineContent, lineNumber, offset)
}
}
public typealias RangeLine = (content: String, number: UInt, offset: String.IndexDistance)

View File

@@ -107,9 +107,11 @@ public class TokenParser {
return try FilterExpression(token: filterToken, parser: self)
} catch {
if var error = error as? TemplateSyntaxError, error.token == nil {
// find range of filter in the containing token so that only filter is highligted, not the whole token
if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) {
error.token = Token.variable(value: filterToken, at: filterTokenRange)
// find offset of filter in the containing token so that only filter is highligted, not the whole token
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
var rangeLine = containingToken.sourceMap.line
rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
error.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine))
} else {
error.token = containingToken
}

View File

@@ -20,7 +20,7 @@ open class Template: ExpressibleByStringLiteral {
self.name = name
self.templateString = templateString
let lexer = Lexer(templateString: templateString)
let lexer = Lexer(templateName: name, templateString: templateString)
tokens = lexer.tokenize()
}

View File

@@ -40,18 +40,34 @@ extension String {
}
}
public struct SourceMap: Equatable {
public let filename: String?
public let line: RangeLine
init(filename: String? = nil, line: RangeLine = ("", 0, 0)) {
self.filename = filename
self.line = line
}
static let unknown = SourceMap()
public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool {
return lhs.filename == rhs.filename && lhs.line == rhs.line
}
}
public enum Token : Equatable {
/// A token representing a piece of text.
case text(value: String, at: Range<String.Index>)
case text(value: String, at: SourceMap)
/// A token representing a variable.
case variable(value: String, at: Range<String.Index>)
case variable(value: String, at: SourceMap)
/// A token representing a comment.
case comment(value: String, at: Range<String.Index>)
case comment(value: String, at: SourceMap)
/// A token representing a template block.
case block(value: String, at: Range<String.Index>)
case block(value: String, at: SourceMap)
/// Returns the underlying value as an array seperated by spaces
public func components() -> [String] {
@@ -74,29 +90,29 @@ public enum Token : Equatable {
}
}
public var range: Range<String.Index> {
public var sourceMap: SourceMap {
switch self {
case .block(_, let range),
.variable(_, let range),
.text(_, let range),
.comment(_, let range):
return range
case .block(_, let sourceMap),
.variable(_, let sourceMap),
.text(_, let sourceMap),
.comment(_, let sourceMap):
return sourceMap
}
}
}
public func == (lhs: Token, rhs: Token) -> Bool {
switch (lhs, rhs) {
case (.text(let lhsValue), .text(let rhsValue)):
return lhsValue == rhsValue
case (.variable(let lhsValue), .variable(let rhsValue)):
return lhsValue == rhsValue
case (.block(let lhsValue), .block(let rhsValue)):
return lhsValue == rhsValue
case (.comment(let lhsValue), .comment(let rhsValue)):
return lhsValue == rhsValue
case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)):
return lhsValue == rhsValue && lhsAt == rhsAt
case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)):
return lhsValue == rhsValue && lhsAt == rhsAt
case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)):
return lhsValue == rhsValue && lhsAt == rhsAt
case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)):
return lhsValue == rhsValue && lhsAt == rhsAt
default:
return false
}

View File

@@ -41,15 +41,17 @@ func testEnvironment() {
}
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
let token = Token.block(value: token, at: template.templateString.range(of: token)!)
return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil)
let range = template.templateString.range(of: token)!
let rangeLine = template.templateString.rangeLine(range)
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
let token = Token.block(value: token, at: sourceMap)
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
}
func expectError(reason: String, token: String) throws {
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]))
.toThrow(expectedError)
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])).toThrow() as TemplateSyntaxError
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
}
@@ -200,10 +202,10 @@ func testEnvironment() {
func expectError(reason: String, token: String, includedToken: String) throws {
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
expectedError.parentError = expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason)
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
let error = try expect(environment.render(template: template, context: ["target": "World"]))
.toThrow(expectedError)
.toThrow() as TemplateSyntaxError
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
}
@@ -249,11 +251,10 @@ func testEnvironment() {
func expectError(reason: String, childToken: String, baseToken: String?) throws {
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
if let baseToken = baseToken {
expectedError.parentError = expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason)
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
}
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]))
.toThrow(expectedError)
.toThrow() as TemplateSyntaxError
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
}
@@ -312,7 +313,7 @@ func testEnvironment() {
private extension Expectation {
@discardableResult
func toThrow<T: Error & Equatable>(_ error: T) throws -> T {
func toThrow<T: Error>() throws -> T {
var thrownError: Error? = nil
do {
@@ -323,12 +324,9 @@ private extension Expectation {
if let thrownError = thrownError {
if let thrownError = thrownError as? T {
if error != thrownError {
throw failure("\(thrownError) is not \(error)")
}
return thrownError
} else {
throw failure("\(thrownError) is not \(error)")
throw failure("\(thrownError) is not \(T.self)")
}
} else {
throw failure("expression did not throw an error")

View File

@@ -62,7 +62,7 @@ func testFilter() {
}
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat"))
try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first))
}
$0.it("allows you to override a default filter") {
@@ -91,7 +91,7 @@ func testFilter() {
describe("capitalize filter") {
let template = Template(templateString: "{{ name|capitalize }}")
let template = Template(templateString: "{{ name|capitalize }}")
$0.it("capitalizes a string") {
let result = try template.render(Context(dictionary: ["name": "kyle"]))

View File

@@ -169,11 +169,9 @@ func testForNode() {
}
$0.it("handles invalid input") {
let tokens: [Token] = [
.block(value: "for i", at: .unknown),
]
let tokens: [Token] = [.block(value: "for i", at: .unknown)]
let parser = TokenParser(tokens: tokens, environment: Environment())
let error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.")
let error = TemplateSyntaxError(reason: "'for' statements should use the following syntax 'for x in y where condition'.", token: tokens.first)
try expect(try parser.parse()).toThrow(error)
}

View File

@@ -179,22 +179,18 @@ func testIfNode() {
}
$0.it("throws an error when parsing an if block without an endif") {
let tokens: [Token] = [
.block(value: "if value", at: .unknown),
]
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
let parser = TokenParser(tokens: tokens, environment: Environment())
let error = TemplateSyntaxError("`endif` was not found.")
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
try expect(try parser.parse()).toThrow(error)
}
$0.it("throws an error when parsing an ifnot without an endif") {
let tokens: [Token] = [
.block(value: "ifnot value", at: .unknown),
]
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
let parser = TokenParser(tokens: tokens, environment: Environment())
let error = TemplateSyntaxError("`endif` was not found.")
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
try expect(try parser.parse()).toThrow(error)
}
}

View File

@@ -14,7 +14,7 @@ func testInclude() {
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
let parser = TokenParser(tokens: tokens, environment: Environment())
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
let error = TemplateSyntaxError(reason: "'include' tag takes one argument, the template file to be included", token: tokens.first)
try expect(try parser.parse()).toThrow(error)
}

View File

@@ -9,7 +9,7 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == 1
try expect(tokens.first) == .text(value: "Hello World", at: "Hello World".range)
try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(line: ("Hello World", 1, 0)))
}
$0.it("can tokenize a comment") {
@@ -17,7 +17,7 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == 1
try expect(tokens.first) == .comment(value: "Comment", at: "{# Comment #}".range)
try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(line: ("{# Comment #}", 1, 3)))
}
$0.it("can tokenize a variable") {
@@ -25,7 +25,7 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == 1
try expect(tokens.first) == .variable(value: "Variable", at: "{{ Variable }}".range)
try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(line: ("{{ Variable }}", 1, 3)))
}
$0.it("can tokenize unclosed tag by ignoring it") {
@@ -34,18 +34,18 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == 1
try expect(tokens.first) == .text(value: "", at: "".range)
try expect(tokens.first) == .text(value: "", at: SourceMap(line: ("{{ thing", 1, 0)))
}
$0.it("can tokenize a mixture of content") {
let templateString = "My name is {{ name }}."
let templateString = "My name is {{ myname }}."
let lexer = Lexer(templateString: templateString)
let tokens = lexer.tokenize()
try expect(tokens.count) == 3
try expect(tokens[0]) == Token.text(value: "My name is ", at: templateString.range(of: "My name is ")!)
try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!)
try expect(tokens[2]) == Token.text(value: ".", at: templateString.range(of: ".")!)
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is ")!)))
try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!)))
try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
}
$0.it("can tokenize two variables without being greedy") {
@@ -54,8 +54,8 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == 2
try expect(tokens[0]) == Token.variable(value: "thing", at: templateString.range(of: "{{ thing }}")!)
try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!)
try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "thing")!)))
try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!)))
}
$0.it("can tokenize an unclosed block") {

View File

@@ -52,11 +52,10 @@ func testTokenParser() {
}
$0.it("errors when parsing an unknown tag") {
let parser = TokenParser(tokens: [
.block(value: "unknown", at: .unknown),
], environment: Environment())
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
let parser = TokenParser(tokens: tokens, environment: Environment())
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first))
}
}
}