Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -1105,8 +1105,47 @@ private record struct DescriptorKey(
/// Replaces non-alphanumeric characters in the identifier with the underscore character.
/// Primarily intended to remove characters produced by compiler-generated method name mangling.
/// </returns>
private static string SanitizeMemberName(string memberName) =>
InvalidNameCharsRegex().Replace(memberName, "_");
private static string SanitizeMemberName(string memberName)
{
// Handle compiler-generated local function names: <ContainingMethod>g__LocalFunctionName|ordinal_depth
// Extract: ContainingMethod_LocalFunctionName
var match = LocalFunctionNameRegex().Match(memberName);
if (match.Success)
{
string containingMethod = match.Groups[1].Value;
string localFunctionName = match.Groups[2].Value;
return $"{containingMethod}_{localFunctionName}";
}

// Handle compiler-generated lambda names: <ContainingMethod>b__ordinal_depth
// Extract: ContainingMethod
match = LambdaNameRegex().Match(memberName);
if (match.Success)
{
return match.Groups[1].Value;
}

// For any other cases, just replace invalid characters with underscores
return InvalidNameCharsRegex().Replace(memberName, "_");
}

/// <summary>Regex that matches compiler-generated local function names.</summary>
#if NET
[GeneratedRegex(@"^<([^>]+)>g__([^|]+)\|")]
private static partial Regex LocalFunctionNameRegex();
#else
private static Regex LocalFunctionNameRegex() => _localFunctionNameRegex;
private static readonly Regex _localFunctionNameRegex = new(@"^<([^>]+)>g__([^|]+)\|", RegexOptions.Compiled);
#endif

/// <summary>Regex that matches compiler-generated lambda names.</summary>
#if NET
[GeneratedRegex(@"^<([^>]+)>b__")]
private static partial Regex LambdaNameRegex();
#else
private static Regex LambdaNameRegex() => _lambdaNameRegex;
private static readonly Regex _lambdaNameRegex = new(@"^<([^>]+)>b__", RegexOptions.Compiled);
#endif

/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
#if NET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,8 @@

private sealed class MyArgumentType;

private static int TestStaticMethod(int a, int b) => a + b;

private class A;
private class B : A;
private sealed class C : B;
Expand Down Expand Up @@ -1039,6 +1041,135 @@
},
};

[Fact]
public void LocalFunction_NameCleanup()
{
static void DoSomething()

Check failure on line 1047 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1047

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1047,21): error S1186: (NETCORE_ENGINEERING_TELEMETRY=Build) Add a nested comment explaining why this method is empty, throw a 'NotSupportedException' or complete the implementation. (https://rules.sonarsource.com/csharp/RSPEC-1186)

Check failure on line 1047 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1047

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1047,21): error S1186: (NETCORE_ENGINEERING_TELEMETRY=Build) Add a nested comment explaining why this method is empty, throw a 'NotSupportedException' or complete the implementation. (https://rules.sonarsource.com/csharp/RSPEC-1186)
{

Check failure on line 1048 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1048

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1048,9): error S108: (NETCORE_ENGINEERING_TELEMETRY=Build) Either remove or fill this block of code. (https://rules.sonarsource.com/csharp/RSPEC-108)

Check failure on line 1048 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1048

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1048,9): error S108: (NETCORE_ENGINEERING_TELEMETRY=Build) Either remove or fill this block of code. (https://rules.sonarsource.com/csharp/RSPEC-108)
}

var tool = AIFunctionFactory.Create(DoSomething);

// The name should be: ContainingMethodName_LocalFunctionName
Assert.Equal("LocalFunction_NameCleanup_DoSomething", tool.Name);
}

[Fact]
public void LocalFunction_MultipleInSameMethod()
{
static void FirstLocal()

Check failure on line 1060 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1060

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1060,21): error S1186: (NETCORE_ENGINEERING_TELEMETRY=Build) Add a nested comment explaining why this method is empty, throw a 'NotSupportedException' or complete the implementation. (https://rules.sonarsource.com/csharp/RSPEC-1186)

Check failure on line 1060 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1060

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1060,21): error S1186: (NETCORE_ENGINEERING_TELEMETRY=Build) Add a nested comment explaining why this method is empty, throw a 'NotSupportedException' or complete the implementation. (https://rules.sonarsource.com/csharp/RSPEC-1186)
{

Check failure on line 1061 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1061

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1061,9): error S108: (NETCORE_ENGINEERING_TELEMETRY=Build) Either remove or fill this block of code. (https://rules.sonarsource.com/csharp/RSPEC-108)
}

static void SecondLocal()

Check failure on line 1064 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci (Correctness WarningsCheck)

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1064

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1064,21): error S1186: (NETCORE_ENGINEERING_TELEMETRY=Build) Add a nested comment explaining why this method is empty, throw a 'NotSupportedException' or complete the implementation. (https://rules.sonarsource.com/csharp/RSPEC-1186)

Check failure on line 1064 in test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs

View check run for this annotation

Azure Pipelines / extensions-ci

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs#L1064

test/Libraries/Microsoft.Extensions.AI.Tests/Functions/AIFunctionFactoryTest.cs(1064,21): error S1186: (NETCORE_ENGINEERING_TELEMETRY=Build) Add a nested comment explaining why this method is empty, throw a 'NotSupportedException' or complete the implementation. (https://rules.sonarsource.com/csharp/RSPEC-1186)
{
}

var tool1 = AIFunctionFactory.Create(FirstLocal);
var tool2 = AIFunctionFactory.Create(SecondLocal);

// Each should have unique names based on the local function name
Assert.Equal("LocalFunction_MultipleInSameMethod_FirstLocal", tool1.Name);
Assert.Equal("LocalFunction_MultipleInSameMethod_SecondLocal", tool2.Name);
Assert.NotEqual(tool1.Name, tool2.Name);
}

[Fact]
public void Lambda_NameCleanup()
{
Action lambda = () =>
{
};

var tool = AIFunctionFactory.Create(lambda);

// The name should be the containing method name
Assert.Equal("Lambda_NameCleanup", tool.Name);
}

[Fact]
public void Lambda_MultipleInSameMethod()
{
Action lambda1 = () =>
{
};

Action lambda2 = () =>
{
};

var tool1 = AIFunctionFactory.Create(lambda1);
var tool2 = AIFunctionFactory.Create(lambda2);

// Both should have the same name since they're from the same method
// and lambdas don't have distinct names
Assert.Equal("Lambda_MultipleInSameMethod", tool1.Name);
Assert.Equal("Lambda_MultipleInSameMethod", tool2.Name);
}

[Fact]
public void LocalFunction_WithParameters()
{
static int Add(int a, int b) => a + b;

var tool = AIFunctionFactory.Create(Add);

Assert.Equal("LocalFunction_WithParameters_Add", tool.Name);
Assert.Contains("a", tool.JsonSchema.ToString());
Assert.Contains("b", tool.JsonSchema.ToString());
}

[Fact]
public async Task LocalFunction_AsyncFunction()
{
static async Task<string> FetchDataAsync()
{
await Task.Yield();
return "data";
}

var tool = AIFunctionFactory.Create(FetchDataAsync);

// Should strip "Async" suffix
Assert.Equal("LocalFunction_AsyncFunction_FetchData", tool.Name);

var result = await tool.InvokeAsync();
AssertExtensions.EqualFunctionCallResults("data", result);
}

[Fact]
public void LocalFunction_ExplicitNameOverride()
{
static void DoSomething()
{
}

var tool = AIFunctionFactory.Create(DoSomething, name: "CustomName");

Assert.Equal("CustomName", tool.Name);
}

[Fact]
public void LocalFunction_InsideTestMethod()
{
// Even local functions defined in test methods get cleaned up
var tool = AIFunctionFactory.Create(Add, serializerOptions: JsonContext.Default.Options);

Assert.Equal("LocalFunction_InsideTestMethod_Add", tool.Name);

[return: Description("The summed result")]
static int Add(int a, int b) => a + b;
}

[Fact]
public void RegularStaticMethod_NameUnchanged()
{
// Test that actual static methods (not local functions) have names unchanged
var tool = AIFunctionFactory.Create(TestStaticMethod, null, serializerOptions: JsonContext.Default.Options);

Assert.Equal("TestStaticMethod", tool.Name);
}

[JsonSerializable(typeof(IAsyncEnumerable<int>))]
[JsonSerializable(typeof(int[]))]
[JsonSerializable(typeof(string))]
Expand Down
Loading