added parent context to ErrorReporterContext and handling errors in include and extend nodes

This commit is contained in:
Ilya Puchka
2017-10-07 21:02:27 +02:00
parent e59609f140
commit 079fdf39b8
9 changed files with 77 additions and 15 deletions

View File

@@ -48,7 +48,6 @@ public struct Environment {
return try template.render(context) return try template.render(context)
} catch { } catch {
try errorReporter.report(error: error) try errorReporter.report(error: error)
return ""
} }
} }
@@ -56,4 +55,19 @@ public struct Environment {
return errorReporter.context?.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 {
try errorReporter.report(error: error)
}
}
} }

View File

@@ -35,26 +35,31 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
public class ErrorReporterContext { public class ErrorReporterContext {
public let template: Template public let template: Template
public init(template: Template) { public typealias ParentContext = (context: ErrorReporterContext, token: Token)
public let parent: ParentContext?
public init(template: Template, parent: ParentContext? = nil) {
self.template = template self.template = template
self.parent = parent
} }
} }
public protocol ErrorReporter: class { public protocol ErrorReporter: class {
var context: ErrorReporterContext! { get set } var context: ErrorReporterContext! { get set }
func report(error: Error) throws func report(error: Error) throws -> Never
func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error?
} }
open class SimpleErrorReporter: ErrorReporter { open class SimpleErrorReporter: ErrorReporter {
public var context: ErrorReporterContext! public var context: ErrorReporterContext!
open func report(error: Error) throws { open func report(error: Error) throws -> Never {
guard let syntaxError = error as? TemplateSyntaxError else { throw error } guard let syntaxError = error as? TemplateSyntaxError else { throw error }
guard let context = context else { throw error } guard let context = context else { throw error }
throw contextAwareError(syntaxError, context: context) ?? error throw contextAwareError(syntaxError, context: context) ?? error
} }
// TODO: add stack trace using parent context
open func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? { open func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? {
guard let lexeme = error.lexeme, lexeme.range != .unknown else { return nil } guard let lexeme = error.lexeme, lexeme.range != .unknown else { return nil }
let templateName = context.template.name.map({ "\($0):" }) ?? "" let templateName = context.template.name.map({ "\($0):" }) ?? ""

View File

@@ -3,6 +3,7 @@ import PathKit
class IncludeNode : NodeType { class IncludeNode : NodeType {
let templateName: Variable let templateName: Variable
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()
@@ -11,11 +12,12 @@ class IncludeNode : NodeType {
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included") throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
} }
return IncludeNode(templateName: Variable(bits[1])) return IncludeNode(templateName: Variable(bits[1]), token: token)
} }
init(templateName: Variable) { init(templateName: Variable, token: Token) {
self.templateName = templateName self.templateName = templateName
self.token = token
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
@@ -25,8 +27,10 @@ class IncludeNode : NodeType {
let template = try context.environment.loadTemplate(name: templateName) let template = try context.environment.loadTemplate(name: templateName)
return try context.push { return try context.environment.pushTemplate(template, token: token) {
return try template.render(context) try context.push {
return try template.render(context)
}
} }
} }
} }

View File

@@ -51,6 +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
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()
@@ -72,12 +73,13 @@ class ExtendsNode : NodeType {
return dict return dict
} }
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes) return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token)
} }
init(templateName: Variable, blocks: [String: BlockNode]) { init(templateName: Variable, blocks: [String: BlockNode], token: Token) {
self.templateName = templateName self.templateName = templateName
self.blocks = blocks self.blocks = blocks
self.token = token
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
@@ -98,8 +100,10 @@ class ExtendsNode : NodeType {
blockContext = BlockContext(blocks: blocks) blockContext = BlockContext(blocks: blocks)
} }
return 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)
}
} }
} }
} }

View File

@@ -1,4 +1,5 @@
import Spectre import Spectre
import PathKit
@testable import Stencil @testable import Stencil
@@ -124,6 +125,34 @@ func testEnvironment() {
} }
} }
$0.context("given related templates") {
let path = Path(#file) + ".." + "fixtures"
let loader = FileSystemLoader(paths: [path])
let environment = Environment(loader: loader)
$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)
}
$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)
}
}
} }
} }

View File

@@ -31,7 +31,7 @@ func testInclude() {
$0.describe("rendering") { $0.describe("rendering") {
$0.it("throws an error when rendering without a loader") { $0.it("throws an error when rendering without a loader") {
let node = IncludeNode(templateName: Variable("\"test.html\"")) let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
do { do {
_ = try node.render(Context()) _ = try node.render(Context())
@@ -41,7 +41,7 @@ func testInclude() {
} }
$0.it("throws an error when it cannot find the included template") { $0.it("throws an error when it cannot find the included template") {
let node = IncludeNode(templateName: Variable("\"unknown.html\"")) let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown))
do { do {
_ = try node.render(Context(environment: environment)) _ = try node.render(Context(environment: environment))
@@ -51,7 +51,7 @@ func testInclude() {
} }
$0.it("successfully renders a found included template") { $0.it("successfully renders a found included template") {
let node = IncludeNode(templateName: Variable("\"test.html\"")) let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
let context = Context(dictionary: ["target": "World"], environment: environment) let context = Context(dictionary: ["target": "World"], environment: environment)
let value = try node.render(context) let value = try node.render(context)
try expect(value) == "Hello World!" try expect(value) == "Hello World!"

View File

@@ -0,0 +1,2 @@
{% block header %}Header{% endblock %}
{% block body %}Body {{ target|unknown }} {% endblock %}

View File

@@ -0,0 +1,3 @@
{% extends "invalid-base.html" %}
{% block body %}Child {{ block.super }}{% endblock %}

View File

@@ -0,0 +1 @@
Hello {{ target|unknown }}!