| /* XML support for diagnostics. |
| Copyright (C) 2024-2025 Free Software Foundation, Inc. |
| Contributed by David Malcolm <dmalcolm@redhat.com>. |
| |
| This file is part of GCC. |
| |
| GCC is free software; you can redistribute it and/or modify it under |
| the terms of the GNU General Public License as published by the Free |
| Software Foundation; either version 3, or (at your option) any later |
| version. |
| |
| GCC is distributed in the hope that it will be useful, but WITHOUT ANY |
| WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GCC; see the file COPYING3. If not see |
| <http://www.gnu.org/licenses/>. */ |
| |
| #include "config.h" |
| #define INCLUDE_MAP |
| #define INCLUDE_STRING |
| #define INCLUDE_VECTOR |
| #include "system.h" |
| #include "coretypes.h" |
| #include "xml.h" |
| #include "xml-printer.h" |
| #include "pretty-print.h" |
| #include "selftest.h" |
| #include "selftest-xml.h" |
| |
| namespace xml { |
| |
| /* Disable warnings about quoting issues in the pp_xxx calls below |
| that (intentionally) don't follow GCC diagnostic conventions. */ |
| #if __GNUC__ >= 10 |
| # pragma GCC diagnostic push |
| # pragma GCC diagnostic ignored "-Wformat-diag" |
| #endif |
| |
| |
| /* Implementation. */ |
| |
| static void |
| write_escaped_text (pretty_printer *pp, const char *text) |
| { |
| gcc_assert (text); |
| |
| for (const char *p = text; *p; ++p) |
| { |
| char ch = *p; |
| switch (ch) |
| { |
| default: |
| pp_character (pp, ch); |
| break; |
| case '\'': |
| pp_string (pp, "'"); |
| break; |
| case '"': |
| pp_string (pp, """); |
| break; |
| case '&': |
| pp_string (pp, "&"); |
| break; |
| case '<': |
| pp_string (pp, "<"); |
| break; |
| case '>': |
| pp_string (pp, ">"); |
| break; |
| } |
| } |
| } |
| |
| /* struct node. */ |
| |
| void |
| node::dump (FILE *out) const |
| { |
| pretty_printer pp; |
| pp.set_output_stream (out); |
| write_as_xml (&pp, 0, true); |
| pp_flush (&pp); |
| } |
| |
| /* struct text : public node. */ |
| |
| void |
| text::write_as_xml (pretty_printer *pp, int depth, bool indent) const |
| { |
| if (indent) |
| { |
| for (int i = 0; i < depth; ++i) |
| pp_string (pp, " "); |
| } |
| write_escaped_text (pp, m_str.c_str ()); |
| if (indent) |
| pp_newline (pp); |
| } |
| |
| /* struct node_with_children : public node. */ |
| |
| void |
| node_with_children::add_child (std::unique_ptr<node> node) |
| { |
| gcc_assert (node.get ()); |
| m_children.push_back (std::move (node)); |
| } |
| |
| void |
| node_with_children::add_text (std::string str) |
| { |
| // Consolidate runs of text |
| if (!m_children.empty ()) |
| if (text *t = m_children.back ()->dyn_cast_text ()) |
| { |
| t->m_str += std::move (str); |
| return; |
| } |
| add_child (std::make_unique <text> (std::move (str))); |
| } |
| |
| void |
| node_with_children::add_text_from_pp (pretty_printer &pp) |
| { |
| add_text (pp_formatted_text (&pp)); |
| } |
| |
| void |
| node_with_children::add_comment (std::string str) |
| { |
| add_child (std::make_unique <comment> (std::move (str))); |
| } |
| |
| element * |
| node_with_children::find_child_element (std::string kind) const |
| { |
| for (auto &iter : m_children) |
| if (element *e = iter->dyn_cast_element ()) |
| if (e->m_kind == kind) |
| return e; |
| return nullptr; |
| } |
| |
| /* struct document : public node_with_children. */ |
| |
| void |
| document::write_as_xml (pretty_printer *pp, int depth, bool indent) const |
| { |
| pp_string (pp, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); |
| if (m_doctypedecl) |
| m_doctypedecl->write_as_xml (pp, depth, indent); |
| for (auto &iter : m_children) |
| iter->write_as_xml (pp, depth, indent); |
| } |
| |
| /* struct element : public node_with_children. */ |
| |
| void |
| element::write_as_xml (pretty_printer *pp, int depth, bool indent) const |
| { |
| if (indent) |
| { |
| for (int i = 0; i < depth; ++i) |
| pp_string (pp, " "); |
| } |
| |
| pp_printf (pp, "<%s", m_kind.c_str ()); |
| for (auto &key : m_key_insertion_order) |
| { |
| auto iter = m_attributes.find (key); |
| if (iter != m_attributes.end ()) |
| { |
| pp_printf (pp, " %s=\"", key.c_str ()); |
| write_escaped_text (pp, iter->second.c_str ()); |
| pp_string (pp, "\""); |
| } |
| } |
| if (m_children.empty ()) |
| pp_string (pp, "/>"); |
| else |
| { |
| const bool indent_children = m_preserve_whitespace ? false : indent; |
| pp_string (pp, ">"); |
| if (indent_children) |
| pp_newline (pp); |
| for (auto &child : m_children) |
| child->write_as_xml (pp, depth + 1, indent_children); |
| if (indent_children) |
| { |
| for (int i = 0; i < depth; ++i) |
| pp_string (pp, " "); |
| } |
| pp_printf (pp, "</%s>", m_kind.c_str ()); |
| } |
| |
| if (indent) |
| pp_newline (pp); |
| } |
| |
| void |
| element::set_attr (const char *name, std::string value) |
| { |
| auto iter = m_attributes.find (name); |
| if (iter == m_attributes.end ()) |
| m_key_insertion_order.push_back (name); |
| m_attributes[name] = std::move (value); |
| } |
| |
| const char * |
| element::get_attr (const char *name) const |
| { |
| auto iter = m_attributes.find (name); |
| if (iter == m_attributes.end ()) |
| return nullptr; |
| return iter->second.c_str (); |
| } |
| |
| // struct comment : public node |
| |
| void |
| comment::write_as_xml (pretty_printer *pp, |
| int depth, bool indent) const |
| { |
| if (indent) |
| { |
| for (int i = 0; i < depth; ++i) |
| pp_string (pp, " "); |
| } |
| pp_string (pp, "<!-- "); |
| write_escaped_text (pp, m_text.c_str ()); |
| pp_string (pp, " -->"); |
| if (indent) |
| pp_newline (pp); |
| } |
| |
| // struct raw : public node |
| |
| void |
| raw::write_as_xml (pretty_printer *pp, |
| int /*depth*/, bool /*indent*/) const |
| { |
| pp_string (pp, m_xml_src.c_str ()); |
| } |
| |
| // class printer |
| |
| printer::printer (element &insertion_point, |
| bool check_popped_tags) |
| : m_check_popped_tags (check_popped_tags) |
| { |
| m_open_tags.push_back (&insertion_point); |
| } |
| |
| void |
| printer::push_tag (std::string name, |
| bool preserve_whitespace) |
| { |
| push_element |
| (std::make_unique<element> (std::move (name), |
| preserve_whitespace)); |
| } |
| |
| void |
| printer::push_tag_with_class (std::string name, std::string class_, |
| bool preserve_whitespace) |
| { |
| auto new_element |
| = std::make_unique<element> (std::move (name), |
| preserve_whitespace); |
| new_element->set_attr ("class", class_); |
| push_element (std::move (new_element)); |
| } |
| |
| /* Pop the current topmost tag. |
| If m_check_popped_tags, assert that the tag we're popping is |
| EXPECTED_NAME. */ |
| |
| void |
| printer::pop_tag (const char *expected_name ATTRIBUTE_UNUSED) |
| { |
| gcc_assert (!m_open_tags.empty ()); |
| if (m_check_popped_tags) |
| gcc_assert (expected_name == get_insertion_point ()->m_kind); |
| m_open_tags.pop_back (); |
| } |
| |
| void |
| printer::set_attr (const char *name, std::string value) |
| { |
| m_open_tags.back ()->set_attr (name, value); |
| } |
| |
| void |
| printer::add_text (std::string text) |
| { |
| element *parent = m_open_tags.back (); |
| parent->add_text (std::move (text)); |
| } |
| |
| void |
| printer::add_text_from_pp (pretty_printer &pp) |
| { |
| element *parent = m_open_tags.back (); |
| parent->add_text_from_pp (pp); |
| } |
| |
| void |
| printer::add_raw (std::string text) |
| { |
| element *parent = m_open_tags.back (); |
| parent->add_child (std::make_unique<xml::raw> (std::move (text))); |
| } |
| |
| void |
| printer::push_element (std::unique_ptr<element> new_element) |
| { |
| gcc_assert (new_element.get ()); |
| element *parent = m_open_tags.back (); |
| m_open_tags.push_back (new_element.get ()); |
| parent->add_child (std::move (new_element)); |
| } |
| |
| void |
| printer::append (std::unique_ptr<node> new_node) |
| { |
| gcc_assert (new_node.get ()); |
| element *parent = m_open_tags.back (); |
| parent->add_child (std::move (new_node)); |
| } |
| |
| element * |
| printer::get_insertion_point () const |
| { |
| return m_open_tags.back (); |
| } |
| |
| void |
| printer::dump () const |
| { |
| pretty_printer pp; |
| pp.set_output_stream (stderr); |
| pp_printf (&pp, "open tags: %i:", (int)m_open_tags.size ()); |
| for (auto iter : m_open_tags) |
| pp_printf (&pp, " <%s>", iter->m_kind.c_str ()); |
| pp_newline (&pp); |
| pp_printf (&pp, "xml:"); |
| pp_newline (&pp); |
| m_open_tags[0]->write_as_xml (&pp, 1, true); |
| pp_flush (&pp); |
| } |
| |
| #if __GNUC__ >= 10 |
| # pragma GCC diagnostic pop |
| #endif |
| |
| } // namespace xml |
| |
| #if CHECKING_P |
| |
| namespace selftest { |
| |
| void |
| assert_xml_print_eq (const location &loc, |
| const xml::node &node, |
| const char *expected_value) |
| { |
| pretty_printer pp; |
| node.write_as_xml (&pp, 0, true); |
| ASSERT_STREQ_AT (loc, pp_formatted_text (&pp), expected_value); |
| } |
| |
| static void |
| test_no_dtd () |
| { |
| xml::document doc; |
| ASSERT_XML_PRINT_EQ |
| (doc, |
| "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); |
| } |
| |
| static void |
| test_printer () |
| { |
| xml::element top ("top", false); |
| xml::printer xp (top); |
| xp.push_tag ("foo"); |
| xp.add_text ("hello"); |
| xp.push_tag ("bar"); |
| xp.set_attr ("size", "3"); |
| xp.set_attr ("color", "red"); |
| xp.add_text ("world"); |
| xp.push_tag ("baz"); |
| xp.pop_tag ("baz"); |
| xp.pop_tag ("bar"); |
| xp.pop_tag ("foo"); |
| |
| ASSERT_XML_PRINT_EQ |
| (top, |
| "<top>\n" |
| " <foo>\n" |
| " hello\n" |
| " <bar size=\"3\" color=\"red\">\n" |
| " world\n" |
| " <baz/>\n" |
| " </bar>\n" |
| " </foo>\n" |
| "</top>\n"); |
| |
| xml::element *foo = top.find_child_element ("foo"); |
| ASSERT_TRUE (foo); |
| ASSERT_EQ (top.find_child_element ("not-foo"), nullptr); |
| xml::element *bar = foo->find_child_element ("bar"); |
| ASSERT_TRUE (bar); |
| ASSERT_STREQ (bar->get_attr ("size"), "3"); |
| ASSERT_STREQ (bar->get_attr ("color"), "red"); |
| ASSERT_EQ (bar->get_attr ("airspeed-velocity"), nullptr); |
| } |
| |
| // Verify that element attributes preserve insertion order. |
| |
| static void |
| test_attribute_ordering () |
| { |
| xml::element top ("top", false); |
| xml::printer xp (top); |
| xp.push_tag ("chronological"); |
| xp.set_attr ("maldon", "991"); |
| xp.set_attr ("hastings", "1066"); |
| xp.set_attr ("edgehill", "1642"); |
| xp.set_attr ("naseby", "1645"); |
| xp.pop_tag ("chronological"); |
| xp.push_tag ("alphabetical"); |
| xp.set_attr ("edgehill", "1642"); |
| xp.set_attr ("hastings", "1066"); |
| xp.set_attr ("maldon", "991"); |
| xp.set_attr ("naseby", "1645"); |
| xp.pop_tag ("alphabetical"); |
| |
| ASSERT_XML_PRINT_EQ |
| (top, |
| "<top>\n" |
| " <chronological maldon=\"991\" hastings=\"1066\" edgehill=\"1642\" naseby=\"1645\"/>\n" |
| " <alphabetical edgehill=\"1642\" hastings=\"1066\" maldon=\"991\" naseby=\"1645\"/>\n" |
| "</top>\n"); |
| } |
| |
| static void |
| test_comment () |
| { |
| xml::document doc; |
| doc.add_comment ("hello"); |
| doc.add_comment ("world"); |
| ASSERT_XML_PRINT_EQ |
| (doc, |
| "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" |
| "<!-- hello -->\n" |
| "<!-- world -->\n"); |
| |
| } |
| /* Run all of the selftests within this file. */ |
| |
| void |
| xml_cc_tests () |
| { |
| test_no_dtd (); |
| test_printer (); |
| test_attribute_ordering (); |
| test_comment (); |
| } |
| |
| } // namespace selftest |
| |
| #endif /* CHECKING_P */ |