reporting node rendering errors using reference to node’s token

This commit is contained in:
Ilya Puchka
2017-12-23 15:19:36 +01:00
parent 27135f3ea3
commit 53c1550c5b
15 changed files with 186 additions and 47 deletions

View File

@@ -55,8 +55,7 @@ public struct Environment {
return errorReporter.context?.template return errorReporter.context?.template
} }
public func pushTemplate<Result>(_ template: Template, token: Token?, closure: (() throws -> Result)) rethrows -> Result {
public func pushTemplate<Result>(_ template: Template, token: Token, closure: (() throws -> Result)) rethrows -> Result {
let errorReporterContext = errorReporter.context let errorReporterContext = errorReporter.context
defer { errorReporter.context = errorReporterContext } defer { errorReporter.context = errorReporterContext }
errorReporter.context = ErrorReporterContext( errorReporter.context = ErrorReporterContext(

View File

@@ -35,7 +35,7 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
public class ErrorReporterContext { public class ErrorReporterContext {
public let template: Template public let template: Template
public typealias ParentContext = (context: ErrorReporterContext, token: Token) public typealias ParentContext = (context: ErrorReporterContext, token: Token?)
public let parent: ParentContext? public let parent: ParentContext?
public init(template: Template, parent: ParentContext? = nil) { public init(template: Template, parent: ParentContext? = nil) {
@@ -47,30 +47,29 @@ public class ErrorReporterContext {
public protocol ErrorReporter: class { public protocol ErrorReporter: class {
var context: ErrorReporterContext! { get set } var context: ErrorReporterContext! { get set }
func reportError(_ error: Error) -> Error func reportError(_ error: Error) -> Error
func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? func contextAwareError(_ error: Error, at range: Range<String.Index>?, context: ErrorReporterContext) -> Error?
} }
open class SimpleErrorReporter: ErrorReporter { open class SimpleErrorReporter: ErrorReporter {
public var context: ErrorReporterContext! public var context: ErrorReporterContext!
open func reportError(_ error: Error) -> Error { open func reportError(_ error: Error) -> Error {
guard let syntaxError = error as? TemplateSyntaxError else { return error }
guard let context = context else { return error } guard let context = context else { return error }
return contextAwareError(syntaxError, context: context) ?? error return contextAwareError(error, at: (error as? TemplateSyntaxError)?.lexeme?.range, context: context) ?? error
} }
// TODO: add stack trace using parent context // TODO: add stack trace using parent context
open func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? { open func contextAwareError(_ error: Error, at range: Range<String.Index>?, context: ErrorReporterContext) -> Error? {
guard let lexeme = error.lexeme, lexeme.range != .unknown else { return nil } guard let range = range, range != .unknown else { return nil }
let templateName = context.template.name.map({ "\($0):" }) ?? "" let templateName = context.template.name.map({ "\($0):" }) ?? ""
let tokenContent = context.template.templateString.substring(with: lexeme.range) let tokenContent = context.template.templateString.substring(with: range)
let lexer = Lexer(templateString: context.template.templateString) let line = context.template.templateString.rangeLine(range)
let line = lexer.lexemeLine(lexeme)
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))" let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))"
let description = "\(templateName)\(line.number):\(line.offset): error: \(error.description)\n\(line.content)\n\(highlight)" let description = "\(templateName)\(line.number):\(line.offset): error: \(error)\n\(line.content)\n\(highlight)"
let error = TemplateSyntaxError(description) let error = TemplateSyntaxError(description)
return error return error
} }
} }
extension Range where Bound == String.Index { extension Range where Bound == String.Index {

View File

@@ -15,7 +15,7 @@ open class Extension {
/// Registers a simple template tag with a name and a handler /// Registers a simple template tag with a name and a handler
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) { public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
registerTag(name, parser: { parser, token in registerTag(name, parser: { parser, token in
return SimpleNode(handler: handler) return SimpleNode(token: token, handler: handler)
}) })
} }

View File

@@ -1,6 +1,7 @@
class FilterNode : NodeType { class FilterNode : NodeType {
let resolvable: Resolvable let resolvable: Resolvable
let nodes: [NodeType] let nodes: [NodeType]
let token: Token?
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components() let bits = token.components()
@@ -16,19 +17,20 @@ class FilterNode : NodeType {
} }
let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token) let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token)
return FilterNode(nodes: blocks, resolvable: resolvable) return FilterNode(nodes: blocks, resolvable: resolvable, token: token)
} }
init(nodes: [NodeType], resolvable: Resolvable) { init(nodes: [NodeType], resolvable: Resolvable, token: Token) {
self.nodes = nodes self.nodes = nodes
self.resolvable = resolvable self.resolvable = resolvable
self.token = token
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
let value = try renderNodes(nodes, context) let value = try renderNodes(nodes, context)
return try context.push(dictionary: ["filter_value": value]) { return try context.push(dictionary: ["filter_value": value]) {
return try VariableNode(variable: resolvable).render(context) return try VariableNode(variable: resolvable, token: token).render(context)
} }
} }
} }

View File

@@ -6,6 +6,7 @@ class ForNode : NodeType {
let nodes:[NodeType] let nodes:[NodeType]
let emptyNodes: [NodeType] let emptyNodes: [NodeType]
let `where`: Expression? let `where`: Expression?
let token: Token?
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components() let components = token.components()
@@ -42,15 +43,16 @@ class ForNode : NodeType {
} else { } else {
`where` = nil `where` = nil
} }
return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`) return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token)
} }
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) { init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil, token: Token? = nil) {
self.resolvable = resolvable self.resolvable = resolvable
self.loopVariables = loopVariables self.loopVariables = loopVariables
self.nodes = nodes self.nodes = nodes
self.emptyNodes = emptyNodes self.emptyNodes = emptyNodes
self.where = `where` self.where = `where`
self.token = token
} }
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result { func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result {

View File

@@ -182,6 +182,7 @@ final class IfCondition {
class IfNode : NodeType { class IfNode : NodeType {
let conditions: [IfCondition] let conditions: [IfCondition]
let token: Token?
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
var components = token.components() var components = token.components()
@@ -193,27 +194,27 @@ class IfNode : NodeType {
IfCondition(expression: expression, nodes: nodes) IfCondition(expression: expression, nodes: nodes)
] ]
var token = parser.nextToken() var nextToken = parser.nextToken()
while let current = token, current.contents.hasPrefix("elif") { while let current = nextToken, current.contents.hasPrefix("elif") {
var components = current.components() var components = current.components()
components.removeFirst() components.removeFirst()
let expression = try parseExpression(components: components, tokenParser: parser, token: current) let expression = try parseExpression(components: components, tokenParser: parser, token: current)
let nodes = try parser.parse(until(["endif", "elif", "else"])) let nodes = try parser.parse(until(["endif", "elif", "else"]))
token = parser.nextToken() nextToken = parser.nextToken()
conditions.append(IfCondition(expression: expression, nodes: nodes)) conditions.append(IfCondition(expression: expression, nodes: nodes))
} }
if let current = token, current.contents == "else" { if let current = nextToken, current.contents == "else" {
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"])))) conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
token = parser.nextToken() nextToken = parser.nextToken()
} }
guard let current = token, current.contents == "endif" else { guard let current = nextToken, current.contents == "endif" else {
throw TemplateSyntaxError("`endif` was not found.") throw TemplateSyntaxError("`endif` was not found.")
} }
return IfNode(conditions: conditions) return IfNode(conditions: conditions, token: token)
} }
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType { class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
@@ -240,11 +241,12 @@ class IfNode : NodeType {
return IfNode(conditions: [ return IfNode(conditions: [
IfCondition(expression: expression, nodes: trueNodes), IfCondition(expression: expression, nodes: trueNodes),
IfCondition(expression: nil, nodes: falseNodes), IfCondition(expression: nil, nodes: falseNodes),
]) ], token: token)
} }
init(conditions: [IfCondition]) { init(conditions: [IfCondition], token: Token? = nil) {
self.conditions = conditions self.conditions = conditions
self.token = token
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {

View File

@@ -3,7 +3,7 @@ import PathKit
class IncludeNode : NodeType { class IncludeNode : NodeType {
let templateName: Variable let templateName: Variable
let token: Token let token: Token?
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components() let bits = token.components()

View File

@@ -51,7 +51,7 @@ extension Collection {
class ExtendsNode : NodeType { class ExtendsNode : NodeType {
let templateName: Variable let templateName: Variable
let blocks: [String:BlockNode] let blocks: [String:BlockNode]
let token: Token let token: Token?
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components() let bits = token.components()
@@ -112,6 +112,7 @@ class ExtendsNode : NodeType {
class BlockNode : NodeType { class BlockNode : NodeType {
let name: String let name: String
let nodes: [NodeType] let nodes: [NodeType]
let token: Token?
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components() let bits = token.components()
@@ -123,12 +124,13 @@ class BlockNode : NodeType {
let blockName = bits[1] let blockName = bits[1]
let nodes = try parser.parse(until(["endblock"])) let nodes = try parser.parse(until(["endblock"]))
_ = parser.nextToken() _ = parser.nextToken()
return BlockNode(name:blockName, nodes:nodes) return BlockNode(name:blockName, nodes:nodes, token: token)
} }
init(name: String, nodes: [NodeType]) { init(name: String, nodes: [NodeType], token: Token) {
self.name = name self.name = name
self.nodes = nodes self.nodes = nodes
self.token = token
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {

View File

@@ -187,4 +187,21 @@ extension String {
return String(self[first..<last]) return String(self[first..<last])
} }
public func rangeLine(_ range: Range<String.Index>) -> (content: String, number: Int, offset: String.IndexDistance) {
var lineNumber: Int = 0
var offset = 0
var lineContent = ""
for line in components(separatedBy: CharacterSet.newlines) {
lineNumber += 1
lineContent = line
if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) {
offset = distance(from: rangeOfLine.lowerBound, to:
range.lowerBound)
break
}
}
return (lineContent, lineNumber, offset)
}
} }

View File

@@ -3,18 +3,38 @@ import Foundation
public protocol NodeType { public protocol NodeType {
/// Render the node in the given context /// Render the node in the given context
func render(_ context:Context) throws -> String func render(_ context:Context) throws -> String
/// Reference to this node's token
var token: Token? { get }
} }
/// Render the collection of nodes in the given context /// Render the collection of nodes in the given context
public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String { public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String {
return try nodes.map { try $0.render(context) }.joined(separator: "") return try nodes.map({
do {
return try $0.render(context)
} catch {
if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil, let token = $0.token {
if let contentsRange = context.environment.template?.templateString.range(of: token.contents, range: token.range) {
syntaxError.lexeme = Token.block(value: token.contents, at: contentsRange)
} else {
syntaxError.lexeme = token
}
throw syntaxError
} else {
throw error
}
}
}).joined(separator: "")
} }
public class SimpleNode : NodeType { public class SimpleNode : NodeType {
public let handler:(Context) throws -> String public let handler:(Context) throws -> String
public let token: Token?
public init(handler: @escaping (Context) throws -> String) { public init(token: Token, handler: @escaping (Context) throws -> String) {
self.token = token
self.handler = handler self.handler = handler
} }
@@ -26,9 +46,11 @@ public class SimpleNode : NodeType {
public class TextNode : NodeType { public class TextNode : NodeType {
public let text:String public let text:String
public let token: Token?
public init(text:String) { public init(text:String) {
self.text = text self.text = text
self.token = nil
} }
public func render(_ context:Context) throws -> String { public func render(_ context:Context) throws -> String {
@@ -44,13 +66,16 @@ public protocol Resolvable {
public class VariableNode : NodeType { public class VariableNode : NodeType {
public let variable: Resolvable public let variable: Resolvable
public var token: Token?
public init(variable: Resolvable) { public init(variable: Resolvable, token: Token? = nil) {
self.variable = variable self.variable = variable
self.token = token
} }
public init(variable: String) { public init(variable: String, token: Token? = nil) {
self.variable = Variable(variable) self.variable = Variable(variable)
self.token = token
} }
public func render(_ context: Context) throws -> String { public func render(_ context: Context) throws -> String {

View File

@@ -4,6 +4,7 @@ import Foundation
class NowNode : NodeType { class NowNode : NodeType {
let format:Variable let format:Variable
let token: Token?
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
var format:Variable? var format:Variable?
@@ -16,11 +17,12 @@ class NowNode : NodeType {
format = Variable(components[1]) format = Variable(components[1])
} }
return NowNode(format:format) return NowNode(format:format, token: token)
} }
init(format:Variable?) { init(format:Variable?, token: Token? = nil) {
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"") self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
self.token = token
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {

View File

@@ -40,7 +40,8 @@ public class TokenParser {
case .text(let text, _): case .text(let text, _):
nodes.append(TextNode(text: text)) nodes.append(TextNode(text: text))
case .variable: case .variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents, containedIn: token))) let filter = try compileFilter(token.contents, containedIn: token)
nodes.append(VariableNode(variable: filter, token: token))
case .block: case .block:
if let parse_until = parse_until , parse_until(self, token) { if let parse_until = parse_until , parse_until(self, token) {
prependToken(token) prependToken(token)
@@ -54,8 +55,8 @@ public class TokenParser {
nodes.append(node) nodes.append(node)
} catch { } catch {
if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil {
syntaxError.lexeme = token syntaxError.lexeme = token
throw syntaxError throw syntaxError
} else { } else {
throw error throw error
} }
@@ -105,10 +106,12 @@ public class TokenParser {
do { do {
return try FilterExpression(token: filterToken, parser: self) return try FilterExpression(token: filterToken, parser: self)
} catch { } catch {
if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil, if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil {
let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) {
syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange)
syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange) } else {
syntaxError.lexeme = containingToken
}
throw syntaxError throw syntaxError
} else { } else {
throw error throw error

View File

@@ -38,7 +38,7 @@ func testEnvironment() {
var error = TemplateSyntaxError(description) var error = TemplateSyntaxError(description)
error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!)
let context = ErrorReporterContext(template: template) let context = ErrorReporterContext(template: template)
error = environment.errorReporter.contextAwareError(error, context: context) as! TemplateSyntaxError error = environment.errorReporter.contextAwareError(error, at: error.lexeme?.range, context: context) as! TemplateSyntaxError
return error return error
} }
@@ -122,6 +122,86 @@ func testEnvironment() {
let error = expectedFilterError(token: "name|unknown", template: template) let error = expectedFilterError(token: "name|unknown", template: template)
try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error)
} }
}
$0.context("given rendering error") {
$0.it("reports rendering error in variable filter") {
let template: Template = "{{ name|throw }}"
var environment = environment
let filterExtension = Extension()
filterExtension.registerFilter("throw") { (value: Any?) in
throw TemplateSyntaxError("Filter rendering error")
}
environment.extensions += [filterExtension]
let error = expectedSyntaxError(token: "name|throw", template: template, description: "Filter rendering error")
try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error)
}
$0.it("reports rendering error in filter tag") {
let template: Template = "{% filter throw %}Test{% endfilter %}"
var environment = environment
let filterExtension = Extension()
filterExtension.registerFilter("throw") { (value: Any?) in
throw TemplateSyntaxError("Filter rendering error")
}
environment.extensions += [filterExtension]
let error = expectedSyntaxError(token: "filter throw", template: template, description: "Filter rendering error")
try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error)
}
$0.it("reports rendering error in simple tag") {
let template: Template = "{% simpletag %}"
var environment = environment
let tagExtension = Extension()
tagExtension.registerSimpleTag("simpletag") { context in
throw TemplateSyntaxError("simpletag error")
}
environment.extensions += [tagExtension]
let error = expectedSyntaxError(token: "simpletag", template: template, description: "simpletag error")
try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error)
}
$0.it("reporsts passing argument to simple filter") {
let template: Template = "{{ name|uppercase:5 }}"
let error = expectedSyntaxError(token: "name|uppercase:5", template: template, description: "cannot invoke filter with an argument")
try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "kyle"])).toThrow(error)
}
$0.it("reports rendering error in custom tag") {
let template: Template = "{% customtag %}"
var environment = environment
let tagExtension = Extension()
tagExtension.registerTag("customtag") { parser, token in
return ErrorNode(token: token)
}
environment.extensions += [tagExtension]
let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error")
try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error)
}
$0.it("reports rendering error in for body") {
let template: Template = "{% for item in array %}{% customtag %}{% endfor %}"
var environment = environment
let tagExtension = Extension()
tagExtension.registerTag("customtag") { parser, token in
return ErrorNode(token: token)
}
environment.extensions += [tagExtension]
let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error")
try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(error)
}
} }
$0.context("given related templates") { $0.context("given related templates") {

View File

@@ -3,6 +3,11 @@ import Spectre
class ErrorNode : NodeType { class ErrorNode : NodeType {
let token: Token?
init(token: Token? = nil) {
self.token = token
}
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
throw TemplateSyntaxError("Custom Error") throw TemplateSyntaxError("Custom Error")
} }

View File

@@ -2,7 +2,8 @@ import Spectre
import Stencil import Stencil
fileprivate class CustomNode : NodeType { fileprivate struct CustomNode : NodeType {
let token: Token?
func render(_ context:Context) throws -> String { func render(_ context:Context) throws -> String {
return "Hello World" return "Hello World"
} }
@@ -24,7 +25,7 @@ func testStencil() {
} }
exampleExtension.registerTag("customtag") { parser, token in exampleExtension.registerTag("customtag") { parser, token in
return CustomNode() return CustomNode(token: token)
} }
let environment = Environment(extensions: [exampleExtension]) let environment = Environment(extensions: [exampleExtension])