Skip to content

Commit

Permalink
Merge pull request #167 from stencilproject/errors-logs-improvements
Browse files Browse the repository at this point in the history
Errors logs improvements
  • Loading branch information
djbe authored Aug 15, 2018
2 parents 4f14b4b + 96a004e commit ffe8f9d
Show file tree
Hide file tree
Showing 36 changed files with 911 additions and 325 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
[David Jennes](https://github.com/djbe)
[#215](https://github.com/stencilproject/Stencil/pull/215)
- Adds support for using spaces in filter expression.
[Ilya Puchka](https://github.com/yonaskolb)
[Ilya Puchka](https://github.com/ilyapuchka)
[#178](https://github.com/stencilproject/Stencil/pull/178)
- Improvements in error reporting.
[Ilya Puchka](https://github.com/ilyapuchka)
[#167](https://github.com/stencilproject/Stencil/pull/167)

### Bug Fixes

Expand Down
2 changes: 1 addition & 1 deletion Sources/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ public class Context {
var dictionaries: [[String: Any?]]

public let environment: Environment

init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
if let dictionary = dictionary {
dictionaries = [dictionary]
Expand Down
14 changes: 12 additions & 2 deletions Sources/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ public struct Environment {

public var loader: Loader?

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) {

self.templateClass = templateClass
self.loader = loader
self.extensions = (extensions ?? []) + [DefaultExtension()]
Expand All @@ -28,11 +31,18 @@ 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 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)
}

}
64 changes: 64 additions & 0 deletions Sources/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,67 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
return "Template named `\(templates)` does not exist. No loaders found"
}
}

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 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, 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
}

}

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
}

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")
}

}
12 changes: 6 additions & 6 deletions Sources/Expression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? Int, let rhs = rhsValue as? CountableClosedRange<Int> {
Expand All @@ -115,10 +115,10 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {
return true
}
}

return false
}

}

final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
Expand Down
8 changes: 4 additions & 4 deletions Sources/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}

Expand All @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions Sources/FilterTag.swift
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -15,20 +16,21 @@ class FilterNode : NodeType {
throw TemplateSyntaxError("`endfilter` was not found.")
}

let resolvable = try parser.compileFilter("filter_value|\(bits[1])")
return FilterNode(nodes: blocks, resolvable: resolvable)
let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token)
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)
}
}
}
Expand Down
24 changes: 13 additions & 11 deletions Sources/ForTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,56 @@ 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()

func hasToken(_ token: String, at index: Int) -> Bool {
return components.count > (index + 1) && components[index] == token
}

func endsOrHasToken(_ token: String, at index: Int) -> Bool {
return components.count == index || hasToken(token, at: index)
}

guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.")
}

let loopVariables = components[1].characters
.split(separator: ",")
.map(String.init)
.map { $0.trim(character: " ") }

var emptyNodes = [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

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()
}

let resolvable = try parser.compileResolvable(components[3])

let `where` = hasToken("where", at: 4)
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
: nil

return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
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) {
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<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
Expand Down Expand Up @@ -143,7 +145,7 @@ class ForNode : NodeType {
try renderNodes(nodes, context)
}
}
}.joined(separator: "")
}.joined(separator: "")
}

return try context.push {
Expand Down
Loading

0 comments on commit ffe8f9d

Please sign in to comment.