use crate::config::E2eConfig;
use crate::escape::{escape_csharp, sanitize_filename};
use crate::field_access::FieldResolver;
use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup};
use alef_core::backend::GeneratedFile;
use alef_core::config::AlefConfig;
use alef_core::hash::{self, CommentStyle};
use anyhow::Result;
use heck::ToUpperCamelCase;
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
pub struct CSharpCodegen;
impl E2eCodegen for CSharpCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
alef_config: &AlefConfig,
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let mut files = Vec::new();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.to_upper_camel_case());
let class_name = overrides
.and_then(|o| o.class.as_ref())
.cloned()
.unwrap_or_else(|| format!("{}Lib", alef_config.crate_config.name.to_upper_camel_case()));
let exception_class = format!("{}Exception", alef_config.crate_config.name.to_upper_camel_case());
let namespace = overrides.and_then(|o| o.module.as_ref()).cloned().unwrap_or_else(|| {
if call.module.is_empty() {
"Kreuzberg".to_string()
} else {
call.module.to_upper_camel_case()
}
});
let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
let result_var = &call.result_var;
let is_async = call.r#async;
let cs_pkg = e2e_config.resolve_package("csharp");
let pkg_name = cs_pkg
.as_ref()
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| alef_config.crate_config.name.to_upper_camel_case());
let pkg_path = cs_pkg
.as_ref()
.and_then(|p| p.path.as_ref())
.cloned()
.unwrap_or_else(|| format!("../../packages/csharp/{pkg_name}.csproj"));
let pkg_version = cs_pkg
.as_ref()
.and_then(|p| p.version.as_ref())
.cloned()
.unwrap_or_else(|| "0.1.0".to_string());
let csproj_name = format!("{pkg_name}.E2eTests.csproj");
files.push(GeneratedFile {
path: output_base.join(&csproj_name),
content: render_csproj(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
generated_header: false,
});
let tests_base = output_base.join("tests");
let field_resolver = FieldResolver::new(
&e2e_config.fields,
&e2e_config.fields_optional,
&e2e_config.result_fields,
&e2e_config.fields_array,
);
static EMPTY_ENUM_FIELDS: std::sync::LazyLock<HashMap<String, String>> = std::sync::LazyLock::new(HashMap::new);
let enum_fields = overrides.map(|o| &o.enum_fields).unwrap_or(&EMPTY_ENUM_FIELDS);
for group in groups {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
.collect();
if active.is_empty() {
continue;
}
let test_class = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
let filename = format!("{test_class}.cs");
let content = render_test_file(
&group.category,
&active,
&namespace,
&class_name,
&function_name,
&exception_class,
result_var,
&test_class,
&e2e_config.call.args,
&field_resolver,
result_is_simple,
is_async,
e2e_config,
enum_fields,
);
files.push(GeneratedFile {
path: tests_base.join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"csharp"
}
}
fn render_csproj(pkg_name: &str, pkg_path: &str, pkg_version: &str, dep_mode: crate::config::DependencyMode) -> String {
let pkg_ref = match dep_mode {
crate::config::DependencyMode::Registry => {
format!(" <PackageReference Include=\"{pkg_name}\" Version=\"{pkg_version}\" />")
}
crate::config::DependencyMode::Local => {
format!(" <ProjectReference Include=\"{pkg_path}\" />")
}
};
format!(
r#"<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
{pkg_ref}
</ItemGroup>
</Project>
"#
)
}
#[allow(clippy::too_many_arguments)]
fn render_test_file(
category: &str,
fixtures: &[&Fixture],
namespace: &str,
class_name: &str,
function_name: &str,
exception_class: &str,
result_var: &str,
test_class: &str,
args: &[crate::config::ArgMapping],
field_resolver: &FieldResolver,
result_is_simple: bool,
is_async: bool,
e2e_config: &E2eConfig,
enum_fields: &HashMap<String, String>,
) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::DoubleSlash));
let _ = writeln!(out, "using System.Text.Json;");
let _ = writeln!(out, "using System.Text.Json.Serialization;");
let _ = writeln!(out, "using System.Threading.Tasks;");
let _ = writeln!(out, "using Xunit;");
let _ = writeln!(out, "using {namespace};");
let _ = writeln!(out);
let _ = writeln!(out, "namespace Kreuzberg.E2e;");
let _ = writeln!(out);
let _ = writeln!(out, "/// <summary>E2e tests for category: {category}.</summary>");
let _ = writeln!(out, "public class {test_class}");
let _ = writeln!(out, "{{");
let _ = writeln!(
out,
" private static readonly JsonSerializerOptions ConfigOptions = new() {{ Converters = {{ new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }}, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }};"
);
let _ = writeln!(out);
for (i, fixture) in fixtures.iter().enumerate() {
render_test_method(
&mut out,
fixture,
class_name,
function_name,
exception_class,
result_var,
args,
field_resolver,
result_is_simple,
is_async,
e2e_config,
enum_fields,
);
if i + 1 < fixtures.len() {
let _ = writeln!(out);
}
}
let _ = writeln!(out, "}}");
out
}
#[allow(clippy::too_many_arguments)]
fn render_test_method(
out: &mut String,
fixture: &Fixture,
class_name: &str,
_function_name: &str,
exception_class: &str,
_result_var: &str,
_args: &[crate::config::ArgMapping],
field_resolver: &FieldResolver,
result_is_simple: bool,
_is_async: bool,
e2e_config: &E2eConfig,
enum_fields: &HashMap<String, String>,
) {
let method_name = fixture.id.to_upper_camel_case();
let description = &fixture.description;
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let call_config = e2e_config.resolve_call(fixture.call.as_deref());
let lang = "csharp";
let cs_overrides = call_config.overrides.get(lang);
let effective_function_name = cs_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.to_upper_camel_case());
let effective_result_var = &call_config.result_var;
let effective_is_async = call_config.r#async;
let function_name = effective_function_name.as_str();
let result_var = effective_result_var.as_str();
let is_async = effective_is_async;
let args = call_config.args.as_slice();
let (mut setup_lines, args_str) =
build_args_and_setup(&fixture.input, args, class_name, e2e_config, enum_fields, &fixture.id);
let mut visitor_arg = String::new();
if let Some(visitor_spec) = &fixture.visitor {
visitor_arg = build_csharp_visitor(&mut setup_lines, visitor_spec);
}
let final_args = if visitor_arg.is_empty() {
args_str
} else {
format!("{args_str}, {visitor_arg}")
};
let return_type = if is_async { "async Task" } else { "void" };
let await_kw = if is_async { "await " } else { "" };
let _ = writeln!(out, " [Fact]");
let _ = writeln!(out, " public {return_type} Test_{method_name}()");
let _ = writeln!(out, " {{");
let _ = writeln!(out, " // {description}");
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
if expects_error {
if is_async {
let _ = writeln!(
out,
" await Assert.ThrowsAsync<{exception_class}>(() => {class_name}.{function_name}({final_args}));"
);
} else {
let _ = writeln!(
out,
" Assert.Throws<{exception_class}>(() => {class_name}.{function_name}({final_args}));"
);
}
let _ = writeln!(out, " }}");
return;
}
let _ = writeln!(
out,
" var {result_var} = {await_kw}{class_name}.{function_name}({final_args});"
);
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
result_var,
class_name,
exception_class,
field_resolver,
result_is_simple,
);
}
let _ = writeln!(out, " }}");
}
fn build_args_and_setup(
input: &serde_json::Value,
args: &[crate::config::ArgMapping],
class_name: &str,
e2e_config: &E2eConfig,
enum_fields: &HashMap<String, String>,
fixture_id: &str,
) -> (Vec<String>, String) {
if args.is_empty() {
return (Vec::new(), json_to_csharp(input));
}
let overrides = e2e_config.call.overrides.get("csharp");
let options_type = overrides.and_then(|o| o.options_type.as_deref());
let mut setup_lines: Vec<String> = Vec::new();
let mut parts: Vec<String> = Vec::new();
for arg in args {
if arg.arg_type == "mock_url" {
setup_lines.push(format!(
"var {} = Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\";",
arg.name,
));
parts.push(arg.name.clone());
continue;
}
if arg.arg_type == "handle" {
let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
if config_value.is_null()
|| config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
{
setup_lines.push(format!("var {} = {class_name}.{constructor_name}(null);", arg.name,));
} else {
let sorted = sort_discriminator_first(config_value.clone());
let json_str = serde_json::to_string(&sorted).unwrap_or_default();
let name = &arg.name;
setup_lines.push(format!(
"var {name}Config = JsonSerializer.Deserialize<CrawlConfig>(\"{}\", ConfigOptions)!;",
escape_csharp(&json_str),
));
setup_lines.push(format!(
"var {} = {class_name}.{constructor_name}({name}Config);",
arg.name,
name = name,
));
}
parts.push(arg.name.clone());
continue;
}
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
parts.push("null".to_string());
continue;
}
None | Some(serde_json::Value::Null) => {
let default_val = match arg.arg_type.as_str() {
"string" => "\"\"".to_string(),
"int" | "integer" => "0".to_string(),
"float" | "number" => "0.0d".to_string(),
"bool" | "boolean" => "false".to_string(),
_ => "null".to_string(),
};
parts.push(default_val);
}
Some(v) => {
if let (Some(opts_type), "json_object") = (options_type, arg.arg_type.as_str()) {
if let Some(obj) = v.as_object() {
let props: Vec<String> = obj
.iter()
.map(|(k, vv)| {
let pascal_key = k.to_upper_camel_case();
let cs_val = if let Some(enum_type) = enum_fields.get(k) {
if let Some(s) = vv.as_str() {
let pascal_val = s.to_upper_camel_case();
format!("{enum_type}.{pascal_val}")
} else {
json_to_csharp(vv)
}
} else {
json_to_csharp(vv)
};
format!("{pascal_key} = {cs_val}")
})
.collect();
parts.push(format!("new {opts_type} {{ {} }}", props.join(", ")));
continue;
}
}
parts.push(json_to_csharp(v));
}
}
}
(setup_lines, parts.join(", "))
}
fn render_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
class_name: &str,
exception_class: &str,
field_resolver: &FieldResolver,
result_is_simple: bool,
) {
if let Some(f) = &assertion.field {
if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
return;
}
}
let field_expr = if result_is_simple {
result_var.to_string()
} else {
match &assertion.field {
Some(f) if !f.is_empty() => field_resolver.accessor(f, "csharp", result_var),
_ => result_var.to_string(),
}
};
let field_is_optional = assertion
.field
.as_deref()
.map(|f| field_resolver.is_optional(field_resolver.resolve(f)))
.unwrap_or(false);
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(expected) = &assertion.value {
let cs_val = json_to_csharp(expected);
if expected.is_string() {
let _ = writeln!(out, " Assert.Equal({cs_val}, {field_expr}.Trim());");
} else if expected.is_number() && field_is_optional {
let _ = writeln!(out, " Assert.Equal((object?){cs_val}, (object?){field_expr});");
} else {
let _ = writeln!(out, " Assert.Equal({cs_val}, {field_expr});");
}
}
}
"contains" => {
if let Some(expected) = &assertion.value {
let lower_expected = expected.as_str().map(|s| s.to_lowercase());
let cs_val = lower_expected
.as_deref()
.map(|s| format!("\"{}\"", escape_csharp(s)))
.unwrap_or_else(|| json_to_csharp(expected));
let _ = writeln!(
out,
" Assert.Contains({cs_val}, {field_expr}.ToString().ToLower());"
);
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let lower_val = val.as_str().map(|s| s.to_lowercase());
let cs_val = lower_val
.as_deref()
.map(|s| format!("\"{}\"", escape_csharp(s)))
.unwrap_or_else(|| json_to_csharp(val));
let _ = writeln!(
out,
" Assert.Contains({cs_val}, {field_expr}.ToString().ToLower());"
);
}
}
}
"not_contains" => {
if let Some(expected) = &assertion.value {
let cs_val = json_to_csharp(expected);
let _ = writeln!(out, " Assert.DoesNotContain({cs_val}, {field_expr}.ToString());");
}
}
"not_empty" => {
let _ = writeln!(
out,
" Assert.False(string.IsNullOrEmpty({field_expr}?.ToString()));"
);
}
"is_empty" => {
let _ = writeln!(
out,
" Assert.True(string.IsNullOrEmpty({field_expr}?.ToString()));"
);
}
"contains_any" => {
if let Some(values) = &assertion.values {
let checks: Vec<String> = values
.iter()
.map(|v| {
let cs_val = json_to_csharp(v);
format!("{field_expr}.ToString().Contains({cs_val})")
})
.collect();
let joined = checks.join(" || ");
let _ = writeln!(
out,
" Assert.True({joined}, \"expected to contain at least one of the specified values\");"
);
}
}
"greater_than" => {
if let Some(val) = &assertion.value {
let cs_val = json_to_csharp(val);
let _ = writeln!(
out,
" Assert.True({field_expr} > {cs_val}, \"expected > {cs_val}\");"
);
}
}
"less_than" => {
if let Some(val) = &assertion.value {
let cs_val = json_to_csharp(val);
let _ = writeln!(
out,
" Assert.True({field_expr} < {cs_val}, \"expected < {cs_val}\");"
);
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let cs_val = json_to_csharp(val);
let _ = writeln!(
out,
" Assert.True({field_expr} >= {cs_val}, \"expected >= {cs_val}\");"
);
}
}
"less_than_or_equal" => {
if let Some(val) = &assertion.value {
let cs_val = json_to_csharp(val);
let _ = writeln!(
out,
" Assert.True({field_expr} <= {cs_val}, \"expected <= {cs_val}\");"
);
}
}
"starts_with" => {
if let Some(expected) = &assertion.value {
let cs_val = json_to_csharp(expected);
let _ = writeln!(out, " Assert.StartsWith({cs_val}, {field_expr});");
}
}
"ends_with" => {
if let Some(expected) = &assertion.value {
let cs_val = json_to_csharp(expected);
let _ = writeln!(out, " Assert.EndsWith({cs_val}, {field_expr});");
}
}
"min_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(
out,
" Assert.True({field_expr}.Length >= {n}, \"expected length >= {n}\");"
);
}
}
}
"max_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(
out,
" Assert.True({field_expr}.Length <= {n}, \"expected length <= {n}\");"
);
}
}
}
"count_min" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(
out,
" Assert.True({field_expr}.Count >= {n}, \"expected at least {n} elements\");"
);
}
}
}
"count_equals" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " Assert.Equal({n}, {field_expr}.Count);");
}
}
}
"is_true" => {
let _ = writeln!(out, " Assert.True({field_expr});");
}
"is_false" => {
let _ = writeln!(out, " Assert.False({field_expr});");
}
"not_error" => {
}
"error" => {
}
"method_result" => {
if let Some(method_name) = &assertion.method {
let call_expr = build_csharp_method_call(result_var, method_name, assertion.args.as_ref(), class_name);
let check = assertion.check.as_deref().unwrap_or("is_true");
match check {
"equals" => {
if let Some(val) = &assertion.value {
if val.as_bool() == Some(true) {
let _ = writeln!(out, " Assert.True({call_expr});");
} else if val.as_bool() == Some(false) {
let _ = writeln!(out, " Assert.False({call_expr});");
} else {
let cs_val = json_to_csharp(val);
let _ = writeln!(out, " Assert.Equal({cs_val}, {call_expr});");
}
}
}
"is_true" => {
let _ = writeln!(out, " Assert.True({call_expr});");
}
"is_false" => {
let _ = writeln!(out, " Assert.False({call_expr});");
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(out, " Assert.True({call_expr} >= {n}, \"expected >= {n}\");");
}
}
"count_min" => {
if let Some(val) = &assertion.value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(
out,
" Assert.True({call_expr}.Count >= {n}, \"expected at least {n} elements\");"
);
}
}
"is_error" => {
let _ = writeln!(
out,
" Assert.Throws<{exception_class}>(() => {{ {call_expr}; }});"
);
}
"contains" => {
if let Some(val) = &assertion.value {
let cs_val = json_to_csharp(val);
let _ = writeln!(out, " Assert.Contains({cs_val}, {call_expr});");
}
}
other_check => {
panic!("C# e2e generator: unsupported method_result check type: {other_check}");
}
}
} else {
panic!("C# e2e generator: method_result assertion missing 'method' field");
}
}
other => {
panic!("C# e2e generator: unsupported assertion type: {other}");
}
}
}
fn sort_discriminator_first(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let mut sorted = serde_json::Map::with_capacity(map.len());
if let Some(type_val) = map.get("type") {
sorted.insert("type".to_string(), sort_discriminator_first(type_val.clone()));
}
for (k, v) in map {
if k != "type" {
sorted.insert(k, sort_discriminator_first(v));
}
}
serde_json::Value::Object(sorted)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(sort_discriminator_first).collect())
}
other => other,
}
}
fn json_to_csharp(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => format!("\"{}\"", escape_csharp(s)),
serde_json::Value::Bool(true) => "true".to_string(),
serde_json::Value::Bool(false) => "false".to_string(),
serde_json::Value::Number(n) => {
if n.is_f64() {
format!("{}d", n)
} else {
n.to_string()
}
}
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(json_to_csharp).collect();
format!("new[] {{ {} }}", items.join(", "))
}
serde_json::Value::Object(_) => {
let json_str = serde_json::to_string(value).unwrap_or_default();
format!("\"{}\"", escape_csharp(&json_str))
}
}
}
fn build_csharp_visitor(setup_lines: &mut Vec<String>, visitor_spec: &crate::fixture::VisitorSpec) -> String {
setup_lines.push("var _testVisitor = new TestVisitor();".to_string());
setup_lines.push("class TestVisitor : IVisitor".to_string());
setup_lines.push("{".to_string());
for (method_name, action) in &visitor_spec.callbacks {
emit_csharp_visitor_method(setup_lines, method_name, action);
}
setup_lines.push("}".to_string());
"_testVisitor".to_string()
}
fn emit_csharp_visitor_method(setup_lines: &mut Vec<String>, method_name: &str, action: &CallbackAction) {
let camel_method = method_to_camel(method_name);
let params = match method_name {
"visit_link" => "VisitContext ctx, string href, string text, string title",
"visit_image" => "VisitContext ctx, string src, string alt, string title",
"visit_heading" => "VisitContext ctx, int level, string text, string id",
"visit_code_block" => "VisitContext ctx, string lang, string code",
"visit_code_inline"
| "visit_strong"
| "visit_emphasis"
| "visit_strikethrough"
| "visit_underline"
| "visit_subscript"
| "visit_superscript"
| "visit_mark"
| "visit_button"
| "visit_summary"
| "visit_figcaption"
| "visit_definition_term"
| "visit_definition_description" => "VisitContext ctx, string text",
"visit_text" => "VisitContext ctx, string text",
"visit_list_item" => "VisitContext ctx, bool ordered, string marker, string text",
"visit_blockquote" => "VisitContext ctx, string content, int depth",
"visit_table_row" => "VisitContext ctx, IReadOnlyList<string> cells, bool isHeader",
"visit_custom_element" => "VisitContext ctx, string tagName, string html",
"visit_form" => "VisitContext ctx, string actionUrl, string method",
"visit_input" => "VisitContext ctx, string inputType, string name, string value",
"visit_audio" | "visit_video" | "visit_iframe" => "VisitContext ctx, string src",
"visit_details" => "VisitContext ctx, bool isOpen",
_ => "VisitContext ctx",
};
setup_lines.push(format!(" public VisitResult {camel_method}({params})"));
setup_lines.push(" {".to_string());
match action {
CallbackAction::Skip => {
setup_lines.push(" return VisitResult.Skip();".to_string());
}
CallbackAction::Continue => {
setup_lines.push(" return VisitResult.Continue();".to_string());
}
CallbackAction::PreserveHtml => {
setup_lines.push(" return VisitResult.PreserveHtml();".to_string());
}
CallbackAction::Custom { output } => {
let escaped = escape_csharp(output);
setup_lines.push(format!(" return VisitResult.Custom(\"{escaped}\");"));
}
CallbackAction::CustomTemplate { template } => {
setup_lines.push(format!(" return VisitResult.Custom($\"{template}\");"));
}
}
setup_lines.push(" }".to_string());
}
fn method_to_camel(snake: &str) -> String {
use heck::ToUpperCamelCase;
snake.to_upper_camel_case()
}
fn build_csharp_method_call(
result_var: &str,
method_name: &str,
args: Option<&serde_json::Value>,
class_name: &str,
) -> String {
match method_name {
"root_child_count" => format!("{result_var}.RootNode.ChildCount"),
"root_node_type" => format!("{result_var}.RootNode.Kind"),
"named_children_count" => format!("{result_var}.RootNode.NamedChildCount"),
"has_error_nodes" => format!("{class_name}.TreeHasErrorNodes({result_var})"),
"error_count" | "tree_error_count" => format!("{class_name}.TreeErrorCount({result_var})"),
"tree_to_sexp" => format!("{class_name}.TreeToSexp({result_var})"),
"contains_node_type" => {
let node_type = args
.and_then(|a| a.get("node_type"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{class_name}.TreeContainsNodeType({result_var}, \"{node_type}\")")
}
"find_nodes_by_type" => {
let node_type = args
.and_then(|a| a.get("node_type"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{class_name}.FindNodesByType({result_var}, \"{node_type}\")")
}
"run_query" => {
let query_source = args
.and_then(|a| a.get("query_source"))
.and_then(|v| v.as_str())
.unwrap_or("");
let language = args
.and_then(|a| a.get("language"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{class_name}.RunQuery({result_var}, \"{language}\", \"{query_source}\", source)")
}
_ => {
use heck::ToUpperCamelCase;
let pascal = method_name.to_upper_camel_case();
format!("{result_var}.{pascal}()")
}
}
}