From 6300dbc7bf809908970d5e3e02d6b4fa3622f208 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 3 Oct 2017 22:47:28 +0200 Subject: [PATCH 01/32] improved template syntax errors with file, line number and failed token highlighted in error message --- Sources/ForTag.swift | 2 +- Sources/IfTag.swift | 2 +- Sources/Lexer.swift | 56 ++++++++++++---- Sources/Node.swift | 12 ++++ Sources/NowTag.swift | 2 +- Sources/Parser.swift | 16 ++++- Sources/Template.swift | 15 ++++- Sources/Tokenizer.swift | 52 +++++++++------ Tests/StencilTests/FilterTagSpec.swift | 2 +- Tests/StencilTests/ForNodeSpec.swift | 81 +---------------------- Tests/StencilTests/IfNodeSpec.swift | 90 +++++++++++++------------- Tests/StencilTests/IncludeSpec.swift | 4 +- Tests/StencilTests/LexerSpec.swift | 27 ++++---- Tests/StencilTests/NowNodeSpec.swift | 4 +- Tests/StencilTests/ParserSpec.swift | 10 +-- Tests/StencilTests/TemplateSpec.swift | 27 +++++++- Tests/StencilTests/TokenSpec.swift | 8 +-- 17 files changed, 220 insertions(+), 190 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 25005e6..032653f 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -12,7 +12,7 @@ class ForNode : NodeType { guard components.count >= 3 && components[2] == "in" && (components.count == 4 || (components.count >= 6 && components[4] == "where")) else { - throw TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `\(token.contents)`.") + throw TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") } let loopVariables = components[1].characters diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 8f3b0fd..e4305a9 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -219,7 +219,7 @@ class IfNode : NodeType { class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType { var components = token.components() guard components.count == 2 else { - throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.") + throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.") } components.removeFirst() var trueNodes = [NodeType]() diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 5bd590d..58ce89b 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -5,7 +5,7 @@ struct Lexer { self.templateString = templateString } - func createToken(string: String) -> Token { + func createToken(string: String, at range: Range) -> Token { func strip() -> String { guard string.characters.count > 4 else { return "" } let start = string.index(string.startIndex, offsetBy: 2) @@ -14,14 +14,14 @@ struct Lexer { } if string.hasPrefix("{{") { - return .variable(value: strip()) + return .variable(value: strip(), at: range) } else if string.hasPrefix("{%") { - return .block(value: strip()) + return .block(value: strip(), at: range) } else if string.hasPrefix("{#") { - return .comment(value: strip()) + return .comment(value: strip(), at: range) } - return .text(value: string) + return .text(value: string, at: range) } /// Returns an array of tokens from a given template string. @@ -39,14 +39,14 @@ struct Lexer { while !scanner.isEmpty { if let text = scanner.scan(until: ["{{", "{%", "{#"]) { if !text.1.isEmpty { - tokens.append(createToken(string: text.1)) + tokens.append(createToken(string: text.1, at: scanner.range)) } let end = map[text.0]! let result = scanner.scan(until: end, returnUntil: true) - tokens.append(createToken(string: result)) + tokens.append(createToken(string: result, at: scanner.range)) } else { - tokens.append(createToken(string: scanner.content)) + tokens.append(createToken(string: scanner.content, at: scanner.range)) scanner.content = "" } } @@ -57,41 +57,50 @@ struct Lexer { class Scanner { + let _content: String //stores original content var content: String - + var range: Range + init(_ content: String) { + self._content = content self.content = content + range = content.startIndex.. String { + var index = content.startIndex + if until.isEmpty { return "" } - var index = content.startIndex + range = range.upperBound..) -> (content: String, number: Int, offset: String.IndexDistance) { + var lineNumber: Int = 0 + var offset = 0 + var lineContent = "" + + enumerateLines { (line, stop) in + lineNumber += 1 + lineContent = line + if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) { + offset = self.distance(from: rangeOfLine.lowerBound, to: range.lowerBound) + stop = true + } + } + return (lineContent, lineNumber, offset) + } + } diff --git a/Sources/Node.swift b/Sources/Node.swift index 5b47177..1c2eed4 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -3,10 +3,22 @@ import Foundation public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public let description:String + public var token: Token? public init(_ description:String) { self.description = description } + + public func contextAwareError(templateName: String?, templateContent: String) -> TemplateSyntaxError? { + guard let token = token, token.range != .unknown else { return nil } + let templateName = templateName.map({ "\($0):" }) ?? "" + let (line, lineNumber, offset) = templateContent.lineAndPosition(at: token.range) + let tokenContent = templateContent.substring(with: token.range) + let highlight = "\(String(Array(repeating: " ", count: offset)))^\(String(Array(repeating: "~", count: max(tokenContent.count - 1, 0))))" + let description = "\(templateName)\(lineNumber):\(offset): error: " + self.description + "\n\(line)\n\(highlight)\n" + return TemplateSyntaxError(description) + } + } diff --git a/Sources/NowTag.swift b/Sources/NowTag.swift index cd6e4ea..17a62a6 100644 --- a/Sources/NowTag.swift +++ b/Sources/NowTag.swift @@ -10,7 +10,7 @@ class NowNode : NodeType { let components = token.components() guard components.count <= 2 else { - throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.") + throw TemplateSyntaxError("'now' tags may only have one argument: the format string.") } if components.count == 2 { format = Variable(components[1]) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 1a59edb..5c840bf 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -37,7 +37,7 @@ public class TokenParser { let token = nextToken()! switch token { - case .text(let text): + case .text(let text, _): nodes.append(TextNode(text: text)) case .variable: nodes.append(VariableNode(variable: try compileFilter(token.contents))) @@ -48,8 +48,18 @@ public class TokenParser { } if let tag = token.components().first { - let parser = try findTag(name: tag) - nodes.append(try parser(self, token)) + do { + let parser = try findTag(name: tag) + let node = try parser(self, token) + nodes.append(node) + } catch { + if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { + syntaxError.token = token + throw syntaxError + } else { + throw error + } + } } case .comment: continue diff --git a/Sources/Template.swift b/Sources/Template.swift index d9b1893..7a80ee8 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -7,6 +7,7 @@ let NSFileNoSuchFileError = 4 /// A class representing a template open class Template: ExpressibleByStringLiteral { + let templateString: String let environment: Environment let tokens: [Token] @@ -17,6 +18,7 @@ open class Template: ExpressibleByStringLiteral { public required init(templateString: String, environment: Environment? = nil, name: String? = nil) { self.environment = environment ?? Environment() self.name = name + self.templateString = templateString let lexer = Lexer(templateString: templateString) tokens = lexer.tokenize() @@ -66,8 +68,17 @@ open class Template: ExpressibleByStringLiteral { func render(_ context: Context) throws -> String { let context = context let parser = TokenParser(tokens: tokens, environment: context.environment) - let nodes = try parser.parse() - return try renderNodes(nodes, context) + do { + let nodes = try parser.parse() + return try renderNodes(nodes, context) + } catch { + if let syntaxError = error as? TemplateSyntaxError, + let error = syntaxError.contextAwareError(templateName: name, templateContent: templateString) { + throw error + } else { + throw error + } + } } /// Render the given template diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 0e4bf1e..cae09cc 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -40,46 +40,62 @@ extension String { } } +extension Range where Bound == String.Index { + internal static var unknown: Range { + return "".range + } +} + +extension String { + var range: Range { + return startIndex..) /// A token representing a variable. - case variable(value: String) + case variable(value: String, at: Range) /// A token representing a comment. - case comment(value: String) + case comment(value: String, at: Range) /// A token representing a template block. - case block(value: String) + case block(value: String, at: Range) /// Returns the underlying value as an array seperated by spaces public func components() -> [String] { switch self { - case .block(let value): - return value.smartSplit() - case .variable(let value): - return value.smartSplit() - case .text(let value): - return value.smartSplit() - case .comment(let value): + case .block(let value, _), + .variable(let value, _), + .text(let value, _), + .comment(let value, _): return value.smartSplit() } } public var contents: String { switch self { - case .block(let value): - return value - case .variable(let value): - return value - case .text(let value): - return value - case .comment(let value): + case .block(let value, _), + .variable(let value, _), + .text(let value, _), + .comment(let value, _): return value } } + + public var range: Range { + switch self { + case .block(_, let range), + .variable(_, let range), + .text(_, let range), + .comment(_, let range): + return range + } + } + } diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index 2470450..a6e4513 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -17,7 +17,7 @@ func testFilterTag() { } $0.it("errors without a filter") { - let template = Template(templateString: "{% filter %}Test{% endfilter %}") + let template = Template(templateString: "Some {% filter %}Test{% endfilter %}") try expect(try template.render()).toThrow() } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 9cc98cb..6cf69b3 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -170,88 +170,13 @@ func testForNode() { $0.it("handles invalid input") { let tokens: [Token] = [ - .block(value: "for i"), + .block(value: "for i", at: .unknown), ] let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `for i`.") + let error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") try expect(try parser.parse()).toThrow(error) } - - $0.it("can iterate over struct properties") { - struct MyStruct { - let string: String - let number: Int - } - - let context = Context(dictionary: [ - "struct": MyStruct(string: "abc", number: 123) - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "property"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == "string=abc\nnumber=123\n" - } - - $0.it("can iterate tuple items") { - let context = Context(dictionary: [ - "tuple": (one: 1, two: "dva"), - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - - let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == "one=1\ntwo=dva\n" - } - - $0.it("can iterate over class properties") { - class MyClass { - var baseString: String - var baseInt: Int - init(_ string: String, _ int: Int) { - baseString = string - baseInt = int - } - } - - class MySubclass: MyClass { - var childString: String - init(_ childString: String, _ string: String, _ int: Int) { - self.childString = childString - super.init(string, int) - } - } - - let context = Context(dictionary: [ - "class": MySubclass("child", "base", 1) - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - - let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n" - } - + } } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index c77122f..25119eb 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -7,9 +7,9 @@ func testIfNode() { $0.describe("parsing") { $0.it("can parse an if block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -25,11 +25,11 @@ func testIfNode() { $0.it("can parse an if with else block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "else"), - .text(value: "false"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -50,13 +50,13 @@ func testIfNode() { $0.it("can parse an if with elif block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "elif something"), - .text(value: "some"), - .block(value: "else"), - .text(value: "false"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something", at: .unknown), + .text(value: "some", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -81,11 +81,11 @@ func testIfNode() { $0.it("can parse an if with elif block without else") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "elif something"), - .text(value: "some"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something", at: .unknown), + .text(value: "some", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -106,15 +106,15 @@ func testIfNode() { $0.it("can parse an if with multiple elif block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "elif something1"), - .text(value: "some1"), - .block(value: "elif something2"), - .text(value: "some2"), - .block(value: "else"), - .text(value: "false"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something1", at: .unknown), + .text(value: "some1", at: .unknown), + .block(value: "elif something2", at: .unknown), + .text(value: "some2", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -144,9 +144,9 @@ func testIfNode() { $0.it("can parse an if with complex expression") { let tokens: [Token] = [ - .block(value: "if value == \"test\" and not name"), - .text(value: "true"), - .block(value: "endif") + .block(value: "if value == \"test\" and not name", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -156,11 +156,11 @@ func testIfNode() { $0.it("can parse an ifnot block") { let tokens: [Token] = [ - .block(value: "ifnot value"), - .text(value: "false"), - .block(value: "else"), - .text(value: "true"), - .block(value: "endif") + .block(value: "ifnot value", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -180,7 +180,7 @@ func testIfNode() { $0.it("throws an error when parsing an if block without an endif") { let tokens: [Token] = [ - .block(value: "if value"), + .block(value: "if value", at: .unknown), ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -190,7 +190,7 @@ func testIfNode() { $0.it("throws an error when parsing an ifnot without an endif") { let tokens: [Token] = [ - .block(value: "ifnot value"), + .block(value: "ifnot value", at: .unknown), ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -242,9 +242,9 @@ func testIfNode() { $0.it("supports variable filters in the if expression") { let tokens: [Token] = [ - .block(value: "if value|uppercase == \"TEST\""), - .text(value: "true"), - .block(value: "endif") + .block(value: "if value|uppercase == \"TEST\"", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -256,9 +256,9 @@ func testIfNode() { $0.it("evaluates nil properties as false") { let tokens: [Token] = [ - .block(value: "if instance.value"), - .text(value: "true"), - .block(value: "endif") + .block(value: "if instance.value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index 0297896..9f789b7 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -11,7 +11,7 @@ func testInclude() { $0.describe("parsing") { $0.it("throws an error when no template is given") { - let tokens: [Token] = [ .block(value: "include") ] + let tokens: [Token] = [ .block(value: "include", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included") @@ -19,7 +19,7 @@ func testInclude() { } $0.it("can parse a valid include block") { - let tokens: [Token] = [ .block(value: "include \"test.html\"") ] + let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let nodes = try parser.parse() diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index ac9eb50..b74df68 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -9,7 +9,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "Hello World") + try expect(tokens.first) == .text(value: "Hello World", at: "Hello World".range) } $0.it("can tokenize a comment") { @@ -17,7 +17,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .comment(value: "Comment") + try expect(tokens.first) == .comment(value: "Comment", at: "{# Comment #}".range) } $0.it("can tokenize a variable") { @@ -25,34 +25,37 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .variable(value: "Variable") + try expect(tokens.first) == .variable(value: "Variable", at: "{{ Variable }}".range) } $0.it("can tokenize unclosed tag by ignoring it") { - let lexer = Lexer(templateString: "{{ thing") + let templateString = "{{ thing" + let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "") + try expect(tokens.first) == .text(value: "", at: "".range) } $0.it("can tokenize a mixture of content") { - let lexer = Lexer(templateString: "My name is {{ name }}.") + let templateString = "My name is {{ name }}." + let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() try expect(tokens.count) == 3 - try expect(tokens[0]) == Token.text(value: "My name is ") - try expect(tokens[1]) == Token.variable(value: "name") - try expect(tokens[2]) == Token.text(value: ".") + try expect(tokens[0]) == Token.text(value: "My name is ", at: templateString.range(of: "My name is ")!) + try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!) + try expect(tokens[2]) == Token.text(value: ".", at: templateString.range(of: ".")!) } $0.it("can tokenize two variables without being greedy") { - let lexer = Lexer(templateString: "{{ thing }}{{ name }}") + let templateString = "{{ thing }}{{ name }}" + let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() try expect(tokens.count) == 2 - try expect(tokens[0]) == Token.variable(value: "thing") - try expect(tokens[1]) == Token.variable(value: "name") + try expect(tokens[0]) == Token.variable(value: "thing", at: templateString.range(of: "{{ thing }}")!) + try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!) } $0.it("can tokenize an unclosed block") { diff --git a/Tests/StencilTests/NowNodeSpec.swift b/Tests/StencilTests/NowNodeSpec.swift index 4adba36..33a0d3f 100644 --- a/Tests/StencilTests/NowNodeSpec.swift +++ b/Tests/StencilTests/NowNodeSpec.swift @@ -8,7 +8,7 @@ func testNowNode() { describe("NowNode") { $0.describe("parsing") { $0.it("parses default format without any now arguments") { - let tokens: [Token] = [ .block(value: "now") ] + let tokens: [Token] = [ .block(value: "now", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let nodes = try parser.parse() @@ -18,7 +18,7 @@ func testNowNode() { } $0.it("parses now with a format") { - let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ] + let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let nodes = try parser.parse() let node = nodes.first as? NowNode diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index b5c9bb2..facd07a 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -6,7 +6,7 @@ func testTokenParser() { describe("TokenParser") { $0.it("can parse a text token") { let parser = TokenParser(tokens: [ - .text(value: "Hello World") + .text(value: "Hello World", at: .unknown) ], environment: Environment()) let nodes = try parser.parse() @@ -18,7 +18,7 @@ func testTokenParser() { $0.it("can parse a variable token") { let parser = TokenParser(tokens: [ - .variable(value: "'name'") + .variable(value: "'name'", at: .unknown) ], environment: Environment()) let nodes = try parser.parse() @@ -30,7 +30,7 @@ func testTokenParser() { $0.it("can parse a comment token") { let parser = TokenParser(tokens: [ - .comment(value: "Secret stuff!") + .comment(value: "Secret stuff!", at: .unknown) ], environment: Environment()) let nodes = try parser.parse() @@ -44,7 +44,7 @@ func testTokenParser() { } let parser = TokenParser(tokens: [ - .block(value: "known"), + .block(value: "known", at: .unknown), ], environment: Environment(extensions: [simpleExtension])) let nodes = try parser.parse() @@ -53,7 +53,7 @@ func testTokenParser() { $0.it("errors when parsing an unknown tag") { let parser = TokenParser(tokens: [ - .block(value: "unknown"), + .block(value: "unknown", at: .unknown), ], environment: Environment()) try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'")) diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index ad03851..42dac28 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -1,5 +1,5 @@ import Spectre -import Stencil +@testable import Stencil func testTemplate() { @@ -15,5 +15,30 @@ func testTemplate() { let result = try template.render([ "name": "Kyle" ]) try expect(result) == "Hello World" } + + $0.it("throws syntax error on invalid for tag syntax") { + let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + var error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") + error.token = Token.block(value: "{% for name in %}", at: template.templateString.range(of: "{% for name in %}")!) + error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! + try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on missing endfor") { + let template: Template = "{% for name in names %}{{ name }}" + var error = TemplateSyntaxError("`endfor` was not found.") + error.token = Token.block(value: "{% for name in names %}", at: template.templateString.range(of: "{% for name in names %}")!) + error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! + try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on unknown tag") { + let template: Template = "{% for name in names %}{{ name }}{% end %}" + var error = TemplateSyntaxError("Unknown template tag 'end'") + error.token = Token.block(value: "{% end %}", at: template.templateString.range(of: "{% end %}")!) + error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! + try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) + } + } } diff --git a/Tests/StencilTests/TokenSpec.swift b/Tests/StencilTests/TokenSpec.swift index c7f9db1..dd73298 100644 --- a/Tests/StencilTests/TokenSpec.swift +++ b/Tests/StencilTests/TokenSpec.swift @@ -1,11 +1,11 @@ import Spectre -import Stencil +@testable import Stencil func testToken() { describe("Token") { $0.it("can split the contents into components") { - let token = Token.text(value: "hello world") + let token = Token.text(value: "hello world", at: .unknown) let components = token.components() try expect(components.count) == 2 @@ -14,7 +14,7 @@ func testToken() { } $0.it("can split the contents into components with single quoted strings") { - let token = Token.text(value: "hello 'kyle fuller'") + let token = Token.text(value: "hello 'kyle fuller'", at: .unknown) let components = token.components() try expect(components.count) == 2 @@ -23,7 +23,7 @@ func testToken() { } $0.it("can split the contents into components with double quoted strings") { - let token = Token.text(value: "hello \"kyle fuller\"") + let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown) let components = token.components() try expect(components.count) == 2 From 69cd8e4d3bf4682cb9a022adbb7b3322a1dea657 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 20:54:42 +0200 Subject: [PATCH 02/32] replaced Token with Lexeme protocol on TemplateSyntaxError --- Sources/Errors.swift | 26 ++++++++++++++++++++++++++ Sources/Lexer.swift | 37 +++++++++++++++++++++---------------- Sources/Node.swift | 27 --------------------------- Sources/Parser.swift | 4 ++-- Sources/Template.swift | 13 ++----------- Sources/Tokenizer.swift | 14 +------------- 6 files changed, 52 insertions(+), 69 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 964eeae..1030d6d 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -17,3 +17,29 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { return "Template named `\(templates)` does not exist. No loaders found" } } + +public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { + public let description:String + var lexeme: Lexeme? + + public init(_ description:String) { + self.description = description + } + + public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { + return lhs.description == rhs.description + } + +} + +extension Range where Bound == String.Index { + internal static var unknown: Range { + return "".range + } +} + +extension String { + var range: Range { + return startIndex.. (content: String, number: Int, offset: String.IndexDistance) { + var lineNumber: Int = 0 + var offset = 0 + var lineContent = "" + + templateString.enumerateLines { (line, stop) in + lineNumber += 1 + lineContent = line + if let rangeOfLine = self.templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { + offset = self.templateString.distance(from: rangeOfLine.lowerBound, to: + lexeme.range.lowerBound) + stop = true + } + } + return (lineContent, lineNumber, offset) + } + } +protocol Lexeme { + var range: Range { get } +} class Scanner { let _content: String //stores original content @@ -163,20 +184,4 @@ extension String { return String(self[first..) -> (content: String, number: Int, offset: String.IndexDistance) { - var lineNumber: Int = 0 - var offset = 0 - var lineContent = "" - - enumerateLines { (line, stop) in - lineNumber += 1 - lineContent = line - if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) { - offset = self.distance(from: rangeOfLine.lowerBound, to: range.lowerBound) - stop = true - } - } - return (lineContent, lineNumber, offset) - } - } diff --git a/Sources/Node.swift b/Sources/Node.swift index 1c2eed4..1d2020d 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -1,32 +1,5 @@ import Foundation - -public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { - public let description:String - public var token: Token? - - public init(_ description:String) { - self.description = description - } - - public func contextAwareError(templateName: String?, templateContent: String) -> TemplateSyntaxError? { - guard let token = token, token.range != .unknown else { return nil } - let templateName = templateName.map({ "\($0):" }) ?? "" - let (line, lineNumber, offset) = templateContent.lineAndPosition(at: token.range) - let tokenContent = templateContent.substring(with: token.range) - let highlight = "\(String(Array(repeating: " ", count: offset)))^\(String(Array(repeating: "~", count: max(tokenContent.count - 1, 0))))" - let description = "\(templateName)\(lineNumber):\(offset): error: " + self.description + "\n\(line)\n\(highlight)\n" - return TemplateSyntaxError(description) - } - -} - - -public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { - return lhs.description == rhs.description -} - - public protocol NodeType { /// Render the node in the given context func render(_ context:Context) throws -> String diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 5c840bf..24fcdab 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -53,8 +53,8 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { - syntaxError.token = token + if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { + syntaxError.lexeme = token throw syntaxError } else { throw error diff --git a/Sources/Template.swift b/Sources/Template.swift index 7a80ee8..db570e0 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -68,17 +68,8 @@ open class Template: ExpressibleByStringLiteral { func render(_ context: Context) throws -> String { let context = context let parser = TokenParser(tokens: tokens, environment: context.environment) - do { - let nodes = try parser.parse() - return try renderNodes(nodes, context) - } catch { - if let syntaxError = error as? TemplateSyntaxError, - let error = syntaxError.contextAwareError(templateName: name, templateContent: templateString) { - throw error - } else { - throw error - } - } + let nodes = try parser.parse() + return try renderNodes(nodes, context) } /// Render the given template diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index cae09cc..60f621b 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -40,19 +40,7 @@ extension String { } } -extension Range where Bound == String.Index { - internal static var unknown: Range { - return "".range - } -} - -extension String { - var range: Range { - return startIndex..) From 0edb38588d4d94a6bf5de83c577b4469875b13fd Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 21:00:52 +0200 Subject: [PATCH 03/32] added ErrorReporter protocol with default implementation --- Sources/Errors.swift | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 1030d6d..e3c1441 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -32,6 +32,45 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } +public class ErrorReporterContext { + public let template: Template + + public init(template: Template) { + self.template = template + } +} + +public protocol ErrorReporter: class { + var context: ErrorReporterContext! { get set } + func report(error: Error) throws + func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? +} + +open class SimpleErrorReporter: ErrorReporter { + public var context: ErrorReporterContext! + + open func report(error: Error) throws { + guard let syntaxError = error as? TemplateSyntaxError else { throw error } + guard let context = context else { throw error } + throw contextAwareError(syntaxError, context: context) ?? error + } + + 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):" }) ?? "" + let tokenContent = context.template.templateString.substring(with: lexeme.range) + let lexer = Lexer(templateString: context.template.templateString) + let line = lexer.lexemeLine(lexeme) + let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.count - 1, 0))))" + let description = """ + \(templateName)\(line.number):\(line.offset): error: \(error.description) + \(line.content) + \(highlight) + """ + return TemplateSyntaxError(description) + } +} + extension Range where Bound == String.Index { internal static var unknown: Range { return "".range From d5f0be959f6f1c5c52486414539095618ed67de7 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 21:01:28 +0200 Subject: [PATCH 04/32] using error reporter from environment to handle syntax errors --- Sources/Environment.swift | 22 +++++++++++-- Tests/StencilTests/EnvironmentSpec.swift | 42 +++++++++++++++++++++++- Tests/StencilTests/TemplateSpec.swift | 24 -------------- 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 6b78fec..940c146 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -3,9 +3,15 @@ public struct Environment { public var extensions: [Extension] public var loader: Loader? + public var errorReporter: ErrorReporter - public init(loader: Loader? = nil, extensions: [Extension]? = nil, templateClass: Template.Type = Template.self) { + public init(loader: Loader? = nil, + extensions: [Extension]? = nil, + templateClass: Template.Type = Template.self, + errorReporter: ErrorReporter = SimpleErrorReporter()) { + self.templateClass = templateClass + self.errorReporter = errorReporter self.loader = loader self.extensions = (extensions ?? []) + [DefaultExtension()] } @@ -28,11 +34,21 @@ public struct Environment { public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String { let template = try loadTemplate(name: name) - return try template.render(context) + return try render(template: template, context: context) } public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String { let template = templateClass.init(templateString: string, environment: self) - return try template.render(context) + return try render(template: template, context: context) + } + + func render(template: Template, context: [String: Any]?) throws -> String { + errorReporter.context = ErrorReporterContext(template: template) + do { + return try template.render(context) + } catch { + try errorReporter.report(error: error) + return "" + } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 8bb5bfc..afc8d9b 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -1,5 +1,5 @@ import Spectre -import Stencil +@testable import Stencil func testEnvironment() { @@ -32,6 +32,46 @@ func testEnvironment() { try expect(result) == "here" } + + 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, context: context) as! TemplateSyntaxError + print(error) + return error + } + + $0.it("throws syntax error on invalid for tag syntax") { + let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + let error = expectedSyntaxError( + token: "{% for name in %}", + template: template, + description: "'for' statements should use the following syntax 'for x in y where condition'." + ) + try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on missing endfor") { + let template: Template = "{% for name in names %}{{ name }}" + let error = expectedSyntaxError( + token: "{% for name in names %}", + template: template, + description: "`endfor` was not found." + ) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on unknown tag") { + let template: Template = "{% for name in names %}{{ name }}{% end %}" + let error = expectedSyntaxError( + token: "{% end %}", + template: template, + description: "Unknown template tag 'end'" + ) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + } } diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index 42dac28..fee0c5e 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -15,30 +15,6 @@ func testTemplate() { let result = try template.render([ "name": "Kyle" ]) try expect(result) == "Hello World" } - - $0.it("throws syntax error on invalid for tag syntax") { - let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - var error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") - error.token = Token.block(value: "{% for name in %}", at: template.templateString.range(of: "{% for name in %}")!) - error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! - try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) - } - - $0.it("throws syntax error on missing endfor") { - let template: Template = "{% for name in names %}{{ name }}" - var error = TemplateSyntaxError("`endfor` was not found.") - error.token = Token.block(value: "{% for name in names %}", at: template.templateString.range(of: "{% for name in names %}")!) - error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! - try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) - } - - $0.it("throws syntax error on unknown tag") { - let template: Template = "{% for name in names %}{{ name }}{% end %}" - var error = TemplateSyntaxError("Unknown template tag 'end'") - error.token = Token.block(value: "{% end %}", at: template.templateString.range(of: "{% end %}")!) - error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! - try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) - } } } From e59609f1404deaf2ffd13d4533486c71f1567dcc Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 23:10:27 +0200 Subject: [PATCH 05/32] handling unknown filter errors --- Sources/Environment.swift | 5 ++ Sources/FilterTag.swift | 2 +- Sources/ForTag.swift | 16 +++---- Sources/IfTag.swift | 26 +++++------ Sources/Parser.swift | 20 +++++++- Sources/Variable.swift | 2 - Tests/StencilTests/EnvironmentSpec.swift | 58 ++++++++++++++++++++++-- Tests/StencilTests/ExpressionSpec.swift | 24 +++++----- Tests/StencilTests/ForNodeSpec.swift | 4 +- 9 files changed, 114 insertions(+), 43 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 940c146..c6fe7e2 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -51,4 +51,9 @@ public struct Environment { return "" } } + + var template: Template? { + return errorReporter.context?.template + } + } diff --git a/Sources/FilterTag.swift b/Sources/FilterTag.swift index 63ce321..5448996 100644 --- a/Sources/FilterTag.swift +++ b/Sources/FilterTag.swift @@ -15,7 +15,7 @@ class FilterNode : NodeType { throw TemplateSyntaxError("`endfilter` was not found.") } - let resolvable = try parser.compileFilter("filter_value|\(bits[1])") + let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token) return FilterNode(nodes: blocks, resolvable: resolvable) } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 032653f..5eb181d 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -21,24 +21,24 @@ class ForNode : NodeType { .map { $0.trimmingCharacters(in: CharacterSet.whitespaces) } let variable = components[3] + let filter = try parser.compileFilter(variable, containedIn: token) var emptyNodes = [NodeType]() let forNodes = try parser.parse(until(["endfor", "empty"])) - guard let token = parser.nextToken() else { + if let token = parser.nextToken() { + if token.contents == "empty" { + emptyNodes = try parser.parse(until(["endfor"])) + _ = parser.nextToken() + } + } else { throw TemplateSyntaxError("`endfor` was not found.") } - if token.contents == "empty" { - emptyNodes = try parser.parse(until(["endfor"])) - _ = parser.nextToken() - } - - let filter = try parser.compileFilter(variable) let `where`: Expression? if components.count >= 6 { - `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser) + `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) } else { `where` = nil } diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index e4305a9..706744a 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -100,7 +100,7 @@ final class IfExpressionParser { let tokens: [IfToken] var position: Int = 0 - init(components: [String], tokenParser: TokenParser) throws { + init(components: [String], tokenParser: TokenParser, token: Token) throws { self.tokens = try components.map { component in if let op = findOperator(name: component) { switch op { @@ -111,7 +111,7 @@ final class IfExpressionParser { } } - return .variable(try tokenParser.compileFilter(component)) + return .variable(try tokenParser.compileFilter(component, containedIn: token)) } } @@ -155,8 +155,8 @@ final class IfExpressionParser { } -func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression { - let parser = try IfExpressionParser(components: components, tokenParser: tokenParser) +func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression { + let parser = try IfExpressionParser(components: components, tokenParser: tokenParser, token: token) return try parser.parse() } @@ -187,7 +187,7 @@ class IfNode : NodeType { var components = token.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parseExpression(components: components, tokenParser: parser, token: token) let nodes = try parser.parse(until(["endif", "elif", "else"])) var conditions: [IfCondition] = [ IfCondition(expression: expression, nodes: nodes) @@ -197,7 +197,7 @@ class IfNode : NodeType { while let current = token, current.contents.hasPrefix("elif") { var components = current.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parseExpression(components: components, tokenParser: parser, token: current) let nodes = try parser.parse(until(["endif", "elif", "else"])) token = parser.nextToken() @@ -227,16 +227,16 @@ class IfNode : NodeType { falseNodes = try parser.parse(until(["endif", "else"])) - guard let token = parser.nextToken() else { + if let token = parser.nextToken() { + if token.contents == "else" { + trueNodes = try parser.parse(until(["endif"])) + _ = parser.nextToken() + } + } else { throw TemplateSyntaxError("`endif` was not found.") } - if token.contents == "else" { - trueNodes = try parser.parse(until(["endif"])) - _ = parser.nextToken() - } - - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parseExpression(components: components, tokenParser: parser, token: token) return IfNode(conditions: [ IfCondition(expression: expression, nodes: trueNodes), IfCondition(expression: nil, nodes: falseNodes), diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 24fcdab..f59d205 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -40,7 +40,7 @@ public class TokenParser { case .text(let text, _): nodes.append(TextNode(text: text)) case .variable: - nodes.append(VariableNode(variable: try compileFilter(token.contents))) + nodes.append(VariableNode(variable: try compileFilter(token.contents, containedIn: token))) case .block: if let parse_until = parse_until , parse_until(self, token) { prependToken(token) @@ -100,7 +100,23 @@ public class TokenParser { throw TemplateSyntaxError("Unknown filter '\(name)'") } - + + public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { + 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) + throw syntaxError + } else { + throw error + } + } + } + + @available(*, deprecated, message: "Use compileFilter(_:containedIn:)") public func compileFilter(_ token: String) throws -> Resolvable { return try FilterExpression(token: token, parser: self) } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index c17b966..bc40900 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -11,8 +11,6 @@ class FilterExpression : Resolvable { init(token: String, parser: TokenParser) throws { let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") }) if bits.isEmpty { - filters = [] - variable = Variable("") throw TemplateSyntaxError("Variable tags must include at least 1 argument") } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index afc8d9b..070655c 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -42,7 +42,7 @@ func testEnvironment() { return error } - $0.it("throws syntax error on invalid for tag syntax") { + $0.it("reports syntax error on invalid for tag syntax") { let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" let error = expectedSyntaxError( token: "{% for name in %}", @@ -52,7 +52,7 @@ func testEnvironment() { try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) } - $0.it("throws syntax error on missing endfor") { + $0.it("reports syntax error on missing endfor") { let template: Template = "{% for name in names %}{{ name }}" let error = expectedSyntaxError( token: "{% for name in names %}", @@ -62,7 +62,7 @@ func testEnvironment() { try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } - $0.it("throws syntax error on unknown tag") { + $0.it("reports syntax error on unknown tag") { let template: Template = "{% for name in names %}{{ name }}{% end %}" let error = expectedSyntaxError( token: "{% end %}", @@ -72,6 +72,58 @@ func testEnvironment() { try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } + $0.context("given unknown filter") { + func expectedFilterError(token: String, template: Template) -> TemplateSyntaxError { + return expectedSyntaxError( + token: token, + template: template, + description: "Unknown filter 'unknown'" + ) + } + + $0.it("reports syntax error in for tag") { + let template: Template = "{% for name in names|unknown %}{{ name }}{% endfor %}" + let error = expectedFilterError(token: "names|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("reports syntax error in for-where tag") { + let template: Template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("reports syntax error in if tag") { + let template: Template = "{% if name|unknown %}{{ name }}{% endif %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + + $0.it("reports syntax error in elif tag") { + let template: Template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + + $0.it("reports syntax error in ifnot tag") { + let template: Template = "{% ifnot name|unknown %}{{ name }}{% endif %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + + $0.it("reports syntax error in filter tag") { + let template: Template = "{% filter unknown %}Text{% endfilter %}" + let error = expectedFilterError(token: "{% filter unknown %}", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + } + + $0.it("reports syntax error in variable tag") { + let template: Template = "{{ name|unknown }}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + } + } } diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 4b7958d..00bd68a 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -105,19 +105,19 @@ func testExpressions() { $0.describe("expression parsing") { $0.it("can parse a variable expression") { - let expression = try parseExpression(components: ["value"], tokenParser: parser) + let expression = try parseExpression(components: ["value"], tokenParser: parser, token: .text(value: "", at: .unknown)) try expect(expression.evaluate(context: Context())).to.beFalse() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() } $0.it("can parse a not expression") { - let expression = try parseExpression(components: ["not", "value"], tokenParser: parser) + let expression = try parseExpression(components: ["not", "value"], tokenParser: parser, token: .text(value: "", at: .unknown)) try expect(expression.evaluate(context: Context())).to.beTrue() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() } $0.describe("and expression") { - let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to false with lhs false") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() @@ -137,7 +137,7 @@ func testExpressions() { } $0.describe("or expression") { - let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs true") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() @@ -157,7 +157,7 @@ func testExpressions() { } $0.describe("equality expression") { - let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with equal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() @@ -193,7 +193,7 @@ func testExpressions() { } $0.describe("inequality expression") { - let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with inequal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() @@ -205,7 +205,7 @@ func testExpressions() { } $0.describe("more than expression") { - let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs > rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() @@ -217,7 +217,7 @@ func testExpressions() { } $0.describe("more than equal expression") { - let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -229,7 +229,7 @@ func testExpressions() { } $0.describe("less than expression") { - let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs < rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() @@ -241,7 +241,7 @@ func testExpressions() { } $0.describe("less than equal expression") { - let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -253,7 +253,7 @@ func testExpressions() { } $0.describe("multiple expression") { - let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser) + let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with one") { try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() @@ -281,7 +281,7 @@ func testExpressions() { } $0.describe("in expression") { - let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true when rhs contains lhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 6cf69b3..8d41c1d 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -91,7 +91,7 @@ func testForNode() { $0.it("renders the given nodes while filtering items using where expression") { let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment())) + let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) try expect(try node.render(context)) == "2132" } @@ -99,7 +99,7 @@ func testForNode() { $0.it("renders the given empty nodes when all items filtered out with where expression") { let nodes: [NodeType] = [VariableNode(variable: "item")] let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment())) + let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) try expect(try node.render(context)) == "empty" } From 079fdf39b8561fa9059c6bfb860adbbc1d845979 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 21:02:27 +0200 Subject: [PATCH 06/32] 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 }}! From 768832620464bb47eb047e9d954d0ae5f73a9924 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 8 Oct 2017 00:07:10 +0200 Subject: [PATCH 07/32] renamed Scanner property for original content --- Sources/Lexer.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 7ea6ebc..68ec385 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -78,12 +78,12 @@ protocol Lexeme { } class Scanner { - let _content: String //stores original content + let originalContent: String var content: String var range: Range init(_ content: String) { - self._content = content + self.originalContent = content self.content = content range = content.startIndex.. Date: Sun, 8 Oct 2017 00:38:49 +0200 Subject: [PATCH 08/32] xcode backward compatibility fixes - moved back to single line string literal - fixed calculating string lenght --- Sources/Errors.swift | 20 +++++++++++++------- Sources/Lexer.swift | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 8ab3c49..8fb988e 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -66,13 +66,10 @@ open class SimpleErrorReporter: ErrorReporter { let tokenContent = context.template.templateString.substring(with: lexeme.range) let lexer = Lexer(templateString: context.template.templateString) let line = lexer.lexemeLine(lexeme) - let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.count - 1, 0))))" - let description = """ - \(templateName)\(line.number):\(line.offset): error: \(error.description) - \(line.content) - \(highlight) - """ - return TemplateSyntaxError(description) + 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 error = TemplateSyntaxError(description) + return error } } @@ -86,4 +83,13 @@ extension String { var range: Range { return startIndex..=3.2) + return count + #else + return characters.count + #endif + } + } diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 68ec385..51d8372 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -107,7 +107,7 @@ class Scanner { let result = content.substring(to: index) if returnUntil { - range = range.lowerBound.. Date: Sun, 8 Oct 2017 01:53:41 +0200 Subject: [PATCH 09/32] fixed iterating over template lines on linux --- Sources/ForTag.swift | 2 +- Sources/Lexer.swift | 13 ++++++++----- Tests/StencilTests/EnvironmentSpec.swift | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 5eb181d..ae32479 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -87,7 +87,7 @@ class ForNode : NodeType { var values: [Any] if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { - values = dictionary.map { ($0.key, $0.value) } + values = dictionary.map { ($0.key, $0.value) } as [(String, Any)] } else if let array = resolved as? [Any] { values = array } else if let range = resolved as? CountableClosedRange { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 51d8372..8d350e7 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -1,3 +1,5 @@ +import Foundation + struct Lexer { let templateString: String @@ -58,16 +60,17 @@ struct Lexer { var lineNumber: Int = 0 var offset = 0 var lineContent = "" - - templateString.enumerateLines { (line, stop) in + + for line in templateString.components(separatedBy: CharacterSet.newlines) { lineNumber += 1 lineContent = line - if let rangeOfLine = self.templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { - offset = self.templateString.distance(from: rangeOfLine.lowerBound, to: + if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { + offset = templateString.distance(from: rangeOfLine.lowerBound, to: lexeme.range.lowerBound) - stop = true + break } } + return (lineContent, lineNumber, offset) } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index cebb73c..3b33a15 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -39,7 +39,6 @@ func testEnvironment() { 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 - print(error) return error } From 27135f3ea3baa242050256d2df44425eb0fe0fed Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 29 Nov 2017 23:41:18 +0100 Subject: [PATCH 10/32] changer Never return type to Error in ErrorReporter this resolves warning related to Never type --- Sources/Environment.swift | 4 ++-- Sources/Errors.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 611e14e..d612ae4 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -47,7 +47,7 @@ public struct Environment { do { return try template.render(context) } catch { - try errorReporter.report(error: error) + throw errorReporter.reportError(error) } } @@ -66,7 +66,7 @@ public struct Environment { do { return try closure() } catch { - try errorReporter.report(error: error) + throw errorReporter.reportError(error) } } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 8fb988e..97e93b7 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -46,17 +46,17 @@ public class ErrorReporterContext { public protocol ErrorReporter: class { var context: ErrorReporterContext! { get set } - func report(error: Error) throws -> Never + func reportError(_ error: Error) -> Error func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? } open class SimpleErrorReporter: ErrorReporter { public var context: ErrorReporterContext! - 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 + 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 } // TODO: add stack trace using parent context From 53c1550c5bc7ef281293a4a3b236d79ce9c9fcc4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 23 Dec 2017 15:19:36 +0100 Subject: [PATCH 11/32] =?UTF-8?q?reporting=20node=20rendering=20errors=20u?= =?UTF-8?q?sing=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]) From 9a28142fa6809190c38981550068b7557846e263 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 24 Dec 2017 15:34:17 +0100 Subject: [PATCH 12/32] reporting error with its parent context --- Sources/Context.swift | 4 ++ Sources/Errors.swift | 84 +++++++++++++++++++----- Sources/Include.swift | 14 +++- Sources/Inheritence.swift | 18 +++-- Sources/Lexer.swift | 6 +- Tests/StencilTests/EnvironmentSpec.swift | 79 +++++++++++++++++----- 6 files changed, 161 insertions(+), 44 deletions(-) diff --git a/Sources/Context.swift b/Sources/Context.swift index 60bec17..a976e1e 100644 --- a/Sources/Context.swift +++ b/Sources/Context.swift @@ -3,6 +3,10 @@ public class Context { var dictionaries: [[String: Any?]] public let environment: Environment + + public var errorReporter: ErrorReporter { + return environment.errorReporter + } init(dictionary: [String: Any]? = nil, environment: Environment? = nil) { if let dictionary = dictionary { diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 32450cd..0cb40eb 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -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) + } + } - public init(_ description:String) { - self.description = description + 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.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?, context: ErrorReporterContext) -> Error? + func renderError(_ error: Error) -> String } open class SimpleErrorReporter: ErrorReporter { @@ -55,21 +96,28 @@ 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?, 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") } - + } extension Range where Bound == String.Index { diff --git a/Sources/Include.swift b/Sources/Include.swift index bb85a2d..58bde91 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -27,9 +27,17 @@ class IncludeNode : NodeType { let template = try context.environment.loadTemplate(name: templateName) - return try context.environment.pushTemplate(template, token: token) { - try context.push { - return try template.render(context) + 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 } } } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 189e581..7457c37 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -100,9 +100,17 @@ class ExtendsNode : NodeType { blockContext = BlockContext(blocks: blocks) } - return try context.environment.pushTemplate(template, token: token) { - try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try template.render(context) + 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) } diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index f2af91e..d9f972f 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -187,9 +187,9 @@ extension String { return String(self[first..) -> (content: String, number: Int, offset: String.IndexDistance) { - var lineNumber: Int = 0 - var offset = 0 + public func rangeLine(_ range: Range) -> (content: String, number: UInt, offset: String.IndexDistance) { + var lineNumber: UInt = 0 + var offset: Int = 0 var lineContent = "" for line in components(separatedBy: CharacterSet.newlines) { diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 62e99b5..738ed86 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -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") { @@ -210,25 +221,61 @@ func testEnvironment() { 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 template: Template = "{% include \"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") { 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) } } From c4866178541aa63bdda1e848cd9860b1619082e4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 00:34:13 +0100 Subject: [PATCH 13/32] fixed reporting errors in child templates --- Sources/Errors.swift | 2 +- Sources/Inheritence.swift | 79 +++++++++++++++--------- Tests/StencilTests/EnvironmentSpec.swift | 65 +++++++++++++------ 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 0cb40eb..ab5eda3 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -99,7 +99,7 @@ open class SimpleErrorReporter: ErrorReporter { return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)", lexeme: (error as? TemplateSyntaxError)?.lexeme, - template: context.template, + template: (error as? TemplateSyntaxError)?.template ?? context.template, parentError: (error as? TemplateSyntaxError)?.parentError ) } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 7457c37..55f94ef 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -1,25 +1,23 @@ class BlockContext { class var contextKey: String { return "block_context" } - var blocks: [String: [BlockNode]] + // contains mapping of block names to their nodes and templates where they are defined + var blocks: [String: [(BlockNode, Template?)]] - init(blocks: [String: BlockNode]) { - self.blocks = [:] - blocks.forEach { (key, value) in - self.blocks[key] = [value] - } + init(blocks: [String: (BlockNode, Template?)]) { + self.blocks = blocks.mapValues({ [$0] }) } - func push(_ block: BlockNode, forKey blockName: String) { + func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) { if var blocks = blocks[blockName] { - blocks.append(block) + blocks.append((block, template)) self.blocks[blockName] = blocks } else { - self.blocks[blockName] = [block] + self.blocks[blockName] = [(block, template)] } } - func pop(_ blockName: String) -> BlockNode? { + func popBlock(named blockName: String) -> (node: BlockNode, template: Template?)? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() if blocks.isEmpty { @@ -87,28 +85,33 @@ class ExtendsNode : NodeType { throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") } - let template = try context.environment.loadTemplate(name: templateName) - + let baseTemplate = try context.environment.loadTemplate(name: templateName) + let template = context.environment.template + let blockContext: BlockContext - if let context = context[BlockContext.contextKey] as? BlockContext { - blockContext = context - - for (key, value) in blocks { - blockContext.push(value, forKey: key) + if let _blockContext = context[BlockContext.contextKey] as? BlockContext { + blockContext = _blockContext + for (name, block) in blocks { + blockContext.pushBlock(block, named: name, definedIn: template) } } else { - blockContext = BlockContext(blocks: blocks) + blockContext = BlockContext(blocks: blocks.mapValues({ ($0, template) })) } do { - return try context.environment.pushTemplate(template, token: token) { + // pushes base template and renders it's content + // block_context contains all blocks from child templates + return try context.environment.pushTemplate(baseTemplate, token: token) { try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try template.render(context) + return try baseTemplate.render(context) } } } catch { - if let parentError = error as? TemplateSyntaxError { - throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError) + // if error template is already set (see catch in BlockNode) + // and it happend in the same template as current template + // there is no need to wrap it in another error + if let error = error as? TemplateSyntaxError, error.template !== context.environment.template { + throw TemplateSyntaxError(reason: error.reason, parentError: error) } else { throw error } @@ -142,15 +145,31 @@ class BlockNode : NodeType { } 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 child = blockContext.popBlock(named: name) { + // node is a block node from child template that extends this node (has the same name) 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) + newContext = [ + BlockContext.contextKey: blockContext, + // render current node so that it's content can be used as part of node that extends it + "block": ["super": try self.render(context)] + ] + // render extension node + do { + return try context.push(dictionary: newContext) { + return try child.node.render(context) + } + } catch { + // child node belongs to child template, which is currently not on top of stack + // so we need to use node's template to report errors, not current template + // unless it's already set + if var error = error as? TemplateSyntaxError { + error.template = error.template ?? child.template + error.lexeme = error.lexeme ?? child.node.token + + throw error + } else { + throw error + } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 738ed86..4d98f7b 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -228,18 +228,8 @@ func testEnvironment() { 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 baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - - 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) + try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } $0.it("reports runtime error in included template") { @@ -259,24 +249,59 @@ func testEnvironment() { try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } - - $0.it("reports runtime error in extended template") { + + $0.it("reports syntax error in base template") { + let template = try environment.loadTemplate(name: "invalid-child-super.html") + let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + 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 base template") { var environment = environment let filterExtension = Extension() - filterExtension.registerFilter("throw", filter: { (_: Any?) in + filterExtension.registerFilter("unknown", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] + + let template = try environment.loadTemplate(name: "invalid-child-super.html") + let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "filter error") + var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "filter error") + error.parentError = parentError + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports syntax error in child template") { + let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) + let error = expectedSyntaxError(token: "target|unknown", template: template, description: "Unknown filter 'unknown'") + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports runtime error in child template") { + var environment = environment + let filterExtension = Extension() + filterExtension.registerFilter("unknown", 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 + let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) + let error = expectedSyntaxError(token: "{{ target|unknown }}", template: template, description: "filter error") try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) } + } } From bb3f33724bad62e488887c1119ea6e41fac903b4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 01:10:58 +0100 Subject: [PATCH 14/32] unified setting higlighting range for errors --- Sources/Errors.swift | 21 ++++++++--------- Sources/Lexer.swift | 1 + Sources/Node.swift | 10 +++----- Tests/StencilTests/EnvironmentSpec.swift | 29 +++++------------------- 4 files changed, 19 insertions(+), 42 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index ab5eda3..76c7559 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -32,18 +32,15 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } 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 - } + guard let template = template, let lexeme = lexeme else { return reason } + let templateName = template.name.map({ "\($0):" }) ?? "" + let range = template.templateString.range(of: lexeme.contents, range: lexeme.range) ?? lexeme.range + let line = template.templateString.rangeLine(range) + let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(lexeme.contents.length - 1, 0))))" + + return "\(templateName)\(line.number):\(line.offset): error: \(reason)\n" + + "\(line.content)\n" + + "\(highlight)\n" } init(reason: String, lexeme: Lexeme? = nil, template: Template? = nil, parentError: Error? = nil) { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index d9f972f..9a7052b 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -77,6 +77,7 @@ struct Lexer { } protocol Lexeme { + var contents: String { get } var range: Range { get } } diff --git a/Sources/Node.swift b/Sources/Node.swift index 10ee16a..5656b79 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -15,13 +15,9 @@ public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String 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 + if var error = error as? TemplateSyntaxError { + error.lexeme = error.lexeme ?? $0.token + throw error } else { throw error } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 4d98f7b..67d2152 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -41,41 +41,25 @@ func testEnvironment() { $0.it("reports syntax error on invalid for tag syntax") { let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - let error = expectedSyntaxError( - token: "{% for name in %}", - template: template, - description: "'for' statements should use the following syntax 'for x in y where condition'." - ) + let error = expectedSyntaxError(token: "for name in", template: template, description: "'for' statements should use the following syntax 'for x in y where condition'.") try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) } $0.it("reports syntax error on missing endfor") { let template: Template = "{% for name in names %}{{ name }}" - let error = expectedSyntaxError( - token: "{% for name in names %}", - template: template, - description: "`endfor` was not found." - ) + let error = expectedSyntaxError(token: "for name in names", template: template, description: "`endfor` was not found.") try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } $0.it("reports syntax error on unknown tag") { let template: Template = "{% for name in names %}{{ name }}{% end %}" - let error = expectedSyntaxError( - token: "{% end %}", - template: template, - description: "Unknown template tag 'end'" - ) + let error = expectedSyntaxError(token: "end", template: template, description: "Unknown template tag 'end'") try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } $0.context("given unknown filter") { func expectedFilterError(token: String, template: Template) -> TemplateSyntaxError { - return expectedSyntaxError( - token: token, - template: template, - description: "Unknown filter 'unknown'" - ) + return expectedSyntaxError(token: token, template: template, description: "Unknown filter 'unknown'") } $0.it("reports syntax error in for tag") { @@ -110,7 +94,7 @@ func testEnvironment() { $0.it("reports syntax error in filter tag") { let template: Template = "{% filter unknown %}Text{% endfilter %}" - let error = expectedFilterError(token: "{% filter unknown %}", template: template) + let error = expectedFilterError(token: "filter unknown", template: template) try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) } @@ -228,7 +212,6 @@ func testEnvironment() { 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) } @@ -297,7 +280,7 @@ func testEnvironment() { let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) - let error = expectedSyntaxError(token: "{{ target|unknown }}", template: template, description: "filter error") + let error = expectedSyntaxError(token: "target|unknown", template: template, description: "filter error") try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) } From ea7e1efac72809ae3948a6721ae70d7636bc0be1 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 01:55:37 +0100 Subject: [PATCH 15/32] fixed highlighting of errors happening in {{ block.super }} --- Sources/Inheritence.swift | 29 +++++++++++++++++------- Tests/StencilTests/EnvironmentSpec.swift | 2 +- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 55f94ef..c143a22 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -146,13 +146,27 @@ class BlockNode : NodeType { func render(_ context: Context) throws -> String { if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.popBlock(named: name) { - // node is a block node from child template that extends this node (has the same name) - let newContext: [String: Any] - newContext = [ - BlockContext.contextKey: blockContext, - // render current node so that it's content can be used as part of node that extends it - "block": ["super": try self.render(context)] - ] + // child node is a block node from child template that extends this node (has the same name) + + var newContext: [String: Any] = [BlockContext.contextKey: blockContext] + + if let blockSuperNode = child.node.nodes.first(where: { + if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } + else { return false} + }) { + do { + // render current (base) node so that its content can be used as part of node that extends it + newContext["block"] = ["super": try self.render(context)] + } catch { + let baseError = context.errorReporter.reportError(error) + throw TemplateSyntaxError( + reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)", + lexeme: blockSuperNode.token, + template: child.template, + parentError: baseError) + } + } + // render extension node do { return try context.push(dictionary: newContext) { @@ -165,7 +179,6 @@ class BlockNode : NodeType { if var error = error as? TemplateSyntaxError { error.template = error.template ?? child.template error.lexeme = error.lexeme ?? child.node.token - throw error } else { throw error diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 67d2152..1b6fdb8 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -256,7 +256,7 @@ func testEnvironment() { let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "filter error") - var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "filter error") + var error = expectedSyntaxError(token: "block.super", template: template, description: "filter error") error.parentError = parentError try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) From 218822fcb0482009e817e3bd80cc07ca182e9855 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 01:56:46 +0100 Subject: [PATCH 16/32] updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce60106..95d780b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added support for resolving superclass properties for not-NSObject subclasses - The `{% for %}` tag can now iterate over tuples, structures and classes via their stored properties. +- Drastic improvements in error reporting ### Bug Fixes From 8d68edd7258a64b1a665d2a99628d4a5f043b419 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 15:28:46 +0100 Subject: [PATCH 17/32] replaced Lexeme protocol with Token --- Sources/Errors.swift | 54 +++++++++--------------- Sources/Inheritence.swift | 4 +- Sources/Lexer.swift | 13 ++---- Sources/Node.swift | 2 +- Sources/Parser.swift | 10 ++--- Sources/Tokenizer.swift | 2 +- Tests/StencilTests/EnvironmentSpec.swift | 4 +- 7 files changed, 35 insertions(+), 54 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 76c7559..d13a0ec 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -20,35 +20,16 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public let reason: String - public private(set) var description: String - + public var description: String { return reason } + public internal(set) var token: Token? 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 { - guard let template = template, let lexeme = lexeme else { return reason } - let templateName = template.name.map({ "\($0):" }) ?? "" - let range = template.templateString.range(of: lexeme.contents, range: lexeme.range) ?? lexeme.range - let line = template.templateString.rangeLine(range) - let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(lexeme.contents.length - 1, 0))))" - - return "\(templateName)\(line.number):\(line.offset): error: \(reason)\n" - + "\(line.content)\n" - + "\(highlight)\n" - } - - init(reason: String, lexeme: Lexeme? = nil, template: Template? = nil, parentError: Error? = nil) { + public init(reason: String, token: Token? = 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) + self.token = token } public init(_ description: String) { @@ -95,7 +76,7 @@ open class SimpleErrorReporter: ErrorReporter { guard let context = context else { return error } return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)", - lexeme: (error as? TemplateSyntaxError)?.lexeme, + token: (error as? TemplateSyntaxError)?.token, template: (error as? TemplateSyntaxError)?.template ?? context.template, parentError: (error as? TemplateSyntaxError)?.parentError ) @@ -104,7 +85,21 @@ open class SimpleErrorReporter: ErrorReporter { open func renderError(_ error: Error) -> String { guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } - var descriptions = [templateError.description] + let description: String + if let template = templateError.template, let token = templateError.token { + let templateName = template.name.map({ "\($0):" }) ?? "" + let range = template.templateString.range(of: token.contents, range: token.range) ?? token.range + let line = template.templateString.rangeLine(range) + let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))" + + description = "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n" + + "\(line.content)\n" + + "\(highlight)\n" + } else { + description = templateError.reason + } + + var descriptions = [description] var currentError: TemplateSyntaxError? = templateError while let parentError = currentError?.parentError { @@ -127,13 +122,4 @@ extension String { var range: Range { return startIndex..=3.2) - return count - #else - return characters.count - #endif - } - } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index c143a22..9662540 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -161,7 +161,7 @@ class BlockNode : NodeType { let baseError = context.errorReporter.reportError(error) throw TemplateSyntaxError( reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)", - lexeme: blockSuperNode.token, + token: blockSuperNode.token, template: child.template, parentError: baseError) } @@ -178,7 +178,7 @@ class BlockNode : NodeType { // unless it's already set if var error = error as? TemplateSyntaxError { error.template = error.template ?? child.template - error.lexeme = error.lexeme ?? child.node.token + error.token = error.token ?? child.node.token throw error } else { throw error diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 9a7052b..30d70f0 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -56,7 +56,7 @@ struct Lexer { return tokens } - func lexemeLine(_ lexeme: Lexeme) -> (content: String, number: Int, offset: String.IndexDistance) { + func tokenLine(_ token: Token) -> (content: String, number: Int, offset: String.IndexDistance) { var lineNumber: Int = 0 var offset = 0 var lineContent = "" @@ -64,9 +64,9 @@ struct Lexer { for line in templateString.components(separatedBy: CharacterSet.newlines) { lineNumber += 1 lineContent = line - if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { + if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(token.range.lowerBound) { offset = templateString.distance(from: rangeOfLine.lowerBound, to: - lexeme.range.lowerBound) + token.range.lowerBound) break } } @@ -76,11 +76,6 @@ struct Lexer { } -protocol Lexeme { - var contents: String { get } - var range: Range { get } -} - class Scanner { let originalContent: String var content: String @@ -111,7 +106,7 @@ class Scanner { let result = content.substring(to: index) if returnUntil { - range = range.lowerBound.. String return try $0.render(context) } catch { if var error = error as? TemplateSyntaxError { - error.lexeme = error.lexeme ?? $0.token + error.token = error.token ?? $0.token throw error } else { throw error diff --git a/Sources/Parser.swift b/Sources/Parser.swift index a4d2d88..dc42f91 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -54,8 +54,8 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { - syntaxError.lexeme = token + if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { + syntaxError.token = token throw syntaxError } else { throw error @@ -106,11 +106,11 @@ public class TokenParser { do { return try FilterExpression(token: filterToken, parser: self) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { + if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { - syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange) + syntaxError.token = Token.block(value: filterToken, at: filterTokenRange) } else { - syntaxError.lexeme = containingToken + syntaxError.token = containingToken } throw syntaxError } else { diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 60f621b..55482ed 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -40,7 +40,7 @@ extension String { } } -public enum Token : Equatable, Lexeme { +public enum Token : Equatable { /// A token representing a piece of text. case text(value: String, at: Range) diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 1b6fdb8..0e20e76 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -35,8 +35,8 @@ func testEnvironment() { } func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) - return TemplateSyntaxError(reason: description, lexeme: lexeme, template: template, parentError: nil) + let token = Token.block(value: token, at: template.templateString.range(of: token)!) + return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil) } $0.it("reports syntax error on invalid for tag syntax") { From 7756522317d8d076410f7a530fa5d7ac9da66b29 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 15:51:17 +0100 Subject: [PATCH 18/32] fixed error on swift 3.1 --- Sources/Inheritence.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 9662540..dba09cf 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -4,8 +4,8 @@ class BlockContext { // contains mapping of block names to their nodes and templates where they are defined var blocks: [String: [(BlockNode, Template?)]] - init(blocks: [String: (BlockNode, Template?)]) { - self.blocks = blocks.mapValues({ [$0] }) + init(blocks: [String: [(BlockNode, Template?)]]) { + self.blocks = blocks } func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) { @@ -95,7 +95,9 @@ class ExtendsNode : NodeType { blockContext.pushBlock(block, named: name, definedIn: template) } } else { - blockContext = BlockContext(blocks: blocks.mapValues({ ($0, template) })) + var blocks = [String: [(BlockNode, Template?)]]() + self.blocks.forEach { blocks[$0.key] = [($0.value, template)] } + blockContext = BlockContext(blocks: blocks) } do { From ed885f462a3865c28f167f138ce444cfe63c8cfe Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 20:48:43 +0100 Subject: [PATCH 19/32] refactored environment tests --- Tests/StencilTests/EnvironmentSpec.swift | 299 +++++++++++++---------- 1 file changed, 172 insertions(+), 127 deletions(-) diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 0e20e76..24f2136 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -5,7 +5,13 @@ import PathKit func testEnvironment() { describe("Environment") { - let environment = Environment(loader: ExampleLoader()) + var environment: Environment! + var template: Template! + + $0.before { + environment = Environment(loader: ExampleLoader()) + template = nil + } $0.it("can load a template from a name") { let template = try environment.loadTemplate(name: "example.html") @@ -33,256 +39,270 @@ func testEnvironment() { try expect(result) == "here" } - + func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { let token = Token.block(value: token, at: template.templateString.range(of: token)!) return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil) } - - $0.it("reports syntax error on invalid for tag syntax") { - let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - let error = expectedSyntaxError(token: "for name in", template: template, description: "'for' statements should use the following syntax 'for x in y where condition'.") - try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) + + func expectError(reason: String, token: String) throws { + let expectedError = expectedSyntaxError(token: token, template: template, description: reason) + + let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])) + .toThrow(expectedError) + try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } - - $0.it("reports syntax error on missing endfor") { - let template: Template = "{% for name in names %}{{ name }}" - let error = expectedSyntaxError(token: "for name in names", template: template, description: "`endfor` was not found.") - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) - } - - $0.it("reports syntax error on unknown tag") { - let template: Template = "{% for name in names %}{{ name }}{% end %}" - let error = expectedSyntaxError(token: "end", template: template, description: "Unknown template tag 'end'") - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + + $0.context("given syntax error") { + + $0.it("reports syntax error on invalid for tag syntax") { + template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + try expectError(reason: "'for' statements should use the following syntax 'for x in y where condition'.", token: "for name in") + } + + $0.it("reports syntax error on missing endfor") { + template = "{% for name in names %}{{ name }}" + try expectError(reason: "`endfor` was not found.", token: "for name in names") + } + + $0.it("reports syntax error on unknown tag") { + template = "{% for name in names %}{{ name }}{% end %}" + try expectError(reason: "Unknown template tag 'end'", token: "end") + } + } $0.context("given unknown filter") { - func expectedFilterError(token: String, template: Template) -> TemplateSyntaxError { - return expectedSyntaxError(token: token, template: template, description: "Unknown filter 'unknown'") - } $0.it("reports syntax error in for tag") { - let template: Template = "{% for name in names|unknown %}{{ name }}{% endfor %}" - let error = expectedFilterError(token: "names|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + template = "{% for name in names|unknown %}{{ name }}{% endfor %}" + try expectError(reason: "Unknown filter 'unknown'", token: "names|unknown") } $0.it("reports syntax error in for-where tag") { - let template: Template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in if tag") { - let template: Template = "{% if name|unknown %}{{ name }}{% endif %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{% if name|unknown %}{{ name }}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in elif tag") { - let template: Template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in ifnot tag") { - let template: Template = "{% ifnot name|unknown %}{{ name }}{% endif %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{% ifnot name|unknown %}{{ name }}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in filter tag") { - let template: Template = "{% filter unknown %}Text{% endfilter %}" - let error = expectedFilterError(token: "filter unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + template = "{% filter unknown %}Text{% endfilter %}" + try expectError(reason: "Unknown filter 'unknown'", token: "filter unknown") } $0.it("reports syntax error in variable tag") { - let template: Template = "{{ name|unknown }}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{{ name|unknown }}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } } $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") + throw TemplateSyntaxError("filter 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) + + template = Template(templateString: "{{ name|throw }}", environment: environment) + try expectError(reason: "filter error", token: "name|throw") } $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") + throw TemplateSyntaxError("filter 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) + + template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment) + try expectError(reason: "filter error", token: "filter throw") } $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) + + template = Template(templateString: "{% simpletag %}", environment: environment) + try expectError(reason: "simpletag error", token: "simpletag") } $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) + template = "{{ name|uppercase:5 }}" + try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5") } $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) + template = Template(templateString: "{% customtag %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") } $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) + + template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") } $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) + + template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") } } - $0.context("given related templates") { + $0.context("given included template") { let path = Path(#file) + ".." + "fixtures" let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) + var environment = Environment(loader: loader) + var template: Template! + var includedTemplate: Template! + + $0.before { + environment = Environment(loader: loader) + template = nil + includedTemplate = nil + } + + func expectError(reason: String, token: String, includedToken: String) throws { + var expectedError = expectedSyntaxError(token: token, template: template, description: reason) + expectedError.parentError = expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason) + + let error = try expect(environment.render(template: template, context: ["target": "World"])) + .toThrow(expectedError) + try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) + } $0.it("reports syntax error in included template") { - 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: "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) + template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment) + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + + try expectError(reason: "Unknown filter 'unknown'", + token: "include \"invalid-include.html\"", + includedToken: "target|unknown") } $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") + template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment) + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + + try expectError(reason: "filter error", + token: "include \"invalid-include.html\"", + includedToken: "target|unknown") + } + + } + + $0.context("given base and child templates") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + var environment: Environment! + var childTemplate: Template! + var baseTemplate: Template! + + $0.before { + environment = Environment(loader: loader) + childTemplate = nil + baseTemplate = nil + } + + func expectError(reason: String, childToken: String, baseToken: String?) throws { + var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) + if let baseToken = baseToken { + expectedError.parentError = expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason) + } - 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) + let error = try expect(environment.render(template: childTemplate, context: ["target": "World"])) + .toThrow(expectedError) + try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } $0.it("reports syntax error in base template") { - let template = try environment.loadTemplate(name: "invalid-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - - 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) + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + try expectError(reason: "Unknown filter 'unknown'", + childToken: "extends \"invalid-base.html\"", + baseToken: "target|unknown") } - + $0.it("reports runtime error in base template") { - var environment = environment let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - let template = try environment.loadTemplate(name: "invalid-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "filter error") - var error = expectedSyntaxError(token: "block.super", template: template, description: "filter error") - error.parentError = parentError - - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + try expectError(reason: "filter error", + childToken: "block.super", + baseToken: "target|unknown") } $0.it("reports syntax error in child template") { - let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) - let error = expectedSyntaxError(token: "target|unknown", template: template, description: "Unknown filter 'unknown'") - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + try expectError(reason: "Unknown filter 'unknown'", + childToken: "target|unknown", + baseToken: nil) } $0.it("reports runtime error in child template") { - var environment = environment let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - - let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + + childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) - let error = expectedSyntaxError(token: "target|unknown", template: template, description: "filter error") - - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + + try expectError(reason: "filter error", + childToken: "target|unknown", + baseToken: nil) } } @@ -290,6 +310,31 @@ func testEnvironment() { } } +private extension Expectation { + @discardableResult + func toThrow(_ error: T) throws -> T { + var thrownError: Error? = nil + + do { + _ = try expression() + } catch { + thrownError = error + } + + if let thrownError = thrownError { + if let thrownError = thrownError as? T { + if error != thrownError { + throw failure("\(thrownError) is not \(error)") + } + return thrownError + } else { + throw failure("\(thrownError) is not \(error)") + } + } else { + throw failure("expression did not throw an error") + } + } +} fileprivate class ExampleLoader: Loader { func loadTemplate(name: String, environment: Environment) throws -> Template { From abeb30bb1c6c13c814a03e7a638e4975cd683d02 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 21:20:09 +0100 Subject: [PATCH 20/32] fix rendering templates created from string literals --- Sources/Environment.swift | 2 ++ Sources/Template.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index aafab8a..ce2d2c5 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -43,6 +43,8 @@ public struct Environment { } func render(template: Template, context: [String: Any]?) throws -> String { + // update temaplte environment as it cen be created from string literal with default environment + template.environment = self errorReporter.context = ErrorReporterContext(template: template) do { return try template.render(context) diff --git a/Sources/Template.swift b/Sources/Template.swift index db570e0..03176b2 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -8,7 +8,7 @@ let NSFileNoSuchFileError = 4 /// A class representing a template open class Template: ExpressibleByStringLiteral { let templateString: String - let environment: Environment + internal(set) var environment: Environment let tokens: [Token] /// The name of the loaded Template if the Template was loaded from a Loader From cb124319ec32e0c79dd38c25c532662e4368a9ba Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 21:32:28 +0100 Subject: [PATCH 21/32] removed unneeded changes --- Sources/ForTag.swift | 19 ++++++++++--------- Sources/IfTag.swift | 14 +++++++------- Sources/Lexer.swift | 18 ------------------ Sources/Parser.swift | 15 ++++++++------- 4 files changed, 25 insertions(+), 41 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 8e3f5c9..fcb5d68 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -28,21 +28,22 @@ class ForNode : NodeType { let forNodes = try parser.parse(until(["endfor", "empty"])) - if let token = parser.nextToken() { - if token.contents == "empty" { - emptyNodes = try parser.parse(until(["endfor"])) - _ = parser.nextToken() - } - } else { - throw TemplateSyntaxError("`endfor` was not found.") - } - let `where`: Expression? if components.count >= 6 { `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) } else { `where` = nil } + + guard let token = parser.nextToken() else { + throw TemplateSyntaxError("`endfor` was not found.") + } + + if token.contents == "empty" { + emptyNodes = try parser.parse(until(["endfor"])) + _ = parser.nextToken() + } + return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token) } diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 39a3606..07e08f9 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -226,18 +226,18 @@ class IfNode : NodeType { var trueNodes = [NodeType]() var falseNodes = [NodeType]() + let expression = try parseExpression(components: components, tokenParser: parser, token: token) falseNodes = try parser.parse(until(["endif", "else"])) - if let token = parser.nextToken() { - if token.contents == "else" { - trueNodes = try parser.parse(until(["endif"])) - _ = parser.nextToken() - } - } else { + guard let token = parser.nextToken() else { throw TemplateSyntaxError("`endif` was not found.") } - let expression = try parseExpression(components: components, tokenParser: parser, token: token) + if token.contents == "else" { + trueNodes = try parser.parse(until(["endif"])) + _ = parser.nextToken() + } + return IfNode(conditions: [ IfCondition(expression: expression, nodes: trueNodes), IfCondition(expression: nil, nodes: falseNodes), diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 30d70f0..c6cc15c 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -56,24 +56,6 @@ struct Lexer { return tokens } - func tokenLine(_ token: Token) -> (content: String, number: Int, offset: String.IndexDistance) { - var lineNumber: Int = 0 - var offset = 0 - var lineContent = "" - - for line in templateString.components(separatedBy: CharacterSet.newlines) { - lineNumber += 1 - lineContent = line - if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(token.range.lowerBound) { - offset = templateString.distance(from: rangeOfLine.lowerBound, to: - token.range.lowerBound) - break - } - } - - return (lineContent, lineNumber, offset) - } - } class Scanner { diff --git a/Sources/Parser.swift b/Sources/Parser.swift index dc42f91..e2a583c 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -54,9 +54,9 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { - syntaxError.token = token - throw syntaxError + if var error = error as? TemplateSyntaxError { + error.token = error.token ?? token + throw error } else { throw error } @@ -106,13 +106,14 @@ public class TokenParser { do { return try FilterExpression(token: filterToken, parser: self) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { + if var error = error as? TemplateSyntaxError, error.token == nil { + // find range of filter in the containing token so that only filter is highligted, not the whole token if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { - syntaxError.token = Token.block(value: filterToken, at: filterTokenRange) + error.token = Token.variable(value: filterToken, at: filterTokenRange) } else { - syntaxError.token = containingToken + error.token = containingToken } - throw syntaxError + throw error } else { throw error } From ac2fd56e8e8e52ec47eccce56bf8638f6ff2e31c Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 02:31:47 +0100 Subject: [PATCH 22/32] storing full sourcemap in token, refactored error reporting --- Sources/Environment.swift | 25 +------- Sources/Errors.swift | 73 ++++++------------------ Sources/Include.swift | 10 ++-- Sources/Inheritence.swift | 56 +++++++++--------- Sources/Lexer.swift | 32 +++++++++-- Sources/Parser.swift | 8 ++- Sources/Template.swift | 2 +- Sources/Tokenizer.swift | 54 ++++++++++++------ Tests/StencilTests/EnvironmentSpec.swift | 26 ++++----- Tests/StencilTests/FilterSpec.swift | 4 +- Tests/StencilTests/ForNodeSpec.swift | 6 +- Tests/StencilTests/IfNodeSpec.swift | 12 ++-- Tests/StencilTests/IncludeSpec.swift | 2 +- Tests/StencilTests/LexerSpec.swift | 20 +++---- Tests/StencilTests/ParserSpec.swift | 7 +-- 15 files changed, 149 insertions(+), 188 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index ce2d2c5..5fb9e19 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -45,30 +45,7 @@ public struct Environment { func render(template: Template, context: [String: Any]?) throws -> String { // update temaplte environment as it cen be created from string literal with default environment template.environment = self - errorReporter.context = ErrorReporterContext(template: template) - do { - return try template.render(context) - } catch { - throw errorReporter.reportError(error) - } - } - - var template: Template? { - 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 { - throw errorReporter.reportError(error) - } + return try template.render(context) } } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index d13a0ec..4b7cafe 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -22,13 +22,15 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public let reason: String public var description: String { return reason } public internal(set) var token: Token? - public internal(set) var template: Template? - public internal(set) var parentError: Error? + public internal(set) var stackTrace: [Token] + public var templateName: String? { return token?.sourceMap.filename } + var allTokens: [Token] { + return stackTrace + (token.map({ [$0] }) ?? []) + } - public init(reason: String, token: Token? = nil, template: Template? = nil, parentError: Error? = nil) { + public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { self.reason = reason - self.parentError = parentError - self.template = template + self.stackTrace = stackTrace self.token = token } @@ -37,77 +39,34 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { - 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 - } + return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace } } -public class ErrorReporterContext { - public let 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 reportError(_ error: Error) -> Error func renderError(_ error: Error) -> String } open class SimpleErrorReporter: ErrorReporter { - public var context: ErrorReporterContext! - - open func reportError(_ error: Error) -> Error { - guard let context = context else { return error } - - return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)", - token: (error as? TemplateSyntaxError)?.token, - template: (error as? TemplateSyntaxError)?.template ?? context.template, - parentError: (error as? TemplateSyntaxError)?.parentError - ) - } open func renderError(_ error: Error) -> String { guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } - let description: String - if let template = templateError.template, let token = templateError.token { - let templateName = template.name.map({ "\($0):" }) ?? "" - let range = template.templateString.range(of: token.contents, range: token.range) ?? token.range - let line = template.templateString.rangeLine(range) + func describe(token: Token) -> String { + let templateName = token.sourceMap.filename ?? "" + let line = token.sourceMap.line let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))" - description = "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n" + return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n" + "\(line.content)\n" + "\(highlight)\n" - } else { - description = templateError.reason } - var descriptions = [description] - - var currentError: TemplateSyntaxError? = templateError - while let parentError = currentError?.parentError { - descriptions.append(renderError(parentError)) - currentError = parentError as? TemplateSyntaxError - } - - return descriptions.reversed().joined(separator: "\n") + var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] } + let description = templateError.token.map(describe(token:)) ?? templateError.reason + descriptions.append(description) + return descriptions.joined(separator: "\n") } } diff --git a/Sources/Include.swift b/Sources/Include.swift index 58bde91..8ebec9f 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -28,14 +28,12 @@ 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) - } + return try context.push { + return try template.render(context) } } catch { - if let parentError = error as? TemplateSyntaxError { - throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError) + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) } else { throw error } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index dba09cf..8cb7db2 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -2,22 +2,22 @@ class BlockContext { class var contextKey: String { return "block_context" } // contains mapping of block names to their nodes and templates where they are defined - var blocks: [String: [(BlockNode, Template?)]] + var blocks: [String: [BlockNode]] - init(blocks: [String: [(BlockNode, Template?)]]) { + init(blocks: [String: [BlockNode]]) { self.blocks = blocks } - func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) { + func pushBlock(_ block: BlockNode, named blockName: String) { if var blocks = blocks[blockName] { - blocks.append((block, template)) + blocks.append(block) self.blocks[blockName] = blocks } else { - self.blocks[blockName] = [(block, template)] + self.blocks[blockName] = [block] } } - func popBlock(named blockName: String) -> (node: BlockNode, template: Template?)? { + func popBlock(named blockName: String) -> BlockNode? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() if blocks.isEmpty { @@ -86,34 +86,31 @@ class ExtendsNode : NodeType { } let baseTemplate = try context.environment.loadTemplate(name: templateName) - let template = context.environment.template let blockContext: BlockContext if let _blockContext = context[BlockContext.contextKey] as? BlockContext { blockContext = _blockContext for (name, block) in blocks { - blockContext.pushBlock(block, named: name, definedIn: template) + blockContext.pushBlock(block, named: name) } } else { - var blocks = [String: [(BlockNode, Template?)]]() - self.blocks.forEach { blocks[$0.key] = [($0.value, template)] } + var blocks = [String: [BlockNode]]() + self.blocks.forEach { blocks[$0.key] = [$0.value] } blockContext = BlockContext(blocks: blocks) } do { // pushes base template and renders it's content // block_context contains all blocks from child templates - return try context.environment.pushTemplate(baseTemplate, token: token) { - try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try baseTemplate.render(context) - } + return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { + return try baseTemplate.render(context) } } catch { // if error template is already set (see catch in BlockNode) // and it happend in the same template as current template // there is no need to wrap it in another error - if let error = error as? TemplateSyntaxError, error.template !== context.environment.template { - throw TemplateSyntaxError(reason: error.reason, parentError: error) + if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) } else { throw error } @@ -152,7 +149,7 @@ class BlockNode : NodeType { var newContext: [String: Any] = [BlockContext.contextKey: blockContext] - if let blockSuperNode = child.node.nodes.first(where: { + if let blockSuperNode = child.nodes.first(where: { if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } else { return false} }) { @@ -160,27 +157,28 @@ class BlockNode : NodeType { // render current (base) node so that its content can be used as part of node that extends it newContext["block"] = ["super": try self.render(context)] } catch { - let baseError = context.errorReporter.reportError(error) - throw TemplateSyntaxError( - reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)", - token: blockSuperNode.token, - template: child.template, - parentError: baseError) + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError( + reason: error.reason, + token: blockSuperNode.token, + stackTrace: error.allTokens) + } else { + throw TemplateSyntaxError( + reason: "\(error)", + token: blockSuperNode.token, + stackTrace: []) + } } } // render extension node do { return try context.push(dictionary: newContext) { - return try child.node.render(context) + return try child.render(context) } } catch { - // child node belongs to child template, which is currently not on top of stack - // so we need to use node's template to report errors, not current template - // unless it's already set if var error = error as? TemplateSyntaxError { - error.template = error.template ?? child.template - error.token = error.token ?? child.node.token + error.token = error.token ?? child.token throw error } else { throw error diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index c6cc15c..caebac4 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -1,9 +1,11 @@ import Foundation struct Lexer { + let templateName: String? let templateString: String - init(templateString: String) { + init(templateName: String? = nil, templateString: String) { + self.templateName = templateName self.templateString = templateString } @@ -16,14 +18,28 @@ struct Lexer { } if string.hasPrefix("{{") { - return .variable(value: strip(), at: range) + let value = strip() + let range = templateString.range(of: value, range: range) ?? range + let line = templateString.rangeLine(range) + let sourceMap = SourceMap(filename: templateName, line: line) + return .variable(value: value, at: sourceMap) } else if string.hasPrefix("{%") { - return .block(value: strip(), at: range) + let value = strip() + let range = templateString.range(of: value, range: range) ?? range + let line = templateString.rangeLine(range) + let sourceMap = SourceMap(filename: templateName, line: line) + return .block(value: value, at: sourceMap) } else if string.hasPrefix("{#") { - return .comment(value: strip(), at: range) + let value = strip() + let range = templateString.range(of: value, range: range) ?? range + let line = templateString.rangeLine(range) + let sourceMap = SourceMap(filename: templateName, line: line) + return .comment(value: value, at: sourceMap) } - return .text(value: string, at: range) + let line = templateString.rangeLine(range) + let sourceMap = SourceMap(filename: templateName, line: line) + return .text(value: string, at: sourceMap) } /// Returns an array of tokens from a given template string. @@ -41,6 +57,7 @@ struct Lexer { while !scanner.isEmpty { if let text = scanner.scan(until: ["{{", "{%", "{#"]) { if !text.1.isEmpty { + let line = templateString.rangeLine(scanner.range) tokens.append(createToken(string: text.1, at: scanner.range)) } @@ -48,6 +65,7 @@ struct Lexer { let result = scanner.scan(until: end, returnUntil: true) tokens.append(createToken(string: result, at: scanner.range)) } else { + let line = templateString.rangeLine(scanner.range) tokens.append(createToken(string: scanner.content, at: scanner.range)) scanner.content = "" } @@ -165,7 +183,7 @@ extension String { return String(self[first..) -> (content: String, number: UInt, offset: String.IndexDistance) { + public func rangeLine(_ range: Range) -> RangeLine { var lineNumber: UInt = 0 var offset: Int = 0 var lineContent = "" @@ -183,3 +201,5 @@ extension String { return (lineContent, lineNumber, offset) } } + +public typealias RangeLine = (content: String, number: UInt, offset: String.IndexDistance) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index e2a583c..40bdab8 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -107,9 +107,11 @@ public class TokenParser { return try FilterExpression(token: filterToken, parser: self) } catch { if var error = error as? TemplateSyntaxError, error.token == nil { - // find range of filter in the containing token so that only filter is highligted, not the whole token - if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { - error.token = Token.variable(value: filterToken, at: filterTokenRange) + // find offset of filter in the containing token so that only filter is highligted, not the whole token + if let filterTokenRange = containingToken.contents.range(of: filterToken) { + var rangeLine = containingToken.sourceMap.line + rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound) + error.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine)) } else { error.token = containingToken } diff --git a/Sources/Template.swift b/Sources/Template.swift index 03176b2..0bf1c78 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -20,7 +20,7 @@ open class Template: ExpressibleByStringLiteral { self.name = name self.templateString = templateString - let lexer = Lexer(templateString: templateString) + let lexer = Lexer(templateName: name, templateString: templateString) tokens = lexer.tokenize() } diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 55482ed..7f23f0e 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -40,18 +40,34 @@ extension String { } } +public struct SourceMap: Equatable { + public let filename: String? + public let line: RangeLine + + init(filename: String? = nil, line: RangeLine = ("", 0, 0)) { + self.filename = filename + self.line = line + } + + static let unknown = SourceMap() + + public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool { + return lhs.filename == rhs.filename && lhs.line == rhs.line + } +} + public enum Token : Equatable { /// A token representing a piece of text. - case text(value: String, at: Range) + case text(value: String, at: SourceMap) /// A token representing a variable. - case variable(value: String, at: Range) + case variable(value: String, at: SourceMap) /// A token representing a comment. - case comment(value: String, at: Range) + case comment(value: String, at: SourceMap) /// A token representing a template block. - case block(value: String, at: Range) + case block(value: String, at: SourceMap) /// Returns the underlying value as an array seperated by spaces public func components() -> [String] { @@ -74,29 +90,29 @@ public enum Token : Equatable { } } - public var range: Range { + public var sourceMap: SourceMap { switch self { - case .block(_, let range), - .variable(_, let range), - .text(_, let range), - .comment(_, let range): - return range + case .block(_, let sourceMap), + .variable(_, let sourceMap), + .text(_, let sourceMap), + .comment(_, let sourceMap): + return sourceMap } } - + } public func == (lhs: Token, rhs: Token) -> Bool { switch (lhs, rhs) { - case (.text(let lhsValue), .text(let rhsValue)): - return lhsValue == rhsValue - case (.variable(let lhsValue), .variable(let rhsValue)): - return lhsValue == rhsValue - case (.block(let lhsValue), .block(let rhsValue)): - return lhsValue == rhsValue - case (.comment(let lhsValue), .comment(let rhsValue)): - return lhsValue == rhsValue + case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt + case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt + case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt + case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt default: return false } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 24f2136..ca097e1 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -41,15 +41,17 @@ func testEnvironment() { } func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let token = Token.block(value: token, at: template.templateString.range(of: token)!) - return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil) + let range = template.templateString.range(of: token)! + let rangeLine = template.templateString.rangeLine(range) + let sourceMap = SourceMap(filename: template.name, line: rangeLine) + let token = Token.block(value: token, at: sourceMap) + return TemplateSyntaxError(reason: description, token: token, stackTrace: []) } func expectError(reason: String, token: String) throws { let expectedError = expectedSyntaxError(token: token, template: template, description: reason) - let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])) - .toThrow(expectedError) + let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])).toThrow() as TemplateSyntaxError try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } @@ -200,10 +202,10 @@ func testEnvironment() { func expectError(reason: String, token: String, includedToken: String) throws { var expectedError = expectedSyntaxError(token: token, template: template, description: reason) - expectedError.parentError = expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason) + expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!] let error = try expect(environment.render(template: template, context: ["target": "World"])) - .toThrow(expectedError) + .toThrow() as TemplateSyntaxError try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } @@ -249,11 +251,10 @@ func testEnvironment() { func expectError(reason: String, childToken: String, baseToken: String?) throws { var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) if let baseToken = baseToken { - expectedError.parentError = expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason) + expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!] } - let error = try expect(environment.render(template: childTemplate, context: ["target": "World"])) - .toThrow(expectedError) + .toThrow() as TemplateSyntaxError try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } @@ -312,7 +313,7 @@ func testEnvironment() { private extension Expectation { @discardableResult - func toThrow(_ error: T) throws -> T { + func toThrow() throws -> T { var thrownError: Error? = nil do { @@ -323,12 +324,9 @@ private extension Expectation { if let thrownError = thrownError { if let thrownError = thrownError as? T { - if error != thrownError { - throw failure("\(thrownError) is not \(error)") - } return thrownError } else { - throw failure("\(thrownError) is not \(error)") + throw failure("\(thrownError) is not \(T.self)") } } else { throw failure("expression did not throw an error") diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 2501610..7898cc0 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -62,7 +62,7 @@ func testFilter() { } let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension])) - try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat")) + try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first)) } $0.it("allows you to override a default filter") { @@ -91,7 +91,7 @@ func testFilter() { describe("capitalize filter") { - let template = Template(templateString: "{{ name|capitalize }}") + let template = Template(templateString: "{{ name|capitalize }}") $0.it("capitalizes a string") { let result = try template.render(Context(dictionary: ["name": "kyle"])) diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 8d41c1d..1177b35 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -169,11 +169,9 @@ func testForNode() { } $0.it("handles invalid input") { - let tokens: [Token] = [ - .block(value: "for i", at: .unknown), - ] + let tokens: [Token] = [.block(value: "for i", at: .unknown)] let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") + let error = TemplateSyntaxError(reason: "'for' statements should use the following syntax 'for x in y where condition'.", token: tokens.first) try expect(try parser.parse()).toThrow(error) } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index 25119eb..9fada9f 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -179,22 +179,18 @@ func testIfNode() { } $0.it("throws an error when parsing an if block without an endif") { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - ] + let tokens: [Token] = [.block(value: "if value", at: .unknown)] let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError("`endif` was not found.") + let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) try expect(try parser.parse()).toThrow(error) } $0.it("throws an error when parsing an ifnot without an endif") { - let tokens: [Token] = [ - .block(value: "ifnot value", at: .unknown), - ] + let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)] let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError("`endif` was not found.") + let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) try expect(try parser.parse()).toThrow(error) } } diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index bf95c18..153d7f6 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -14,7 +14,7 @@ func testInclude() { let tokens: [Token] = [ .block(value: "include", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included") + let error = TemplateSyntaxError(reason: "'include' tag takes one argument, the template file to be included", token: tokens.first) try expect(try parser.parse()).toThrow(error) } diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index b74df68..270cb4b 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -9,7 +9,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "Hello World", at: "Hello World".range) + try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(line: ("Hello World", 1, 0))) } $0.it("can tokenize a comment") { @@ -17,7 +17,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .comment(value: "Comment", at: "{# Comment #}".range) + try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(line: ("{# Comment #}", 1, 3))) } $0.it("can tokenize a variable") { @@ -25,7 +25,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .variable(value: "Variable", at: "{{ Variable }}".range) + try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(line: ("{{ Variable }}", 1, 3))) } $0.it("can tokenize unclosed tag by ignoring it") { @@ -34,18 +34,18 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "", at: "".range) + try expect(tokens.first) == .text(value: "", at: SourceMap(line: ("{{ thing", 1, 0))) } $0.it("can tokenize a mixture of content") { - let templateString = "My name is {{ name }}." + let templateString = "My name is {{ myname }}." let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() try expect(tokens.count) == 3 - try expect(tokens[0]) == Token.text(value: "My name is ", at: templateString.range(of: "My name is ")!) - try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!) - try expect(tokens[2]) == Token.text(value: ".", at: templateString.range(of: ".")!) + try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is ")!))) + try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!))) + try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!))) } $0.it("can tokenize two variables without being greedy") { @@ -54,8 +54,8 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 2 - try expect(tokens[0]) == Token.variable(value: "thing", at: templateString.range(of: "{{ thing }}")!) - try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!) + try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "thing")!))) + try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!))) } $0.it("can tokenize an unclosed block") { diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index facd07a..25c485f 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -52,11 +52,10 @@ func testTokenParser() { } $0.it("errors when parsing an unknown tag") { - let parser = TokenParser(tokens: [ - .block(value: "unknown", at: .unknown), - ], environment: Environment()) + let tokens: [Token] = [.block(value: "unknown", at: .unknown)] + let parser = TokenParser(tokens: tokens, environment: Environment()) - try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'")) + try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first)) } } } From a165a6715f4f315ece845214b2d98ff365ff1053 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 13:29:13 +0100 Subject: [PATCH 23/32] fixed typos --- Sources/Environment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 5fb9e19..44757e7 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -43,7 +43,7 @@ public struct Environment { } func render(template: Template, context: [String: Any]?) throws -> String { - // update temaplte environment as it cen be created from string literal with default environment + // update template environment as it can be created from string literal with default environment template.environment = self return try template.render(context) } From 4bfdb73175a795d6a3f7b1301df06510fc422d5e Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 13:32:03 +0100 Subject: [PATCH 24/32] removed unneeded code --- Sources/Errors.swift | 12 ------------ Sources/Lexer.swift | 26 +++++++++----------------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 4b7cafe..03f2bb1 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -70,15 +70,3 @@ open class SimpleErrorReporter: ErrorReporter { } } - -extension Range where Bound == String.Index { - internal static var unknown: Range { - return "".range - } -} - -extension String { - var range: Range { - return startIndex.. Date: Wed, 27 Dec 2017 18:48:37 +0100 Subject: [PATCH 25/32] removed trailing witespaces --- Sources/Environment.swift | 6 +++--- Sources/Errors.swift | 18 +++++++++--------- Sources/Expression.swift | 12 ++++++------ Sources/Extension.swift | 6 +++--- Sources/ForTag.swift | 10 +++++----- Sources/IfTag.swift | 2 +- Sources/Inheritence.swift | 8 ++++---- Sources/Lexer.swift | 16 ++++++++-------- Sources/Node.swift | 2 +- Sources/Parser.swift | 4 ++-- Sources/Tokenizer.swift | 8 ++++---- Sources/Variable.swift | 10 +++++----- 12 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 44757e7..5ccbfa5 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -9,7 +9,7 @@ public struct Environment { extensions: [Extension]? = nil, templateClass: Template.Type = Template.self, errorReporter: ErrorReporter = SimpleErrorReporter()) { - + self.templateClass = templateClass self.errorReporter = errorReporter self.loader = loader @@ -41,11 +41,11 @@ public struct Environment { let template = templateClass.init(templateString: string, environment: self) return try render(template: template, context: context) } - + func render(template: Template, context: [String: Any]?) throws -> String { // update template environment as it can be created from string literal with default environment template.environment = self return try template.render(context) } - + } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 03f2bb1..4c01dfe 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -27,21 +27,21 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { var allTokens: [Token] { return stackTrace + (token.map({ [$0] }) ?? []) } - + public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { self.reason = reason self.stackTrace = stackTrace self.token = token } - + public init(_ description: String) { self.init(reason: description) } - + public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace } - + } public protocol ErrorReporter: class { @@ -49,24 +49,24 @@ public protocol ErrorReporter: class { } open class SimpleErrorReporter: ErrorReporter { - + open func renderError(_ error: Error) -> String { guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } - + func describe(token: Token) -> String { let templateName = token.sourceMap.filename ?? "" let line = token.sourceMap.line let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))" - + return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n" + "\(line.content)\n" + "\(highlight)\n" } - + var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] } let description = templateError.token.map(describe(token:)) ?? templateError.reason descriptions.append(description) return descriptions.joined(separator: "\n") } - + } diff --git a/Sources/Expression.swift b/Sources/Expression.swift index 1f41afe..afea2b1 100644 --- a/Sources/Expression.swift +++ b/Sources/Expression.swift @@ -88,21 +88,21 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { final class InExpression: Expression, InfixOperator, CustomStringConvertible { let lhs: Expression let rhs: Expression - + init(lhs: Expression, rhs: Expression) { self.lhs = lhs self.rhs = rhs } - + var description: String { return "(\(lhs) in \(rhs))" } - + func evaluate(context: Context) throws -> Bool { if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { let lhsValue = try lhs.variable.resolve(context) let rhsValue = try rhs.variable.resolve(context) - + if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] { return rhs.contains(lhs) } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { @@ -111,10 +111,10 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible { return true } } - + return false } - + } final class OrExpression: Expression, InfixOperator, CustomStringConvertible { diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 6e77aad..bfa9454 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -42,9 +42,9 @@ class DefaultExtension: Extension { registerTag("for", parser: ForNode.parse) registerTag("if", parser: IfNode.parse) registerTag("ifnot", parser: IfNode.parse_ifnot) -#if !os(Linux) - registerTag("now", parser: NowNode.parse) -#endif + #if !os(Linux) + registerTag("now", parser: NowNode.parse) + #endif registerTag("include", parser: IncludeNode.parse) registerTag("extends", parser: ExtendsNode.parse) registerTag("block", parser: BlockNode.parse) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index fcb5d68..6685d0b 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -12,8 +12,8 @@ class ForNode : NodeType { let components = token.components() guard components.count >= 3 && components[2] == "in" && - (components.count == 4 || (components.count >= 6 && components[4] == "where")) else { - throw TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") + (components.count == 4 || (components.count >= 6 && components[4] == "where")) else { + throw TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") } let loopVariables = components[1].characters @@ -38,7 +38,7 @@ class ForNode : NodeType { guard let token = parser.nextToken() else { throw TemplateSyntaxError("`endfor` was not found.") } - + if token.contents == "empty" { emptyNodes = try parser.parse(until(["endfor"])) _ = parser.nextToken() @@ -134,14 +134,14 @@ class ForNode : NodeType { "last": index == (count - 1), "counter": index + 1, "counter0": index, - ] + ] return try context.push(dictionary: ["forloop": forContext]) { return try push(value: item, context: context) { try renderNodes(nodes, context) } } - }.joined(separator: "") + }.joined(separator: "") } return try context.push { diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 07e08f9..ec7952c 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -53,7 +53,7 @@ enum IfToken { case .variable(_): return 0 case .end: - return 0 + return 0 } } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 8cb7db2..9d70e72 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -16,7 +16,7 @@ class BlockContext { self.blocks[blockName] = [block] } } - + func popBlock(named blockName: String) -> BlockNode? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() @@ -86,7 +86,7 @@ class ExtendsNode : NodeType { } let baseTemplate = try context.environment.loadTemplate(name: templateName) - + let blockContext: BlockContext if let _blockContext = context[BlockContext.contextKey] as? BlockContext { blockContext = _blockContext @@ -148,7 +148,7 @@ class BlockNode : NodeType { // child node is a block node from child template that extends this node (has the same name) var newContext: [String: Any] = [BlockContext.contextKey: blockContext] - + if let blockSuperNode = child.nodes.first(where: { if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } else { return false} @@ -170,7 +170,7 @@ class BlockNode : NodeType { } } } - + // render extension node do { return try context.push(dictionary: newContext) { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index badad26..6642f18 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -47,7 +47,7 @@ struct Lexer { "{{": "}}", "{%": "%}", "{#": "#}", - ] + ] while !scanner.isEmpty { if let text = scanner.scan(until: ["{{", "{%", "{#"]) { @@ -66,14 +66,14 @@ struct Lexer { return tokens } - + } class Scanner { let originalContent: String var content: String var range: Range - + init(_ content: String) { self.originalContent = content self.content = content @@ -83,7 +83,7 @@ class Scanner { var isEmpty: Bool { return content.isEmpty } - + func scan(until: String, returnUntil: Bool = false) -> String { var index = content.startIndex @@ -94,7 +94,7 @@ class Scanner { range = range.upperBound..) -> RangeLine { var lineNumber: UInt = 0 var offset: Int = 0 var lineContent = "" - + for line in components(separatedBy: CharacterSet.newlines) { lineNumber += 1 lineContent = line @@ -189,7 +189,7 @@ extension String { break } } - + return (lineContent, lineNumber, offset) } } diff --git a/Sources/Node.swift b/Sources/Node.swift index 79a3f34..1e8195f 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -3,7 +3,7 @@ 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 } } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 40bdab8..09c5e18 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -101,7 +101,7 @@ public class TokenParser { throw TemplateSyntaxError("Unknown filter '\(name)'") } - + public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { do { return try FilterExpression(token: filterToken, parser: self) @@ -121,7 +121,7 @@ public class TokenParser { } } } - + @available(*, deprecated, message: "Use compileFilter(_:containedIn:)") public func compileFilter(_ token: String) throws -> Resolvable { return try FilterExpression(token: token, parser: self) diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 7f23f0e..23947be 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -43,14 +43,14 @@ extension String { public struct SourceMap: Equatable { public let filename: String? public let line: RangeLine - + init(filename: String? = nil, line: RangeLine = ("", 0, 0)) { self.filename = filename self.line = line } - + static let unknown = SourceMap() - + public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool { return lhs.filename == rhs.filename && lhs.line == rhs.line } @@ -89,7 +89,7 @@ public enum Token : Equatable { return value } } - + public var sourceMap: SourceMap { switch self { case .block(_, let sourceMap), diff --git a/Sources/Variable.swift b/Sources/Variable.swift index bc40900..ee81467 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -95,11 +95,11 @@ public struct Variable : Equatable, Resolvable { current = array.count } } else if let object = current as? NSObject { // NSKeyValueCoding -#if os(Linux) - return nil -#else - current = object.value(forKey: bit) -#endif + #if os(Linux) + return nil + #else + current = object.value(forKey: bit) + #endif } else if let value = current { current = Mirror(reflecting: value).getValue(for: bit) if current == nil { From d6766b43da8c32f6c5cbfc14b8d3cb2a15f00276 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 19:29:17 +0100 Subject: [PATCH 26/32] minor code refactoring --- Sources/Errors.swift | 11 +++++ Sources/ForTag.swift | 9 ++--- Sources/Inheritence.swift | 84 +++++++++++++++++++-------------------- Sources/Node.swift | 7 +--- Sources/Parser.swift | 7 +--- 5 files changed, 58 insertions(+), 60 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 4c01dfe..407a9e2 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -44,6 +44,17 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } +extension Error { + func withToken(_ token: Token?) -> Error { + if var error = self as? TemplateSyntaxError { + error.token = error.token ?? token + return error + } else { + return TemplateSyntaxError(reason: "\(self)", token: token) + } + } +} + public protocol ErrorReporter: class { func renderError(_ error: Error) -> String } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 6685d0b..1b6b1d2 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -24,10 +24,6 @@ class ForNode : NodeType { let variable = components[3] let filter = try parser.compileFilter(variable, containedIn: token) - var emptyNodes = [NodeType]() - - let forNodes = try parser.parse(until(["endfor", "empty"])) - let `where`: Expression? if components.count >= 6 { `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) @@ -35,10 +31,13 @@ class ForNode : NodeType { `where` = nil } + let forNodes = try parser.parse(until(["endfor", "empty"])) + guard let token = parser.nextToken() else { throw TemplateSyntaxError("`endfor` was not found.") } + var emptyNodes = [NodeType]() if token.contents == "empty" { emptyNodes = try parser.parse(until(["endfor"])) _ = parser.nextToken() @@ -90,7 +89,7 @@ class ForNode : NodeType { var values: [Any] if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { - values = dictionary.map { ($0.key, $0.value) } as [(String, Any)] + values = dictionary.map { ($0.key, $0.value) } } else if let array = resolved as? [Any] { values = array } else if let range = resolved as? CountableClosedRange { diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 9d70e72..db2d67f 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -4,11 +4,12 @@ class BlockContext { // contains mapping of block names to their nodes and templates where they are defined var blocks: [String: [BlockNode]] - init(blocks: [String: [BlockNode]]) { - self.blocks = blocks + init(blocks: [String: BlockNode]) { + self.blocks = [:] + blocks.forEach { self.blocks[$0.key] = [$0.value] } } - func pushBlock(_ block: BlockNode, named blockName: String) { + func push(_ block: BlockNode, forKey blockName: String) { if var blocks = blocks[blockName] { blocks.append(block) self.blocks[blockName] = blocks @@ -17,7 +18,7 @@ class BlockContext { } } - func popBlock(named blockName: String) -> BlockNode? { + func pop(_ blockName: String) -> BlockNode? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() if blocks.isEmpty { @@ -88,14 +89,12 @@ class ExtendsNode : NodeType { let baseTemplate = try context.environment.loadTemplate(name: templateName) let blockContext: BlockContext - if let _blockContext = context[BlockContext.contextKey] as? BlockContext { - blockContext = _blockContext + if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext { + blockContext = currentBlockContext for (name, block) in blocks { - blockContext.pushBlock(block, named: name) + blockContext.push(block, forKey: name) } } else { - var blocks = [String: [BlockNode]]() - self.blocks.forEach { blocks[$0.key] = [$0.value] } blockContext = BlockContext(blocks: blocks) } @@ -144,48 +143,47 @@ class BlockNode : NodeType { } func render(_ context: Context) throws -> String { - if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.popBlock(named: name) { - // child node is a block node from child template that extends this node (has the same name) - - var newContext: [String: Any] = [BlockContext.contextKey: blockContext] - - if let blockSuperNode = child.nodes.first(where: { - if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } - else { return false} - }) { - do { - // render current (base) node so that its content can be used as part of node that extends it - newContext["block"] = ["super": try self.render(context)] - } catch { - if let error = error as? TemplateSyntaxError { - throw TemplateSyntaxError( - reason: error.reason, - token: blockSuperNode.token, - stackTrace: error.allTokens) - } else { - throw TemplateSyntaxError( - reason: "\(error)", - token: blockSuperNode.token, - stackTrace: []) - } - } - } - + if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) { + let childContext = try self.childContext(child, blockContext: blockContext, context: context) // render extension node do { - return try context.push(dictionary: newContext) { + return try context.push(dictionary: childContext) { return try child.render(context) } } catch { - if var error = error as? TemplateSyntaxError { - error.token = error.token ?? child.token - throw error - } else { - throw error - } + throw error.withToken(child.token) } } return try renderNodes(nodes, context) } + + // child node is a block node from child template that extends this node (has the same name) + func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any?] { + var childContext: [String: Any?] = [BlockContext.contextKey: blockContext] + + if let blockSuperNode = child.nodes.first(where: { + if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } + else { return false} + }) { + do { + // render base node so that its content can be used as part of child node that extends it + childContext["block"] = ["super": try self.render(context)] + } catch { + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError( + reason: error.reason, + token: blockSuperNode.token, + stackTrace: error.allTokens) + } else { + throw TemplateSyntaxError( + reason: "\(error)", + token: blockSuperNode.token, + stackTrace: []) + } + } + } + return childContext + } + } diff --git a/Sources/Node.swift b/Sources/Node.swift index 1e8195f..84a114f 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -15,12 +15,7 @@ public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String do { return try $0.render(context) } catch { - if var error = error as? TemplateSyntaxError { - error.token = error.token ?? $0.token - throw error - } else { - throw error - } + throw error.withToken($0.token) } }).joined(separator: "") } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 09c5e18..4a0b2c6 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -54,12 +54,7 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var error = error as? TemplateSyntaxError { - error.token = error.token ?? token - throw error - } else { - throw error - } + throw error.withToken(token) } } case .comment: From b54292788f618a8d0779f9c684c73e956cc758e1 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Thu, 28 Dec 2017 21:15:33 +0100 Subject: [PATCH 27/32] fixed swift 3 compiler crash --- Sources/Lexer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 6642f18..bf4d9f8 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -194,4 +194,4 @@ extension String { } } -public typealias RangeLine = (content: String, number: UInt, offset: String.IndexDistance) +public typealias RangeLine = (content: String, number: UInt, offset: Int) From 4f1a5b3e3d0f65bdf86042387959890a740273bd Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 12 Aug 2018 22:25:25 +0100 Subject: [PATCH 28/32] store reference to token when parsing range variable --- Sources/Parser.swift | 2 +- Sources/Variable.swift | 13 ++++++++++++- Tests/StencilTests/VariableSpec.swift | 8 +++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 5dde40c..fd984a4 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -148,7 +148,7 @@ public class TokenParser { } public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { - return try RangeVariable(token, parser: self) + return try RangeVariable(token, parser: self, containedIn: containingToken) ?? compileFilter(token, containedIn: containingToken) } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 1c54a55..1da7439 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -50,7 +50,7 @@ public struct Variable : Equatable, Resolvable { // Split the lookup string and resolve references if possible fileprivate func lookup(_ context: Context) throws -> [String] { - var keyPath = KeyPath(variable, in: context) + let keyPath = KeyPath(variable, in: context) return try keyPath.parse() } @@ -138,6 +138,7 @@ public struct RangeVariable: Resolvable { public let from: Resolvable public let to: Resolvable + @available(*, deprecated, message: "Use init?(_:parser:containedIn:)") public init?(_ token: String, parser: TokenParser) throws { let components = token.components(separatedBy: "...") guard components.count == 2 else { @@ -148,6 +149,16 @@ public struct RangeVariable: Resolvable { self.to = try parser.compileFilter(components[1]) } + public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws { + let components = token.components(separatedBy: "...") + guard components.count == 2 else { + return nil + } + + self.from = try parser.compileFilter(components[0], containedIn: containingToken) + self.to = try parser.compileFilter(components[1], containedIn: containingToken) + } + public func resolve(_ context: Context) throws -> Any? { let fromResolved = try from.resolve(context) let toResolved = try to.resolve(context) diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 7b386bc..ac85f1b 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -292,7 +292,9 @@ func testVariable() { }() func makeVariable(_ token: String) throws -> RangeVariable? { - return try RangeVariable(token, parser: TokenParser(tokens: [], environment: context.environment)) + let token = Token.variable(value: token, at: .unknown) + let parser = TokenParser(tokens: [token], environment: context.environment) + return try RangeVariable(token.contents, parser: parser, containedIn: token) } $0.it("can resolve closed range as array") { @@ -321,11 +323,11 @@ func testVariable() { } $0.it("throws is left range value is missing") { - try expect(makeVariable("...1")).toThrow() + try expect(makeVariable("...1")).toThrow() } $0.it("throws is right range value is missing") { - try expect(makeVariable("1...")).toThrow() + try expect(makeVariable("1...")).toThrow() } } From b9702afbd46d676fbe5d4e141703a389a9b81457 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 13 Aug 2018 20:00:27 +0100 Subject: [PATCH 29/32] fixed indetnations --- Sources/ForTag.swift | 6 ++-- Sources/Lexer.swift | 3 +- Sources/Node.swift | 4 +-- Sources/Parser.swift | 21 ++++++------ Tests/StencilTests/ForNodeSpec.swift | 50 ++++++++++++++-------------- Tests/StencilTests/LexerSpec.swift | 2 +- 6 files changed, 42 insertions(+), 44 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index d5bfee7..3432b95 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -31,8 +31,8 @@ class ForNode : NodeType { let resolvable = try parser.compileResolvable(components[3], containedIn: token) let `where` = hasToken("where", at: 4) - ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) - : nil + ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) + : nil let forNodes = try parser.parse(until(["endfor", "empty"])) @@ -145,7 +145,7 @@ class ForNode : NodeType { try renderNodes(nodes, context) } } - }.joined(separator: "") + }.joined(separator: "") } return try context.push { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 4c8f8a8..ec833d5 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -189,8 +189,7 @@ extension String { lineNumber += 1 lineContent = line if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) { - offset = distance(from: rangeOfLine.lowerBound, to: - range.lowerBound) + offset = distance(from: rangeOfLine.lowerBound, to: range.lowerBound) break } } diff --git a/Sources/Node.swift b/Sources/Node.swift index c0e644c..c4bb77a 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -11,13 +11,13 @@ public protocol NodeType { /// Render the collection of nodes in the given context public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String { - return try nodes.map({ + return try nodes.map { do { return try $0.render(context) } catch { throw error.withToken($0.token) } - }).joined(separator: "") + }.joined(separator: "") } public class SimpleNode : NodeType { diff --git a/Sources/Parser.swift b/Sources/Parser.swift index fd984a4..81d9335 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -120,19 +120,18 @@ public class TokenParser { do { return try FilterExpression(token: filterToken, parser: self) } catch { - if var error = error as? TemplateSyntaxError, error.token == nil { - // find offset of filter in the containing token so that only filter is highligted, not the whole token - if let filterTokenRange = containingToken.contents.range(of: filterToken) { - var rangeLine = containingToken.sourceMap.line - rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound) - error.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine)) - } else { - error.token = containingToken - } - throw error - } else { + guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else { throw error } + // find offset of filter in the containing token so that only filter is highligted, not the whole token + if let filterTokenRange = containingToken.contents.range(of: filterToken) { + var rangeLine = containingToken.sourceMap.line + rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound) + syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine)) + } else { + syntaxError.token = containingToken + } + throw syntaxError } } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 4fb1a04..ddb7692 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -13,7 +13,7 @@ func testForNode() { "two": "II", ], "tuples": [(1, 2, 3), (4, 5, 6)] - ]) + ]) $0.it("renders the given nodes for each item") { let nodes: [NodeType] = [VariableNode(variable: "item")] @@ -31,7 +31,7 @@ func testForNode() { $0.it("renders a context variable of type Array") { let any_context = Context(dictionary: [ "items": ([1, 2, 3] as [Any]) - ]) + ]) let nodes: [NodeType] = [VariableNode(variable: "item")] let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) @@ -54,17 +54,17 @@ func testForNode() { try expect(try node.render(context)) == "123" } -#if os(OSX) + #if os(OSX) $0.it("renders a context variable of type NSArray") { let nsarray_context = Context(dictionary: [ "items": NSArray(array: [1, 2, 3]) - ]) + ]) let nodes: [NodeType] = [VariableNode(variable: "item")] let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) try expect(try node.render(nsarray_context)) == "123" } -#endif + #endif $0.it("renders the given nodes while providing if the item is first in the context") { let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")] @@ -97,31 +97,31 @@ func testForNode() { } $0.it("renders the given nodes while filtering items using where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) - try expect(try node.render(context)) == "2132" + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) + try expect(try node.render(context)) == "2132" } $0.it("renders the given empty nodes when all items filtered out with where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let nodes: [NodeType] = [VariableNode(variable: "item")] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) - let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) - try expect(try node.render(context)) == "empty" + let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) + try expect(try node.render(context)) == "empty" } $0.it("can render a filter with spaces") { let templateString = "{% for article in ars | default: a, b , articles %}" + "- {{ article.title }} by {{ article.author }}.\n" + - "{% endfor %}\n" + "{% endfor %}\n" let context = Context(dictionary: [ "articles": [ Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), Article(title: "Memory Management with ARC", author: "Kyle Fuller"), ] - ]) + ]) let template = Template(templateString: templateString) let result = try template.render(context) @@ -129,7 +129,7 @@ func testForNode() { let fixture = "" + "- Migrating from OCUnit to XCTest by Kyle Fuller.\n" + "- Memory Management with ARC by Kyle Fuller.\n" + - "\n" + "\n" try expect(result) == fixture } @@ -184,7 +184,7 @@ func testForNode() { $0.it("can iterate over dictionary") { let templateString = "{% for key, value in dict %}" + "{{ key }}: {{ value }}," + - "{% endfor %}" + "{% endfor %}" let template = Template(templateString: templateString) let result = try template.render(context) @@ -197,7 +197,7 @@ func testForNode() { let nodes: [NodeType] = [ VariableNode(variable: "key"), TextNode(text: ","), - ] + ] let emptyNodes: [NodeType] = [TextNode(text: "empty")] let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil) let result = try node.render(context) @@ -212,7 +212,7 @@ func testForNode() { TextNode(text: "="), VariableNode(variable: "value"), TextNode(text: ","), - ] + ] let emptyNodes: [NodeType] = [TextNode(text: "empty")] let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil) @@ -237,14 +237,14 @@ func testForNode() { let context = Context(dictionary: [ "struct": MyStruct(string: "abc", number: 123) - ]) + ]) let nodes: [NodeType] = [ VariableNode(variable: "property"), TextNode(text: "="), VariableNode(variable: "value"), TextNode(text: "\n"), - ] + ] let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: []) let result = try node.render(context) @@ -254,14 +254,14 @@ func testForNode() { $0.it("can iterate tuple items") { let context = Context(dictionary: [ "tuple": (one: 1, two: "dva"), - ]) + ]) let nodes: [NodeType] = [ VariableNode(variable: "label"), TextNode(text: "="), VariableNode(variable: "value"), TextNode(text: "\n"), - ] + ] let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) let result = try node.render(context) @@ -289,15 +289,15 @@ func testForNode() { let context = Context(dictionary: [ "class": MySubclass("child", "base", 1) - ]) + ]) let nodes: [NodeType] = [ VariableNode(variable: "label"), TextNode(text: "="), VariableNode(variable: "value"), TextNode(text: "\n"), - ] - + ] + let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) let result = try node.render(context) diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 9fe8df0..2a9f1e1 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -77,7 +77,7 @@ func testLexer() { "%}{{\n" + "name\n" + "}}{%\n" + - "endif %}." + "endif %}." let lexer = Lexer(templateString: templateString) From 71ad162268028ac3acaec1dab04aa8e748c3126f Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 13 Aug 2018 20:02:07 +0100 Subject: [PATCH 30/32] more indentations fixed --- Sources/ForTag.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 3432b95..284498f 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -46,10 +46,10 @@ class ForNode : NodeType { _ = parser.nextToken() } - return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token) + return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token) } - init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil, token: Token? = 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 From 92ebfe59b1fda57ec4f5acd17d6924d6bd1a3110 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 13 Aug 2018 23:05:33 +0100 Subject: [PATCH 31/32] removed unneeded error reporter references --- Sources/Context.swift | 4 ---- Sources/Environment.swift | 5 +---- Tests/StencilTests/EnvironmentSpec.swift | 9 ++++++--- Tests/StencilTests/FilterSpec.swift | 4 ++-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Sources/Context.swift b/Sources/Context.swift index a976e1e..157f230 100644 --- a/Sources/Context.swift +++ b/Sources/Context.swift @@ -4,10 +4,6 @@ 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] diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 5ccbfa5..2778a5d 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -3,15 +3,12 @@ public struct Environment { public var extensions: [Extension] public var loader: Loader? - public var errorReporter: ErrorReporter public init(loader: Loader? = nil, extensions: [Extension]? = nil, - templateClass: Template.Type = Template.self, - errorReporter: ErrorReporter = SimpleErrorReporter()) { + templateClass: Template.Type = Template.self) { self.templateClass = templateClass - self.errorReporter = errorReporter self.loader = loader self.extensions = (extensions ?? []) + [DefaultExtension()] } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 8fd4684..5dc6796 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -54,7 +54,8 @@ func testEnvironment() { let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.context("given syntax error") { @@ -209,7 +210,8 @@ func testEnvironment() { let error = try expect(environment.render(template: template, context: ["target": "World"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.it("reports syntax error in included template") { @@ -259,7 +261,8 @@ func testEnvironment() { } let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.it("reports syntax error in base template") { diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 710d5ae..1de6a8e 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -232,8 +232,8 @@ func testFilter() { let error = try expect(environment.render(template: template, context: [:]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.it("made for unknown filter") { From 96a004eb3426d3c25df6f88774d1bf773f9af026 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 14 Aug 2018 00:56:10 +0100 Subject: [PATCH 32/32] replace implicitly unwrapped optional with fatalError --- Tests/StencilTests/EnvironmentSpec.swift | 4 +++- Tests/StencilTests/FilterSpec.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 5dc6796..aa68c3a 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -41,7 +41,9 @@ func testEnvironment() { } func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let range = template.templateString.range(of: token)! + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } let rangeLine = template.templateString.rangeLine(range) let sourceMap = SourceMap(filename: template.name, line: rangeLine) let token = Token.block(value: token, at: sourceMap) diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 1de6a8e..6c9139f 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -218,7 +218,9 @@ func testFilter() { var filterExtension: Extension! func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let range = template.templateString.range(of: token)! + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } let rangeLine = template.templateString.rangeLine(range) let sourceMap = SourceMap(filename: template.name, line: rangeLine) let token = Token.block(value: token, at: sourceMap)