Skip to content

Commit 1885ca9

Browse files
abradymeta-codesync[bot]
authored andcommitted
Add C++ utility library for C# code generation
Summary: Add the C++ helper library for the C# Thrift code generator. This provides: - C# keyword detection and escaping (with @ prefix) - PascalCase naming conversion for types, properties, and enum values - C# namespace resolution from Thrift program annotations - Type mapping (Thrift types → C# types) - Default value generation for Thrift Object Model semantics - Constant value rendering for C# literal expressions - String quoting with proper C# escape sequences Includes unit tests for all public utility functions. Reviewer feedback incorporated: - Use C++20 unordered_set::contains() instead of .count() > 0 - Use fmt::format for escape_csharp_keyword Reviewed By: aristidisp, vitaut Differential Revision: D94160931 fbshipit-source-id: 48d4a7bca2c9835baac9f2707c92569961ee7cb1
1 parent c5e2b04 commit 1885ca9

7 files changed

Lines changed: 553 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <gtest/gtest.h>
18+
#include <thrift/compiler/generate/csharp/util.h>
19+
20+
namespace apache::thrift::compiler::csharp {
21+
22+
// === get_csharp_property_name ===
23+
24+
TEST(CSharpUtilTest, PropertyName_PassesThrough) {
25+
// Property names pass through unchanged — CS0542 collisions are
26+
// banned at the IDL level rather than silently mangled.
27+
EXPECT_EQ(get_csharp_property_name("my_field"), "my_field");
28+
EXPECT_EQ(get_csharp_property_name("id"), "id");
29+
EXPECT_EQ(get_csharp_property_name("class"), "class");
30+
EXPECT_EQ(get_csharp_property_name("namespace"), "namespace");
31+
EXPECT_EQ(get_csharp_property_name("path"), "path");
32+
}
33+
34+
// === quote_csharp_string ===
35+
36+
TEST(CSharpUtilTest, QuoteString_Basic) {
37+
EXPECT_EQ(quote_csharp_string("hello"), "\"hello\"");
38+
EXPECT_EQ(quote_csharp_string(""), "\"\"");
39+
}
40+
41+
TEST(CSharpUtilTest, QuoteString_EscapesSpecialChars) {
42+
EXPECT_EQ(quote_csharp_string("a\"b"), "\"a\\\"b\"");
43+
EXPECT_EQ(quote_csharp_string("a\\b"), "\"a\\\\b\"");
44+
EXPECT_EQ(quote_csharp_string("a\nb"), "\"a\\nb\"");
45+
EXPECT_EQ(quote_csharp_string("a\tb"), "\"a\\tb\"");
46+
EXPECT_EQ(quote_csharp_string("a\rb"), "\"a\\rb\"");
47+
}
48+
49+
TEST(CSharpUtilTest, QuoteString_Utf8PassThrough) {
50+
// UTF-8 bytes for "é" (U+00E9) should pass through unchanged
51+
// since C# source files are UTF-8 encoded
52+
EXPECT_EQ(quote_csharp_string("café"), "\"café\"");
53+
EXPECT_EQ(quote_csharp_string("日本語"), "\"日本語\"");
54+
55+
// Control characters should still be escaped
56+
EXPECT_EQ(escape_csharp_string("\x01"), "\\u0001");
57+
EXPECT_EQ(escape_csharp_string("\x1f"), "\\u001f");
58+
}
59+
60+
} // namespace apache::thrift::compiler::csharp
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include <thrift/compiler/generate/csharp/util.h>
18+
19+
#include <fmt/core.h>
20+
21+
#include <thrift/compiler/ast/t_container.h>
22+
#include <thrift/compiler/ast/t_enum.h>
23+
#include <thrift/compiler/ast/t_primitive_type.h>
24+
#include <thrift/compiler/ast/t_struct.h>
25+
#include <thrift/compiler/ast/t_union.h>
26+
#include <thrift/compiler/diagnostic.h>
27+
28+
namespace apache::thrift::compiler::csharp {
29+
30+
std::string get_csharp_property_name(const std::string& name) {
31+
return name;
32+
}
33+
34+
uint8_t get_csharp_ttype(const t_type* type) {
35+
type = type->get_true_type();
36+
37+
if (const auto* prim_type = dynamic_cast<const t_primitive_type*>(type)) {
38+
switch (prim_type->primitive_type()) {
39+
case t_primitive_type::type::t_void:
40+
return 0; // STOP
41+
case t_primitive_type::type::t_bool:
42+
return 2; // BOOL
43+
case t_primitive_type::type::t_byte:
44+
return 3; // BYTE
45+
case t_primitive_type::type::t_i16:
46+
return 6; // I16
47+
case t_primitive_type::type::t_i32:
48+
return 8; // I32
49+
case t_primitive_type::type::t_i64:
50+
return 10; // I64
51+
case t_primitive_type::type::t_float:
52+
return 19; // FLOAT
53+
case t_primitive_type::type::t_double:
54+
return 4; // DOUBLE
55+
case t_primitive_type::type::t_string:
56+
case t_primitive_type::type::t_binary:
57+
return 11; // STRING
58+
default:
59+
return 0; // STOP
60+
}
61+
}
62+
63+
if (dynamic_cast<const t_list*>(type)) {
64+
return 15; // LIST
65+
}
66+
67+
if (dynamic_cast<const t_set*>(type)) {
68+
return 14; // SET
69+
}
70+
71+
if (dynamic_cast<const t_map*>(type)) {
72+
return 13; // MAP
73+
}
74+
75+
if (dynamic_cast<const t_enum*>(type)) {
76+
return 8; // I32 (enums are serialized as i32)
77+
}
78+
79+
if (dynamic_cast<const t_struct*>(type) ||
80+
dynamic_cast<const t_union*>(type)) {
81+
return 12; // STRUCT
82+
}
83+
84+
return 0; // STOP as default
85+
}
86+
87+
std::string get_csharp_namespace(
88+
const t_program& program, diagnostics_engine& diags) {
89+
const std::string& csharp_ns = program.get_namespace("csharp");
90+
if (!csharp_ns.empty()) {
91+
return csharp_ns;
92+
}
93+
94+
// For included programs (e.g., annotation files) that lack an explicit
95+
// namespace csharp, derive a fallback from the package or program name.
96+
// The root program is validated separately by the generator entry point.
97+
const t_package& pkg = program.package();
98+
if (pkg.empty()) {
99+
diags.error(
100+
program,
101+
"No namespace 'csharp' in `{}`. Please add "
102+
"'namespace csharp <YourNamespace>' to the .thrift file.",
103+
program.name());
104+
return program.name();
105+
}
106+
107+
// Convert package path (e.g., "facebook.com/thrift/annotation/hack")
108+
// to C# namespace (e.g., "Facebook.Thrift.Annotation.Hack")
109+
// Note: std::toupper is safe here because package components are ASCII-only.
110+
std::string ns;
111+
for (std::string_view component : pkg.domain()) {
112+
if (!ns.empty()) {
113+
ns += ".";
114+
}
115+
std::string part(component);
116+
if (!part.empty()) {
117+
part[0] = std::toupper(static_cast<unsigned char>(part[0]));
118+
}
119+
ns += part;
120+
}
121+
for (std::string_view component : pkg.path()) {
122+
if (!ns.empty()) {
123+
ns += ".";
124+
}
125+
std::string part(component);
126+
if (!part.empty()) {
127+
part[0] = std::toupper(static_cast<unsigned char>(part[0]));
128+
}
129+
ns += part;
130+
}
131+
if (!ns.empty()) {
132+
return ns;
133+
}
134+
135+
diags.error(
136+
program,
137+
"No namespace 'csharp' in `{}`. Please add "
138+
"'namespace csharp <YourNamespace>' to the .thrift file.",
139+
program.name());
140+
return program.name();
141+
}
142+
143+
std::string escape_csharp_string(const std::string& value) {
144+
std::string escaped;
145+
escaped.reserve(value.size());
146+
for (unsigned char c : value) {
147+
switch (c) {
148+
case '\\':
149+
escaped += "\\\\";
150+
break;
151+
case '"':
152+
escaped += "\\\"";
153+
break;
154+
case '\n':
155+
escaped += "\\n";
156+
break;
157+
case '\r':
158+
escaped += "\\r";
159+
break;
160+
case '\t':
161+
escaped += "\\t";
162+
break;
163+
case '\0':
164+
escaped += "\\0";
165+
break;
166+
default:
167+
if (c < 0x20) {
168+
// Escape control characters (except those handled above)
169+
escaped += fmt::format("\\u{:04x}", static_cast<int>(c));
170+
} else {
171+
escaped += c;
172+
}
173+
break;
174+
}
175+
}
176+
return escaped;
177+
}
178+
179+
std::string quote_csharp_string(const std::string& value) {
180+
return "\"" + escape_csharp_string(value) + "\"";
181+
}
182+
183+
} // namespace apache::thrift::compiler::csharp
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <cstdint>
20+
#include <string>
21+
22+
#include <thrift/compiler/ast/t_program.h>
23+
#include <thrift/compiler/ast/t_type.h>
24+
25+
namespace apache::thrift::compiler {
26+
class diagnostics_engine;
27+
}
28+
29+
namespace apache::thrift::compiler::csharp {
30+
31+
/**
32+
* Returns the property name for a C# field.
33+
* Fields whose name collides with the enclosing type (CS0542) should be
34+
* banned at the IDL level rather than silently mangled.
35+
*/
36+
std::string get_csharp_property_name(const std::string& name);
37+
38+
/**
39+
* Returns the TType wire type byte for a given Thrift type.
40+
* Examples:
41+
* bool -> 2 (BOOL)
42+
* i32 -> 8 (I32)
43+
* string -> 11 (STRING)
44+
* struct -> 12 (STRUCT)
45+
*/
46+
uint8_t get_csharp_ttype(const t_type* type);
47+
48+
/**
49+
* Gets the C# namespace for a program.
50+
* Checks for explicit "namespace csharp" annotation, then falls back to
51+
* deriving from the package. Emits a diagnostic error if neither is available.
52+
*/
53+
std::string get_csharp_namespace(
54+
const t_program& program, diagnostics_engine& diags);
55+
56+
/**
57+
* Escapes a string for use in a C# string literal (without surrounding quotes).
58+
* Handles common escape sequences like \t, \n, \r, \0, and uses Unicode escapes
59+
* for other non-printable characters.
60+
*
61+
* Examples:
62+
* "hello" -> "hello"
63+
* "hello\tworld" -> "hello\\tworld"
64+
* "line1\nline2" -> "line1\\nline2"
65+
* "say \"hi\"" -> "say \\\"hi\\\""
66+
* "\x01" -> "\\u0001"
67+
*/
68+
std::string escape_csharp_string(const std::string& value);
69+
70+
/**
71+
* Quotes a string for use as a C# string literal.
72+
* Handles escaping of special characters.
73+
*/
74+
std::string quote_csharp_string(const std::string& value);
75+
76+
} // namespace apache::thrift::compiler::csharp

0 commit comments

Comments
 (0)