diff --git a/.swiftlint.yml b/.swiftlint.yml index d227e2a..f8f3d47 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,90 +1,112 @@ swiftlint_version: 0.48.0 -disabled_rules: - # Remove this once we remove old swift support - - implicit_return - opt_in_rules: - - anyobject_protocol - - array_init - - attributes - - closure_body_length - - closure_end_indentation - - closure_spacing - - collection_alignment - - contains_over_filter_count - - contains_over_filter_is_empty - - contains_over_first_not_nil - - contains_over_range_nil_comparison - - convenience_type - - discouraged_optional_boolean - - discouraged_optional_collection - - duplicate_enum_cases - - duplicate_imports - - empty_collection_literal - - empty_count - - empty_string - - fallthrough - - fatal_error_message - - first_where - - flatmap_over_map_reduce - - force_unwrapping - - identical_operands - - inert_defer - - joined_default_parameter - - last_where - - legacy_hashing - - legacy_random - - literal_expression_end_indentation - - lower_acl_than_parent - - modifier_order - - multiline_arguments - - multiline_function_chains - - multiline_literal_brackets - - multiline_parameters - - multiline_parameters_brackets - - nslocalizedstring_key - - nsobject_prefer_isequal - - number_separator - - object_literal - - operator_usage_whitespace - - overridden_super_call - - override_in_extension - - prefer_self_type_over_type_of_self - - private_action - - private_outlet - - prohibited_super_call - - raw_value_for_camel_cased_codable_enum - - reduce_boolean - - reduce_into - - redundant_nil_coalescing - - redundant_objc_attribute - - sorted_first_last - - sorted_imports - - static_operator - - strong_iboutlet - - toggle_bool - - trailing_closure - - unavailable_function - - unneeded_parentheses_in_closure_argument - - unowned_variable_capture - - unused_capture_list - - unused_control_flow_label - - unused_declaration - - unused_setter_value - - vertical_parameter_alignment_on_call - - vertical_whitespace_closing_braces - - vertical_whitespace_opening_braces - - xct_specific_matcher - - yoda_condition - # Enable this again once we remove old swift support - # - optional_enum_case_matching - # - legacy_multiple + - accessibility_label_for_image + - anonymous_argument_in_multiline_closure + - anyobject_protocol + - array_init + - attributes + - balanced_xctest_lifecycle + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - comment_spacing + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discarded_notification_center_observer + - discouraged_assert + - discouraged_none_name + - discouraged_optional_boolean + - discouraged_optional_collection + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - fallthrough + - fatal_error_message + - file_header + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - inclusive_language + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_objc_type + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - prefer_self_in_static_references + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - prefixed_toplevel_constant + - private_action + - private_outlet + - private_subject + - prohibited_super_call + - raw_value_for_camel_cased_codable_enum + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - return_value_from_void_function + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - switch_case_on_newline + - test_case_accessibility + - toggle_bool + - trailing_closure + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - unused_closure_parameter + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - void_function_in_ternary + - weak_delegate + - xct_specific_matcher + - yoda_condition # Rules customization closure_body_length: warning: 25 +conditional_returns_on_newline: + if_only: true + +indentation_width: + indentation_width: 2 + line_length: warning: 120 error: 200 @@ -92,8 +114,3 @@ line_length: nesting: type_level: warning: 2 - -# Exclude generated files -excluded: - - .build - - Tests/StencilTests/XCTestManifests.swift diff --git a/Sources/Context.swift b/Sources/Context.swift index 734518a..1858694 100644 --- a/Sources/Context.swift +++ b/Sources/Context.swift @@ -2,8 +2,14 @@ public class Context { var dictionaries: [[String: Any?]] + /// The context's environment, such as registered extensions, classes, … public let environment: Environment + /// Create a context from a dictionary (and an env.) + /// + /// - Parameters: + /// - dictionary: The context's data + /// - environment: Environment such as extensions, … public init(dictionary: [String: Any] = [:], environment: Environment? = nil) { if !dictionary.isEmpty { dictionaries = [dictionary] @@ -14,6 +20,7 @@ public class Context { self.environment = environment ?? Environment() } + /// Access variables in this context by name public subscript(key: String) -> Any? { /// Retrieves a variable's value, starting at the current context and going upwards get { @@ -36,22 +43,35 @@ public class Context { } /// Push a new level into the Context + /// + /// - Parameters: + /// - dictionary: The new level data fileprivate func push(_ dictionary: [String: Any] = [:]) { dictionaries.append(dictionary) } /// Pop the last level off of the Context + /// + /// - returns: The popped level fileprivate func pop() -> [String: Any?]? { - return dictionaries.popLast() + dictionaries.popLast() } /// Push a new level onto the context for the duration of the execution of the given closure + /// + /// - Parameters: + /// - dictionary: The new level data + /// - closure: The closure to execute + /// - returns: Return value of the closure public func push(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result { push(dictionary) defer { _ = pop() } return try closure() } + /// Flatten all levels of context data into 1, merging duplicate variables + /// + /// - returns: All collected variables public func flatten() -> [String: Any] { var accumulator: [String: Any] = [:] diff --git a/Sources/DynamicMemberLookup.swift b/Sources/DynamicMemberLookup.swift index 47aae01..be1eec8 100644 --- a/Sources/DynamicMemberLookup.swift +++ b/Sources/DynamicMemberLookup.swift @@ -6,10 +6,13 @@ public protocol DynamicMemberLookup { } public extension DynamicMemberLookup where Self: RawRepresentable { + /// Get a value for a given `String` key subscript(dynamicMember member: String) -> Any? { switch member { - case "rawValue": return rawValue - default: return nil + case "rawValue": + return rawValue + default: + return nil } } } diff --git a/Sources/Environment.swift b/Sources/Environment.swift index bdc0fbd..97c9cc1 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -1,8 +1,18 @@ +/// Container for environment data, such as registered extensions public struct Environment { + /// The class for loading new templates public let templateClass: Template.Type + /// List of registered extensions public var extensions: [Extension] + /// Mechanism for loading new files public var loader: Loader? + /// Basic initializer + /// + /// - Parameters: + /// - loader: Mechanism for loading new files + /// - extensions: List of extension containers + /// - templateClass: Class for newly loaded templates public init( loader: Loader? = nil, extensions: [Extension] = [], @@ -13,6 +23,11 @@ public struct Environment { self.extensions = extensions + [DefaultExtension()] } + /// Load a template with the given name + /// + /// - Parameters: + /// - name: Name of the template + /// - returns: Loaded template instance public func loadTemplate(name: String) throws -> Template { if let loader = loader { return try loader.loadTemplate(name: name, environment: self) @@ -21,6 +36,11 @@ public struct Environment { } } + /// Load a template with the given names + /// + /// - Parameters: + /// - names: Names of the template + /// - returns: Loaded template instance public func loadTemplate(names: [String]) throws -> Template { if let loader = loader { return try loader.loadTemplate(names: names, environment: self) @@ -29,11 +49,23 @@ public struct Environment { } } + /// Render a template with the given name, providing some data + /// + /// - Parameters: + /// - name: Name of the template + /// - context: Data for rendering + /// - returns: Rendered output public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String { let template = try loadTemplate(name: name) return try render(template: template, context: context) } + /// Render the given template string, providing some data + /// + /// - Parameters: + /// - string: Template string + /// - context: Data for rendering + /// - returns: Rendered output public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String { let template = templateClass.init(templateString: string, environment: self) return try render(template: template, context: context) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 2c54519..423fe87 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -20,12 +20,12 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible { public let reason: String - public var description: String { return reason } + public var description: String { reason } public internal(set) var token: Token? public internal(set) var stackTrace: [Token] - public var templateName: String? { return token?.sourceMap.filename } + public var templateName: String? { token?.sourceMap.filename } var allTokens: [Token] { - return stackTrace + (token.map { [$0] } ?? []) + stackTrace + (token.map { [$0] } ?? []) } public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { diff --git a/Sources/Expression.swift b/Sources/Expression.swift index 1505209..1af0102 100644 --- a/Sources/Expression.swift +++ b/Sources/Expression.swift @@ -18,11 +18,11 @@ final class StaticExpression: Expression, CustomStringConvertible { } func evaluate(context: Context) throws -> Bool { - return value + value } var description: String { - return "\(value)" + "\(value)" } } @@ -34,7 +34,7 @@ final class VariableExpression: Expression, CustomStringConvertible { } var description: String { - return "(variable: \(variable))" + "(variable: \(variable))" } /// Resolves a variable in the given context as boolean @@ -60,7 +60,7 @@ final class VariableExpression: Expression, CustomStringConvertible { } func evaluate(context: Context) throws -> Bool { - return try resolve(context: context, variable: variable) + try resolve(context: context, variable: variable) } } @@ -72,11 +72,11 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { } var description: String { - return "not \(expression)" + "not \(expression)" } func evaluate(context: Context) throws -> Bool { - return try !expression.evaluate(context: context) + try !expression.evaluate(context: context) } } @@ -90,7 +90,7 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible { } var description: String { - return "(\(lhs) in \(rhs))" + "(\(lhs) in \(rhs))" } func evaluate(context: Context) throws -> Bool { @@ -125,7 +125,7 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible { } var description: String { - return "(\(lhs) or \(rhs))" + "(\(lhs) or \(rhs))" } func evaluate(context: Context) throws -> Bool { @@ -148,7 +148,7 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible { } var description: String { - return "(\(lhs) and \(rhs))" + "(\(lhs) and \(rhs))" } func evaluate(context: Context) throws -> Bool { @@ -171,7 +171,7 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible { } var description: String { - return "(\(lhs) == \(rhs))" + "(\(lhs) == \(rhs))" } func evaluate(context: Context) throws -> Bool { @@ -206,7 +206,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible { } var description: String { - return "(\(lhs) \(symbol) \(rhs))" + "(\(lhs) \(symbol) \(rhs))" } func evaluate(context: Context) throws -> Bool { @@ -225,97 +225,97 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible { } var symbol: String { - return "" + "" } func compare(lhs: Number, rhs: Number) -> Bool { - return false + false } } class MoreThanExpression: NumericExpression { override var symbol: String { - return ">" + ">" } override func compare(lhs: Number, rhs: Number) -> Bool { - return lhs > rhs + lhs > rhs } } class MoreThanEqualExpression: NumericExpression { override var symbol: String { - return ">=" + ">=" } override func compare(lhs: Number, rhs: Number) -> Bool { - return lhs >= rhs + lhs >= rhs } } class LessThanExpression: NumericExpression { override var symbol: String { - return "<" + "<" } override func compare(lhs: Number, rhs: Number) -> Bool { - return lhs < rhs + lhs < rhs } } class LessThanEqualExpression: NumericExpression { override var symbol: String { - return "<=" + "<=" } override func compare(lhs: Number, rhs: Number) -> Bool { - return lhs <= rhs + lhs <= rhs } } class InequalityExpression: EqualityExpression { override var description: String { - return "(\(lhs) != \(rhs))" + "(\(lhs) != \(rhs))" } override func evaluate(context: Context) throws -> Bool { - return try !super.evaluate(context: context) + try !super.evaluate(context: context) } } // swiftlint:disable:next cyclomatic_complexity func toNumber(value: Any) -> Number? { - if let value = value as? Float { - return Number(value) - } else if let value = value as? Double { - return Number(value) - } else if let value = value as? UInt { - return Number(value) - } else if let value = value as? Int { - return Number(value) - } else if let value = value as? Int8 { - return Number(value) - } else if let value = value as? Int16 { - return Number(value) - } else if let value = value as? Int32 { - return Number(value) - } else if let value = value as? Int64 { - return Number(value) - } else if let value = value as? UInt8 { - return Number(value) - } else if let value = value as? UInt16 { - return Number(value) - } else if let value = value as? UInt32 { - return Number(value) - } else if let value = value as? UInt64 { - return Number(value) - } else if let value = value as? Number { - return value - } else if let value = value as? Float64 { - return Number(value) - } else if let value = value as? Float32 { - return Number(value) - } + if let value = value as? Float { + return Number(value) + } else if let value = value as? Double { + return Number(value) + } else if let value = value as? UInt { + return Number(value) + } else if let value = value as? Int { + return Number(value) + } else if let value = value as? Int8 { + return Number(value) + } else if let value = value as? Int16 { + return Number(value) + } else if let value = value as? Int32 { + return Number(value) + } else if let value = value as? Int64 { + return Number(value) + } else if let value = value as? UInt8 { + return Number(value) + } else if let value = value as? UInt16 { + return Number(value) + } else if let value = value as? UInt32 { + return Number(value) + } else if let value = value as? UInt64 { + return Number(value) + } else if let value = value as? Number { + return value + } else if let value = value as? Float64 { + return Number(value) + } else if let value = value as? Float32 { + return Number(value) + } - return nil + return nil } diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 26bfae7..20f6d5d 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -1,9 +1,11 @@ +/// Container for registered tags and filters open class Extension { typealias TagParser = (TokenParser, Token) throws -> NodeType - var tags = [String: TagParser]() + var tags = [String: TagParser]() var filters = [String: Filter]() + /// Simple initializer public init() { } @@ -20,11 +22,11 @@ open class Extension { } /// Registers boolean filter with it's negative counterpart - // swiftlint:disable:next discouraged_optional_boolean public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) { + // swiftlint:disable:previous discouraged_optional_boolean filters[name] = .simple(filter) - filters[negativeFilterName] = .simple { - guard let result = try filter($0) else { return nil } + filters[negativeFilterName] = .simple { value in + guard let result = try filter(value) else { return nil } return !result } } diff --git a/Sources/Filters.swift b/Sources/Filters.swift index b44f93e..2a719e4 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -74,9 +74,11 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { var indentWidth = 4 if !arguments.isEmpty { guard let value = arguments[0] as? Int else { - throw TemplateSyntaxError(""" + throw TemplateSyntaxError( + """ 'indent' filter width argument must be an Integer (\(String(describing: arguments[0]))) - """) + """ + ) } indentWidth = value } @@ -84,9 +86,11 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { var indentationChar = " " if arguments.count > 1 { guard let value = arguments[1] as? String else { - throw TemplateSyntaxError(""" + throw TemplateSyntaxError( + """ 'indent' filter indentation argument must be a String (\(String(describing: arguments[1])) - """) + """ + ) } indentationChar = value } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index f727324..4c0ad49 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -12,11 +12,11 @@ class ForNode: NodeType { let components = token.components func hasToken(_ token: String, at index: Int) -> Bool { - return components.count > (index + 1) && components[index] == token + components.count > (index + 1) && components[index] == token } func endsOrHasToken(_ token: String, at index: Int) -> Bool { - return components.count == index || hasToken(token, at: index) + components.count == index || hasToken(token, at: index) } guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else { @@ -154,9 +154,9 @@ class ForNode: NodeType { } else if let resolved = resolved { let mirror = Mirror(reflecting: resolved) switch mirror.displayStyle { - case .struct?, .tuple?: + case .struct, .tuple: values = Array(mirror.children) - case .class?: + case .class: var children = Array(mirror.children) var currentMirror: Mirror? = mirror while let superclassMirror = currentMirror?.superclassMirror { diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 061914a..bde3354 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -10,23 +10,23 @@ enum Operator { return name } } + + static let all: [Operator] = [ + .infix("in", 5, InExpression.self), + .infix("or", 6, OrExpression.self), + .infix("and", 7, AndExpression.self), + .prefix("not", 8, NotExpression.self), + .infix("==", 10, EqualityExpression.self), + .infix("!=", 10, InequalityExpression.self), + .infix(">", 10, MoreThanExpression.self), + .infix(">=", 10, MoreThanEqualExpression.self), + .infix("<", 10, LessThanExpression.self), + .infix("<=", 10, LessThanEqualExpression.self) + ] } -let operators: [Operator] = [ - .infix("in", 5, InExpression.self), - .infix("or", 6, OrExpression.self), - .infix("and", 7, AndExpression.self), - .prefix("not", 8, NotExpression.self), - .infix("==", 10, EqualityExpression.self), - .infix("!=", 10, InequalityExpression.self), - .infix(">", 10, MoreThanExpression.self), - .infix(">=", 10, MoreThanEqualExpression.self), - .infix("<", 10, LessThanExpression.self), - .infix("<=", 10, LessThanEqualExpression.self) -] - func findOperator(name: String) -> Operator? { - for `operator` in operators where `operator`.name == name { + for `operator` in Operator.all where `operator`.name == name { return `operator` } @@ -106,7 +106,7 @@ final class IfExpressionParser { } static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser { - return try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) + try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) } private init(components: ArraySlice, environment: Environment, token: Token) throws { @@ -117,7 +117,7 @@ final class IfExpressionParser { if component == "(" { bracketsBalance += 1 - let (expression, parsedCount) = try IfExpressionParser.subExpression( + let (expression, parsedCount) = try Self.subExpression( from: components.suffix(from: index + 1), environment: environment, token: token @@ -152,11 +152,11 @@ final class IfExpressionParser { token: Token ) throws -> (Expression, Int) { var bracketsBalance = 1 - let subComponents = components.prefix { - if $0 == "(" { - bracketsBalance += 1 - } else if $0 == ")" { - bracketsBalance -= 1 + let subComponents = components.prefix { component in + if component == "(" { + bracketsBalance += 1 + } else if component == ")" { + bracketsBalance -= 1 } return bracketsBalance != 0 } @@ -220,7 +220,7 @@ final class IfCondition { } func render(_ context: Context) throws -> String { - return try context.push { + try context.push { try renderNodes(nodes, context) } } diff --git a/Sources/Include.swift b/Sources/Include.swift index 9d49ed3..48f422c 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -9,11 +9,13 @@ class IncludeNode: NodeType { let bits = token.components guard bits.count == 2 || bits.count == 3 else { - throw TemplateSyntaxError(""" + throw TemplateSyntaxError( + """ 'include' tag requires one argument, the template file to be included. \ A second optional argument can be used to specify the context that will \ be passed to the included file - """) + """ + ) } return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token) diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 33950fd..1f75e9b 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -1,5 +1,5 @@ class BlockContext { - class var contextKey: String { return "block_context" } + class var contextKey: String { "block_context" } // contains mapping of block names to their nodes and templates where they are defined var blocks: [String: [BlockNode]] diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index d24533a..28b4e5e 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -23,10 +23,10 @@ struct Lexer { self.templateName = templateName self.templateString = templateString - self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap { - guard !$0.element.isEmpty, - let range = templateString.range(of: $0.element) else { return nil } - return (content: $0.element, number: UInt($0.offset + 1), range) + self.lines = zip(1..., templateString.components(separatedBy: .newlines)).compactMap { index, line in + guard !line.isEmpty, + let range = templateString.range(of: line) else { return nil } + return (content: line, number: UInt(index), range) } } @@ -79,12 +79,12 @@ struct Lexer { let scanner = Scanner(templateString) while !scanner.isEmpty { - if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) { + if let (char, text) = scanner.scanForTokenStart(Self.tokenChars) { if !text.isEmpty { tokens.append(createToken(string: text, at: scanner.range)) } - guard let end = Lexer.tokenCharMap[char] else { continue } + guard let end = Self.tokenCharMap[char] else { continue } let result = scanner.scanForTokenEnd(end) tokens.append(createToken(string: result, at: scanner.range)) } else { @@ -127,7 +127,7 @@ class Scanner { } var isEmpty: Bool { - return content.isEmpty + content.isEmpty } /// Scans for the end of a token, with a specific ending character. If we're @@ -144,8 +144,8 @@ class Scanner { func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String { var foundChar = false - for (index, char) in content.unicodeScalars.enumerated() { - if foundChar && char == Scanner.tokenEndDelimiter { + for (index, char) in zip(0..., content.unicodeScalars) { + if foundChar && char == Self.tokenEndDelimiter { let result = String(content.unicodeScalars.prefix(index + 1)) content = String(content.unicodeScalars.dropFirst(index + 1)) range = range.upperBound.. Template + /// Load a template with the given list of names func loadTemplate(names: [String], environment: Environment) throws -> Template } extension Loader { + /// Default implementation, tries to load the first template that exists from the list of given names public func loadTemplate(names: [String], environment: Environment) throws -> Template { for name in names { do { @@ -31,13 +35,13 @@ public class FileSystemLoader: Loader, CustomStringConvertible { } public init(bundle: [Bundle]) { - self.paths = bundle.map { - Path($0.bundlePath) + self.paths = bundle.map { bundle in + Path(bundle.bundlePath) } } public var description: String { - return "FileSystemLoader(\(paths))" + "FileSystemLoader(\(paths))" } public func loadTemplate(name: String, environment: Environment) throws -> Template { @@ -119,6 +123,6 @@ class SuspiciousFileOperation: Error { } var description: String { - return "Path `\(path)` is located outside of base path `\(basePath)`" + "Path `\(path)` is located outside of base path `\(basePath)`" } } diff --git a/Sources/Node.swift b/Sources/Node.swift index 60075a9..bd16083 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -1,5 +1,6 @@ import Foundation +/// Represents a parsed node public protocol NodeType { /// Render the node in the given context func render(_ context: Context) throws -> String @@ -10,17 +11,18 @@ 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 { + try nodes + .map { node in do { - return try $0.render(context) + return try node.render(context) } catch { - throw error.withToken($0.token) + throw error.withToken(node.token) } } .joined() } +/// Simple node, used for triggering a closure during rendering public class SimpleNode: NodeType { public let handler: (Context) throws -> String public let token: Token? @@ -31,10 +33,11 @@ public class SimpleNode: NodeType { } public func render(_ context: Context) throws -> String { - return try handler(context) + try handler(context) } } +/// Represents a block of text, renders the text public class TextNode: NodeType { public let text: String public let token: Token? @@ -45,14 +48,17 @@ public class TextNode: NodeType { } public func render(_ context: Context) throws -> String { - return self.text + self.text } } +/// Representing something that can be resolved in a context public protocol Resolvable { + /// Try to resolve this with the given context func resolve(_ context: Context) throws -> Any? } +/// Represents a variable, renders the variable, may have conditional expressions. public class VariableNode: NodeType { public let variable: Resolvable public var token: Token? @@ -63,7 +69,7 @@ public class VariableNode: NodeType { let components = token.components func hasToken(_ token: String, at index: Int) -> Bool { - return components.count > (index + 1) && components[index] == token + components.count > (index + 1) && components[index] == token } let condition: Expression? @@ -137,7 +143,7 @@ func stringify(_ result: Any?) -> String { } func unwrap(_ array: [Any?]) -> [Any] { - return array.map { (item: Any?) -> Any in + array.map { (item: Any?) -> Any in if let item = item { if let items = item as? [Any?] { return unwrap(items) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 355e632..873bf9f 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -1,5 +1,7 @@ +/// Creates a checker that will stop parsing if it encounters a list of tags. +/// Useful for example for scanning until a given "end"-node. public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { - return { _, token in + { _, token in if let name = token.components.first { for tag in tags where name == tag { return true @@ -12,11 +14,13 @@ public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { /// A class for parsing an array of tokens and converts them into a collection of Node's public class TokenParser { + /// Parser for finding a kind of node public typealias TagParser = (TokenParser, Token) throws -> NodeType fileprivate var tokens: [Token] fileprivate let environment: Environment + /// Simple initializer public init(tokens: [Token], environment: Environment) { self.tokens = tokens self.environment = environment @@ -24,9 +28,11 @@ public class TokenParser { /// Parse the given tokens into nodes public func parse() throws -> [NodeType] { - return try parse(nil) + try parse(nil) } + /// Parse nodes until a specific "something" is detected, determined by the provided closure. + /// Combine this with the `until(:)` function above to scan nodes until a given token. public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] { var nodes = [NodeType]() @@ -61,6 +67,7 @@ public class TokenParser { return nodes } + /// Pop the next token (returning it) public func nextToken() -> Token? { if !tokens.isEmpty { return tokens.remove(at: 0) @@ -69,23 +76,24 @@ public class TokenParser { return nil } + /// Insert a token public func prependToken(_ token: Token) { tokens.insert(token, at: 0) } /// Create filter expression from a string contained in provided token public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable { - return try environment.compileFilter(filterToken, containedIn: token) + try environment.compileFilter(filterToken, containedIn: token) } /// Create boolean expression from components contained in provided token public func compileExpression(components: [String], token: Token) throws -> Expression { - return try environment.compileExpression(components: components, containedIn: token) + try environment.compileExpression(components: components, containedIn: token) } /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { - return try environment.compileResolvable(token, containedIn: containingToken) + try environment.compileResolvable(token, containedIn: containingToken) } } @@ -111,10 +119,12 @@ extension Environment { if suggestedFilters.isEmpty { throw TemplateSyntaxError("Unknown filter '\(name)'.") } else { - throw TemplateSyntaxError(""" + throw TemplateSyntaxError( + """ Unknown filter '\(name)'. \ Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")). - """) + """ + ) } } @@ -122,9 +132,9 @@ extension Environment { let allFilters = extensions.flatMap { $0.filters.keys } let filtersWithDistance = allFilters - .map { (filterName: $0, distance: $0.levenshteinDistance(name)) } - // do not suggest filters which names are shorter than the distance - .filter { $0.filterName.count > $0.distance } + .map { (filterName: $0, distance: $0.levenshteinDistance(name)) } + // do not suggest filters which names are shorter than the distance + .filter { $0.filterName.count > $0.distance } guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else { return [] } @@ -134,7 +144,7 @@ extension Environment { /// Create filter expression from a string public func compileFilter(_ token: String) throws -> Resolvable { - return try FilterExpression(token: token, environment: self) + try FilterExpression(token: token, environment: self) } /// Create filter expression from a string contained in provided token @@ -165,26 +175,26 @@ extension Environment { /// Create resolvable (i.e. range variable or filter expression) from a string public func compileResolvable(_ token: String) throws -> Resolvable { - return try RangeVariable(token, environment: self) + try RangeVariable(token, environment: self) ?? compileFilter(token) } /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { - return try RangeVariable(token, environment: self, containedIn: containingToken) - ?? compileFilter(token, containedIn: containingToken) + try RangeVariable(token, environment: self, containedIn: containingToken) + ?? compileFilter(token, containedIn: containingToken) } /// Create boolean expression from components contained in provided token public func compileExpression(components: [String], containedIn token: Token) throws -> Expression { - return try IfExpressionParser.parser(components: components, environment: self, token: token).parse() + try IfExpressionParser.parser(components: components, environment: self, token: token).parse() } } // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows extension String { subscript(_ index: Int) -> Character { - return self[self.index(self.startIndex, offsetBy: index)] + self[self.index(self.startIndex, offsetBy: index)] } func levenshteinDistance(_ target: String) -> Int { diff --git a/Sources/Template.swift b/Sources/Template.swift index c07be62..a75af24 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -2,6 +2,7 @@ import Foundation import PathKit #if os(Linux) +// swiftlint:disable:next prefixed_toplevel_constant let NSFileNoSuchFileError = 4 #endif @@ -77,6 +78,6 @@ open class Template: ExpressibleByStringLiteral { // swiftlint:disable discouraged_optional_collection /// Render the given template open func render(_ dictionary: [String: Any]? = nil) throws -> String { - return try render(Context(dictionary: dictionary ?? [:], environment: environment)) + try render(Context(dictionary: dictionary ?? [:], environment: environment)) } } diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 30f3117..03c1afe 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -19,7 +19,7 @@ extension String { if character == separate { if separate != separator { word.append(separate) - } else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty { + } else if (singleQuoteCount.isMultiple(of: 2) || doubleQuoteCount.isMultiple(of: 2)) && !word.isEmpty { appendWord(word, to: &components) word = "" } @@ -75,7 +75,7 @@ public struct SourceMap: Equatable { static let unknown = SourceMap() public static func == (lhs: SourceMap, rhs: SourceMap) -> Bool { - return lhs.filename == rhs.filename && lhs.location == rhs.location + lhs.filename == rhs.filename && lhs.location == rhs.location } } @@ -106,25 +106,25 @@ public class Token: Equatable { /// A token representing a piece of text. public static func text(value: String, at sourceMap: SourceMap) -> Token { - return Token(contents: value, kind: .text, sourceMap: sourceMap) + Token(contents: value, kind: .text, sourceMap: sourceMap) } /// A token representing a variable. public static func variable(value: String, at sourceMap: SourceMap) -> Token { - return Token(contents: value, kind: .variable, sourceMap: sourceMap) + Token(contents: value, kind: .variable, sourceMap: sourceMap) } /// A token representing a comment. public static func comment(value: String, at sourceMap: SourceMap) -> Token { - return Token(contents: value, kind: .comment, sourceMap: sourceMap) + Token(contents: value, kind: .comment, sourceMap: sourceMap) } /// A token representing a template block. public static func block(value: String, at sourceMap: SourceMap) -> Token { - return Token(contents: value, kind: .block, sourceMap: sourceMap) + Token(contents: value, kind: .block, sourceMap: sourceMap) } public static func == (lhs: Token, rhs: Token) -> Bool { - return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap + lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap } } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index ca2d245..9d09134 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -16,8 +16,8 @@ class FilterExpression: Resolvable { let filterBits = bits[bits.indices.suffix(from: 1)] do { - filters = try filterBits.map { - let (name, arguments) = parseFilterComponents(token: $0) + filters = try filterBits.map { bit in + let (name, arguments) = parseFilterComponents(token: bit) let filter = try environment.findFilter(name) return (filter, arguments) } @@ -208,13 +208,14 @@ protocol Normalizable { extension Array: Normalizable { func normalize() -> Any? { - return map { $0 as Any } + map { $0 as Any } } } +// swiftlint:disable:next legacy_objc_type extension NSArray: Normalizable { func normalize() -> Any? { - return map { $0 as Any } + map { $0 as Any } } } @@ -273,8 +274,10 @@ protocol AnyOptional { extension Optional: AnyOptional { var wrapped: Any? { switch self { - case let .some(value): return value - case .none: return nil + case let .some(value): + return value + case .none: + return nil } } } diff --git a/Tests/StencilTests/ContextSpec.swift b/Tests/StencilTests/ContextSpec.swift index 191529c..dd2a74a 100644 --- a/Tests/StencilTests/ContextSpec.swift +++ b/Tests/StencilTests/ContextSpec.swift @@ -4,35 +4,35 @@ import XCTest final class ContextTests: XCTestCase { func testContextSubscripting() { - describe("Context Subscripting") { + describe("Context Subscripting") { test in var context = Context() - $0.before { + test.before { context = Context(dictionary: ["name": "Kyle"]) } - $0.it("allows you to get a value via subscripting") { + test.it("allows you to get a value via subscripting") { try expect(context["name"] as? String) == "Kyle" } - $0.it("allows you to set a value via subscripting") { + test.it("allows you to set a value via subscripting") { context["name"] = "Katie" try expect(context["name"] as? String) == "Katie" } - $0.it("allows you to remove a value via subscripting") { + test.it("allows you to remove a value via subscripting") { context["name"] = nil try expect(context["name"]).to.beNil() } - $0.it("allows you to retrieve a value from a parent") { + test.it("allows you to retrieve a value from a parent") { try context.push { try expect(context["name"] as? String) == "Kyle" } } - $0.it("allows you to override a parent's value") { + test.it("allows you to override a parent's value") { try context.push { context["name"] = "Katie" try expect(context["name"] as? String) == "Katie" @@ -42,13 +42,13 @@ final class ContextTests: XCTestCase { } func testContextRestoration() { - describe("Context Restoration") { + describe("Context Restoration") { test in var context = Context() - $0.before { + test.before { context = Context(dictionary: ["name": "Kyle"]) } - $0.it("allows you to pop to restore previous state") { + test.it("allows you to pop to restore previous state") { context.push { context["name"] = "Katie" } @@ -56,7 +56,7 @@ final class ContextTests: XCTestCase { try expect(context["name"] as? String) == "Kyle" } - $0.it("allows you to remove a parent's value in a level") { + test.it("allows you to remove a parent's value in a level") { try context.push { context["name"] = nil try expect(context["name"]).to.beNil() @@ -65,7 +65,7 @@ final class ContextTests: XCTestCase { try expect(context["name"] as? String) == "Kyle" } - $0.it("allows you to push a dictionary and run a closure then restoring previous state") { + test.it("allows you to push a dictionary and run a closure then restoring previous state") { var didRun = false try context.push(dictionary: ["name": "Katie"]) { @@ -77,7 +77,7 @@ final class ContextTests: XCTestCase { try expect(context["name"] as? String) == "Kyle" } - $0.it("allows you to flatten the context contents") { + test.it("allows you to flatten the context contents") { try context.push(dictionary: ["test": "abc"]) { let flattened = context.flatten() diff --git a/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift b/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift new file mode 100644 index 0000000..c163239 --- /dev/null +++ b/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift @@ -0,0 +1,125 @@ +import PathKit +import Spectre +@testable import Stencil +import XCTest + +final class EnvironmentBaseAndChildTemplateTests: XCTestCase { + private var environment = Environment(loader: ExampleLoader()) + private var childTemplate: Template = "" + private var baseTemplate: Template = "" + + override func setUp() { + super.setUp() + + let path = Path(#file as String) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + environment = Environment(loader: loader) + childTemplate = "" + baseTemplate = "" + } + + override func tearDown() { + super.tearDown() + } + + func testSyntaxErrorInBaseTemplate() throws { + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + try expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + childToken: "extends \"invalid-base.html\"", + baseToken: "target|unknown" + ) + } + + func testRuntimeErrorInBaseTemplate() throws { + let filterExtension = Extension() + filterExtension.registerFilter("unknown") { (_: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] + + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + try expectError( + reason: "filter error", + childToken: "extends \"invalid-base.html\"", + baseToken: "target|unknown" + ) + } + + func testSyntaxErrorInChildTemplate() throws { + childTemplate = Template( + templateString: """ + {% extends "base.html" %} + {% block body %}Child {{ target|unknown }}{% endblock %} + """, + environment: environment, + name: nil + ) + + try expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + childToken: "target|unknown", + baseToken: nil + ) + } + + func testRuntimeErrorInChildTemplate() throws { + let filterExtension = Extension() + filterExtension.registerFilter("unknown") { (_: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] + + childTemplate = Template( + templateString: """ + {% extends "base.html" %} + {% block body %}Child {{ target|unknown }}{% endblock %} + """, + environment: environment, + name: nil + ) + + try expectError( + reason: "filter error", + childToken: "target|unknown", + baseToken: nil + ) + } + + private func expectError( + reason: String, + childToken: String, + baseToken: String?, + file: String = #file, + line: Int = #line, + function: String = #function + ) throws { + var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) + if let baseToken = baseToken { + expectedError.stackTrace = [ + expectedSyntaxError( + token: baseToken, + template: baseTemplate, + description: reason + ).token + ].compactMap { $0 } + } + let error = try expect( + self.environment.render(template: self.childTemplate, context: ["target": "World"]), + file: file, + line: line, + function: function + ).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect( + reporter.renderError(error), + file: file, + line: line, + function: function + ) == reporter.renderError(expectedError) + } +} diff --git a/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift b/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift new file mode 100644 index 0000000..1ed975f --- /dev/null +++ b/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift @@ -0,0 +1,88 @@ +import PathKit +import Spectre +@testable import Stencil +import XCTest + +final class EnvironmentIncludeTemplateTests: XCTestCase { + private var environment = Environment(loader: ExampleLoader()) + private var template: Template = "" + private var includedTemplate: Template = "" + + override func setUp() { + super.setUp() + + let path = Path(#file as String) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + environment = Environment(loader: loader) + template = "" + includedTemplate = "" + } + + override func tearDown() { + super.tearDown() + } + + func testSyntaxError() throws { + template = Template(templateString: """ + {% include "invalid-include.html" %} + """, environment: environment) + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + + try expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: #"include "invalid-include.html""#, + includedToken: "target|unknown" + ) + } + + func testRuntimeError() throws { + let filterExtension = Extension() + filterExtension.registerFilter("unknown") { (_: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] + + 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" + ) + } + + private func expectError( + reason: String, + token: String, + includedToken: String, + file: String = #file, + line: Int = #line, + function: String = #function + ) throws { + var expectedError = expectedSyntaxError(token: token, template: template, description: reason) + expectedError.stackTrace = [ + expectedSyntaxError( + token: includedToken, + template: includedTemplate, + description: reason + ).token + ].compactMap { $0 } + + let error = try expect( + self.environment.render(template: self.template, context: ["target": "World"]), + file: file, + line: line, + function: function + ).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect( + reporter.renderError(error), + file: file, + line: line, + function: function + ) == reporter.renderError(expectedError) + } +} diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 7d5ad96..ef2d0fb 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -4,8 +4,8 @@ import Spectre import XCTest final class EnvironmentTests: XCTestCase { - var environment = Environment(loader: ExampleLoader()) - var template: Template = "" + private var environment = Environment(loader: ExampleLoader()) + private var template: Template = "" override func setUp() { super.setUp() @@ -26,6 +26,10 @@ final class EnvironmentTests: XCTestCase { template = "" } + override func tearDown() { + super.tearDown() + } + func testLoading() { it("can load a template from a name") { let template = try self.environment.loadTemplate(name: "example.html") @@ -207,242 +211,11 @@ final class EnvironmentTests: XCTestCase { } } -final class EnvironmentIncludeTemplateTests: XCTestCase { - var environment = Environment(loader: ExampleLoader()) - var template: Template = "" - var includedTemplate: Template = "" - - override func setUp() { - super.setUp() - - let path = Path(#file as String) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - environment = Environment(loader: loader) - template = "" - includedTemplate = "" - } - - func testSyntaxError() throws { - template = Template(templateString: """ - {% include "invalid-include.html" %} - """, environment: environment) - includedTemplate = try environment.loadTemplate(name: "invalid-include.html") - - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: """ - include "invalid-include.html" - """, - includedToken: "target|unknown") - } - - func testRuntimeError() throws { - let filterExtension = Extension() - filterExtension.registerFilter("unknown") { (_: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] - - 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") - } - - private func expectError( - reason: String, - token: String, - includedToken: String, - file: String = #file, - line: Int = #line, - function: String = #function - ) throws { - var expectedError = expectedSyntaxError(token: token, template: template, description: reason) - expectedError.stackTrace = [ - expectedSyntaxError( - token: includedToken, - template: includedTemplate, - description: reason - ).token - ].compactMap { $0 } - - let error = try expect( - self.environment.render(template: self.template, context: ["target": "World"]), - file: file, - line: line, - function: function - ).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect( - reporter.renderError(error), - file: file, - line: line, - function: function - ) == reporter.renderError(expectedError) - } -} - -final class EnvironmentBaseAndChildTemplateTests: XCTestCase { - var environment = Environment(loader: ExampleLoader()) - var childTemplate: Template = "" - var baseTemplate: Template = "" - - override func setUp() { - super.setUp() - - let path = Path(#file as String) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - environment = Environment(loader: loader) - childTemplate = "" - baseTemplate = "" - } - - func testSyntaxErrorInBaseTemplate() throws { - childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") - baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - childToken: "extends \"invalid-base.html\"", - baseToken: "target|unknown") - } - - func testRuntimeErrorInBaseTemplate() throws { - let filterExtension = Extension() - filterExtension.registerFilter("unknown") { (_: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] - - childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") - baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - - try expectError(reason: "filter error", - childToken: "extends \"invalid-base.html\"", - baseToken: "target|unknown") - } - - func testSyntaxErrorInChildTemplate() throws { - childTemplate = Template( - templateString: """ - {% extends "base.html" %} - {% block body %}Child {{ target|unknown }}{% endblock %} - """, - environment: environment, - name: nil - ) - - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - childToken: "target|unknown", - baseToken: nil) - } - - func testRuntimeErrorInChildTemplate() throws { - let filterExtension = Extension() - filterExtension.registerFilter("unknown") { (_: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] - - childTemplate = Template( - templateString: """ - {% extends "base.html" %} - {% block body %}Child {{ target|unknown }}{% endblock %} - """, - environment: environment, - name: nil - ) - - try expectError(reason: "filter error", - childToken: "target|unknown", - baseToken: nil) - } - - private func expectError( - reason: String, - childToken: String, - baseToken: String?, - file: String = #file, - line: Int = #line, - function: String = #function - ) throws { - var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) - if let baseToken = baseToken { - expectedError.stackTrace = [ - expectedSyntaxError( - token: baseToken, - template: baseTemplate, - description: reason - ).token - ].compactMap { $0 } - } - let error = try expect( - self.environment.render(template: self.childTemplate, context: ["target": "World"]), - file: file, - line: line, - function: function - ).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect( - reporter.renderError(error), - file: file, - line: line, - function: function - ) == reporter.renderError(expectedError) - } -} - -extension Expectation { - @discardableResult - func toThrow() throws -> T { - var thrownError: Error? - - do { - _ = try expression() - } catch { - thrownError = error - } - - if let thrownError = thrownError { - if let thrownError = thrownError as? T { - return thrownError - } else { - throw failure("\(thrownError) is not \(T.self)") - } - } else { - throw failure("expression did not throw an error") - } - } -} - -extension XCTestCase { - func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - guard let range = template.templateString.range(of: token) else { - fatalError("Can't find '\(token)' in '\(template)'") - } - let lexer = Lexer(templateString: template.templateString) - let location = lexer.rangeLocation(range) - let sourceMap = SourceMap(filename: template.name, location: location) - let token = Token.block(value: token, at: sourceMap) - return TemplateSyntaxError(reason: description, token: token, stackTrace: []) - } -} - -private class ExampleLoader: Loader { - func loadTemplate(name: String, environment: Environment) throws -> Template { - if name == "example.html" { - return Template(templateString: "Hello World!", environment: environment, name: name) - } - - throw TemplateDoesNotExist(templateNames: [name], loader: self) - } -} +// MARK: - Helpers private class CustomTemplate: Template { // swiftlint:disable discouraged_optional_collection override func render(_ dictionary: [String: Any]? = nil) throws -> String { - return "here" + "here" } } diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 6659d39..6b34697 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -3,7 +3,7 @@ import Spectre import XCTest final class ExpressionsTests: XCTestCase { - let parser = TokenParser(tokens: [], environment: Environment()) + private let parser = TokenParser(tokens: [], environment: Environment()) private func makeExpression(_ components: [String]) -> Expression { do { diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 9b7b9bb..092c943 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -103,30 +103,30 @@ final class FilterTests: XCTestCase { } it("allows you to register a custom filter which accepts several arguments") { - let template = Template(templateString: """ - {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} - """) + let template = Template(templateString: """ + {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} + """) - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { value, arguments in - guard let value = value else { return nil } - let args = arguments.compactMap { $0 } - return "\(value) \(value) with args 0: \(args[0]), 1: \(args[1]), 2: \(args[2])" - } + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { value, arguments in + guard let value = value else { return nil } + let args = arguments.compactMap { $0 } + return "\(value) \(value) with args 0: \(args[0]), 1: \(args[1]), 2: \(args[2])" + } - let result = try template.render(Context( - dictionary: context, - environment: Environment(extensions: [repeatExtension]) - )) - try expect(result) == """ - Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) - """ + let result = try template.render(Context( + dictionary: context, + environment: Environment(extensions: [repeatExtension]) + )) + try expect(result) == """ + Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) + """ } it("allows whitespace in expression") { let template = Template(templateString: """ - {{ value | join : ", " }} - """) + {{ value | join : ", " }} + """) let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) try expect(result) == "One, Two" } @@ -363,10 +363,12 @@ final class FilterTests: XCTestCase { Two """ ])) + // swiftlint:disable indentation_width try expect(result) == """ One Two """ + // swiftlint:enable indentation_width } func testIndentNotEmptyLines() throws { @@ -383,6 +385,7 @@ final class FilterTests: XCTestCase { """ ])) + // swiftlint:disable indentation_width try expect(result) == """ One @@ -391,6 +394,7 @@ final class FilterTests: XCTestCase { """ + // swiftlint:enable indentation_width } func testDynamicFilters() throws { diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index d9a82b8..d73cf40 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -23,9 +23,9 @@ final class FilterTagTests: XCTestCase { it("can render filters with arguments") { let ext = Extension() - ext.registerFilter("split") { - guard let value = $0 as? String, - let argument = $1.first as? String else { return $0 } + ext.registerFilter("split") { value, args in + guard let value = value as? String, + let argument = args.first as? String else { return value } return value.components(separatedBy: argument) } let env = Environment(extensions: [ext]) @@ -37,11 +37,11 @@ final class FilterTagTests: XCTestCase { it("can render filters with quote as an argument") { let ext = Extension() - ext.registerFilter("replace") { - guard let value = $0 as? String, - $1.count == 2, - let search = $1.first as? String, - let replacement = $1.last as? String else { return $0 } + ext.registerFilter("replace") { value, args in + guard let value = value as? String, + args.count == 2, + let search = args.first as? String, + let replacement = args.last as? String else { return value } return value.replacingOccurrences(of: search, with: replacement) } let env = Environment(extensions: [ext]) diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index d3fc65e..942385e 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -3,9 +3,10 @@ import Spectre import XCTest final class ForNodeTests: XCTestCase { - let context = Context(dictionary: [ + private let context = Context(dictionary: [ "items": [1, 2, 3], "anyItems": [1, 2, 3] as [Any], + // swiftlint:disable:next legacy_objc_type "nsItems": NSArray(array: [1, 2, 3]), "emptyItems": [Int](), "dict": [ @@ -313,6 +314,8 @@ final class ForNodeTests: XCTestCase { } } +// MARK: - Helpers + private struct MyStruct { let string: String let number: Int diff --git a/Tests/StencilTests/Helpers.swift b/Tests/StencilTests/Helpers.swift new file mode 100644 index 0000000..8a82ee5 --- /dev/null +++ b/Tests/StencilTests/Helpers.swift @@ -0,0 +1,63 @@ +import PathKit +import Spectre +@testable import Stencil +import XCTest + +extension Expectation { + @discardableResult + func toThrow() throws -> T { + var thrownError: Error? + + do { + _ = try expression() + } catch { + thrownError = error + } + + if let thrownError = thrownError { + if let thrownError = thrownError as? T { + return thrownError + } else { + throw failure("\(thrownError) is not \(T.self)") + } + } else { + throw failure("expression did not throw an error") + } + } +} + +extension XCTestCase { + func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } + let lexer = Lexer(templateString: template.templateString) + let location = lexer.rangeLocation(range) + let sourceMap = SourceMap(filename: template.name, location: location) + let token = Token.block(value: token, at: sourceMap) + return TemplateSyntaxError(reason: description, token: token, stackTrace: []) + } +} + +// MARK: - Test Types + +class ExampleLoader: Loader { + func loadTemplate(name: String, environment: Environment) throws -> Template { + if name == "example.html" { + return Template(templateString: "Hello World!", environment: environment, name: name) + } + + throw TemplateDoesNotExist(templateNames: [name], loader: self) + } +} + +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/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index e3b3838..a4c59e9 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -2,10 +2,6 @@ import Spectre @testable import Stencil import XCTest -private struct SomeType { - let value: String? = nil -} - final class IfNodeTests: XCTestCase { func testParseIf() { it("can parse an if block") { @@ -286,3 +282,9 @@ final class IfNodeTests: XCTestCase { try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" } } + +// MARK: - Helpers + +private struct SomeType { + let value: String? = nil +} diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index 4743a94..10bb4c3 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -4,9 +4,9 @@ import Spectre import XCTest final class IncludeTests: XCTestCase { - let path = Path(#file as String) + ".." + "fixtures" - lazy var loader = FileSystemLoader(paths: [path]) - lazy var environment = Environment(loader: loader) + private let path = Path(#file as String) + ".." + "fixtures" + private lazy var loader = FileSystemLoader(paths: [path]) + private lazy var environment = Environment(loader: loader) func testParsing() { it("throws an error when no template is given") { diff --git a/Tests/StencilTests/InheritanceSpec.swift b/Tests/StencilTests/InheritanceSpec.swift index 60f4fff..e16d83e 100644 --- a/Tests/StencilTests/InheritanceSpec.swift +++ b/Tests/StencilTests/InheritanceSpec.swift @@ -4,9 +4,9 @@ import Stencil import XCTest final class InheritanceTests: XCTestCase { - let path = Path(#file as String) + ".." + "fixtures" - lazy var loader = FileSystemLoader(paths: [path]) - lazy var environment = Environment(loader: loader) + private let path = Path(#file as String) + ".." + "fixtures" + private lazy var loader = FileSystemLoader(paths: [path]) + private lazy var environment = Environment(loader: loader) func testInheritance() { it("can inherit from another template") { diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 7961458..588ad18 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -82,6 +82,7 @@ final class LexerTests: XCTestCase { } func testNewlines() throws { + // swiftlint:disable indentation_width let templateString = """ My name is {% if name @@ -92,6 +93,7 @@ final class LexerTests: XCTestCase { }}{% endif %}. """ + // swiftlint:enable indentation_width let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() diff --git a/Tests/StencilTests/LoaderSpec.swift b/Tests/StencilTests/LoaderSpec.swift index 3cd189b..983e5ba 100644 --- a/Tests/StencilTests/LoaderSpec.swift +++ b/Tests/StencilTests/LoaderSpec.swift @@ -32,7 +32,7 @@ final class TemplateLoaderTests: XCTestCase { func testDictionaryLoader() { let loader = DictionaryLoader(templates: [ - "index.html": "Hello World" + "index.html": "Hello World" ]) let environment = Environment(loader: loader) diff --git a/Tests/StencilTests/NodeSpec.swift b/Tests/StencilTests/NodeSpec.swift index 0a67cf5..9814494 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -2,19 +2,8 @@ import Spectre @testable import Stencil import XCTest -class ErrorNode: NodeType { - let token: Token? - init(token: Token? = nil) { - self.token = token - } - - func render(_ context: Context) throws -> String { - throw TemplateSyntaxError("Custom Error") - } -} - final class NodeTests: XCTestCase { - let context = Context(dictionary: [ + private let context = Context(dictionary: [ "name": "Kyle", "age": 27, "items": [1, 2, 3] diff --git a/Tests/StencilTests/NowNodeSpec.swift b/Tests/StencilTests/NowNodeSpec.swift index 7e5d23f..a213a32 100644 --- a/Tests/StencilTests/NowNodeSpec.swift +++ b/Tests/StencilTests/NowNodeSpec.swift @@ -5,9 +5,9 @@ import XCTest final class NowNodeTests: XCTestCase { func testParsing() { it("parses default format without any now arguments") { -#if os(Linux) + #if os(Linux) throw skip() -#else + #else let tokens: [Token] = [ .block(value: "now", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -15,36 +15,36 @@ final class NowNodeTests: XCTestCase { let node = nodes.first as? NowNode try expect(nodes.count) == 1 try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\"" -#endif + #endif } it("parses now with a format") { -#if os(Linux) + #if os(Linux) throw skip() -#else + #else 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 try expect(nodes.count) == 1 try expect(node?.format.variable) == "\"HH:mm\"" -#endif + #endif } } func testRendering() { it("renders the date") { -#if os(Linux) + #if os(Linux) throw skip() -#else + #else let node = NowNode(format: Variable("\"yyyy-MM-dd\"")) let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" - let date = formatter.string(from: NSDate() as Date) + let date = formatter.string(from: Date()) try expect(try node.render(Context())) == date -#endif + #endif } } } diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index ce5f8c0..cd8b12f 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -57,8 +57,8 @@ final class TokenParserTests: XCTestCase { try expect(try parser.parse()).toThrow(TemplateSyntaxError( reason: "Unknown template tag 'unknown'", - token: tokens.first) - ) + token: tokens.first + )) } } } diff --git a/Tests/StencilTests/StencilSpec.swift b/Tests/StencilTests/StencilSpec.swift index dc8aa6e..8e01b63 100644 --- a/Tests/StencilTests/StencilSpec.swift +++ b/Tests/StencilTests/StencilSpec.swift @@ -2,20 +2,8 @@ import Spectre import Stencil import XCTest -private struct CustomNode: NodeType { - let token: Token? - func render(_ context: Context) throws -> String { - return "Hello World" - } -} - -private struct Article { - let title: String - let author: String -} - final class StencilTests: XCTestCase { - lazy var environment: Environment = { + private lazy var environment: Environment = { let exampleExtension = Extension() exampleExtension.registerSimpleTag("simpletag") { _ in "Hello World" @@ -32,7 +20,7 @@ final class StencilTests: XCTestCase { There are {{ articles.count }} articles. {% for article in articles %}\ - - {{ article.title }} by {{ article.author }}. + - {{ article.title }} by {{ article.author }}. {% endfor %} """ @@ -49,8 +37,8 @@ final class StencilTests: XCTestCase { try expect(result) == """ There are 2 articles. - - Migrating from OCUnit to XCTest by Kyle Fuller. - - Memory Management with ARC by Kyle Fuller. + - Migrating from OCUnit to XCTest by Kyle Fuller. + - Memory Management with ARC by Kyle Fuller. """ } @@ -66,3 +54,17 @@ final class StencilTests: XCTestCase { } } } + +// MARK: - Helpers + +private struct CustomNode: NodeType { + let token: Token? + func render(_ context: Context) throws -> String { + "Hello World" + } +} + +private struct Article { + let title: String + let author: String +} diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index b1c66a1..433f1cb 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -11,9 +11,9 @@ final class TemplateTests: XCTestCase { } it("can render a template from a string literal") { - let template: Template = "Hello World" - let result = try template.render([ "name": "Kyle" ]) - try expect(result) == "Hello World" + let template: Template = "Hello World" + let result = try template.render([ "name": "Kyle" ]) + try expect(result) == "Hello World" } } } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 52b276d..8155775 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -2,47 +2,8 @@ import Spectre @testable import Stencil import XCTest -#if os(OSX) -@objc -class Superclass: NSObject { - @objc let name = "Foo" -} -@objc -class Object: Superclass { - @objc let title = "Hello World" -} -#endif - -private struct Person { - let name: String -} - -private struct Article { - let author: Person -} - -private class WebSite { - let url: String = "blog.com" -} - -private class Blog: WebSite { - let articles: [Article] = [Article(author: Person(name: "Kyle"))] - let featuring: Article? = Article(author: Person(name: "Jhon")) -} - -@dynamicMemberLookup -private struct DynamicStruct: DynamicMemberLookup { - subscript(dynamicMember member: String) -> Any? { - member == "test" ? "this is a dynamic response" : nil - } -} - -private enum DynamicEnum: String, DynamicMemberLookup { - case someValue = "this is raw value" -} - final class VariableTests: XCTestCase { - let context: Context = { + private let context: Context = { let ext = Extension() ext.registerFilter("incr") { arg in (arg.flatMap { toNumber(value: $0) } ?? 0) + 1 @@ -66,9 +27,9 @@ final class VariableTests: XCTestCase { "struct": DynamicStruct() ] ], environment: environment) -#if os(OSX) + #if os(OSX) context["object"] = Object() -#endif + #endif return context }() @@ -145,9 +106,9 @@ final class VariableTests: XCTestCase { let result = try variable.resolve(self.context) as? String try expect(result) == "Katie" - let variable1 = Variable("contacts.1") - let result1 = try variable1.resolve(self.context) as? String - try expect(result1) == "Carlton" + let variable1 = Variable("contacts.1") + let result1 = try variable1.resolve(self.context) as? String + try expect(result1) == "Carlton" } it("can resolve an item from an array via unknown index") { @@ -214,7 +175,7 @@ final class VariableTests: XCTestCase { } func testKVO() { -#if os(OSX) + #if os(OSX) it("can resolve a value via KVO") { let variable = Variable("object.title") let result = try variable.resolve(self.context) as? String @@ -232,7 +193,7 @@ final class VariableTests: XCTestCase { let result = try variable.resolve(self.context) as? String try expect(result).to.beNil() } -#endif + #endif } func testTuple() { @@ -285,7 +246,7 @@ final class VariableTests: XCTestCase { } } -#if os(OSX) + #if os(OSX) it("can resolve a subscript via KVO") { try self.context.push(dictionary: ["property": "name"]) { let variable = Variable("object[property]") @@ -293,7 +254,7 @@ final class VariableTests: XCTestCase { try expect(result) == "Foo" } } -#endif + #endif it("can resolve an optional subscript via reflection") { try self.context.push(dictionary: ["property": "featuring"]) { @@ -394,3 +355,44 @@ final class VariableTests: XCTestCase { } } } + +// MARK: - Helpers + +#if os(OSX) +@objc +class Superclass: NSObject { + @objc let name = "Foo" +} +@objc +class Object: Superclass { + @objc let title = "Hello World" +} +#endif + +private struct Person { + let name: String +} + +private struct Article { + let author: Person +} + +private class WebSite { + let url: String = "blog.com" +} + +private class Blog: WebSite { + let articles: [Article] = [Article(author: Person(name: "Kyle"))] + let featuring: Article? = Article(author: Person(name: "Jhon")) +} + +@dynamicMemberLookup +private struct DynamicStruct: DynamicMemberLookup { + subscript(dynamicMember member: String) -> Any? { + member == "test" ? "this is a dynamic response" : nil + } +} + +private enum DynamicEnum: String, DynamicMemberLookup { + case someValue = "this is raw value" +}