Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Errors logs improvements #167

Merged
merged 33 commits into from Aug 15, 2018
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6300dbc
improved template syntax errors with file, line number and failed tok…
ilyapuchka Oct 3, 2017
69cd8e4
replaced Token with Lexeme protocol on TemplateSyntaxError
ilyapuchka Oct 7, 2017
0edb385
added ErrorReporter protocol with default implementation
ilyapuchka Oct 7, 2017
d5f0be9
using error reporter from environment to handle syntax errors
ilyapuchka Oct 7, 2017
e59609f
handling unknown filter errors
ilyapuchka Oct 7, 2017
079fdf3
added parent context to ErrorReporterContext and handling errors in i…
ilyapuchka Oct 7, 2017
7688326
renamed Scanner property for original content
ilyapuchka Oct 7, 2017
97ab3cf
xcode backward compatibility fixes
ilyapuchka Oct 7, 2017
5878c32
fixed iterating over template lines on linux
ilyapuchka Oct 7, 2017
27135f3
changer Never return type to Error in ErrorReporter
ilyapuchka Nov 29, 2017
53c1550
reporting node rendering errors using reference to node’s token
ilyapuchka Dec 23, 2017
9a28142
reporting error with its parent context
ilyapuchka Dec 24, 2017
c486617
fixed reporting errors in child templates
ilyapuchka Dec 24, 2017
bb3f337
unified setting higlighting range for errors
ilyapuchka Dec 25, 2017
ea7e1ef
fixed highlighting of errors happening in {{ block.super }}
ilyapuchka Dec 25, 2017
218822f
updated CHANGELOG
ilyapuchka Dec 25, 2017
8d68edd
replaced Lexeme protocol with Token
ilyapuchka Dec 26, 2017
7756522
fixed error on swift 3.1
ilyapuchka Dec 26, 2017
ed885f4
refactored environment tests
ilyapuchka Dec 26, 2017
abeb30b
fix rendering templates created from string literals
ilyapuchka Dec 26, 2017
cb12431
removed unneeded changes
ilyapuchka Dec 26, 2017
ac2fd56
storing full sourcemap in token, refactored error reporting
ilyapuchka Dec 27, 2017
a165a67
fixed typos
ilyapuchka Dec 27, 2017
4bfdb73
removed unneeded code
ilyapuchka Dec 27, 2017
662849e
removed trailing witespaces
ilyapuchka Dec 27, 2017
d6766b4
minor code refactoring
ilyapuchka Dec 27, 2017
b542927
fixed swift 3 compiler crash
ilyapuchka Dec 28, 2017
3a4cd8a
Merge branch 'master' into errors-logs-improvements
ilyapuchka Aug 12, 2018
4f1a5b3
store reference to token when parsing range variable
ilyapuchka Aug 12, 2018
b9702af
fixed indetnations
ilyapuchka Aug 13, 2018
71ad162
more indentations fixed
ilyapuchka Aug 13, 2018
92ebfe5
removed unneeded error reporter references
ilyapuchka Aug 13, 2018
96a004e
replace implicitly unwrapped optional with fatalError
ilyapuchka Aug 13, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
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
4 changes: 4 additions & 0 deletions Sources/Context.swift
Expand Up @@ -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 {
Expand Down
17 changes: 15 additions & 2 deletions Sources/Environment.swift
Expand Up @@ -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,
errorReporter: ErrorReporter = SimpleErrorReporter()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, the errorReporter property is only used by context's errorReporter property. But that isn't used anywhere (I may have missed this)?

What is the reason that the Environment and Context should hold the errorReporter? If I understand how this is intended to be used, a consumer of the library would catch an error and pass it to the errorReporter themselves? In that case I think it would make sense to remove the property from Environment and Context leaving the consumer free to create their own ErrorReporter as desired. Decoupling these a little bit.


public init(loader: Loader? = nil, extensions: [Extension]? = nil, templateClass: Template.Type = Template.self) {
self.templateClass = templateClass
self.errorReporter = errorReporter
self.loader = loader
self.extensions = (extensions ?? []) + [DefaultExtension()]
}
Expand All @@ -28,11 +34,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
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
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
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
@@ -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
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