Skip to content

Commit 3da5bf4

Browse files
j-piaseckizoontek
authored andcommitted
Unmangle templates in doxygen output (react#55848)
Summary: Pull Request resolved: react#55848 Changelog: [Internal] Updates doxygen refid resolver to unmangle the template arguments. Reviewed By: cipolleschi Differential Revision: D94901504 fbshipit-source-id: 8082b3e22882c8b313caa6ea97e2bfd0b8b588b7
1 parent 7894488 commit 3da5bf4

3 files changed

Lines changed: 140 additions & 17 deletions

File tree

scripts/cxx-api/parser/main.py

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,109 @@
2626
from .utils import Argument, extract_qualifiers, parse_qualified_path
2727

2828

29+
def decode_doxygen_template_encoding(encoded: str) -> str:
30+
"""Decode Doxygen's encoding for template specializations in refids.
31+
32+
Doxygen encodes special characters in refids using underscore-prefixed codes:
33+
- '_3' = '<' (template open)
34+
- '_4' = '>' (template close)
35+
- '_01' = ' ' (space)
36+
- '_07' = '(' (open paren)
37+
- '_08' = ')' (close paren)
38+
- '_8_8_8' = '...' (variadic ellipsis)
39+
- '_00' = ',' (comma)
40+
- '_02' = '*' (pointer)
41+
- '_05' = '=' (equals)
42+
- '_06' = '&' (reference)
43+
44+
e.g. 'SyncCallback_3_01R_07Args_8_8_8_08_4' -> 'SyncCallback< R(Args...)>'
45+
"""
46+
result = encoded
47+
48+
# Process longer patterns first to avoid partial matches
49+
result = result.replace("_8_8_8", "...") # Variadic ellipsis
50+
51+
# Process two-char patterns (_0X codes)
52+
result = result.replace("_00", ", ") # Comma (with space for readability)
53+
result = result.replace("_01", " ") # Space
54+
result = result.replace("_02", "*") # Pointer
55+
result = result.replace("_05", "=") # Equals
56+
result = result.replace("_06", "&") # Reference
57+
result = result.replace("_07", "(") # Open paren
58+
result = result.replace("_08", ")") # Close paren
59+
60+
# Process single-char patterns last
61+
result = result.replace("_3", "<") # Template open
62+
result = result.replace("_4", ">") # Template close
63+
64+
return result
65+
66+
67+
def _strip_template_args(name: str) -> str:
68+
"""Strip template arguments from a type name.
69+
70+
e.g. 'SyncCallback< R(Args...)>' -> 'SyncCallback'
71+
"""
72+
angle_idx = name.find("<")
73+
return name[:angle_idx].rstrip() if angle_idx != -1 else name
74+
75+
76+
def _qualify_text_with_refid(text: str, refid: str) -> str:
77+
"""Qualify a text symbol using the namespace extracted from its doxygen refid.
78+
79+
For ref elements, doxygen provides a refid that encodes the fully qualified
80+
path to the referenced symbol. This function extracts the namespace from
81+
that refid and prepends it to the text, avoiding redundant qualification.
82+
83+
Args:
84+
text: The symbol text (e.g., "SyncCallback")
85+
refid: The doxygen refid (e.g., "classfacebook_1_1react_1_1SyncCallback...")
86+
87+
Returns:
88+
The qualified text (e.g., "facebook::react::SyncCallback")
89+
"""
90+
ns = extract_namespace_from_refid(refid)
91+
92+
# Skip re-qualification if text is already globally qualified
93+
# (starts with "::") - it's already an absolute path
94+
if not ns or text.startswith(ns) or text.startswith("::"):
95+
return text
96+
97+
# The text may already start with a trailing portion of the namespace.
98+
# For example ns="facebook::react::HighResDuration" and
99+
# text="HighResDuration::zero". We need to find the longest suffix of ns
100+
# that is a prefix of text (on a "::" boundary) and only prepend the
101+
# missing part.
102+
ns_parts = ns.split("::")
103+
prepend = ns
104+
105+
for i in range(1, len(ns_parts)):
106+
suffix = "::".join(ns_parts[i:])
107+
# Also compare without template args - for template specializations
108+
# like "SyncCallback< R(Args...)>", text "SyncCallback" should match
109+
base_suffix = _strip_template_args(ns_parts[i])
110+
if (
111+
text.startswith(suffix + "::")
112+
or text == suffix
113+
or text.startswith(base_suffix + "::")
114+
or text == base_suffix
115+
):
116+
prepend = "::".join(ns_parts[:i])
117+
break
118+
119+
return prepend + "::" + text
120+
121+
29122
def extract_namespace_from_refid(refid: str) -> str:
30123
"""Extract the namespace prefix from a doxygen refid.
31124
e.g. 'namespacefacebook_1_1yoga_1a...' -> 'facebook::yoga'
32125
'structfacebook_1_1react_1_1detail_1_1is__dynamic' -> 'facebook::react::detail::is_dynamic'
126+
'classfacebook_1_1react_1_1SyncCallback_3_01R_07Args_8_8_8_08_4' -> 'facebook::react::SyncCallback< R(Args...)>'
33127
34128
Doxygen encoding:
35129
- '::' is encoded as '_1_1'
36130
- '_' in identifiers is encoded as '__' (double underscore)
131+
- Template specializations are encoded with hex-like codes (see decode_doxygen_template_encoding)
37132
"""
38133
for prefix in ("namespace", "struct", "class", "union"):
39134
if refid.startswith(prefix):
@@ -46,6 +141,8 @@ def extract_namespace_from_refid(refid: str) -> str:
46141
# Then replace double underscore with single underscore
47142
# (Doxygen encodes '_' in identifiers as '__')
48143
result = result.replace("__", "_")
144+
# Decode template specialization encodings
145+
result = decode_doxygen_template_encoding(result)
49146
return result
50147
return ""
51148

@@ -86,23 +183,7 @@ def resolve_linked_text_name(
86183
# incorrectly treat symbols in strings as references
87184
refid = getattr(part.value, "refid", None)
88185
if refid and not in_string:
89-
ns = extract_namespace_from_refid(refid)
90-
# Skip re-qualification if text is already globally qualified
91-
# (starts with "::") - it's already an absolute path
92-
if ns and not text.startswith(ns) and not text.startswith("::"):
93-
# The text may already start with a trailing portion of
94-
# the namespace. For example ns="facebook::react::HighResDuration"
95-
# and text="HighResDuration::zero". We need to find the
96-
# longest suffix of ns that is a prefix of text (on a "::"
97-
# boundary) and only prepend the missing part.
98-
ns_parts = ns.split("::")
99-
prepend = ns
100-
for i in range(1, len(ns_parts)):
101-
suffix = "::".join(ns_parts[i:])
102-
if text.startswith(suffix + "::") or text == suffix:
103-
prepend = "::".join(ns_parts[:i])
104-
break
105-
text = prepend + "::" + text
186+
text = _qualify_text_with_refid(text, refid)
106187

107188
name += text
108189
elif type_def.ref:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
template <typename R, typename... Args>
2+
class test::Callback {
3+
}
4+
5+
template <typename R, typename... Args>
6+
class test::Callback< R(Args...)> {
7+
public Callback(const test::Callback&) = delete;
8+
public Callback(test::Callback&& other) noexcept;
9+
public R call(Args... args) const;
10+
public test::Callback& operator=(const test::Callback&) = delete;
11+
public test::Callback& operator=(test::Callback&& other) noexcept;
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
namespace test {
11+
12+
template <typename R, typename... Args>
13+
class Callback;
14+
15+
template <typename R, typename... Args>
16+
class Callback<R(Args...)> {
17+
public:
18+
Callback(const Callback &) = delete;
19+
Callback &operator=(const Callback &) = delete;
20+
21+
Callback(Callback &&other) noexcept {}
22+
Callback &operator=(Callback &&other) noexcept
23+
{
24+
return *this;
25+
}
26+
27+
R call(Args... args) const {}
28+
};
29+
30+
} // namespace test

0 commit comments

Comments
 (0)