reporting error with its parent context
This commit is contained in:
@@ -4,6 +4,10 @@ public class Context {
|
||||
|
||||
public let environment: Environment
|
||||
|
||||
public var errorReporter: ErrorReporter {
|
||||
return environment.errorReporter
|
||||
}
|
||||
|
||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||
if let dictionary = dictionary {
|
||||
dictionaries = [dictionary]
|
||||
|
||||
@@ -19,15 +19,56 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||
}
|
||||
|
||||
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public let description:String
|
||||
var lexeme: Lexeme?
|
||||
public let reason: String
|
||||
public private(set) var description: String
|
||||
|
||||
public internal(set) var template: Template?
|
||||
public internal(set) var parentError: Error?
|
||||
|
||||
var lexeme: Lexeme? {
|
||||
didSet {
|
||||
description = TemplateSyntaxError.description(reason: reason, lexeme: lexeme, template: template)
|
||||
}
|
||||
}
|
||||
|
||||
static func description(reason: String, lexeme: Lexeme?, template: Template?) -> String {
|
||||
if let template = template, let range = lexeme?.range {
|
||||
let templateName = template.name.map({ "\($0):" }) ?? ""
|
||||
let tokenContent = template.templateString.substring(with: range)
|
||||
let line = template.templateString.rangeLine(range)
|
||||
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))"
|
||||
|
||||
return "\(templateName)\(line.number):\(line.offset): error: \(reason)\n"
|
||||
+ "\(line.content)\n"
|
||||
+ "\(highlight)\n"
|
||||
} else {
|
||||
return reason
|
||||
}
|
||||
}
|
||||
|
||||
init(reason: String, lexeme: Lexeme? = nil, template: Template? = nil, parentError: Error? = nil) {
|
||||
self.reason = reason
|
||||
self.parentError = parentError
|
||||
self.template = template
|
||||
self.lexeme = lexeme
|
||||
self.description = TemplateSyntaxError.description(reason: reason, lexeme: lexeme, template: template)
|
||||
}
|
||||
|
||||
public init(_ description: String) {
|
||||
self.description = description
|
||||
self.init(reason: description)
|
||||
}
|
||||
|
||||
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,7 +88,7 @@ public class ErrorReporterContext {
|
||||
public protocol ErrorReporter: class {
|
||||
var context: ErrorReporterContext! { get set }
|
||||
func reportError(_ error: Error) -> Error
|
||||
func contextAwareError(_ error: Error, at range: Range<String.Index>?, context: ErrorReporterContext) -> Error?
|
||||
func renderError(_ error: Error) -> String
|
||||
}
|
||||
|
||||
open class SimpleErrorReporter: ErrorReporter {
|
||||
@@ -55,19 +96,26 @@ open class SimpleErrorReporter: ErrorReporter {
|
||||
|
||||
open func reportError(_ error: Error) -> Error {
|
||||
guard let context = context else { return error }
|
||||
return contextAwareError(error, at: (error as? TemplateSyntaxError)?.lexeme?.range, context: context) ?? error
|
||||
|
||||
return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)",
|
||||
lexeme: (error as? TemplateSyntaxError)?.lexeme,
|
||||
template: context.template,
|
||||
parentError: (error as? TemplateSyntaxError)?.parentError
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: add stack trace using parent context
|
||||
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: 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)\n\(line.content)\n\(highlight)"
|
||||
let error = TemplateSyntaxError(description)
|
||||
return error
|
||||
open func renderError(_ error: Error) -> String {
|
||||
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
||||
|
||||
var descriptions = [templateError.description]
|
||||
|
||||
var currentError: TemplateSyntaxError? = templateError
|
||||
while let parentError = currentError?.parentError {
|
||||
descriptions.append(renderError(parentError))
|
||||
currentError = parentError as? TemplateSyntaxError
|
||||
}
|
||||
|
||||
return descriptions.reversed().joined(separator: "\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,11 +27,19 @@ 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)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if let parentError = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,11 +100,19 @@ class ExtendsNode : NodeType {
|
||||
blockContext = BlockContext(blocks: blocks)
|
||||
}
|
||||
|
||||
do {
|
||||
return try context.environment.pushTemplate(template, token: token) {
|
||||
try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||
return try template.render(context)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if let parentError = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +143,12 @@ class BlockNode : NodeType {
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
|
||||
let newContext: [String: Any] = [
|
||||
let newContext: [String: Any]
|
||||
newContext = [
|
||||
BlockContext.contextKey: blockContext,
|
||||
"block": ["super": try self.render(context)]
|
||||
]
|
||||
|
||||
return try context.push(dictionary: newContext) {
|
||||
return try node.render(context)
|
||||
}
|
||||
|
||||
@@ -187,9 +187,9 @@ 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
|
||||
public func rangeLine(_ range: Range<String.Index>) -> (content: String, number: UInt, offset: String.IndexDistance) {
|
||||
var lineNumber: UInt = 0
|
||||
var offset: Int = 0
|
||||
var lineContent = ""
|
||||
|
||||
for line in components(separatedBy: CharacterSet.newlines) {
|
||||
|
||||
@@ -35,11 +35,8 @@ func testEnvironment() {
|
||||
}
|
||||
|
||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||
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, at: error.lexeme?.range, context: context) as! TemplateSyntaxError
|
||||
return error
|
||||
let lexeme = Token.block(value: token, at: template.templateString.range(of: token)!)
|
||||
return TemplateSyntaxError(reason: description, lexeme: lexeme, template: template, parentError: nil)
|
||||
}
|
||||
|
||||
$0.it("reports syntax error on invalid for tag syntax") {
|
||||
@@ -202,6 +199,20 @@ func testEnvironment() {
|
||||
let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error")
|
||||
try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in block") {
|
||||
let template: Template = "{% block some %}{% customtag %}{% endblock %}"
|
||||
|
||||
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") {
|
||||
@@ -211,24 +222,60 @@ func testEnvironment() {
|
||||
|
||||
$0.it("reports syntax error in included template") {
|
||||
let template: Template = "{% include \"invalid-include.html\" %}"
|
||||
environment.errorReporter.context = ErrorReporterContext(template: template)
|
||||
|
||||
let context = Context(dictionary: ["target": "World"], environment: environment)
|
||||
|
||||
let includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||
let error = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "Unknown filter 'unknown'")
|
||||
|
||||
try expect(try template.render(context)).toThrow(error)
|
||||
let parentError = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "Unknown filter 'unknown'")
|
||||
var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "Unknown filter 'unknown'")
|
||||
error.parentError = parentError
|
||||
|
||||
try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in extended template") {
|
||||
let template = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||
let context = Context(dictionary: ["target": "World"], environment: environment)
|
||||
|
||||
let baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||
let error = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'")
|
||||
|
||||
try expect(try template.render(context)).toThrow(error)
|
||||
let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'")
|
||||
var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "Unknown filter 'unknown'")
|
||||
error.parentError = parentError
|
||||
|
||||
try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in included template") {
|
||||
var environment = environment
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
let template: Template = "{% include \"invalid-include.html\" %}"
|
||||
let includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||
|
||||
let parentError = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "filter error")
|
||||
var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "filter error")
|
||||
error.parentError = parentError
|
||||
|
||||
try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in extended template") {
|
||||
var environment = environment
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("throw", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
let template = try environment.loadTemplate(name: "invalid-runtime-child-super.html")
|
||||
let baseTemplate = try environment.loadTemplate(name: "invalid-runtime-base.html")
|
||||
|
||||
let parentError = expectedSyntaxError(token: "target|throw", template: baseTemplate, description: "filter error")
|
||||
var error = expectedSyntaxError(token: "extends \"invalid-runtime-base.html\"", template: template, description: "filter error")
|
||||
error.parentError = parentError
|
||||
|
||||
try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user