diff --git a/CMakeLists.txt b/CMakeLists.txt index ea1c45b..09dbdd9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.5) -project(inja LANGUAGES CXX VERSION 2.2.0) +project(inja LANGUAGES CXX VERSION 2.3.0) option(INJA_USE_EMBEDDED_JSON "Use the shipped json header if not available on the system" ON) diff --git a/include/inja/exceptions.hpp b/include/inja/exceptions.hpp new file mode 100644 index 0000000..6b8fc8d --- /dev/null +++ b/include/inja/exceptions.hpp @@ -0,0 +1,59 @@ +// Copyright (c) 2020 Pantor. All rights reserved. + +#ifndef INCLUDE_INJA_EXCEPTIONS_HPP_ +#define INCLUDE_INJA_EXCEPTIONS_HPP_ + +#include +#include + + +namespace inja { + +struct SourceLocation { + size_t line; + size_t column; +}; + +struct InjaError : public std::runtime_error { + std::string type; + std::string message; + + bool has_location {false}; + SourceLocation location; + + InjaError(const std::string& type, const std::string& message) + : std::runtime_error("[inja.exception." + type + "] " + message), type(type), message(message) { } + + InjaError(const std::string& type, const std::string& message, SourceLocation location) + : std::runtime_error( + "[inja.exception." + type + "] (at " + std::to_string(location.line) + ":" + std::to_string(location.column) + ") " + message + ), type(type), message(message), has_location(true), location(location) { } +}; + +struct ParserError : public InjaError { + ParserError(const std::string& message) : InjaError("parser_error", message) { } + ParserError(const std::string& message, SourceLocation location) + : InjaError("parser_error", message, location) { } +}; + +struct RenderError : public InjaError { + RenderError(const std::string& message) : InjaError("render_error", message) { } + RenderError(const std::string& message, SourceLocation location) + : InjaError("render_error", message, location) { } +}; + +struct FileError : public InjaError { + FileError(const std::string& message) : InjaError("file_error", message) { } + FileError(const std::string& message, SourceLocation location) + : InjaError("file_error", message, location) { } +}; + +struct JsonError : public InjaError { + JsonError(const std::string& message) : InjaError("json_error", message) { } + JsonError(const std::string& message, SourceLocation location) + : InjaError("json_error", message, location) { } +}; + +} // namespace inja + +#endif // INCLUDE_INJA_EXCEPTIONS_HPP_ diff --git a/include/inja/inja.hpp b/include/inja/inja.hpp index e8dd05a..3e8ddd0 100644 --- a/include/inja/inja.hpp +++ b/include/inja/inja.hpp @@ -13,6 +13,7 @@ #include +#include "exceptions.hpp" #include "environment.hpp" #include "string_view.hpp" #include "template.hpp" diff --git a/include/inja/lexer.hpp b/include/inja/lexer.hpp index c9b4d69..cbe891a 100644 --- a/include/inja/lexer.hpp +++ b/include/inja/lexer.hpp @@ -37,6 +37,26 @@ class Lexer { public: explicit Lexer(const LexerConfig& config) : m_config(config) {} + SourceLocation current_position() const { + // Get line and offset position (starts at 1:1) + auto sliced = string_view::slice(m_in, 0, m_tok_start); + std::size_t last_newline = sliced.rfind("\n"); + + if (last_newline == nonstd::string_view::npos) { + return {1, sliced.length() + 1}; + } + + // Count newlines + size_t count_lines = 0; + size_t search_start = 0; + while (search_start < sliced.size()) { + search_start = sliced.find("\n", search_start + 1); + count_lines += 1; + } + + return {count_lines + 1, sliced.length() - last_newline + 1}; + } + void start(nonstd::string_view in) { m_in = in; m_tok_start = 0; @@ -287,8 +307,7 @@ class Lexer { } } - static nonstd::string_view clear_final_line_if_whitespace(nonstd::string_view text) - { + static nonstd::string_view clear_final_line_if_whitespace(nonstd::string_view text) { nonstd::string_view result = text; while (!result.empty()) { char ch = result.back(); diff --git a/include/inja/parser.hpp b/include/inja/parser.hpp index 4de9879..17371ce 100644 --- a/include/inja/parser.hpp +++ b/include/inja/parser.hpp @@ -10,6 +10,7 @@ #include "bytecode.hpp" #include "config.hpp" +#include "exceptions.hpp" #include "function_storage.hpp" #include "lexer.hpp" #include "template.hpp" @@ -147,7 +148,7 @@ class Parser { get_next_token(); if (!parse_expression(tmpl)) return false; if (m_tok.kind != Token::Kind::RightParen) { - inja_throw("parser_error", "unmatched '('"); + throw_parser_error("unmatched '('"); } get_next_token(); return true; @@ -166,7 +167,7 @@ class Parser { } else { for (;;) { if (!parse_expression(tmpl)) { - inja_throw("parser_error", "expected expression, got '" + m_tok.describe() + "'"); + throw_parser_error("expected expression, got '" + m_tok.describe() + "'"); } num_args += 1; if (m_tok.kind == Token::Kind::RightParen) { @@ -174,7 +175,7 @@ class Parser { break; } if (m_tok.kind != Token::Kind::Comma) { - inja_throw("parser_error", "expected ')' or ',', got '" + m_tok.describe() + "'"); + throw_parser_error("expected ')' or ',', got '" + m_tok.describe() + "'"); } get_next_token(); } @@ -220,7 +221,7 @@ class Parser { case Token::Kind::Comma: case Token::Kind::Colon: if (brace_level == 0 && bracket_level == 0) { - inja_throw("parser_error", "unexpected token '" + m_tok.describe() + "'"); + throw_parser_error("unexpected token '" + m_tok.describe() + "'"); } break; case Token::Kind::LeftBracket: @@ -237,24 +238,24 @@ class Parser { break; case Token::Kind::RightBracket: if (bracket_level == 0) { - inja_throw("parser_error", "unexpected ']'"); + throw_parser_error("unexpected ']'"); } --bracket_level; if (brace_level == 0 && bracket_level == 0) goto returnJson; break; case Token::Kind::RightBrace: if (brace_level == 0) { - inja_throw("parser_error", "unexpected '}'"); + throw_parser_error("unexpected '}'"); } --brace_level; if (brace_level == 0 && bracket_level == 0) goto returnJson; break; default: if (brace_level != 0) { - inja_throw("parser_error", "unmatched '{'"); + throw_parser_error("unmatched '{'"); } if (bracket_level != 0) { - inja_throw("parser_error", "unmatched '['"); + throw_parser_error("unmatched '['"); } return false; } @@ -286,7 +287,7 @@ class Parser { tmpl.bytecodes.emplace_back(Bytecode::Op::ConditionalJump); } else if (m_tok.text == static_cast("endif")) { if (m_if_stack.empty()) { - inja_throw("parser_error", "endif without matching if"); + throw_parser_error("endif without matching if"); } auto& if_data = m_if_stack.back(); get_next_token(); @@ -305,7 +306,7 @@ class Parser { m_if_stack.pop_back(); } else if (m_tok.text == static_cast("else")) { if (m_if_stack.empty()) - inja_throw("parser_error", "else without matching if"); + throw_parser_error("else without matching if"); auto& if_data = m_if_stack.back(); get_next_token(); @@ -336,7 +337,7 @@ class Parser { // options: for a in arr; for a, b in obj if (m_tok.kind != Token::Kind::Id) - inja_throw("parser_error", "expected id, got '" + m_tok.describe() + "'"); + throw_parser_error("expected id, got '" + m_tok.describe() + "'"); Token value_token = m_tok; get_next_token(); @@ -344,14 +345,14 @@ class Parser { if (m_tok.kind == Token::Kind::Comma) { get_next_token(); if (m_tok.kind != Token::Kind::Id) - inja_throw("parser_error", "expected id, got '" + m_tok.describe() + "'"); + throw_parser_error("expected id, got '" + m_tok.describe() + "'"); key_token = std::move(value_token); value_token = m_tok; get_next_token(); } if (m_tok.kind != Token::Kind::Id || m_tok.text != static_cast("in")) - inja_throw("parser_error", + throw_parser_error( "expected 'in', got '" + m_tok.describe() + "'"); get_next_token(); @@ -367,7 +368,7 @@ class Parser { } else if (m_tok.text == static_cast("endfor")) { get_next_token(); if (m_loop_stack.empty()) { - inja_throw("parser_error", "endfor without matching for"); + throw_parser_error("endfor without matching for"); } // update loop with EndLoop index (for empty case) @@ -380,7 +381,7 @@ class Parser { get_next_token(); if (m_tok.kind != Token::Kind::String) { - inja_throw("parser_error", "expected string, got '" + m_tok.describe() + "'"); + throw_parser_error("expected string, got '" + m_tok.describe() + "'"); } // build the relative path @@ -447,8 +448,8 @@ class Parser { get_next_token(); switch (m_tok.kind) { case Token::Kind::Eof: - if (!m_if_stack.empty()) inja_throw("parser_error", "unmatched if"); - if (!m_loop_stack.empty()) inja_throw("parser_error", "unmatched for"); + if (!m_if_stack.empty()) throw_parser_error("unmatched if"); + if (!m_loop_stack.empty()) throw_parser_error("unmatched for"); return; case Token::Kind::Text: tmpl.bytecodes.emplace_back(Bytecode::Op::PrintText, m_tok.text, 0u); @@ -456,10 +457,10 @@ class Parser { case Token::Kind::StatementOpen: get_next_token(); if (!parse_statement(tmpl, path)) { - inja_throw("parser_error", "expected statement, got '" + m_tok.describe() + "'"); + throw_parser_error("expected statement, got '" + m_tok.describe() + "'"); } if (m_tok.kind != Token::Kind::StatementClose) { - inja_throw("parser_error", "expected statement close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected statement close, got '" + m_tok.describe() + "'"); } break; case Token::Kind::LineStatementOpen: @@ -467,27 +468,27 @@ class Parser { parse_statement(tmpl, path); if (m_tok.kind != Token::Kind::LineStatementClose && m_tok.kind != Token::Kind::Eof) { - inja_throw("parser_error", "expected line statement close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected line statement close, got '" + m_tok.describe() + "'"); } break; case Token::Kind::ExpressionOpen: get_next_token(); if (!parse_expression(tmpl)) { - inja_throw("parser_error", "expected expression, got '" + m_tok.describe() + "'"); + throw_parser_error("expected expression, got '" + m_tok.describe() + "'"); } append_function(tmpl, Bytecode::Op::PrintValue, 1); if (m_tok.kind != Token::Kind::ExpressionClose) { - inja_throw("parser_error", "expected expression close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected expression close, got '" + m_tok.describe() + "'"); } break; case Token::Kind::CommentOpen: get_next_token(); if (m_tok.kind != Token::Kind::CommentClose) { - inja_throw("parser_error", "expected comment close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected comment close, got '" + m_tok.describe() + "'"); } break; default: - inja_throw("parser_error", "unexpected token '" + m_tok.describe() + "'"); + throw_parser_error("unexpected token '" + m_tok.describe() + "'"); break; } } @@ -543,6 +544,10 @@ class Parser { std::vector m_if_stack; std::vector m_loop_stack; + void throw_parser_error(const std::string& message) { + throw ParserError(message, m_lexer.current_position()); + } + void get_next_token() { if (m_have_peek_tok) { m_tok = m_peek_tok; diff --git a/include/inja/renderer.hpp b/include/inja/renderer.hpp index de1830d..b162518 100644 --- a/include/inja/renderer.hpp +++ b/include/inja/renderer.hpp @@ -12,6 +12,7 @@ #include #include "bytecode.hpp" +#include "exceptions.hpp" #include "template.hpp" #include "utils.hpp" @@ -92,7 +93,7 @@ class Renderer { m_tmp_val = callback(arguments); return &m_tmp_val; } - inja_throw("render_error", "variable '" + static_cast(bc.str) + "' not found"); + throw RenderError("variable '" + static_cast(bc.str) + "' not found"); return nullptr; } } @@ -109,8 +110,7 @@ class Renderer { try { return var.get(); } catch (json::type_error& e) { - inja_throw("json_error", e.what()); - throw; + throw JsonError(e.what()); } } @@ -463,7 +463,7 @@ class Renderer { case Bytecode::Op::Callback: { auto callback = m_callbacks.find_callback(bc.str, bc.args); if (!callback) { - inja_throw("render_error", "function '" + static_cast(bc.str) + "' (" + std::to_string(static_cast(bc.args)) + ") not found"); + throw RenderError("function '" + static_cast(bc.str) + "' (" + std::to_string(static_cast(bc.args)) + ") not found"); } json result = callback(get_args(bc)); pop_args(bc); @@ -500,7 +500,7 @@ class Renderer { // map iterator if (!level.values.is_object()) { m_loop_stack.pop_back(); - inja_throw("render_error", "for key, value requires object"); + throw RenderError("for key, value requires object"); } level.loop_type = LoopLevel::Type::Map; level.key_name = bc.value.get_ref(); @@ -515,7 +515,7 @@ class Renderer { } else { if (!level.values.is_array()) { m_loop_stack.pop_back(); - inja_throw("render_error", "type must be array"); + throw RenderError("type must be array"); } // list iterator @@ -538,7 +538,7 @@ class Renderer { } case Bytecode::Op::EndLoop: { if (m_loop_stack.empty()) { - inja_throw("render_error", "unexpected state in renderer"); + throw RenderError("unexpected state in renderer"); } LoopLevel& level = m_loop_stack.back(); @@ -569,7 +569,7 @@ class Renderer { break; } default: { - inja_throw("render_error", "unknown op in renderer: " + std::to_string(static_cast(bc.op))); + throw RenderError("unknown op in renderer: " + std::to_string(static_cast(bc.op))); } } } diff --git a/include/inja/utils.hpp b/include/inja/utils.hpp index 7a3b2a8..dfdd9ba 100644 --- a/include/inja/utils.hpp +++ b/include/inja/utils.hpp @@ -5,26 +5,22 @@ #include #include -#include #include #include +#include "exceptions.hpp" #include "string_view.hpp" namespace inja { -inline void inja_throw(const std::string& type, const std::string& message) { - throw std::runtime_error("[inja.exception." + type + "] " + message); -} - inline std::ifstream open_file_or_throw(const std::string& path) { std::ifstream file; file.exceptions(std::ifstream::failbit | std::ifstream::badbit); try { file.open(path); } catch(const std::ios_base::failure& /*e*/) { - inja_throw("file_error", "failed accessing file at '" + path + "'"); + throw FileError("failed accessing file at '" + path + "'"); } return file; } diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index 0da6be5..349a78b 100644 --- a/single_include/inja/inja.hpp +++ b/single_include/inja/inja.hpp @@ -13,6 +13,67 @@ #include +// #include "exceptions.hpp" +// Copyright (c) 2020 Pantor. All rights reserved. + +#ifndef INCLUDE_INJA_EXCEPTIONS_HPP_ +#define INCLUDE_INJA_EXCEPTIONS_HPP_ + +#include +#include + + +namespace inja { + +struct SourceLocation { + size_t line; + size_t column; +}; + +struct InjaError : public std::runtime_error { + std::string type; + std::string message; + + bool has_location {false}; + SourceLocation location; + + InjaError(const std::string& type, const std::string& message) + : std::runtime_error("[inja.exception." + type + "] " + message), type(type), message(message) { } + + InjaError(const std::string& type, const std::string& message, SourceLocation location) + : std::runtime_error( + "[inja.exception." + type + "] (at " + std::to_string(location.line) + ":" + std::to_string(location.column) + ") " + message + ), type(type), message(message), has_location(true), location(location) { } +}; + +struct ParserError : public InjaError { + ParserError(const std::string& message) : InjaError("parser_error", message) { } + ParserError(const std::string& message, SourceLocation location) + : InjaError("parser_error", message, location) { } +}; + +struct RenderError : public InjaError { + RenderError(const std::string& message) : InjaError("render_error", message) { } + RenderError(const std::string& message, SourceLocation location) + : InjaError("render_error", message, location) { } +}; + +struct FileError : public InjaError { + FileError(const std::string& message) : InjaError("file_error", message) { } + FileError(const std::string& message, SourceLocation location) + : InjaError("file_error", message, location) { } +}; + +struct JsonError : public InjaError { + JsonError(const std::string& message) : InjaError("json_error", message) { } + JsonError(const std::string& message, SourceLocation location) + : InjaError("json_error", message, location) { } +}; + +} // namespace inja + +#endif // INCLUDE_INJA_EXCEPTIONS_HPP_ + // #include "environment.hpp" // Copyright (c) 2019 Pantor. All rights reserved. @@ -1833,6 +1894,8 @@ class FunctionStorage { // #include "config.hpp" +// #include "exceptions.hpp" + // #include "function_storage.hpp" // #include "lexer.hpp" @@ -1926,27 +1989,24 @@ struct Token { #include #include -#include #include #include +// #include "exceptions.hpp" + // #include "string_view.hpp" namespace inja { -inline void inja_throw(const std::string& type, const std::string& message) { - throw std::runtime_error("[inja.exception." + type + "] " + message); -} - inline std::ifstream open_file_or_throw(const std::string& path) { std::ifstream file; file.exceptions(std::ifstream::failbit | std::ifstream::badbit); try { file.open(path); } catch(const std::ios_base::failure& /*e*/) { - inja_throw("file_error", "failed accessing file at '" + path + "'"); + throw FileError("failed accessing file at '" + path + "'"); } return file; } @@ -2003,6 +2063,26 @@ class Lexer { public: explicit Lexer(const LexerConfig& config) : m_config(config) {} + SourceLocation current_position() const { + // Get line and offset position (starts at 1:1) + auto sliced = string_view::slice(m_in, 0, m_tok_start); + std::size_t last_newline = sliced.rfind("\n"); + + if (last_newline == nonstd::string_view::npos) { + return {1, sliced.length() + 1}; + } + + // Count newlines + size_t count_lines = 0; + size_t search_start = 0; + while (search_start < sliced.size()) { + search_start = sliced.find("\n", search_start + 1); + count_lines += 1; + } + + return {count_lines + 1, sliced.length() - last_newline + 1}; + } + void start(nonstd::string_view in) { m_in = in; m_tok_start = 0; @@ -2253,8 +2333,7 @@ class Lexer { } } - static nonstd::string_view clear_final_line_if_whitespace(nonstd::string_view text) - { + static nonstd::string_view clear_final_line_if_whitespace(nonstd::string_view text) { nonstd::string_view result = text; while (!result.empty()) { char ch = result.back(); @@ -2439,7 +2518,7 @@ class Parser { get_next_token(); if (!parse_expression(tmpl)) return false; if (m_tok.kind != Token::Kind::RightParen) { - inja_throw("parser_error", "unmatched '('"); + throw_parser_error("unmatched '('"); } get_next_token(); return true; @@ -2458,7 +2537,7 @@ class Parser { } else { for (;;) { if (!parse_expression(tmpl)) { - inja_throw("parser_error", "expected expression, got '" + m_tok.describe() + "'"); + throw_parser_error("expected expression, got '" + m_tok.describe() + "'"); } num_args += 1; if (m_tok.kind == Token::Kind::RightParen) { @@ -2466,7 +2545,7 @@ class Parser { break; } if (m_tok.kind != Token::Kind::Comma) { - inja_throw("parser_error", "expected ')' or ',', got '" + m_tok.describe() + "'"); + throw_parser_error("expected ')' or ',', got '" + m_tok.describe() + "'"); } get_next_token(); } @@ -2512,7 +2591,7 @@ class Parser { case Token::Kind::Comma: case Token::Kind::Colon: if (brace_level == 0 && bracket_level == 0) { - inja_throw("parser_error", "unexpected token '" + m_tok.describe() + "'"); + throw_parser_error("unexpected token '" + m_tok.describe() + "'"); } break; case Token::Kind::LeftBracket: @@ -2529,24 +2608,24 @@ class Parser { break; case Token::Kind::RightBracket: if (bracket_level == 0) { - inja_throw("parser_error", "unexpected ']'"); + throw_parser_error("unexpected ']'"); } --bracket_level; if (brace_level == 0 && bracket_level == 0) goto returnJson; break; case Token::Kind::RightBrace: if (brace_level == 0) { - inja_throw("parser_error", "unexpected '}'"); + throw_parser_error("unexpected '}'"); } --brace_level; if (brace_level == 0 && bracket_level == 0) goto returnJson; break; default: if (brace_level != 0) { - inja_throw("parser_error", "unmatched '{'"); + throw_parser_error("unmatched '{'"); } if (bracket_level != 0) { - inja_throw("parser_error", "unmatched '['"); + throw_parser_error("unmatched '['"); } return false; } @@ -2578,7 +2657,7 @@ class Parser { tmpl.bytecodes.emplace_back(Bytecode::Op::ConditionalJump); } else if (m_tok.text == static_cast("endif")) { if (m_if_stack.empty()) { - inja_throw("parser_error", "endif without matching if"); + throw_parser_error("endif without matching if"); } auto& if_data = m_if_stack.back(); get_next_token(); @@ -2597,7 +2676,7 @@ class Parser { m_if_stack.pop_back(); } else if (m_tok.text == static_cast("else")) { if (m_if_stack.empty()) - inja_throw("parser_error", "else without matching if"); + throw_parser_error("else without matching if"); auto& if_data = m_if_stack.back(); get_next_token(); @@ -2628,7 +2707,7 @@ class Parser { // options: for a in arr; for a, b in obj if (m_tok.kind != Token::Kind::Id) - inja_throw("parser_error", "expected id, got '" + m_tok.describe() + "'"); + throw_parser_error("expected id, got '" + m_tok.describe() + "'"); Token value_token = m_tok; get_next_token(); @@ -2636,14 +2715,14 @@ class Parser { if (m_tok.kind == Token::Kind::Comma) { get_next_token(); if (m_tok.kind != Token::Kind::Id) - inja_throw("parser_error", "expected id, got '" + m_tok.describe() + "'"); + throw_parser_error("expected id, got '" + m_tok.describe() + "'"); key_token = std::move(value_token); value_token = m_tok; get_next_token(); } if (m_tok.kind != Token::Kind::Id || m_tok.text != static_cast("in")) - inja_throw("parser_error", + throw_parser_error( "expected 'in', got '" + m_tok.describe() + "'"); get_next_token(); @@ -2659,7 +2738,7 @@ class Parser { } else if (m_tok.text == static_cast("endfor")) { get_next_token(); if (m_loop_stack.empty()) { - inja_throw("parser_error", "endfor without matching for"); + throw_parser_error("endfor without matching for"); } // update loop with EndLoop index (for empty case) @@ -2672,7 +2751,7 @@ class Parser { get_next_token(); if (m_tok.kind != Token::Kind::String) { - inja_throw("parser_error", "expected string, got '" + m_tok.describe() + "'"); + throw_parser_error("expected string, got '" + m_tok.describe() + "'"); } // build the relative path @@ -2739,8 +2818,8 @@ class Parser { get_next_token(); switch (m_tok.kind) { case Token::Kind::Eof: - if (!m_if_stack.empty()) inja_throw("parser_error", "unmatched if"); - if (!m_loop_stack.empty()) inja_throw("parser_error", "unmatched for"); + if (!m_if_stack.empty()) throw_parser_error("unmatched if"); + if (!m_loop_stack.empty()) throw_parser_error("unmatched for"); return; case Token::Kind::Text: tmpl.bytecodes.emplace_back(Bytecode::Op::PrintText, m_tok.text, 0u); @@ -2748,10 +2827,10 @@ class Parser { case Token::Kind::StatementOpen: get_next_token(); if (!parse_statement(tmpl, path)) { - inja_throw("parser_error", "expected statement, got '" + m_tok.describe() + "'"); + throw_parser_error("expected statement, got '" + m_tok.describe() + "'"); } if (m_tok.kind != Token::Kind::StatementClose) { - inja_throw("parser_error", "expected statement close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected statement close, got '" + m_tok.describe() + "'"); } break; case Token::Kind::LineStatementOpen: @@ -2759,27 +2838,27 @@ class Parser { parse_statement(tmpl, path); if (m_tok.kind != Token::Kind::LineStatementClose && m_tok.kind != Token::Kind::Eof) { - inja_throw("parser_error", "expected line statement close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected line statement close, got '" + m_tok.describe() + "'"); } break; case Token::Kind::ExpressionOpen: get_next_token(); if (!parse_expression(tmpl)) { - inja_throw("parser_error", "expected expression, got '" + m_tok.describe() + "'"); + throw_parser_error("expected expression, got '" + m_tok.describe() + "'"); } append_function(tmpl, Bytecode::Op::PrintValue, 1); if (m_tok.kind != Token::Kind::ExpressionClose) { - inja_throw("parser_error", "expected expression close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected expression close, got '" + m_tok.describe() + "'"); } break; case Token::Kind::CommentOpen: get_next_token(); if (m_tok.kind != Token::Kind::CommentClose) { - inja_throw("parser_error", "expected comment close, got '" + m_tok.describe() + "'"); + throw_parser_error("expected comment close, got '" + m_tok.describe() + "'"); } break; default: - inja_throw("parser_error", "unexpected token '" + m_tok.describe() + "'"); + throw_parser_error("unexpected token '" + m_tok.describe() + "'"); break; } } @@ -2835,6 +2914,10 @@ class Parser { std::vector m_if_stack; std::vector m_loop_stack; + void throw_parser_error(const std::string& message) { + throw ParserError(message, m_lexer.current_position()); + } + void get_next_token() { if (m_have_peek_tok) { m_tok = m_peek_tok; @@ -2928,6 +3011,8 @@ namespace stdinja = std; // #include "bytecode.hpp" +// #include "exceptions.hpp" + // #include "template.hpp" // #include "utils.hpp" @@ -3010,7 +3095,7 @@ class Renderer { m_tmp_val = callback(arguments); return &m_tmp_val; } - inja_throw("render_error", "variable '" + static_cast(bc.str) + "' not found"); + throw RenderError("variable '" + static_cast(bc.str) + "' not found"); return nullptr; } } @@ -3027,8 +3112,7 @@ class Renderer { try { return var.get(); } catch (json::type_error& e) { - inja_throw("json_error", e.what()); - throw; + throw JsonError(e.what()); } } @@ -3381,7 +3465,7 @@ class Renderer { case Bytecode::Op::Callback: { auto callback = m_callbacks.find_callback(bc.str, bc.args); if (!callback) { - inja_throw("render_error", "function '" + static_cast(bc.str) + "' (" + std::to_string(static_cast(bc.args)) + ") not found"); + throw RenderError("function '" + static_cast(bc.str) + "' (" + std::to_string(static_cast(bc.args)) + ") not found"); } json result = callback(get_args(bc)); pop_args(bc); @@ -3418,7 +3502,7 @@ class Renderer { // map iterator if (!level.values.is_object()) { m_loop_stack.pop_back(); - inja_throw("render_error", "for key, value requires object"); + throw RenderError("for key, value requires object"); } level.loop_type = LoopLevel::Type::Map; level.key_name = bc.value.get_ref(); @@ -3433,7 +3517,7 @@ class Renderer { } else { if (!level.values.is_array()) { m_loop_stack.pop_back(); - inja_throw("render_error", "type must be array"); + throw RenderError("type must be array"); } // list iterator @@ -3456,7 +3540,7 @@ class Renderer { } case Bytecode::Op::EndLoop: { if (m_loop_stack.empty()) { - inja_throw("render_error", "unexpected state in renderer"); + throw RenderError("unexpected state in renderer"); } LoopLevel& level = m_loop_stack.back(); @@ -3487,7 +3571,7 @@ class Renderer { break; } default: { - inja_throw("render_error", "unknown op in renderer: " + std::to_string(static_cast(bc.op))); + throw RenderError("unknown op in renderer: " + std::to_string(static_cast(bc.op))); } } } diff --git a/test/data/error-unknown/data.json b/test/data/error-unknown/data.json new file mode 100755 index 0000000..c009c75 --- /dev/null +++ b/test/data/error-unknown/data.json @@ -0,0 +1,3 @@ +{ + "name": "Jeff" +} \ No newline at end of file diff --git a/test/data/error-unknown/template.txt b/test/data/error-unknown/template.txt new file mode 100755 index 0000000..17af6d7 --- /dev/null +++ b/test/data/error-unknown/template.txt @@ -0,0 +1,6 @@ + +{% for x ins xarray %}{% for y in yarray %} +{{x}}-{{y}} +{% endfor %}{% endfor %} + +Hello {{ unknown }}. \ No newline at end of file diff --git a/test/unit-renderer.cpp b/test/unit-renderer.cpp index 08300f4..8da544b 100644 --- a/test/unit-renderer.cpp +++ b/test/unit-renderer.cpp @@ -67,7 +67,7 @@ TEST_CASE("types") { CHECK( env.render("{% for name in {} %}a{% endfor %}", data) == ""); - CHECK_THROWS_WITH(env.render("{% for name ins names %}a{% endfor %}", data), "[inja.exception.parser_error] expected 'in', got 'ins'"); + CHECK_THROWS_WITH(env.render("{% for name ins names %}a{% endfor %}", data), "[inja.exception.parser_error] (at 1:13) expected 'in', got 'ins'"); CHECK_THROWS_WITH(env.render("{% for name in empty_loop %}a{% endfor %}", data), "[inja.exception.render_error] variable 'empty_loop' not found"); // CHECK_THROWS_WITH( env.render("{% for name in relatives %}{{ name }}{% endfor %}", data), "[inja.exception.json_error] [json.exception.type_error.302] type must be array, but is object" ); } @@ -114,8 +114,8 @@ R"DELIM( CHECK(env.render("{% if age == 26 %}26{% else if age == 27 %}27{% else if age == 28 %}28{% else %}29{% endif %}", data) == "29"); CHECK(env.render("{% if age == 25 %}+{% endif %}{% if age == 29 %}+{% else %}-{% endif %}", data) == "+"); - CHECK_THROWS_WITH(env.render("{% if is_happy %}{% if is_happy %}{% endif %}", data), "[inja.exception.parser_error] unmatched if"); - CHECK_THROWS_WITH(env.render("{% if is_happy %}{% else if is_happy %}{% end if %}", data), "[inja.exception.parser_error] expected statement, got 'end'"); + CHECK_THROWS_WITH(env.render("{% if is_happy %}{% if is_happy %}{% endif %}", data), "[inja.exception.parser_error] (at 1:46) unmatched if"); + CHECK_THROWS_WITH(env.render("{% if is_happy %}{% else if is_happy %}{% end if %}", data), "[inja.exception.parser_error] (at 1:43) expected statement, got 'end'"); } SUBCASE("line statements") {