reporting node rendering errors using reference to node’s token
This commit is contained in:
@@ -55,8 +55,7 @@ public struct Environment {
|
||||
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
|
||||
defer { errorReporter.context = errorReporterContext }
|
||||
errorReporter.context = ErrorReporterContext(
|
||||
|
||||
@@ -35,7 +35,7 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public class ErrorReporterContext {
|
||||
public let template: Template
|
||||
|
||||
public typealias ParentContext = (context: ErrorReporterContext, token: Token)
|
||||
public typealias ParentContext = (context: ErrorReporterContext, token: Token?)
|
||||
public let parent: ParentContext?
|
||||
|
||||
public init(template: Template, parent: ParentContext? = nil) {
|
||||
@@ -47,30 +47,29 @@ public class ErrorReporterContext {
|
||||
public protocol ErrorReporter: class {
|
||||
var context: ErrorReporterContext! { get set }
|
||||
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 {
|
||||
public var context: ErrorReporterContext!
|
||||
|
||||
open func reportError(_ error: Error) -> Error {
|
||||
guard let syntaxError = error as? TemplateSyntaxError 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
|
||||
open func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? {
|
||||
guard let lexeme = error.lexeme, lexeme.range != .unknown else { return nil }
|
||||
open func contextAwareError(_ error: Error, at range: Range<String.Index>?, context: ErrorReporterContext) -> Error? {
|
||||
guard let range = range, range != .unknown else { return nil }
|
||||
let templateName = context.template.name.map({ "\($0):" }) ?? ""
|
||||
let tokenContent = context.template.templateString.substring(with: lexeme.range)
|
||||
let lexer = Lexer(templateString: context.template.templateString)
|
||||
let line = lexer.lexemeLine(lexeme)
|
||||
let tokenContent = context.template.templateString.substring(with: range)
|
||||
let line = context.template.templateString.rangeLine(range)
|
||||
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)
|
||||
return error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Range where Bound == String.Index {
|
||||
|
||||
@@ -15,7 +15,7 @@ open class Extension {
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
||||
registerTag(name, parser: { parser, token in
|
||||
return SimpleNode(handler: handler)
|
||||
return SimpleNode(token: token, handler: handler)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class FilterNode : NodeType {
|
||||
let resolvable: Resolvable
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
@@ -16,19 +17,20 @@ class FilterNode : NodeType {
|
||||
}
|
||||
|
||||
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.resolvable = resolvable
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let value = try renderNodes(nodes, context)
|
||||
|
||||
return try context.push(dictionary: ["filter_value": value]) {
|
||||
return try VariableNode(variable: resolvable).render(context)
|
||||
return try VariableNode(variable: resolvable, token: token).render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ class ForNode : NodeType {
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
let `where`: Expression?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
@@ -42,15 +43,16 @@ class ForNode : NodeType {
|
||||
} else {
|
||||
`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.loopVariables = loopVariables
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
self.where = `where`
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result {
|
||||
|
||||
@@ -182,6 +182,7 @@ final class IfCondition {
|
||||
|
||||
class IfNode : NodeType {
|
||||
let conditions: [IfCondition]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
@@ -193,27 +194,27 @@ class IfNode : NodeType {
|
||||
IfCondition(expression: expression, nodes: nodes)
|
||||
]
|
||||
|
||||
var token = parser.nextToken()
|
||||
while let current = token, current.contents.hasPrefix("elif") {
|
||||
var nextToken = parser.nextToken()
|
||||
while let current = nextToken, current.contents.hasPrefix("elif") {
|
||||
var components = current.components()
|
||||
components.removeFirst()
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: current)
|
||||
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
token = parser.nextToken()
|
||||
nextToken = parser.nextToken()
|
||||
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"]))))
|
||||
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.")
|
||||
}
|
||||
|
||||
return IfNode(conditions: conditions)
|
||||
return IfNode(conditions: conditions, token: token)
|
||||
}
|
||||
|
||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
@@ -240,11 +241,12 @@ class IfNode : NodeType {
|
||||
return IfNode(conditions: [
|
||||
IfCondition(expression: expression, nodes: trueNodes),
|
||||
IfCondition(expression: nil, nodes: falseNodes),
|
||||
])
|
||||
], token: token)
|
||||
}
|
||||
|
||||
init(conditions: [IfCondition]) {
|
||||
init(conditions: [IfCondition], token: Token? = nil) {
|
||||
self.conditions = conditions
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
|
||||
@@ -3,7 +3,7 @@ import PathKit
|
||||
|
||||
class IncludeNode : NodeType {
|
||||
let templateName: Variable
|
||||
let token: Token
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
@@ -51,7 +51,7 @@ extension Collection {
|
||||
class ExtendsNode : NodeType {
|
||||
let templateName: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
let token: Token
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
@@ -112,6 +112,7 @@ class ExtendsNode : NodeType {
|
||||
class BlockNode : NodeType {
|
||||
let name: String
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
@@ -123,12 +124,13 @@ class BlockNode : NodeType {
|
||||
let blockName = bits[1]
|
||||
let nodes = try parser.parse(until(["endblock"]))
|
||||
_ = 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.nodes = nodes
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
|
||||
@@ -187,4 +187,21 @@ extension String {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,38 @@ import Foundation
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
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
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -26,9 +46,11 @@ public class SimpleNode : NodeType {
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
public let token: Token?
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
self.token = nil
|
||||
}
|
||||
|
||||
public func render(_ context:Context) throws -> String {
|
||||
@@ -44,13 +66,16 @@ public protocol Resolvable {
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
public let variable: Resolvable
|
||||
public var token: Token?
|
||||
|
||||
public init(variable: Resolvable) {
|
||||
public init(variable: Resolvable, token: Token? = nil) {
|
||||
self.variable = variable
|
||||
self.token = token
|
||||
}
|
||||
|
||||
public init(variable: String) {
|
||||
public init(variable: String, token: Token? = nil) {
|
||||
self.variable = Variable(variable)
|
||||
self.token = token
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Foundation
|
||||
|
||||
class NowNode : NodeType {
|
||||
let format:Variable
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
@@ -16,11 +17,12 @@ class NowNode : NodeType {
|
||||
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.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
|
||||
@@ -40,7 +40,8 @@ public class TokenParser {
|
||||
case .text(let text, _):
|
||||
nodes.append(TextNode(text: text))
|
||||
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:
|
||||
if let parse_until = parse_until , parse_until(self, token) {
|
||||
prependToken(token)
|
||||
@@ -54,8 +55,8 @@ public class TokenParser {
|
||||
nodes.append(node)
|
||||
} catch {
|
||||
if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil {
|
||||
syntaxError.lexeme = token
|
||||
throw syntaxError
|
||||
syntaxError.lexeme = token
|
||||
throw syntaxError
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
@@ -105,10 +106,12 @@ public class TokenParser {
|
||||
do {
|
||||
return try FilterExpression(token: filterToken, parser: self)
|
||||
} catch {
|
||||
if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil,
|
||||
let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) {
|
||||
|
||||
syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange)
|
||||
if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil {
|
||||
if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) {
|
||||
syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange)
|
||||
} else {
|
||||
syntaxError.lexeme = containingToken
|
||||
}
|
||||
throw syntaxError
|
||||
} else {
|
||||
throw error
|
||||
|
||||
@@ -38,7 +38,7 @@ func testEnvironment() {
|
||||
var error = TemplateSyntaxError(description)
|
||||
error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -122,6 +122,86 @@ func testEnvironment() {
|
||||
let error = expectedFilterError(token: "name|unknown", template: template)
|
||||
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") {
|
||||
|
||||
@@ -3,6 +3,11 @@ import Spectre
|
||||
|
||||
|
||||
class ErrorNode : NodeType {
|
||||
let token: Token?
|
||||
init(token: Token? = nil) {
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
throw TemplateSyntaxError("Custom Error")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
fileprivate class CustomNode : NodeType {
|
||||
fileprivate struct CustomNode : NodeType {
|
||||
let token: Token?
|
||||
func render(_ context:Context) throws -> String {
|
||||
return "Hello World"
|
||||
}
|
||||
@@ -24,7 +25,7 @@ func testStencil() {
|
||||
}
|
||||
|
||||
exampleExtension.registerTag("customtag") { parser, token in
|
||||
return CustomNode()
|
||||
return CustomNode(token: token)
|
||||
}
|
||||
|
||||
let environment = Environment(extensions: [exampleExtension])
|
||||
|
||||
Reference in New Issue
Block a user