diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 47ef929..503ad68 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -17,7 +17,7 @@ class Environment { } if args.count != 1 || !(args[0] is ObjectValue) { - throw JinjaError.runtimeError("`namespace` expects either zero arguments or a single object argument") + throw JinjaError.runtime("`namespace` expects either zero arguments or a single object argument") } return args[0] @@ -41,7 +41,7 @@ class Environment { return arg.value as! Int % 2 != 0 } else { - throw JinjaError.runtimeError("Cannot apply test 'odd' to type: \(type(of:args.first))") + throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of:args.first))") } }, "even": { args in @@ -49,7 +49,7 @@ class Environment { return arg.value as! Int % 2 == 0 } else { - throw JinjaError.runtimeError("Cannot apply test 'even' to type: \(type(of:args.first))") + throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of:args.first))") } }, "false": { args in @@ -100,7 +100,7 @@ class Environment { args[0] is UndefinedValue }, "equalto": { _ in - throw JinjaError.notSupportError + throw JinjaError.syntaxNotSupported }, ] @@ -135,7 +135,7 @@ class Environment { case let value as Bool: arg = String(value) default: - throw JinjaError.runtimeError("Unknown arg type:\(type(of: args[0].value))") + throw JinjaError.runtime("Unknown arg type:\(type(of: args[0].value))") } try fn(arg) @@ -166,7 +166,7 @@ class Environment { return ObjectValue(value: object) default: - throw JinjaError.runtimeError("Cannot convert to runtime value: \(input) type:\(type(of: input))") + throw JinjaError.runtime("Cannot convert to runtime value: \(input) type:\(type(of: input))") } } @@ -177,7 +177,7 @@ class Environment { func declareVariable(name: String, value: any RuntimeValue) throws -> any RuntimeValue { if self.variables.contains(where: { $0.0 == name }) { - throw JinjaError.syntaxError("Variable already declared: \(name)") + throw JinjaError.syntax("Variable already declared: \(name)") } self.variables[name] = value @@ -200,7 +200,7 @@ class Environment { return try parent.resolve(name: name) as! Self } - throw JinjaError.runtimeError("Unknown variable: \(name)") + throw JinjaError.runtime("Unknown variable: \(name)") } func lookupVariable(name: String) -> any RuntimeValue { diff --git a/Sources/Error.swift b/Sources/Error.swift index 54daa92..4b820f3 100644 --- a/Sources/Error.swift +++ b/Sources/Error.swift @@ -7,10 +7,20 @@ import Foundation -enum JinjaError: Error { - case syntaxError(String) - case parserError(String) - case runtimeError(String) +enum JinjaError: Error, LocalizedError { + case syntax(String) + case parser(String) + case runtime(String) case todo(String) - case notSupportError + case syntaxNotSupported + + var errorDescription: String? { + switch self { + case .syntax(let message): return "Syntax error: \(message)" + case .parser(let message): return "Parser error: \(message)" + case .runtime(let message): return "Runtime error: \(message)" + case .todo(let message): return "Todo error: \(message)" + case .syntaxNotSupported: return "Syntax not supported" + } + } } diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 7b58045..a49dd82 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -170,14 +170,14 @@ func tokenize(_ source: String, options: PreprocessOptions = PreprocessOptions() if src[cursorPosition] == "\\" { cursorPosition += 1 if cursorPosition >= src.count { - throw JinjaError.syntaxError("Unexpected end of input") + throw JinjaError.syntax("Unexpected end of input") } let escaped = String(src[cursorPosition]) cursorPosition += 1 guard let unescaped = escapeCharacters[escaped] else { - throw JinjaError.syntaxError("Unexpected escaped character: \(escaped)") + throw JinjaError.syntax("Unexpected escaped character: \(escaped)") } str.append(unescaped) continue @@ -186,7 +186,7 @@ func tokenize(_ source: String, options: PreprocessOptions = PreprocessOptions() str.append(String(src[cursorPosition])) cursorPosition += 1 if cursorPosition >= src.count { - throw JinjaError.syntaxError("Unexpected end of input") + throw JinjaError.syntax("Unexpected end of input") } } return str @@ -219,7 +219,7 @@ func tokenize(_ source: String, options: PreprocessOptions = PreprocessOptions() let lastTokenType = tokens.last?.type if lastTokenType == .text || lastTokenType == nil { - throw JinjaError.syntaxError("Unexpected character: \(char)") + throw JinjaError.syntax("Unexpected character: \(char)") } switch lastTokenType { @@ -283,7 +283,7 @@ func tokenize(_ source: String, options: PreprocessOptions = PreprocessOptions() continue } - throw JinjaError.syntaxError("Unexpected character: \(char)") + throw JinjaError.syntax("Unexpected character: \(char)") } return tokens diff --git a/Sources/Parser.swift b/Sources/Parser.swift index aedbb69..4fca749 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -16,7 +16,7 @@ func parse(tokens: [Token]) throws -> Program { let prev = tokens[current] current += 1 if prev.type != type { - throw JinjaError.parserError("Parser Error: \(error). \(prev.type) != \(type).") + throw JinjaError.parser("Parser Error: \(error). \(prev.type) != \(type).") } return prev @@ -36,7 +36,7 @@ func parse(tokens: [Token]) throws -> Program { argument = KeywordArgumentExpression(key: identifier, value: value as! Expression) } else { - throw JinjaError.syntaxError("Expected identifier for keyword argument") + throw JinjaError.syntax("Expected identifier for keyword argument") } } @@ -100,12 +100,12 @@ func parse(tokens: [Token]) throws -> Program { } if slices.isEmpty { - throw JinjaError.syntaxError("Expected at least one argument for member/slice expression") + throw JinjaError.syntax("Expected at least one argument for member/slice expression") } if isSlice { if slices.count > 3 { - throw JinjaError.syntaxError("Expected 0-3 arguments for slice expression") + throw JinjaError.syntax("Expected 0-3 arguments for slice expression") } return SliceExpression( @@ -135,7 +135,7 @@ func parse(tokens: [Token]) throws -> Program { else { property = try parsePrimaryExpression() if !(property is Identifier) { - throw JinjaError.syntaxError("Expected identifier following dot operator") + throw JinjaError.syntax("Expected identifier following dot operator") } } @@ -166,7 +166,7 @@ func parse(tokens: [Token]) throws -> Program { current += 1 var filter = try parsePrimaryExpression() if !(filter is Identifier) { - throw JinjaError.syntaxError("Expected identifier for the test") + throw JinjaError.syntax("Expected identifier for the test") } if typeof(.openParen) { @@ -202,7 +202,7 @@ func parse(tokens: [Token]) throws -> Program { operand = TestExpression(operand: operand as! Expression, negate: negate, test: test) } else { - throw JinjaError.syntaxError("Expected identifier for the test") + throw JinjaError.syntax("Expected identifier for the test") } } return operand @@ -380,7 +380,7 @@ func parse(tokens: [Token]) throws -> Program { current += 1 let expression = try parseExpressionSequence() if tokens[current].type != .closeParen { - throw JinjaError.syntaxError("Expected closing parenthesis, got \(tokens[current].type) instead") + throw JinjaError.syntax("Expected closing parenthesis, got \(tokens[current].type) instead") } current += 1 return expression @@ -416,7 +416,7 @@ func parse(tokens: [Token]) throws -> Program { return ObjectLiteral(value: values) default: - throw JinjaError.syntaxError("Unexpected token: \(token.type)") + throw JinjaError.syntax("Unexpected token: \(token.type)") } } @@ -449,7 +449,7 @@ func parse(tokens: [Token]) throws -> Program { let loopVariable = try parseExpressionSequence(primary: true) if !(loopVariable is Identifier || loopVariable is TupleLiteral) { - throw JinjaError.syntaxError( + throw JinjaError.syntax( "Expected identifier/tuple for the loop variable, got \(type(of:loopVariable)) instead" ) } @@ -469,7 +469,7 @@ func parse(tokens: [Token]) throws -> Program { return For(loopvar: loopVariable, iterable: iterable as! Expression, body: body) } - throw JinjaError.syntaxError( + throw JinjaError.syntax( "Expected identifier/tuple for the loop variable, got \(type(of:loopVariable)) instead" ) } @@ -496,7 +496,7 @@ func parse(tokens: [Token]) throws -> Program { try expect(type: .endFor, error: "Expected endfor token") try expect(type: .closeStatement, error: "Expected %} token") default: - throw JinjaError.syntaxError("Unknown statement type: \(tokens[current].type)") + throw JinjaError.syntax("Unknown statement type: \(tokens[current].type)") } return result @@ -521,7 +521,7 @@ func parse(tokens: [Token]) throws -> Program { case .openExpression: return try parseJinjaExpression() default: - throw JinjaError.syntaxError("Unexpected token type: \(tokens[current].type)") + throw JinjaError.syntax("Unexpected token type: \(tokens[current].type)") } } diff --git a/Sources/Runtime.swift b/Sources/Runtime.swift index 8eb84f6..66b17e8 100644 --- a/Sources/Runtime.swift +++ b/Sources/Runtime.swift @@ -97,7 +97,7 @@ struct ObjectValue: RuntimeValue { } } else { - throw JinjaError.runtimeError("Object key must be a string: got \(type(of:args[0]))") + throw JinjaError.runtime("Object key must be a string: got \(type(of:args[0]))") } }), "items": FunctionValue(value: { _, _ in @@ -190,7 +190,7 @@ struct Interpreter { case let value as String: result += value default: - throw JinjaError.runtimeError("Unknown value type:\(type(of: lastEvaluated.value))") + throw JinjaError.runtime("Unknown value type:\(type(of: lastEvaluated.value))") } } } @@ -217,15 +217,15 @@ struct Interpreter { object.value[property.value] = rhs } else { - throw JinjaError.runtimeError("Cannot assign to member with non-identifier property") + throw JinjaError.runtime("Cannot assign to member with non-identifier property") } } else { - throw JinjaError.runtimeError("Cannot assign to member of non-object") + throw JinjaError.runtime("Cannot assign to member of non-object") } } else { - throw JinjaError.runtimeError("Invalid assignee type: \(type(of: node.assignee))") + throw JinjaError.runtime("Invalid assignee type: \(type(of: node.assignee))") } return NullValue() @@ -276,7 +276,7 @@ struct Interpreter { case let tupleLiteral as TupleLiteral: if let current = current as? ArrayValue { if tupleLiteral.value.count != current.value.count { - throw JinjaError.runtimeError( + throw JinjaError.runtime( "Too \(tupleLiteral.value.count > current.value.count ? "few" : "many") items to unpack" ) } @@ -286,17 +286,17 @@ struct Interpreter { try scope.setVariable(name: identifier.value, value: current.value[j]) } else { - throw JinjaError.runtimeError( + throw JinjaError.runtime( "Cannot unpack non-identifier type: \(type(of:tupleLiteral.value[j]))" ) } } } else { - throw JinjaError.runtimeError("Cannot unpack non-iterable type: \(type(of:current))") + throw JinjaError.runtime("Cannot unpack non-iterable type: \(type(of:current))") } default: - throw JinjaError.notSupportError + throw JinjaError.syntaxNotSupported } let evaluated = try self.evaluateBlock(statements: node.body, environment: scope) @@ -304,7 +304,7 @@ struct Interpreter { } } else { - throw JinjaError.runtimeError("Expected iterable type in for loop: got \(type(of:iterable))") + throw JinjaError.runtime("Expected iterable type in for loop: got \(type(of:iterable))") } return StringValue(value: result) @@ -331,7 +331,7 @@ struct Interpreter { case let value as Bool: return BooleanValue(value: value == right.value as! Bool) default: - throw JinjaError.runtimeError( + throw JinjaError.runtime( "Unknown left value type:\(type(of: left.value)), right value type:\(type(of: right.value))" ) } @@ -346,41 +346,41 @@ struct Interpreter { } if left is UndefinedValue || right is UndefinedValue { - throw JinjaError.runtimeError("Cannot perform operation on undefined values") + throw JinjaError.runtime("Cannot perform operation on undefined values") } else if left is NullValue || right is NullValue { - throw JinjaError.runtimeError("Cannot perform operation on null values") + throw JinjaError.runtime("Cannot perform operation on null values") } else if let left = left as? NumericValue, let right = right as? NumericValue { switch node.operation.value { - case "+": throw JinjaError.notSupportError - case "-": throw JinjaError.notSupportError - case "*": throw JinjaError.notSupportError - case "/": throw JinjaError.notSupportError + case "+": throw JinjaError.syntaxNotSupported + case "-": throw JinjaError.syntaxNotSupported + case "*": throw JinjaError.syntaxNotSupported + case "/": throw JinjaError.syntaxNotSupported case "%": switch left.value { case is Int: return NumericValue(value: left.value as! Int % (right.value as! Int)) default: - throw JinjaError.runtimeError("Unknown value type:\(type(of: left.value))") + throw JinjaError.runtime("Unknown value type:\(type(of: left.value))") } - case "<": throw JinjaError.notSupportError - case ">": throw JinjaError.notSupportError - case ">=": throw JinjaError.notSupportError - case "<=": throw JinjaError.notSupportError + case "<": throw JinjaError.syntaxNotSupported + case ">": throw JinjaError.syntaxNotSupported + case ">=": throw JinjaError.syntaxNotSupported + case "<=": throw JinjaError.syntaxNotSupported default: - throw JinjaError.runtimeError("Unknown operation type:\(node.operation.value)") + throw JinjaError.runtime("Unknown operation type:\(node.operation.value)") } } else if left is ArrayValue && right is ArrayValue { switch node.operation.value { case "+": break default: - throw JinjaError.runtimeError("Unknown operation type:\(node.operation.value)") + throw JinjaError.runtime("Unknown operation type:\(node.operation.value)") } } else if right is ArrayValue { - throw JinjaError.notSupportError + throw JinjaError.syntaxNotSupported } if left is StringValue || right is StringValue { @@ -396,7 +396,7 @@ struct Interpreter { case let value as Bool: rightValue = String(value) default: - throw JinjaError.runtimeError("Unknown right value type:\(type(of: right.value))") + throw JinjaError.runtime("Unknown right value type:\(type(of: right.value))") } switch left.value { @@ -407,7 +407,7 @@ struct Interpreter { case let value as Bool: rightValue = String(value) default: - throw JinjaError.runtimeError("Unknown left value type:\(type(of: left.value))") + throw JinjaError.runtime("Unknown left value type:\(type(of: left.value))") } return StringValue(value: leftValue + rightValue) @@ -423,15 +423,15 @@ struct Interpreter { case "not in": return BooleanValue(value: !right.value.contains(left.value)) default: - throw JinjaError.runtimeError("Unknown operation type:\(node.operation.value)") + throw JinjaError.runtime("Unknown operation type:\(node.operation.value)") } } if left is StringValue, right is ObjectValue { - throw JinjaError.notSupportError + throw JinjaError.syntaxNotSupported } - throw JinjaError.syntaxError( + throw JinjaError.syntax( "Unknown operator '\(node.operation.value)' between \(type(of:left)) and \(type(of:right))" ) } @@ -442,7 +442,7 @@ struct Interpreter { environment: Environment ) throws -> any RuntimeValue { if !(object is ArrayValue || object is StringValue) { - throw JinjaError.runtimeError("Slice object must be an array or string") + throw JinjaError.runtime("Slice object must be an array or string") } let start = try self.evaluate(statement: expr.start, environment: environment) @@ -450,15 +450,15 @@ struct Interpreter { let step = try self.evaluate(statement: expr.step, environment: environment) if !(start is NumericValue || start is UndefinedValue) { - throw JinjaError.runtimeError("Slice start must be numeric or undefined") + throw JinjaError.runtime("Slice start must be numeric or undefined") } if !(stop is NumericValue || stop is UndefinedValue) { - throw JinjaError.runtimeError("Slice stop must be numeric or undefined") + throw JinjaError.runtime("Slice stop must be numeric or undefined") } if !(step is NumericValue || step is UndefinedValue) { - throw JinjaError.runtimeError("Slice step must be numeric or undefined") + throw JinjaError.runtime("Slice step must be numeric or undefined") } if let object = object as? ArrayValue { @@ -482,7 +482,7 @@ struct Interpreter { ) } - throw JinjaError.runtimeError("Slice object must be an array or string") + throw JinjaError.runtime("Slice object must be an array or string") } func evaluateMemberExpression(expr: MemberExpression, environment: Environment) throws -> any RuntimeValue { @@ -507,7 +507,7 @@ struct Interpreter { value = object.value[property.value] ?? object.builtins[property.value] } else { - throw JinjaError.runtimeError("Cannot access property with non-string: got \(type(of:property))") + throw JinjaError.runtime("Cannot access property with non-string: got \(type(of:property))") } } else if object is ArrayValue || object is StringValue { @@ -530,7 +530,7 @@ struct Interpreter { value = object.builtins[property.value] } else { - throw JinjaError.runtimeError( + throw JinjaError.runtime( "Cannot access property with non-string/non-number: got \(type(of:property))" ) } @@ -540,7 +540,7 @@ struct Interpreter { value = object.builtins[property.value]! } else { - throw JinjaError.runtimeError("Cannot access property with non-string: got \(type(of:property))") + throw JinjaError.runtime("Cannot access property with non-string: got \(type(of:property))") } } @@ -559,7 +559,7 @@ struct Interpreter { case "not": return BooleanValue(value: !argument.bool()) default: - throw JinjaError.syntaxError("Unknown operator: \(node.operation.value)") + throw JinjaError.syntax("Unknown operator: \(node.operation.value)") } } @@ -586,7 +586,7 @@ struct Interpreter { return try fn.value(args, environment) } else { - throw JinjaError.runtimeError("Cannot call something that is not a function: got \(type(of:fn))") + throw JinjaError.runtime("Cannot call something that is not a function: got \(type(of:fn))") } } @@ -609,7 +609,7 @@ struct Interpreter { case "sort": throw JinjaError.todo("TODO: ArrayValue filter sort") default: - throw JinjaError.runtimeError("Unknown ArrayValue filter: \(identifier.value)") + throw JinjaError.runtime("Unknown ArrayValue filter: \(identifier.value)") } } else if let stringValue = operand as? StringValue { @@ -627,7 +627,7 @@ struct Interpreter { case "trim": return StringValue(value: stringValue.value.trimmingCharacters(in: .whitespacesAndNewlines)) default: - throw JinjaError.runtimeError("Unknown StringValue filter: \(identifier.value)") + throw JinjaError.runtime("Unknown StringValue filter: \(identifier.value)") } } else if let numericValue = operand as? NumericValue { @@ -635,7 +635,7 @@ struct Interpreter { case "abs": return NumericValue(value: abs(numericValue.value as! Int32)) default: - throw JinjaError.runtimeError("Unknown NumericValue filter: \(identifier.value)") + throw JinjaError.runtime("Unknown NumericValue filter: \(identifier.value)") } } else if let objectValue = operand as? ObjectValue { @@ -654,14 +654,14 @@ struct Interpreter { case "length": return NumericValue(value: objectValue.value.count) default: - throw JinjaError.runtimeError("Unknown ObjectValue filter: \(identifier.value)") + throw JinjaError.runtime("Unknown ObjectValue filter: \(identifier.value)") } } - throw JinjaError.runtimeError("Cannot apply filter \(operand.value) to type: \(type(of:operand))") + throw JinjaError.runtime("Cannot apply filter \(operand.value) to type: \(type(of:operand))") } - throw JinjaError.runtimeError("Unknown filter: \(node.filter)") + throw JinjaError.runtime("Unknown filter: \(node.filter)") } func evaluate(statement: Statement?, environment: Environment) throws -> any RuntimeValue { @@ -694,7 +694,7 @@ struct Interpreter { case let statement as FilterExpression: return try self.evaluateFilterExpression(node: statement, environment: environment) default: - throw JinjaError.runtimeError("Unknown node type: \(type(of:statement))") + throw JinjaError.runtime("Unknown node type: \(type(of:statement))") } } else { diff --git a/Sources/Template.swift b/Sources/Template.swift index b27449e..ea1605b 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -23,7 +23,7 @@ public struct Template { try env.set( name: "raise_exception", value: { (args: String) throws in - throw JinjaError.runtimeError("\(args)") + throw JinjaError.runtime("\(args)") } ) try env.set(name: "range", value: range)