From e59609f1404deaf2ffd13d4533486c71f1567dcc Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 23:10:27 +0200 Subject: [PATCH] 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" }