From 19109264a5386de3dcd7f6d85397a5841248db24 Mon Sep 17 00:00:00 2001 From: Viacheslav Koryagin Date: Thu, 23 Jul 2026 15:17:49 +0300 Subject: [PATCH 1/2] THRIFT-6108: implemented exception documentation generation Client: cpp,haxe,java,javame,kotlin,netstd,py --- .../src/thrift/generate/t_cpp_generator.cc | 6 ++ .../src/thrift/generate/t_delphi_generator.cc | 15 --- .../src/thrift/generate/t_haxe_generator.cc | 15 ++- .../src/thrift/generate/t_java_generator.cc | 5 + .../src/thrift/generate/t_javame_generator.cc | 29 +++-- .../src/thrift/generate/t_kotlin_generator.cc | 19 +++- .../src/thrift/generate/t_netstd_generator.cc | 38 +++++-- .../cpp/src/thrift/generate/t_oop_generator.h | 37 ++++++- .../cpp/src/thrift/generate/t_py_generator.cc | 39 +++++-- compiler/cpp/test/CMakeLists.txt | 3 +- .../cpp/test/compiler/ExceptionDocTest.thrift | 53 +++++++++ compiler/cpp/test/compiler/Included.thrift | 8 +- .../cpp/test/compiler/exception_doc_test.py | 102 ++++++++++++++++++ test/ThriftTest.thrift | 6 +- 14 files changed, 317 insertions(+), 58 deletions(-) create mode 100644 compiler/cpp/test/compiler/ExceptionDocTest.thrift create mode 100644 compiler/cpp/test/compiler/exception_doc_test.py diff --git a/compiler/cpp/src/thrift/generate/t_cpp_generator.cc b/compiler/cpp/src/thrift/generate/t_cpp_generator.cc index f468ea88d58..2ab76db734f 100644 --- a/compiler/cpp/src/thrift/generate/t_cpp_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_cpp_generator.cc @@ -120,6 +120,7 @@ class t_cpp_generator : public t_oop_generator { void init_generator() override; void close_generator() override; std::string display_name() const override; + std::string get_doc_type_name(t_type* type) override; void generate_consts(std::vector consts) override; @@ -455,6 +456,11 @@ class t_cpp_generator : public t_oop_generator { friend class ProcessorGenerator; }; +std::string t_cpp_generator::get_doc_type_name(t_type* type) { + type = get_true_type(type); + return namespace_prefix(type->get_program()->get_namespace("cpp")) + type->get_name(); +} + /** * Prepares for file generation by opening up the necessary file output * streams. diff --git a/compiler/cpp/src/thrift/generate/t_delphi_generator.cc b/compiler/cpp/src/thrift/generate/t_delphi_generator.cc index e5b9ca26680..87000604e27 100644 --- a/compiler/cpp/src/thrift/generate/t_delphi_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_delphi_generator.cc @@ -359,7 +359,6 @@ class t_delphi_generator : public t_oop_generator { " *)\n"; } - string xml_encode(string contents); string xmldoc_encode(string contents); string xmlattrib_encode(string contents); void generate_delphi_doc(std::ostream& out, t_field* field); @@ -469,20 +468,6 @@ class t_delphi_generator : public t_oop_generator { std::ostream& indent_impl(std::ostream& os) { return os << indent_impl(); }; }; -// XML encoding -string t_delphi_generator::xml_encode(string contents) { - string str(contents); - - // escape the escape - str = replace_all(str, "&", "&"); - - // other standard XML entities - str = replace_all(str, "<", "<"); - str = replace_all(str, ">", ">"); - - return str; -} - // XML attribute encoding string t_delphi_generator::xmlattrib_encode(string contents) { string str(xml_encode(contents)); diff --git a/compiler/cpp/src/thrift/generate/t_haxe_generator.cc b/compiler/cpp/src/thrift/generate/t_haxe_generator.cc index a05736d2c21..36c4011e227 100644 --- a/compiler/cpp/src/thrift/generate/t_haxe_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_haxe_generator.cc @@ -74,6 +74,7 @@ class t_haxe_generator : public t_oop_generator { void init_generator() override; void close_generator() override; std::string display_name() const override; + std::string get_doc_type_name(t_type* type) override; void generate_consts(std::vector consts) override; @@ -3136,8 +3137,8 @@ void t_haxe_generator::generate_haxe_doc(ostream& out, t_doc* tdoc) { * Emits a haxeDoc comment if the provided function object has a doc in Thrift */ void t_haxe_generator::generate_haxe_doc(ostream& out, t_function* tfunction) { + stringstream ss; if (tfunction->has_doc()) { - stringstream ss; ss << tfunction->get_doc(); const vector& fields = tfunction->get_arglist()->get_members(); vector::const_iterator p_iter; @@ -3148,8 +3149,18 @@ void t_haxe_generator::generate_haxe_doc(ostream& out, t_function* tfunction) { ss << " " << p->get_doc(); } } - generate_docstring_comment(out, "/**\n", " * ", ss.str(), " */\n"); } + + generate_throws_doc(ss, tfunction); + + const std::string result_doc = ss.str(); + if (!result_doc.empty()) { + generate_docstring_comment(out, "/**\n", " * ", result_doc, " */\n"); + } +} + +std::string t_haxe_generator::get_doc_type_name(t_type* type) { + return type_name(type); } std::string t_haxe_generator::generate_isset_check(t_field* field) { diff --git a/compiler/cpp/src/thrift/generate/t_java_generator.cc b/compiler/cpp/src/thrift/generate/t_java_generator.cc index d583e4b1a33..7ffaf767515 100644 --- a/compiler/cpp/src/thrift/generate/t_java_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_java_generator.cc @@ -148,6 +148,7 @@ class t_java_generator : public t_oop_generator { void init_generator() override; void close_generator() override; std::string display_name() const override; + std::string get_doc_type_name(t_type* type) override; void generate_consts(std::vector consts) override; @@ -4692,6 +4693,10 @@ string t_java_generator::type_name(t_type* ttype, return make_valid_java_identifier(ttype->get_name()); } +std::string t_java_generator::get_doc_type_name(t_type* type) { + return type_name(type, false, false, false, true); +} + /** * Returns the Java type that corresponds to the thrift type. * diff --git a/compiler/cpp/src/thrift/generate/t_javame_generator.cc b/compiler/cpp/src/thrift/generate/t_javame_generator.cc index 9e7b459cd8d..4ac05fa11cd 100644 --- a/compiler/cpp/src/thrift/generate/t_javame_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_javame_generator.cc @@ -66,6 +66,7 @@ class t_javame_generator : public t_oop_generator { void init_generator() override; void close_generator() override; std::string display_name() const override; + std::string get_doc_type_name(t_type* type) override; void generate_consts(std::vector consts) override; @@ -188,7 +189,7 @@ class t_javame_generator : public t_oop_generator { void generate_java_doc(std::ostream& out, t_doc* tdoc) override; - void generate_java_doc(std::ostream& out, t_function* tdoc) override; + void generate_java_doc(std::ostream& out, t_function* tfunction) override; void generate_java_docstring_comment(std::ostream& out, string contents) override; @@ -2834,6 +2835,17 @@ string t_javame_generator::type_name(t_type* ttype, return ttype->get_name(); } +std::string t_javame_generator::get_doc_type_name(t_type* type) { + type = get_true_type(type); + const std::string type_name_str = type_name(type); + if (type->get_program() != program_) { + return type_name_str; + } + + const std::string package = type->get_program()->get_namespace("java"); + return (package.empty() ? "" : package + ".") + type_name_str; +} + /** * Returns the C++ type that corresponds to the thrift type. * @@ -3058,20 +3070,7 @@ void t_javame_generator::generate_java_doc(ostream& out, t_doc* tdoc) { * Emits a JavaDoc comment if the provided function object has a doc in Thrift */ void t_javame_generator::generate_java_doc(ostream& out, t_function* tfunction) { - if (tfunction->has_doc()) { - stringstream ss; - ss << tfunction->get_doc(); - const vector& fields = tfunction->get_arglist()->get_members(); - vector::const_iterator p_iter; - for (p_iter = fields.begin(); p_iter != fields.end(); ++p_iter) { - t_field* p = *p_iter; - ss << "\n@param " << p->get_name(); - if (p->has_doc()) { - ss << " " << p->get_doc(); - } - } - generate_docstring_comment(out, "/**\n", " * ", ss.str(), " */\n"); - } + t_oop_generator::generate_java_doc(out, tfunction); } void t_javame_generator::generate_deep_copy_container(ostream& out, diff --git a/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc b/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc index 2e81909c48b..fcee6f6e4ef 100644 --- a/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc @@ -77,6 +77,8 @@ class t_kotlin_generator : public t_oop_generator { void init_generator() override; void close_generator() override; std::string display_name() const override; + std::string get_doc_type_name(t_type* type) override; + void generate_java_doc(std::ostream& out, t_function* function) override; void generate_consts(std::vector consts) override; @@ -404,6 +406,21 @@ string t_kotlin_generator::type_name(t_type* ttype, return kotlin_safe_name(ttype->get_name()); } +std::string t_kotlin_generator::get_doc_type_name(t_type* type) { + return type_name(type, false, false, true); +} + +void t_kotlin_generator::generate_java_doc(ostream& out, t_function* function) { + stringstream doc; + if (function->has_doc()) { + doc << function->get_doc(); + } + generate_throws_doc(doc, function); + if (!doc.str().empty()) { + generate_docstring_comment(out, "/**\n", " * ", doc.str(), " */\n"); + } +} + /** * Generates a struct definition for a thrift data type. This will be a org.apache.thrift.TBase * implementor. @@ -1420,7 +1437,7 @@ void t_kotlin_generator::generate_service_interface(t_service* tservice) { out << "interface " << tservice->get_name() << " {" << '\n'; indent_up(); for (auto tfunc : tservice->get_functions()) { - generate_kdoc_comment(out, tfunc); + generate_java_doc(out, tfunc); indent(out) << function_signature(tfunc) << '\n'; } scope_down(out); diff --git a/compiler/cpp/src/thrift/generate/t_netstd_generator.cc b/compiler/cpp/src/thrift/generate/t_netstd_generator.cc index 61e5bdeb0ff..3a43ae41eac 100644 --- a/compiler/cpp/src/thrift/generate/t_netstd_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_netstd_generator.cc @@ -3942,9 +3942,9 @@ void t_netstd_generator::generate_netstd_doc(ostream& out, t_doc* tdoc) void t_netstd_generator::generate_netstd_doc(ostream& out, t_function* tfunction) { - if (tfunction->has_doc()) - { - stringstream ps; + stringstream ps; + if (tfunction->has_doc()) { + ps << "\n" << xml_encode(tfunction->get_doc()) << ""; const vector& fields = tfunction->get_arglist()->get_members(); vector::const_iterator p_iter; for (p_iter = fields.begin(); p_iter != fields.end(); ++p_iter) @@ -3953,18 +3953,34 @@ void t_netstd_generator::generate_netstd_doc(ostream& out, t_function* tfunction ps << '\n' << "get_name() << "\">"; if (p->has_doc()) { - string str = p->get_doc(); - str.erase(remove(str.begin(), str.end(), '\n'), str.end()); - ps << str; + ps << xml_encode(p->get_doc()); } ps << ""; } + } + + const vector& exceptions = tfunction->get_xceptions()->get_members(); + if (!exceptions.empty()) { + vector::const_iterator e_iter; + for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) + { + t_field* e = *e_iter; + ps << '\n' << "get_type()) << "\">"; + if (e->has_doc()) + { + string doc = e->get_doc(); + while (!doc.empty() && (doc.back() == '\n' || doc.back() == '\r')) { + doc.pop_back(); + } + ps << xml_encode(doc); + } + ps << ""; + } + } - docstring_comment(out, - "", - "/// ", - "" + string("\n") + tfunction->get_doc() + "" + ps.str(), - ""); + const std::string result_doc = ps.str(); + if (!result_doc.empty()) { + docstring_comment(out, "", "/// ", result_doc, ""); } } diff --git a/compiler/cpp/src/thrift/generate/t_oop_generator.h b/compiler/cpp/src/thrift/generate/t_oop_generator.h index bae5613fb0a..0e0954c8637 100644 --- a/compiler/cpp/src/thrift/generate/t_oop_generator.h +++ b/compiler/cpp/src/thrift/generate/t_oop_generator.h @@ -37,6 +37,27 @@ class t_oop_generator : public t_generator { public: t_oop_generator(t_program* program) : t_generator(program) {} + virtual std::string get_doc_type_name(t_type* type) { + std::string package = type->get_program()->get_namespace("java"); + return (package.empty() ? "" : package + ".") + type->get_name(); + } + + void generate_throws_doc(std::stringstream& out, t_function* function) { + for (auto* exception : function->get_xceptions()->get_members()) { + if (!out.str().empty()) { + out << '\n'; + } + out << "@throws " << get_doc_type_name(exception->get_type()); + if (exception->has_doc()) { + std::string doc = exception->get_doc(); + while (!doc.empty() && (doc.back() == '\n' || doc.back() == '\r')) { + doc.pop_back(); + } + out << " " << doc; + } + } + } + /** * Scoping, using curly braces! */ @@ -92,8 +113,8 @@ class t_oop_generator : public t_generator { * Emits a JavaDoc comment if the provided function object has a doc in Thrift */ virtual void generate_java_doc(std::ostream& out, t_function* tfunction) { + std::stringstream ss; if (tfunction->has_doc()) { - std::stringstream ss; ss << tfunction->get_doc(); const std::vector& fields = tfunction->get_arglist()->get_members(); std::vector::const_iterator p_iter; @@ -104,7 +125,13 @@ class t_oop_generator : public t_generator { ss << " " << p->get_doc(); } } - generate_docstring_comment(out, "/**\n", " * ", ss.str(), " */\n"); + } + + generate_throws_doc(ss, tfunction); + + const std::string result_doc = ss.str(); + if (!result_doc.empty()) { + generate_java_docstring_comment(out, result_doc); } } @@ -125,6 +152,12 @@ class t_oop_generator : public t_generator { return str; } + + static std::string xml_encode(std::string contents) { + contents = replace_all(contents, "&", "&"); + contents = replace_all(contents, "<", "<"); + return replace_all(contents, ">", ">"); + } }; #endif diff --git a/compiler/cpp/src/thrift/generate/t_py_generator.cc b/compiler/cpp/src/thrift/generate/t_py_generator.cc index 4f82944a11c..a52e9eedfa4 100644 --- a/compiler/cpp/src/thrift/generate/t_py_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_py_generator.cc @@ -250,6 +250,10 @@ class t_py_generator : public t_generator { t_doc* tdoc, t_struct* tstruct, const char* subheader); + void generate_python_params_docstring(ostream& ss, + t_struct* tstruct, + bool* has_doc, + const char* subheader); void generate_python_docstring(std::ostream& out, t_doc* tdoc); @@ -2722,7 +2726,17 @@ void t_py_generator::generate_python_docstring(ostream& out, t_struct* tstruct) * Generates the docstring for a given function. */ void t_py_generator::generate_python_docstring(ostream& out, t_function* tfunction) { - generate_python_docstring(out, tfunction, tfunction->get_arglist(), "Parameters"); + bool has_doc = false; + stringstream ss; + if (tfunction->has_doc()) { + has_doc = true; + ss << tfunction->get_doc(); + } + generate_python_params_docstring(ss, tfunction->get_arglist(), &has_doc, "Parameters"); + generate_python_params_docstring(ss, tfunction->get_xceptions(), &has_doc, "Raises"); + if (has_doc) { + generate_docstring_comment(out, "\"\"\"\n", "", ss.str(), "\"\"\"\n"); + } } /** @@ -2738,18 +2752,31 @@ void t_py_generator::generate_python_docstring(ostream& out, has_doc = true; ss << tdoc->get_doc(); } + generate_python_params_docstring(ss, tstruct, &has_doc, subheader); + if (has_doc) { + generate_docstring_comment(out, "\"\"\"\n", "", ss.str(), "\"\"\"\n"); + } +} +void t_py_generator::generate_python_params_docstring(ostream& ss, + t_struct* tstruct, + bool* has_doc, + const char* subheader) { const vector& fields = tstruct->get_members(); if (fields.size() > 0) { - if (has_doc) { + if (*has_doc) { ss << '\n'; } - has_doc = true; + *has_doc = true; ss << subheader << ":\n"; vector::const_iterator p_iter; for (p_iter = fields.begin(); p_iter != fields.end(); ++p_iter) { t_field* p = *p_iter; - ss << " - " << p->get_name(); + if (tstruct->is_method_xcepts()) { + ss << " - " << type_to_py_type(p->get_type()); + } else { + ss << " - " << p->get_name(); + } if (p->has_doc()) { ss << ": " << p->get_doc(); } else { @@ -2757,10 +2784,6 @@ void t_py_generator::generate_python_docstring(ostream& out, } } } - - if (has_doc) { - generate_docstring_comment(out, "\"\"\"\n", "", ss.str(), "\"\"\"\n"); - } } /** diff --git a/compiler/cpp/test/CMakeLists.txt b/compiler/cpp/test/CMakeLists.txt index c8cfc567512..88058c417ca 100644 --- a/compiler/cpp/test/CMakeLists.txt +++ b/compiler/cpp/test/CMakeLists.txt @@ -33,6 +33,7 @@ find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_Interpreter_FOUND) add_test(NAME StalenessCheckTest COMMAND Python3::Interpreter ${CMAKE_CURRENT_SOURCE_DIR}/compiler/staleness_check.py ${THRIFT_COMPILER}) add_test(NAME MarkdownDocTest COMMAND Python3::Interpreter ${CMAKE_CURRENT_SOURCE_DIR}/compiler/markdown_doc_test.py ${THRIFT_COMPILER}) + add_test(NAME ExceptionDocTest COMMAND Python3::Interpreter ${CMAKE_CURRENT_SOURCE_DIR}/compiler/exception_doc_test.py ${THRIFT_COMPILER}) else() - message(WARNING "Skipping StalenessCheckTest and MarkdownDocTest as there is no python interpreter available.") + message(WARNING "Skipping Python compiler tests as there is no python interpreter available.") endif() diff --git a/compiler/cpp/test/compiler/ExceptionDocTest.thrift b/compiler/cpp/test/compiler/ExceptionDocTest.thrift new file mode 100644 index 00000000000..1ee71cd5132 --- /dev/null +++ b/compiler/cpp/test/compiler/ExceptionDocTest.thrift @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +namespace cpp main_ns +namespace haxe com.main +namespace java com.main +namespace netstd Main.Ns +namespace py main + +include "./Included.thrift" + +exception native { + 1: string message +} + +exception object { + 1: string message +} + +service ExceptionDocTest { + /** Method documentation uses x < y && y > z. */ + void documented( + /** Parameter documentation uses a < b && c > d. */ + 1: i32 value + ) throws (1: object object_error), + + void undocumented() throws ( + /** First line of the explanation + * second line uses a < b && c > d + */ + 1: native native_error, + /** Kotlin reserved identifier. */ + 2: object object_error, + /** Included exception */ + 3: Included.IncludedExc included_exc + ) +} diff --git a/compiler/cpp/test/compiler/Included.thrift b/compiler/cpp/test/compiler/Included.thrift index fe039edbf9e..ab84a1e4739 100644 --- a/compiler/cpp/test/compiler/Included.thrift +++ b/compiler/cpp/test/compiler/Included.thrift @@ -1,6 +1,4 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -19,6 +17,8 @@ * under the License. */ +namespace java com.incl + const string foo = "bar" struct a_struct { @@ -34,6 +34,10 @@ struct a_struct { 10: bool what_who, } +exception IncludedExc { + 1: string message +} + service AService { i32 a_procedure(1: i32 arg) } diff --git a/compiler/cpp/test/compiler/exception_doc_test.py b/compiler/cpp/test/compiler/exception_doc_test.py new file mode 100644 index 00000000000..20df85b8636 --- /dev/null +++ b/compiler/cpp/test/compiler/exception_doc_test.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +"""Regression tests for exception documentation generated for service methods.""" + +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + + +class TestExceptionDocRendering(unittest.TestCase): + + CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) + THRIFT_COMPILER = None + FIXTURE_THRIFT = os.path.join(CURRENT_DIR, "ExceptionDocTest.thrift") + + def setUp(self): + self.tmp_dir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmp_dir, ignore_errors=True) + + def _generate(self, language, relative_path, output_dir=None): + result = subprocess.run( + [self.THRIFT_COMPILER, "--gen", language, "-o", self.tmp_dir, + self.FIXTURE_THRIFT], + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + generated_dir = "gen-" + language if output_dir is None else output_dir + path = os.path.join(self.tmp_dir, generated_dir, relative_path) + with open(path, "r", encoding="utf-8") as generated: + return generated.read() + + def test_java_uses_escaped_qualified_exception_names(self): + content = self._generate("java", "com/main/ExceptionDocTest.java") + self.assertIn("/**\n * @throws com.main.$native", content) + self.assertIn("@throws com.main.$native First line of the explanation", content) + self.assertIn("second line uses a < b && c > d", content) + + def test_javame_uses_escaped_qualified_exception_names(self): + content = self._generate("javame", "com/main/ExceptionDocTest.java") + self.assertIn("@throws com.incl.IncludedExc Included exception", content) + + def test_py_uses_escaped_qualified_exception_names(self): + content = self._generate("py", "main/ExceptionDocTest.py") + self.assertIn("Raises:", content) + self.assertIn("native: First line of the explanation", content) + self.assertIn("second line uses a < b && c > d", content) + + def test_kotlin_keeps_kdoc_params_unchanged_and_escapes_names(self): + content = self._generate("kotlin", "com/main/ExceptionDocTest.kt", output_dir="") + self.assertIn("@throws com.main.`object` Kotlin reserved identifier.", content) + self.assertNotIn("@param", content) + + def test_cpp_uses_canonical_qualified_type_name(self): + content = self._generate("cpp", "ExceptionDocTest.h") + self.assertIn("@throws ::main_ns::native First line of the explanation", content) + + def test_haxe_omits_throws_field_name(self): + content = self._generate("haxe", "com/main/ExceptionDocTest.hx") + self.assertIn("@throws Native First line of the explanation", content) + self.assertNotIn("Native native_error", content) + + def test_netstd_escapes_exception_documentation_as_xml(self): + content = self._generate("netstd", "Main/Ns/ExceptionDocTest.cs") + self.assertIn("Method documentation uses x < y && y > z.", content) + self.assertIn("Parameter documentation uses a < b && c > d.", content) + self.assertIn( + 'First line of the explanation', + content, + ) + self.assertIn("second line uses a < b && c > d", content) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + TestExceptionDocRendering.THRIFT_COMPILER = sys.argv[1] + sys.argv = [sys.argv[0]] + sys.argv[2:] + unittest.main() diff --git a/test/ThriftTest.thrift b/test/ThriftTest.thrift index 42607cc3544..4b0cdb998e3 100644 --- a/test/ThriftTest.thrift +++ b/test/ThriftTest.thrift @@ -314,7 +314,11 @@ service ThriftTest * else do not throw anything * @return Xtruct - an Xtruct with string_thing = arg1 */ - Xtruct testMultiException(1: string arg0, 2: string arg1) throws(1: Xception err1, 2: Xception2 err2) + Xtruct testMultiException(1: string arg0, 2: string arg1) throws( + /** Thrown when a bad thing happens */ + 1: Xception err1, + /** Thrown when the input is in incorrect format, for example */ + 2: Xception2 err2) /** * Print 'testOneway(%d): Sleeping...' with secondsToSleep as '%d' From f037dc420c0fd216f86e90ee14ac36b4239f7aba Mon Sep 17 00:00:00 2001 From: Jens Geyer Date: Mon, 14 Sep 2026 23:27:07 +0200 Subject: [PATCH 2/2] THRIFT-6108: Omit the empty first line of netstd exception-only docs Client: netstd Co-Authored-By: Claude Opus 5 (1M context) --- compiler/cpp/src/thrift/generate/t_netstd_generator.cc | 6 +++++- compiler/cpp/test/compiler/exception_doc_test.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/compiler/cpp/src/thrift/generate/t_netstd_generator.cc b/compiler/cpp/src/thrift/generate/t_netstd_generator.cc index 3a43ae41eac..580995331cc 100644 --- a/compiler/cpp/src/thrift/generate/t_netstd_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_netstd_generator.cc @@ -3965,7 +3965,11 @@ void t_netstd_generator::generate_netstd_doc(ostream& out, t_function* tfunction for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) { t_field* e = *e_iter; - ps << '\n' << "get_type()) << "\">"; + if (!ps.str().empty()) + { + ps << '\n'; + } + ps << "get_type()) << "\">"; if (e->has_doc()) { string doc = e->get_doc(); diff --git a/compiler/cpp/test/compiler/exception_doc_test.py b/compiler/cpp/test/compiler/exception_doc_test.py index 20df85b8636..cf27be97dde 100644 --- a/compiler/cpp/test/compiler/exception_doc_test.py +++ b/compiler/cpp/test/compiler/exception_doc_test.py @@ -92,6 +92,11 @@ def test_netstd_escapes_exception_documentation_as_xml(self): ) self.assertIn("second line uses a < b && c > d", content) + def test_netstd_exception_only_doc_has_no_leading_empty_line(self): + content = self._generate("netstd", "Main/Ns/ExceptionDocTest.cs") + self.assertIn('/// ', content) + self.assertNotRegex(content, r"///[ \t]*\n[ \t]*///