From 079fdf39b8561fa9059c6bfb860adbbc1d845979 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 21:02:27 +0200 Subject: [PATCH] added parent context to ErrorReporterContext and handling errors in include and extend nodes --- Sources/Environment.swift | 16 +++++++++- Sources/Errors.swift | 11 +++++-- Sources/Include.swift | 12 +++++--- Sources/Inheritence.swift | 12 +++++--- Tests/StencilTests/EnvironmentSpec.swift | 29 +++++++++++++++++++ Tests/StencilTests/IncludeSpec.swift | 6 ++-- Tests/StencilTests/fixtures/invalid-base.html | 2 ++ .../fixtures/invalid-child-super.html | 3 ++ .../fixtures/invalid-include.html | 1 + 9 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 Tests/StencilTests/fixtures/invalid-base.html create mode 100644 Tests/StencilTests/fixtures/invalid-child-super.html create mode 100644 Tests/StencilTests/fixtures/invalid-include.html diff --git a/Sources/Environment.swift b/Sources/Environment.swift index c6fe7e2..611e14e 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -48,7 +48,6 @@ public struct Environment { return try template.render(context) } catch { try errorReporter.report(error: error) - return "" } } @@ -56,4 +55,19 @@ public struct Environment { return errorReporter.context?.template } + + public func pushTemplate(_ 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) + } + } + } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index e3c1441..8ab3c49 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -35,26 +35,31 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public class ErrorReporterContext { 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.parent = parent } } public protocol ErrorReporter: class { var context: ErrorReporterContext! { get set } - func report(error: Error) throws + func report(error: Error) throws -> Never func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? } open class SimpleErrorReporter: ErrorReporter { 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 context = context else { throw error } throw contextAwareError(syntaxError, 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 } let templateName = context.template.name.map({ "\($0):" }) ?? "" diff --git a/Sources/Include.swift b/Sources/Include.swift index cd9cc5c..2699312 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -3,6 +3,7 @@ import PathKit class IncludeNode : NodeType { let templateName: Variable + let token: Token class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() @@ -11,11 +12,12 @@ class IncludeNode : NodeType { 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.token = token } func render(_ context: Context) throws -> String { @@ -25,8 +27,10 @@ class IncludeNode : NodeType { let template = try context.environment.loadTemplate(name: templateName) - return try context.push { - return try template.render(context) + return try context.environment.pushTemplate(template, token: token) { + try context.push { + return try template.render(context) + } } } } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index b9bf87a..4097c97 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -51,6 +51,7 @@ extension Collection { class ExtendsNode : NodeType { let templateName: Variable let blocks: [String:BlockNode] + let token: Token class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() @@ -72,12 +73,13 @@ class ExtendsNode : NodeType { 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.blocks = blocks + self.token = token } func render(_ context: Context) throws -> String { @@ -98,8 +100,10 @@ class ExtendsNode : NodeType { blockContext = BlockContext(blocks: blocks) } - return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try template.render(context) + return try context.environment.pushTemplate(template, token: token) { + try context.push(dictionary: [BlockContext.contextKey: blockContext]) { + return try template.render(context) + } } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 070655c..cebb73c 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -1,4 +1,5 @@ import Spectre +import PathKit @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) + } + } + } } diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index 9f789b7..bf95c18 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -31,7 +31,7 @@ func testInclude() { $0.describe("rendering") { $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 { _ = try node.render(Context()) @@ -41,7 +41,7 @@ func testInclude() { } $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 { _ = try node.render(Context(environment: environment)) @@ -51,7 +51,7 @@ func testInclude() { } $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 value = try node.render(context) try expect(value) == "Hello World!" diff --git a/Tests/StencilTests/fixtures/invalid-base.html b/Tests/StencilTests/fixtures/invalid-base.html new file mode 100644 index 0000000..51c27a0 --- /dev/null +++ b/Tests/StencilTests/fixtures/invalid-base.html @@ -0,0 +1,2 @@ +{% block header %}Header{% endblock %} +{% block body %}Body {{ target|unknown }} {% endblock %} diff --git a/Tests/StencilTests/fixtures/invalid-child-super.html b/Tests/StencilTests/fixtures/invalid-child-super.html new file mode 100644 index 0000000..23aea65 --- /dev/null +++ b/Tests/StencilTests/fixtures/invalid-child-super.html @@ -0,0 +1,3 @@ +{% extends "invalid-base.html" %} +{% block body %}Child {{ block.super }}{% endblock %} + diff --git a/Tests/StencilTests/fixtures/invalid-include.html b/Tests/StencilTests/fixtures/invalid-include.html new file mode 100644 index 0000000..014ba0e --- /dev/null +++ b/Tests/StencilTests/fixtures/invalid-include.html @@ -0,0 +1 @@ +Hello {{ target|unknown }}!