Skip to content

THRIFT-6108: implemented exception documentation generation - #3666

Open
SLAVONchick wants to merge 1 commit into
apache:masterfrom
SLAVONchick:master
Open

THRIFT-6108: implemented exception documentation generation#3666
SLAVONchick wants to merge 1 commit into
apache:masterfrom
SLAVONchick:master

Conversation

@SLAVONchick

@SLAVONchick SLAVONchick commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Client: cpp,haxe,java,javame,kotlin,netstd,py

In this PR I've implemented the documentation generation of the exceptions thrown in the service methods.
I skipped the clients where there is no documentation is generated at all, so it only touched cpp,haxe,java,netstd,py.

  • Did you create an Apache Jira ticket? (Request account here, not required for trivial changes) - Yes
  • If a ticket exists: Does your pull request title follow the pattern "THRIFT-NNNN: describe my issue"? - Yes
  • Did you squash your changes to a single commit? (not required, but preferred) - Yes
  • Did you do your best to avoid breaking changes? If one was needed, did you label the Jira ticket with "Breaking-Change"? - Yes
  • If your change does not involve any code, include [skip ci] anywhere in the commit message to free up build resources. - Does involve code.

@mergeable mergeable Bot added c++ Pull requests that update C++ code haxe java Pull requests that update Java code c# Pull requests that update C# code Pull requests that update .NET code python compiler labels Jul 23, 2026
Comment thread compiler/cpp/src/thrift/generate/t_oop_generator.h Outdated
@Jens-G

Jens-G commented Jul 23, 2026

Copy link
Copy Markdown
Member

I like the idea. Would be glad to see this move forward.

Comment thread compiler/cpp/src/thrift/generate/t_oop_generator.h Outdated
@mergeable mergeable Bot added c_glib dart Pull requests that update Dart code d Pull requests that update D code delphi javascript Pull requests that update Javascript code kotlin lua Pull requests that update Lua code nodejs typescript ocaml perl php labels Jul 24, 2026
@mergeable mergeable Bot added ruby Pull requests that update Ruby code testsuite labels Jul 24, 2026
@SLAVONchick

Copy link
Copy Markdown
Contributor Author

I like the idea. Would be glad to see this move forward.

Hi! Thank you for the review! I'm really looking forward to finish this work.

@SLAVONchick
SLAVONchick requested a review from Jens-G July 24, 2026 13:22
Comment thread lib/cpp/test/TTransportFactoryConfigTest.cpp
@Jens-G

Jens-G commented Jul 26, 2026

Copy link
Copy Markdown
Member

Code review

Found 5 issues:

  1. Deleting javame's generate_java_doc(ostream&, t_function*) override silently regresses javame output. t_javame_generator still declares two other overloads of that name and has no using t_oop_generator::generate_java_doc;, so C++ name hiding removes the base t_function* overload from the candidate set. Since t_function : public t_doc, the call sites at lines 1956 and 1981 now bind to the t_doc* overload instead. Javame loses the auto-generated @param tags it emitted before this PR and never gains the new @throws tags. It compiles without a warning.

void generate_java_doc(std::ostream& out, t_field* field) override;
void generate_java_doc(std::ostream& out, t_doc* tdoc) override;

Worth fixing together with this: javame's get_namespace() appends no trailing separator, unlike the base default (".") and the cpp override ("::"), while the call site concatenates straight onto the type name. Once the overload resolution above is corrected, javame will emit @throws thrift.testXception. Kotlin's override has the same shape.

  1. The unconditional ss << '\n'; runs whether or not the function declares any exceptions, and the @throws loop below already prefixes each entry with \n. Functions with no throws() clause get a trailing blank * line; functions with one get a doubled blank line. This affects every documented service function emitted by the cpp and java generators, which are the two that reach this shared method. Guarding it with if (!exceptions.empty()) fixes both cases.

}
ss << '\n';
const std::vector<t_field*>& exceptions = tfunction->get_xceptions()->get_members();
std::vector<t_field*>::const_iterator e_iter;
for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) {
t_field* e = *e_iter;
ss << "\n@throws " << get_namespace(e->get_type()) << e->get_type()->get_name();
if (e->has_doc()) {

  1. t_type::get_name() returns an empty string for inline container types, because t_list/t_set/t_map construct through t_container() into the default t_type() ctor, which never sets name_. A field declared 1: list<string> items renders as - items (). Base types also come out as IDL names (i32, string) rather than Python ones. The file already has type_to_py_type() for exactly this, used by arg_hint/member_hint/func_hint, which renders list[str] and dict[str, int] correctly.

t_field* p = *p_iter;
ss << " - " << p->get_name();
if (gen_type_hints_) {
ss << " (" << p->get_type()->get_name() << ")";
}
if (p->has_doc()) {
ss << ": " << p->get_doc();

  1. The @throws name is taken from e->get_type() without resolving typedefs. validate_throws() in main.cc checks the thrown type via get_true_type(), so typedef SomeException Alias followed by throws (1: Alias e) is legal IDL, and this would emit @throws ns.Alias — a name with no generated class behind it, since typedefs produce no class in Java or C++. The sibling generate_java_doc(ostream&, t_field*) two functions above already goes through get_true_type() for the same reason (THRIFT-4086, 4f63573f5), and the netstd path in this PR gets it right via type_name().

for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) {
t_field* e = *e_iter;
ss << "\n@throws " << get_namespace(e->get_type()) << e->get_type()->get_name();
if (e->has_doc()) {
std::string doc_string = e->get_doc();

  1. This change to lib/cpp/test/TTransportFactoryConfigTest.cpp is unrelated to THRIFT-6108 and is a no-op — it extracts TConfiguration::DEFAULT_MAX_MESSAGE_SIZE into a local and passes that instead of the constant. Worth dropping so the PR stays scoped to the doc generators.

const int kMaxFrameSize = 64;
const int kMaxMessageSize = TConfiguration::DEFAULT_MAX_MESSAGE_SIZE;
auto config = std::make_shared<TConfiguration>(kMaxMessageSize, kMaxFrameSize);

On the design, separately from the defects above: get_gen_name() looks like more machinery than the feature needs.

virtual const std::string& get_gen_name() const = 0;
virtual std::string get_namespace(t_type* type) {
return type->get_program()->get_namespace(get_gen_name()) + ".";
}

Making it pure virtual obliges all 18 t_oop_generator subclasses to implement it, each with its own gen_name_ member, which is most of why this PR touches 21 generator files when the machinery only ever serves the shared doc path. It has three call sites and all of them are inside get_namespace(), whose only consumer is the new @throws line — reachable from cpp, java and javame alone. Since javame's override hardcodes "java", only the cpp and java implementations are ever actually called.

The bigger issue is that the abstraction doesn't fit its users: four of the six get_namespace() overrides discard get_gen_name() outright — javame hardcodes "java", c_glib returns nspace, and perl and php delegate to the existing perl_namespace()/php_namespace() helpers. It is only usable where a generator's registration name and its IDL namespace key happen to coincide, which is exactly the cpp and java case.

That string also already exists in the compiler, in one place. THRIFT_REGISTER_GENERATOR registers each generator under #language:

#define THRIFT_REGISTER_GENERATOR(language, long_name, doc) \
class t_##language##_generator_factory_impl \
: public t_generator_factory_impl<t_##language##_generator> { \
public: \
t_##language##_generator_factory_impl() \
: t_generator_factory_impl<t_##language##_generator>(#language, long_name, doc) {} \
}; \
static t_##language##_generator_factory_impl _registerer;

and t_program::set_namespace() validates every namespace <key> directive against that same map:

t_generator_registry::gen_map_t my_copy = t_generator_registry::get_generator_map();
t_generator_registry::gen_map_t::iterator it;
it = my_copy.find(base_language);
if (it == my_copy.end()) {
std::string warning = "No generator named '" + base_language + "' could be found!";
pwarning(1, warning.c_str());
} else {

Keeping get_namespace() as the virtual hook and dropping get_gen_name() entirely would give the same result: t_cpp_generator::get_namespace() passes "cpp" directly, the base default passes "java", and the other 16 generators need no change at all. That is the pattern the rest of the compiler already uses, e.g. t_netstd_generator.cc:140 and t_kotlin_generator.cc:202.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@SLAVONchick
SLAVONchick force-pushed the master branch 3 times, most recently from 8f2772e to b80a146 Compare July 27, 2026 07:46
@SLAVONchick
SLAVONchick requested a review from Jens-G July 27, 2026 15:05
@Jens-G

Jens-G commented Jul 30, 2026

Copy link
Copy Markdown
Member

Code review

The replace_all() consolidation from this PR is now on master as 2ae9c11db, so a rebase will drop that part. Reviewed the rest. The main path works — testMultiException in test/ThriftTest.thrift comes out as @throws thrift.test.Xception Thrown when a bad thing happens. Everything below was reproduced against a compiler built from b80a1464d.

Found 4 issues:

  1. The @throws name is resolved through typedefs but the namespace is not, so an exception reached through a typedef gets a qualified name that no generated class has. get_true_type() is applied to the name half only, while get_namespace() still receives the raw type — and a t_typedef carries the program the typedef was declared in, not the one the underlying exception lives in. With inc.thrift (namespace java com.inc, exception RealExc) and main.thrift (namespace java com.main, include "inc.thrift", typedef inc.RealExc Alias, void go() throws (1: Alias e)), the generated file contradicts itself three lines apart:

     * @throws com.main.RealExc
     */
    public void go() throws com.inc.RealExc, org.apache.thrift.TException;
    

    Same in C++ (@throws main_ns::RealExc for a class that is really inc_ns::RealExc) and in JavaME through the new delegation. The haxe and netstd paths added in this PR avoid it by going through type_name(), which resolves once and derives both halves from the resolved type.

t_field* e = *e_iter;
ss << "\n@throws " << get_namespace(e->get_type()) << get_true_type(e->get_type())->get_name();
if (e->has_doc()) {

  1. The default get_namespace() appends the . separator unconditionally, so a file with no namespace java yields @throws .SomeError, which is not a resolvable Javadoc reference. exception NoNsErr plus void doThing() throws (1: NoNsErr e) with no namespace declared generates:

     * @throws .NoNsErr
     */
    public void doThing() throws NoNsErr, org.apache.thrift.TException;
    

    type_name() in the java generator guards this case with if (!package.empty()) (t_java_generator.cc:4687), and get_enum_class_name() in this same file sidesteps it by prefixing only for types from another program. C++ is unaffected — ::NoNsErr is a valid global-namespace qualifier — but Java and JavaME are.

virtual std::string get_namespace(t_type* type) {
return type->get_program()->get_namespace("java") + ".";
}

  1. The if (!exceptions.empty()) guard fixed the no-exceptions case from the last round, but for functions that do declare exceptions the guard's own ss << '\n' still runs immediately before a loop whose every entry already begins with \n. Since IDL doc text carries its own trailing newline, a function with no parameters, or one whose last parameter has its own doc comment, gets two blank * lines:

     * zero params, throws
     * 
     * 
     * @throws com.probe.MyError
    

    A function with undocumented parameters renders with a single blank line, which is why testMultiException looks right.

const std::vector<t_field*>& exceptions = tfunction->get_xceptions()->get_members();
if (!exceptions.empty()) {
ss << '\n';
std::vector<t_field*>::const_iterator e_iter;
for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) {
t_field* e = *e_iter;
ss << "\n@throws " << get_namespace(e->get_type()) << get_true_type(e->get_type())->get_name();
if (e->has_doc()) {

  1. Python's Raises: section lists the throws-clause field label rather than the exception type, so in the default configuration the docstring never names the exception. It shows up only as a parenthetical under py:type_hints,enum ( - err1 (Xception)). test/DebugProtoTest.thrift already demonstrates the effect — void methodThatThrowsAnException() throws (1: ExceptionWithAMap xwamap) gains a docstring whose entire content is the label:

    def methodThatThrowsAnException(self):
        """
        Raises:
         - xwamap
    
        """

    The field label is the salient name for a parameter, since it is the real keyword argument, but in a throws clause it is an arbitrary tag — java, haxe and netstd all emit the type as the primary token. (Checked the rest of the refactor: plain --gen py over DebugProtoTest.thrift is otherwise byte-identical to master, so the has_doc pointer change did not disturb existing docstrings.)

}
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) {

t_field* p = *p_iter;
ss << " - " << p->get_name();
if (gen_type_hints_) {
ss << " (" << type_to_py_type(p->get_type()) << ")";
}

Three suggestions, below the bar for the list above but verified:

  • The Client: trailer lists cpp,haxe,java,netstd,py but omits javame, whose output this change alters — swapping its own implementation for a delegation to the base method gains it @throws lines it did not emit before. AGENTS.md asks for a "comma-separated list of affected languages", and this repo tags javame separately from java (e.g. 8e8e58a80).

*/
void t_javame_generator::generate_java_doc(ostream& out, t_function* tfunction) {
t_oop_generator::generate_java_doc(out, tfunction);
}

  • The new kotlin get_namespace() override is unreachable. Its only caller is the @throws block in t_oop_generator::generate_java_doc(ostream&, t_function*), and kotlin emits function docs through its own generate_kdoc_comment() instead, so a kotlin service function with a documented throws clause generates no @throws at all — consistent with Client: omitting kotlin. Either wire kotlin up or drop the override.

std::string t_kotlin_generator::get_namespace(t_type *type) {
std::string namespace_str = type->get_program()->get_namespace("kotlin");
if (namespace_str.empty()) {
namespace_str = type->get_program()->get_namespace("java");
}
return namespace_str + ".";
}

  • The gen_type_hints_ block annotates every parameter and every struct attribute with its Python type, which is a separate feature from documenting exceptions. It changes existing output for current py:type_hints users beyond the new Raises sections: - arg becomes - arg (str) in service docstrings and - m becomes - m (str) in ttypes.py. Worth its own commit so THRIFT-6108 stays about exception docs.

ss << " - " << p->get_name();
if (gen_type_hints_) {
ss << " (" << type_to_py_type(p->get_type()) << ")";
}

One design observation, no action needed for this PR: a base default that reads get_namespace("java") makes "java" the inherited namespace language for all 18 t_oop_generator subclasses, which is a little surprising in a class named for OOP rather than for Java. It is harmless today because the shared doc path is the only caller and only cpp, java and javame reach it. Both fixes for issues 1 and 2 land inside that same two-line default.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@Jens-G

Jens-G commented Jul 30, 2026

Copy link
Copy Markdown
Member

FYI I extracted the replace_all() refactoring into a separate commit, keeping all the credits etc.

@SLAVONchick

Copy link
Copy Markdown
Contributor Author

FYI I extracted the replace_all() refactoring into a separate commit, keeping all the credits etc.

Yeah, thanks! Really appreciate that.

Client: cpp,haxe,java,javame,kotlin,netstd,py
@SLAVONchick

Copy link
Copy Markdown
Contributor Author

@Jens-G Hi! Just a friendly reminder to check on the PR :-)

@Jens-G

Jens-G commented Aug 28, 2026

Copy link
Copy Markdown
Member

Code review

Thanks for the update, and sorry for the slow turnaround. Re-reviewed at c554a5997; everything below was reproduced against compilers built from the PR head and from the merge base (64601a565).

From the last round: issues 1 (typedef namespace) and 4 (Python listing the field label) are fixed, and so are suggestions 1 (Client: trailer) and 3 (the gen_type_hints_ block). Issue 2 is fixed in the base method but reintroduced in the new Kotlin override, and issue 3 is fixed in the base but present in the new haxe copy — both are below as issues 2 and 6. Suggestion 2 was addressed by wiring Kotlin up, which is where issues 2 and 3 come from.

Found 7 issues:

  1. The whole @throws block sits inside if (tfunction->has_doc()), so documenting only the throws clause and not the method emits nothing at all. Python is the only one of the seven bindings where the feature fires in that case — its rewritten path builds has_doc up from the sections, so Raises: can stand alone. cpp, java, javame, kotlin, haxe (t_haxe_generator.cc:3139) and netstd (t_netstd_generator.cc:3945) all generate zero comment lines for:

    void f() throws (/** this exception doc is dropped */ 1: E1 e)

    Documenting the exceptions rather than the method is the natural way to write this, and it silently produces nothing in six of seven languages.

virtual void generate_java_doc(std::ostream& out, t_function* tfunction) {
if (tfunction->has_doc()) {
std::stringstream ss;

  1. The Kotlin override has two problems. It reads namespace kotlin, which is a key nothing else in the compiler uses — the Kotlin generator derives every real package from namespace java (:195, :325, :408), and namespace kotlin appears in no doc/ page and no .thrift in the tree. With both keys set, the generated file contradicts itself:

    package com.main          // from namespace java
    ...
     * @throws com.mainkt.LocalExc   // from namespace kotlin

    Second, it returns namespace_str + "." unconditionally, dropping the empty-package guard the base method grew in this round. exception NoNsErr with no namespace declared yields @throws .NoNsErr — exactly round-2 issue 2, fixed in the base and reintroduced here.

std::string t_kotlin_generator::get_namespace(t_type *type) {
std::string namespace_str = type->get_program()->get_namespace("kotlin");
if (namespace_str.empty()) {
namespace_str = type->get_program()->get_namespace("java");
}
return namespace_str + ".";
}

  1. Swapping generate_kdoc_comment for generate_java_doc at this call site also adopts the base @param loop, which Kotlin did not emit before. generate_kdoc_comment (:2022) writes the doc text and nothing else. Diffing the generated ThriftTest.kt before and after, every interface method gains a blank * line and a description-less @param thing — sitting directly under the hand-written @param string thing - the string to print the IDL already carries. Only the @throws loop was needed here.

out << "interface " << tservice->get_name() << " {" << '\n';
indent_up();
for (auto tfunc : tservice->get_functions()) {
generate_java_doc(out, tfunc);
indent(out) << function_signature(tfunc) << '\n';

  1. get_true_type(...)->get_name() emits the raw IDL name, bypassing the target language's identifier escaping, so the @throws reference does not match the class the same generator emits three lines below it. This is the THRIFT-5927 family — java routes every class name through make_valid_java_identifier() and kotlin through kotlin_safe_name() (:410), and neither is reached from here:

    exception native  →  class file $native.java, signature `throws $native`,
                         but the doc says @throws kw.pkg.native
    

    javadoc with default doclint reports one error (unexpected text * @throws kw.pkg.native) plus warning: no @throws for kw.pkg.$native. Kotlin has the same shape with exception object. netstd gets it right via type_name().

for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) {
t_field* e = *e_iter;
ss << "\n@throws " << get_namespace(get_true_type(e->get_type())) << get_true_type(e->get_type())->get_name();
if (e->has_doc()) {

  1. Newlines are erased from the exception doc without a space in their place, so the last word of one line is glued to the first of the next:

    @throws com.nasty.MultiLineExc First line of the explanationsecond line continues here
    

    Same in the netstd copy at t_netstd_generator.cc:3971. The erase is also unnecessary: the @param loop ten lines above does not strip, and generate_docstring_comment() (t_generator.cc:170-192) already splits on newlines and re-prefixes each line.

std::string doc_string = e->get_doc();
doc_string.erase(remove(doc_string.begin(), doc_string.end(), '\n'), doc_string.end());
ss << " " << doc_string;
}

  1. The haxe copy emits the throws-clause field name between the type and the description, so under the @throws <Type> <description> grammar the field label becomes the first word of the description. The PR's own fixture change shows it: @throws Xception err1 Thrown when a bad thing happens. For an undocumented exception the result is a dangling @throws Boom e whose entire description is a field label. The base, netstd, php and py paths all omit the name.

    The same loop does not strip the trailing newline get_doc() carries, so consecutive documented entries are separated by a stray blank line — round-2 issue 3, in the haxe copy:

     * @throws Xception err1 Thrown when a bad thing happens
     * 
     * @throws Xception2 err2 Thrown when the input is in incorrect format, for example
    

const vector<t_field*>& exceptions = tfunction->get_xceptions()->get_members();
vector<t_field*>::const_iterator e_iter;
for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) {
t_field* e = *e_iter;
ss << "\n@throws " << type_name(e->get_type()) << " " << e->get_name();
if (e->has_doc()) {
ss << " " << e->get_doc();
}
}

  1. Author-controlled doc text goes verbatim into an XML doc element. /** thrown if a < b && c > d */ produces /// <exception cref="global::Ns.XmlExc">thrown if a < b && c > d</exception>; building that with GenerateDocumentationFile=true gives four CS1570 warnings and drops the entire member from the generated .xml (<!-- Badly formed XML comment ignored for member "M:..." -->). The <param>/<summary> text in the same function has the same hole today, so this is a new injection site rather than a new class of bug — t_delphi_generator.cc:477 already carries the &/</> helper.

const vector<t_field*>& exceptions = tfunction->get_xceptions()->get_members();
vector<t_field*>::const_iterator e_iter;
for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter)
{
t_field* e = *e_iter;
ps << '\n' << "<exception cref=\"" << type_name(e->get_type()) << "\">";
if (e->has_doc())
{
string str = e->get_doc();
str.erase(remove(str.begin(), str.end(), '\n'), str.end());
ps << str;
}
ps << "</exception>";

On the design: a correct implementation of this exact feature already exists in a sibling t_oop_generator subclass, and the PR leaves it untouched.

void t_php_generator::generate_php_doc(ostream& out, t_function* function) {
stringstream ss;
if (function->has_doc()) {
ss << function->get_doc() << '\n';
}
// generate parameter types doc
const vector<t_field*>& args = function->get_arglist()->get_members();
vector<t_field*>::const_iterator a_iter;
for (a_iter = args.begin(); a_iter != args.end(); ++a_iter) {
t_field* arg = *a_iter;
ss << "@param " << type_to_phpdoc(arg->get_type()) << " $" << arg->get_name();
if (arg->has_doc()) {
ss << " " << arg->get_doc();
}
ss << '\n';
}
// generate return type doc
t_type* ret_type = function->get_returntype();
if (!ret_type->is_void() || ret_type->has_doc()) {
ss << "@return " << type_to_phpdoc(ret_type);
if (ret_type->has_doc()) {
ss << " " << ret_type->get_doc();
}
ss << '\n';
}
// generate exceptions doc
const vector<t_field*>& excs = function->get_xceptions()->get_members();
vector<t_field*>::const_iterator e_iter;
for (e_iter = excs.begin(); e_iter != excs.end(); ++e_iter) {
t_field* exc = *e_iter;
ss << "@throws " << type_to_phpdoc(exc->get_type());
if (exc->has_doc()) {
ss << " " << exc->get_doc();
}
ss << '\n';
}
generate_php_docstring_comment(out, ss.str());
}

t_php_generator::generate_php_doc(ostream&, t_function*) emits @throws <qualified type> <doc> and avoids issues 1, 4 and 5 structurally rather than by getting three separate details right. has_doc() wraps only the function's own text, so an exception-only doc still renders. It reuses the generator's own type_to_phpdoc(), so qualification and escaping come for free instead of via a new namespace lookup. And it does not strip newlines. Its @param and @return sections are built the same way.

After this PR there are four parallel @throws loops in four formats — t_oop_generator.h:121, t_haxe_generator.cc:3156, t_netstd_generator.cc:3968, t_php_generator.cc:2866 — so every fix above reaches exactly one of them, and php gets none of them. If the shared loop delegated the type rendering to a per-generator hook the way php delegates to type_to_phpdoc(), issues 4 and 5 would not be expressible, and haxe and netstd would not need copies at all.

Three suggestions, below the bar for the list above but verified:

  • The new C++ get_namespace() re-implements namespace_prefix() (:4724) and already disagrees with it: namespace_prefix() returns ::a::b:: with a leading global qualifier and space, and carries a comment explaining why; the new one returns a::b::. So @throws main_ns::LocalExc where the rest of the same generator writes ::main_ns::LocalExc. type_name(e->get_type()) would replace both this override and the get_namespace + get_name concatenation at the call site.

std::string t_cpp_generator::get_namespace(t_type *type) {
std::string namespace_str = type->get_program()->get_namespace("cpp");
return replace_all(namespace_str, ".", "::") + "::";
}

  • The shared params helper branches on is_method_xcepts() to choose type-vs-name rendering, using an AST property as a proxy for which of its two callers invoked it — both of which already pass the mode explicitly as subheader. The flag does not mean "this is a throws clause": t_struct.h:162 documents it as "struct holds the exceptions declared at a service method", and t_delphi_generator.cc:2624 sets it on a <fn>_result struct that also carries success. Passing a bool alongside subheader removes the coupling.

ss << subheader << ":\n";
vector<t_field*>::const_iterator p_iter;
for (p_iter = fields.begin(); p_iter != fields.end(); ++p_iter) {
t_field* p = *p_iter;
if (tstruct->is_method_xcepts()) {
ss << " - " << type_to_py_type(p->get_type());
} else {
ss << " - " << p->get_name();
}
if (p->has_doc()) {

  • A few style points, since the checklist asks for make style: line 117 is whitespace-only, so git diff --check exits non-zero; line 123 is 120 characters against the 100-column limit in .clang-format and doc/coding_standards.md:34, and evaluates get_true_type(e->get_type()) twice — hoisting a local fixes both; and t_cpp_generator.cc:459 / t_kotlin_generator.cc:177 write t_type *type where their own declarations and PointerAlignment: Left say t_type* type.

}
const std::vector<t_field*>& exceptions = tfunction->get_xceptions()->get_members();
if (!exceptions.empty()) {
std::vector<t_field*>::const_iterator e_iter;
for (e_iter = exceptions.begin(); e_iter != exceptions.end(); ++e_iter) {
t_field* e = *e_iter;
ss << "\n@throws " << get_namespace(get_true_type(e->get_type())) << get_true_type(e->get_type())->get_name();
if (e->has_doc()) {

Last thing — the only change under test/ is two doc comments in a cross-language interop fixture that asserts nothing about generated documentation, and it exercises none of the paths above: not the exception-only doc, not the multi-line doc, not the empty namespace, not a reserved identifier, not XML escaping. compiler/cpp/test/compiler/markdown_doc_test.py with DocTest.thrift/DocTest.md is a working golden-output test for generated docs, already wired up at compiler/cpp/test/CMakeLists.txt:36 — it passes unchanged against this build, which is to say it covers none of the new code. That harness looks like the right home for a case or two here.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c_glib c++ Pull requests that update C++ code c# Pull requests that update C# code Pull requests that update .NET code compiler d Pull requests that update D code dart Pull requests that update Dart code delphi haxe java Pull requests that update Java code javascript Pull requests that update Javascript code kotlin lua Pull requests that update Lua code nodejs ocaml perl php python ruby Pull requests that update Ruby code testsuite typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants