reporting error with its parent context

This commit is contained in:
Ilya Puchka
2017-12-24 15:34:17 +01:00
parent 53c1550c5b
commit 9a28142fa6
6 changed files with 161 additions and 44 deletions

View File

@@ -3,6 +3,10 @@ public class Context {
var dictionaries: [[String: Any?]] var dictionaries: [[String: Any?]]
public let environment: Environment public let environment: Environment
public var errorReporter: ErrorReporter {
return environment.errorReporter
}
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) { init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
if let dictionary = dictionary { if let dictionary = dictionary {

View File

@@ -19,15 +19,56 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
} }
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
public let description:String public let reason: String
var lexeme: Lexeme? 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)
}
}
public init(_ description:String) { static func description(reason: String, lexeme: Lexeme?, template: Template?) -> String {
self.description = description 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.init(reason: description)
} }
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { 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 { 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: Error, at range: Range<String.Index>?, context: ErrorReporterContext) -> Error? func renderError(_ error: Error) -> String
} }
open class SimpleErrorReporter: ErrorReporter { open class SimpleErrorReporter: ErrorReporter {
@@ -55,21 +96,28 @@ open class SimpleErrorReporter: ErrorReporter {
open func reportError(_ error: Error) -> Error { open func reportError(_ error: Error) -> Error {
guard let context = context else { return 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 renderError(_ error: Error) -> String {
open func contextAwareError(_ error: Error, at range: Range<String.Index>?, context: ErrorReporterContext) -> Error? { guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
guard let range = range, range != .unknown else { return nil }
let templateName = context.template.name.map({ "\($0):" }) ?? "" var descriptions = [templateError.description]
let tokenContent = context.template.templateString.substring(with: range)
let line = context.template.templateString.rangeLine(range) var currentError: TemplateSyntaxError? = templateError
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))" while let parentError = currentError?.parentError {
let description = "\(templateName)\(line.number):\(line.offset): error: \(error)\n\(line.content)\n\(highlight)" descriptions.append(renderError(parentError))
let error = TemplateSyntaxError(description) currentError = parentError as? TemplateSyntaxError
return error }
return descriptions.reversed().joined(separator: "\n")
} }
} }
extension Range where Bound == String.Index { extension Range where Bound == String.Index {

View File

@@ -27,9 +27,17 @@ class IncludeNode : NodeType {
let template = try context.environment.loadTemplate(name: templateName) let template = try context.environment.loadTemplate(name: templateName)
return try context.environment.pushTemplate(template, token: token) { do {
try context.push { return try context.environment.pushTemplate(template, token: token) {
return try template.render(context) 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
} }
} }
} }

View File

@@ -100,9 +100,17 @@ class ExtendsNode : NodeType {
blockContext = BlockContext(blocks: blocks) blockContext = BlockContext(blocks: blocks)
} }
return try context.environment.pushTemplate(template, token: token) { do {
try context.push(dictionary: [BlockContext.contextKey: blockContext]) { return try context.environment.pushTemplate(template, token: token) {
return try template.render(context) 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 { func render(_ context: Context) throws -> String {
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) { 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, BlockContext.contextKey: blockContext,
"block": ["super": try self.render(context)] "block": ["super": try self.render(context)]
] ]
return try context.push(dictionary: newContext) { return try context.push(dictionary: newContext) {
return try node.render(context) return try node.render(context)
} }

View File

@@ -187,9 +187,9 @@ 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) { public func rangeLine(_ range: Range<String.Index>) -> (content: String, number: UInt, offset: String.IndexDistance) {
var lineNumber: Int = 0 var lineNumber: UInt = 0
var offset = 0 var offset: Int = 0
var lineContent = "" var lineContent = ""
for line in components(separatedBy: CharacterSet.newlines) { for line in components(separatedBy: CharacterSet.newlines) {

View File

@@ -35,11 +35,8 @@ func testEnvironment() {
} }
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
var error = TemplateSyntaxError(description) let lexeme = Token.block(value: token, at: template.templateString.range(of: token)!)
error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) return TemplateSyntaxError(reason: description, lexeme: lexeme, template: template, parentError: nil)
let context = ErrorReporterContext(template: template)
error = environment.errorReporter.contextAwareError(error, at: error.lexeme?.range, context: context) as! TemplateSyntaxError
return error
} }
$0.it("reports syntax error on invalid for tag syntax") { $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") let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error")
try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(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") { $0.context("given related templates") {
@@ -210,25 +221,61 @@ func testEnvironment() {
let environment = Environment(loader: loader) let environment = Environment(loader: loader)
$0.it("reports syntax error in included template") { $0.it("reports syntax error in included template") {
let template: Template = "{% include \"invalid-include.html\"%}" 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 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") { $0.it("reports syntax error in extended template") {
let template = try environment.loadTemplate(name: "invalid-child-super.html") 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 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)
} }
} }