From 53c1550c5bc7ef281293a4a3b236d79ce9c9fcc4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 23 Dec 2017 15:19:36 +0100 Subject: [PATCH] =?UTF-8?q?reporting=20node=20rendering=20errors=20using?= =?UTF-8?q?=20reference=20to=20node=E2=80=99s=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Environment.swift | 3 +- Sources/Errors.swift | 19 +++--- Sources/Extension.swift | 2 +- Sources/FilterTag.swift | 8 ++- Sources/ForTag.swift | 6 +- Sources/IfTag.swift | 20 +++--- Sources/Include.swift | 2 +- Sources/Inheritence.swift | 8 ++- Sources/Lexer.swift | 17 +++++ Sources/Node.swift | 33 ++++++++-- Sources/NowTag.swift | 6 +- Sources/Parser.swift | 17 +++-- Tests/StencilTests/EnvironmentSpec.swift | 82 +++++++++++++++++++++++- Tests/StencilTests/NodeSpec.swift | 5 ++ Tests/StencilTests/StencilSpec.swift | 5 +- 15 files changed, 186 insertions(+), 47 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index d612ae4..aafab8a 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -55,8 +55,7 @@ public struct Environment { return errorReporter.context?.template } - - public func pushTemplate(_ template: Template, token: Token, closure: (() throws -> Result)) rethrows -> Result { + public func pushTemplate(_ template: Template, token: Token?, closure: (() throws -> Result)) rethrows -> Result { let errorReporterContext = errorReporter.context defer { errorReporter.context = errorReporterContext } errorReporter.context = ErrorReporterContext( diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 97e93b7..32450cd 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -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?, 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?, 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 { diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 9dfa879..6e77aad 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -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) }) } diff --git a/Sources/FilterTag.swift b/Sources/FilterTag.swift index 5448996..4cf9746 100644 --- a/Sources/FilterTag.swift +++ b/Sources/FilterTag.swift @@ -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) } } } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index ae32479..8e3f5c9 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -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(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result { diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 706744a..39a3606 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -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 { diff --git a/Sources/Include.swift b/Sources/Include.swift index 2699312..bb85a2d 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -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() diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 4097c97..189e581 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -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 { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 8d350e7..f2af91e 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -187,4 +187,21 @@ extension String { return String(self[first..) -> (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) + } } diff --git a/Sources/Node.swift b/Sources/Node.swift index 1d2020d..10ee16a 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -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 { diff --git a/Sources/NowTag.swift b/Sources/NowTag.swift index 17a62a6..6d354ed 100644 --- a/Sources/NowTag.swift +++ b/Sources/NowTag.swift @@ -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 { diff --git a/Sources/Parser.swift b/Sources/Parser.swift index f59d205..a4d2d88 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -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 diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 3b33a15..62e99b5 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -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") { diff --git a/Tests/StencilTests/NodeSpec.swift b/Tests/StencilTests/NodeSpec.swift index 431d225..1adfa26 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -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") } diff --git a/Tests/StencilTests/StencilSpec.swift b/Tests/StencilTests/StencilSpec.swift index 427cd98..099a407 100644 --- a/Tests/StencilTests/StencilSpec.swift +++ b/Tests/StencilTests/StencilSpec.swift @@ -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])