diff --git a/tracer/src/Datadog.Trace.SourceGenerators/Helpers/TrackingNames.cs b/tracer/src/Datadog.Trace.SourceGenerators/Helpers/TrackingNames.cs
index 928aef5d47cf..9055347f70c6 100644
--- a/tracer/src/Datadog.Trace.SourceGenerators/Helpers/TrackingNames.cs
+++ b/tracer/src/Datadog.Trace.SourceGenerators/Helpers/TrackingNames.cs
@@ -44,4 +44,9 @@ internal class TrackingNames
public const string ConfigurationKeysGenYamlFile = nameof(ConfigurationKeysGenYamlFile);
public const string ConfigurationKeysGenParseYaml = nameof(ConfigurationKeysGenParseYaml);
public const string ConfigurationKeysGenParseConfiguration = nameof(ConfigurationKeysGenParseConfiguration);
+
+ // String case interceptor generator
+ public const string StringCaseCallSites = nameof(StringCaseCallSites);
+ public const string StringCaseIsNetFramework = nameof(StringCaseIsNetFramework);
+ public const string StringCaseCombined = nameof(StringCaseCombined);
}
diff --git a/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/CallSite.cs b/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/CallSite.cs
new file mode 100644
index 000000000000..49b0337950b8
--- /dev/null
+++ b/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/CallSite.cs
@@ -0,0 +1,29 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+namespace Datadog.Trace.SourceGenerators.StringCaseInterception;
+
+///
+/// A single / call site to intercept.
+///
+internal sealed record CallSite
+{
+ public CallSite(string methodName, string interceptsLocationAttribute)
+ {
+ MethodName = methodName;
+ InterceptsLocationAttribute = interceptsLocationAttribute;
+ }
+
+ ///
+ /// Gets either ToUpperInvariant or ToLowerInvariant.
+ ///
+ public string MethodName { get; }
+
+ ///
+ /// Gets the full [InterceptsLocation(...)] attribute syntax for this call site, as returned by
+ /// .
+ ///
+ public string InterceptsLocationAttribute { get; }
+}
diff --git a/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/Sources.cs b/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/Sources.cs
new file mode 100644
index 000000000000..67820a84c61e
--- /dev/null
+++ b/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/Sources.cs
@@ -0,0 +1,59 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+using System.Collections.Generic;
+using System.Text;
+
+namespace Datadog.Trace.SourceGenerators.StringCaseInterception;
+
+internal static class Sources
+{
+ public static string GenerateInterceptors(IReadOnlyList upperAttributes, IReadOnlyList lowerAttributes)
+ {
+ var sb = new StringBuilder(Constants.FileHeader);
+
+ sb.Append(
+ """
+ namespace System.Runtime.CompilerServices
+ {
+ [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : global::System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+ }
+
+ namespace Datadog.Trace.Generated.Interceptors
+ {
+ internal static class StringCaseInterceptors
+ {
+
+ """);
+
+ AppendMethod(sb, "ToUpperInvariant", upperAttributes);
+ AppendMethod(sb, "ToLowerInvariant", lowerAttributes);
+
+ sb.Append(
+ """
+ }
+ }
+ """);
+
+ return sb.ToString();
+ }
+
+ private static void AppendMethod(StringBuilder sb, string methodName, IReadOnlyList attributes)
+ {
+ foreach (var attribute in attributes)
+ {
+ sb.Append(" ").Append(attribute).Append('\n');
+ }
+
+ sb.Append(" public static string ").Append(methodName).Append("(this string value)\n");
+ sb.Append(" => global::System.StringUtil.").Append(methodName).Append("(value);\n\n");
+ }
+}
diff --git a/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/StringCaseInterceptorGenerator.cs b/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/StringCaseInterceptorGenerator.cs
new file mode 100644
index 000000000000..8a49fe891167
--- /dev/null
+++ b/tracer/src/Datadog.Trace.SourceGenerators/StringCaseInterception/StringCaseInterceptorGenerator.cs
@@ -0,0 +1,203 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Text;
+using System.Threading;
+using Datadog.Trace.SourceGenerators.Helpers;
+using Datadog.Trace.SourceGenerators.StringCaseInterception;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+///
+/// On .NET Framework only, intercepts every /
+/// call site in the compilation and redirects it to
+/// System.StringUtil, which avoids the allocation those BCL methods
+/// always incur on that TFM when no character actually needs to change case. See
+/// System.StringUtil for why the helper itself must never be rewritten
+/// by this generator (it would recurse infinitely).
+///
+[Generator]
+public class StringCaseInterceptorGenerator : IIncrementalGenerator
+{
+ private const string ToUpperInvariant = "ToUpperInvariant";
+ private const string ToLowerInvariant = "ToLowerInvariant";
+ private const string SkipAttributeFullName = "Datadog.Trace.Util.SkipStringCaseInterceptionAttribute";
+ private const string HelperFullName = "System.StringUtil";
+ private const string InterceptorsFullName = "Datadog.Trace.Generated.Interceptors.StringCaseInterceptors";
+
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var isNetFramework =
+ context.AnalyzerConfigOptionsProvider
+ .Select(static (provider, _) =>
+ provider.GlobalOptions.TryGetValue("build_property.TargetFrameworkIdentifier", out var tfi)
+ && tfi == ".NETFramework")
+ .WithTrackingName(TrackingNames.StringCaseIsNetFramework);
+
+ IncrementalValuesProvider callSites =
+ context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: static (node, _) => IsCandidate(node),
+ transform: static (ctx, ct) => GetCallSite(ctx, ct))
+ .Where(static x => x is not null)
+ .Select(static (x, _) => x!)
+ .WithTrackingName(TrackingNames.StringCaseCallSites);
+
+ IncrementalValueProvider<(ImmutableArray CallSites, bool IsNetFramework)> combined =
+ callSites.Collect()
+ .Combine(isNetFramework)
+ .WithTrackingName(TrackingNames.StringCaseCombined);
+
+ context.RegisterSourceOutput(combined, static (spc, source) => Execute(source.CallSites, source.IsNetFramework, spc));
+ }
+
+ private static bool IsCandidate(SyntaxNode node)
+ {
+ if (node is not InvocationExpressionSyntax { ArgumentList.Arguments.Count: 0 } invocation)
+ {
+ return false;
+ }
+
+ var name = invocation.Expression switch
+ {
+ MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText,
+ MemberBindingExpressionSyntax memberBinding => memberBinding.Name.Identifier.ValueText,
+ _ => null,
+ };
+
+ if (name is not (ToUpperInvariant or ToLowerInvariant))
+ {
+ return false;
+ }
+
+ // we add a .NET Framework check in here to avoid the expensive GetCallSite calls from running at all on .NET Core
+ if (node.SyntaxTree.Options is not CSharpParseOptions options)
+ {
+ return false;
+ }
+
+ foreach (var symbol in options.PreprocessorSymbolNames)
+ {
+ if (symbol == "NETFRAMEWORK")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static CallSite? GetCallSite(GeneratorSyntaxContext context, CancellationToken ct)
+ {
+ var invocation = (InvocationExpressionSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+
+ if (semanticModel.GetSymbolInfo(invocation, ct).Symbol is not IMethodSymbol { IsStatic: false, Parameters.Length: 0 } method)
+ {
+ return null;
+ }
+
+ if (method.ContainingType?.SpecialType != SpecialType.System_String)
+ {
+ return null;
+ }
+
+ var methodName = method.Name;
+ if (methodName is not (ToUpperInvariant or ToLowerInvariant))
+ {
+ return null;
+ }
+
+ ct.ThrowIfCancellationRequested();
+
+ if (IsExcluded(semanticModel, invocation, ct))
+ {
+ return null;
+ }
+
+ var location = semanticModel.GetInterceptableLocation(invocation, ct);
+ if (location is null)
+ {
+ return null;
+ }
+
+ return new CallSite(methodName, location.GetInterceptsLocationAttributeSyntax());
+ }
+
+ ///
+ /// Opts a call site out when the containing method/type carries ,
+ /// or when the call is inside the helper or the interceptor stub themselves
+ ///
+ private static bool IsExcluded(SemanticModel semanticModel, InvocationExpressionSyntax invocation, CancellationToken ct)
+ {
+ var enclosingSymbol = semanticModel.GetEnclosingSymbol(invocation.SpanStart, ct);
+ if (enclosingSymbol is null)
+ {
+ return false;
+ }
+
+ if (HasSkipAttribute(enclosingSymbol))
+ {
+ return true;
+ }
+
+ for (var type = enclosingSymbol.ContainingType; type is not null; type = type.ContainingType)
+ {
+ var fullName = type.ToDisplayString();
+ if (fullName is HelperFullName or InterceptorsFullName)
+ {
+ return true;
+ }
+
+ if (HasSkipAttribute(type))
+ {
+ return true;
+ }
+ }
+
+ return false;
+
+ static bool HasSkipAttribute(ISymbol symbol)
+ {
+ foreach (var attribute in symbol.GetAttributes())
+ {
+ if (attribute.AttributeClass?.ToDisplayString() == SkipAttributeFullName)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ private static void Execute(ImmutableArray callSites, bool isNetFramework, SourceProductionContext context)
+ {
+ if (!isNetFramework || callSites.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ var upper = new List();
+ var lower = new List();
+
+ foreach (var callSite in callSites)
+ {
+ (callSite.MethodName == ToUpperInvariant ? upper : lower).Add(callSite.InterceptsLocationAttribute);
+ }
+
+ upper.Sort(StringComparer.Ordinal);
+ lower.Sort(StringComparer.Ordinal);
+
+ var source = Sources.GenerateInterceptors(upper, lower);
+ context.AddSource("StringCaseInterceptors.g.cs", SourceText.From(source, Encoding.UTF8));
+ }
+}
diff --git a/tracer/src/Datadog.Trace/Datadog.Trace.csproj b/tracer/src/Datadog.Trace/Datadog.Trace.csproj
index f855e1c35207..b9c297dbf26c 100644
--- a/tracer/src/Datadog.Trace/Datadog.Trace.csproj
+++ b/tracer/src/Datadog.Trace/Datadog.Trace.csproj
@@ -64,6 +64,15 @@
+
+
+
+
+
+
+ $(InterceptorsNamespaces);Datadog.Trace.Generated.Interceptors
+
+
diff --git a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/StringCaseInterceptorGenerator/StringCaseInterceptors.g.cs b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/StringCaseInterceptorGenerator/StringCaseInterceptors.g.cs
new file mode 100644
index 000000000000..95f76317822f
--- /dev/null
+++ b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/StringCaseInterceptorGenerator/StringCaseInterceptors.g.cs
@@ -0,0 +1,111 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+//
+
+#nullable enable
+
+namespace System.Runtime.CompilerServices
+{
+ [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : global::System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+namespace Datadog.Trace.Generated.Interceptors
+{
+ internal static class StringCaseInterceptors
+ {
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "+SDTjDWxVhVmMHB4GoePAc9nAABUeXBlTmFtZVBhcnNlci5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "/V8bwZXa2KGWMECTY20NQNELAABSZXNvdXJjZU5hbWUuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "/V8bwZXa2KGWMECTY20NQPkLAABSZXNvdXJjZU5hbWUuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "/zyShIOcooDzKQypXUYxTmVdAABTaWdDb21wYXJlci5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "0DD/zpbUtc7cH5jWuP/A1gAEAABEaWN0aW9uYXJ5R2V0dGVyQW5kU2V0dGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "44C75CnQDj9YgElaE9xQcipkAABPdGxwU3BhblN0YXRzU2VyaWFsaXplci5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "6p/P1hg7wawi4fNhbRq7LJcNAABXY2ZDb21tb24uY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "9ARKmg8986ju94ACZgzuxcYLAABBc3BOZXRNdmNJbnRlZ3JhdGlvbi5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "9eXD+ewmOQlsy4RM+5t58ukFAABIdHRwQnlwYXNzSGVscGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "AjrsnTjArmrQpnWrnNL4zwgQAABSdW50aW1lUGlwZWxpbmVJbnZva2VTeW5jSW50ZWdyYXRpb24uY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "ErHKBUkpMVUQNAfO/sYCYBpAAABXaW4zMlJlc291cmNlc0NodW5rLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "ErHKBUkpMVUQNAfO/sYCYENAAABXaW4zMlJlc291cmNlc0NodW5rLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "M4vYTy7JgkOoCjgaUAx8iMoFAABDYXNpbmcuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Q1KWWeiHUR3PQ55wdljnU1sZAABBc3BOZXRXZWJBcGkySW50ZWdyYXRpb24uY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "WIxYu1GAjx30kkfO7ufMUberAABUcmFjZXJTZXR0aW5ncy5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "bmaMhpMFjqpoUMVH1WIG2bgqAABVdGlscy5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "fpvPtntOQ4mfSQn5fQW7z6YFAABEaXJlY3RTdWJtaXNzaW9uTG9nTGV2ZWxFeHRlbnNpb25zLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "gJuugukX4hvZ6Y6RY0UZxKxhAABVVEY4U3RyaW5nLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "i7bgnLok3Shk2SlWNgLshSAPAABUcmFjaW5nSHR0cE1vZHVsZS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "i7bgnLok3Shk2SlWNgLshSkfAABUcmFjaW5nSHR0cE1vZHVsZS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "i7bgnLok3Shk2SlWNgLshY4OAABUcmFjaW5nSHR0cE1vZHVsZS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "kMCHUjbSxX7P2rcx8xGmXg0GAABJbmZlcnJlZFByb3h5RGF0YS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "oUMychrHQKPStf5AxIKOGV4PAABFTkNNZXRhZGF0YS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "pV6En3lW2V2MCykLy7/p6NYPAABIdHRwUHJvY2Vzc0FuZFNlbmRJbnRlZ3JhdGlvbi5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "tFINP9HGKFNDiltHIO7S6vsPAABSdW50aW1lUGlwZWxpbmVJbnZva2VBc3luY0ludGVncmF0aW9uLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "yDFfCpjvz5aTdmb6ZiEsTHIZAABTY29wZUZhY3RvcnkuY3M=")]
+ public static string ToUpperInvariant(this string value)
+ => global::System.StringUtil.ToUpperInvariant(value);
+
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "0da+RPFBgPWKfEZlzHyGNh0UAABTdHJpbmdFeHRlbnNpb25zLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "3b+/4CUqSJLploDNhgG/+GVEAABDb25maWd1cmF0aW9uU3RhdGUuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tgITAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tgQRAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tjYUAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tl8MAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tmQOAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tmcVAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tnYPAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tpINAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tr8KAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "70I0uqD8qvlp+KUV2fq4tvUIAABPcGVyYXRpb25OYW1lTWFwcGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "9ARKmg8986ju94ACZgzuxTkYAABBc3BOZXRNdmNJbnRlZ3JhdGlvbi5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "9ARKmg8986ju94ACZgzuxVEXAABBc3BOZXRNdmNJbnRlZ3JhdGlvbi5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "9ARKmg8986ju94ACZgzuxaAZAABBc3BOZXRNdmNJbnRlZ3JhdGlvbi5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "9ARKmg8986ju94ACZgzuxckXAABBc3BOZXRNdmNJbnRlZ3JhdGlvbi5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "D1IYEJ9NXFm/Dr+JD/Pi7OQTAABEb3RuZXRDb21tb24uY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Dz0XXiHszq0LChtc4X8phZUSAABBZ2VudGxlc3NFbmRwb2ludC5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "ExqYKpEmZRK/USfIuTC/Oa+KAABKc29uVGV4dFJlYWRlci5Bc3luYy5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "J5ztnVH6fQPomsJs0BOWmogFAABUZXN0U3VpdGUuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Kn5Ig1uIDsCtk4zRE7GRG6EEAABBenVyZUFwaU1hbmFnZW1lbnRTcGFuRmFjdG9yeS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "M4vYTy7JgkOoCjgaUAx8iBsGAABDYXNpbmcuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Q1KWWeiHUR3PQ55wdljnU6YiAABBc3BOZXRXZWJBcGkySW50ZWdyYXRpb24uY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Q1KWWeiHUR3PQ55wdljnU8ghAABBc3BOZXRXZWJBcGkySW50ZWdyYXRpb24uY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Q1KWWeiHUR3PQ55wdljnUzsiAABBc3BOZXRXZWJBcGkySW50ZWdyYXRpb24uY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "TTdqUVsNCqUjLqtRhH2LK/kQAABUZXN0U2Vzc2lvbi5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "V+SNLTybJSAtZ3plHDoEBDUEAABBd3NBcGlHYXRld2F5U3BhbkZhY3RvcnkuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "W3aG+y9sjmU7LLzQlsME+/IGAABGaWxlVGVzdE9wdGltaXphdGlvbkNsaWVudC5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "WIxYu1GAjx30kkfO7ufMUYuBAABUcmFjZXJTZXR0aW5ncy5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "YIXd53an9LVZr1P0jdwJo4oiAABUZXN0TW9kdWxlLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Z6umcfnQVPDcNZf7f5XrfAoJAABBc3BOZXRSZXNvdXJjZU5hbWVIZWxwZXIuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Z6umcfnQVPDcNZf7f5XrfC0VAABBc3BOZXRSZXNvdXJjZU5hbWVIZWxwZXIuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Z6umcfnQVPDcNZf7f5XrfHcKAABBc3BOZXRSZXNvdXJjZU5hbWVIZWxwZXIuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Z6umcfnQVPDcNZf7f5XrfKELAABBc3BOZXRSZXNvdXJjZU5hbWVIZWxwZXIuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "Z6umcfnQVPDcNZf7f5XrfLsMAABBc3BOZXRSZXNvdXJjZU5hbWVIZWxwZXIuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "e7gyPMkOayZqFIAXKR94nLILAABKZW5raW5zRW52aXJvbm1lbnRWYWx1ZXMuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "exGEs9ridN0/ZxN7ioHQAvEJAABTcGFuVGFnSGVscGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "fN46LIOt/B2o9F7AGWy5OCgfAABJbW11dGFibGVBenVyZUFwcFNlcnZpY2VTZXR0aW5ncy5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "gJuugukX4hvZ6Y6RY0UZxN1eAABVVEY4U3RyaW5nLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "h9yGJpAmj+8mQiHnah0Y76S4AgBDb3ZlcmFnZUJhY2tmaWxsQ29tbWFuZExpbmUuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "h9yGJpAmj+8mQiHnah0Y7xGqAgBDb3ZlcmFnZUJhY2tmaWxsQ29tbWFuZExpbmUuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "i7bgnLok3Shk2SlWNgLshagOAABUcmFjaW5nSHR0cE1vZHVsZS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "iBKAREuc2i82MCdwM7dZIwAMAABCdWlsZGtpdGVFbnZpcm9ubWVudFZhbHVlcy5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "irWrBKTq2BS6B29TelWX1UpWAABTZWN1cml0eUNvb3JkaW5hdG9yLkZyYW1ld29yay5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "jTXQk45PM0ipXipYeZT5ZW0MAABHaXRDb21tYW5kSGVscGVyLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "kc/PoWKlv9PP8yQdq8iCNksjAABTZWN1cml0eUNvb3JkaW5hdG9yLmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "kdP6AMKbtCNH5jUpf8SuUZFdAABPdGxwSGVscGVycy5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "mbJO1vFdnT7PHdGXFscogXMzAABSZXR1cm5lZEhlYWRlcnNBbmFseXplci5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "nMhsK/k4IlOmEcwxi5L/JgMIAABUZXN0LmNz")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "qktOXd9oXedvcH92E6ZicNATAABJYXN0U2V0dGluZ3MuY3M=")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "yTZ0ce2KpZ8YGjJzeRfcTTosAABEYlNjb3BlRmFjdG9yeS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "yTZ0ce2KpZ8YGjJzeRfcTXIrAABEYlNjb3BlRmFjdG9yeS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "yTZ0ce2KpZ8YGjJzeRfcTXwsAABEYlNjb3BlRmFjdG9yeS5jcw==")]
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "yTZ0ce2KpZ8YGjJzeRfcTe8qAABEYlNjb3BlRmFjdG9yeS5jcw==")]
+ public static string ToLowerInvariant(this string value)
+ => global::System.StringUtil.ToLowerInvariant(value);
+
+ }
+}
\ No newline at end of file
diff --git a/tracer/src/Datadog.Trace/Iast/Aspects/System/StringAspects.cs b/tracer/src/Datadog.Trace/Iast/Aspects/System/StringAspects.cs
index e038ce536c79..5851c0be3819 100644
--- a/tracer/src/Datadog.Trace/Iast/Aspects/System/StringAspects.cs
+++ b/tracer/src/Datadog.Trace/Iast/Aspects/System/StringAspects.cs
@@ -12,6 +12,7 @@
using Datadog.Trace.Iast.Helpers;
using Datadog.Trace.Iast.Propagation;
using Datadog.Trace.Logging;
+using Datadog.Trace.Util;
using static Datadog.Trace.Iast.Propagation.StringModuleImpl;
namespace Datadog.Trace.Iast.Aspects.System;
@@ -967,6 +968,7 @@ public static string ToUpper(string target, global::System.Globalization.Culture
/// the target string
/// ToUpperInvariant result
[AspectMethodReplace("System.String::ToUpperInvariant()", AspectFilter.StringLiteral_0)]
+ [SkipStringCaseInterception] // reproduces the instrumented customer call site exactly - see the attribute's remarks
public static string ToUpperInvariant(string target)
{
var result = target.ToUpperInvariant();
@@ -1031,6 +1033,7 @@ public static string ToLower(string target, global::System.Globalization.Culture
/// the target string
/// ToLowerInvariant result
[AspectMethodReplace("System.String::ToLowerInvariant()", AspectFilter.StringLiteral_0)]
+ [SkipStringCaseInterception] // reproduces the instrumented customer call site exactly - see the attribute's remarks
public static string ToLowerInvariant(string target)
{
var result = target.ToLowerInvariant();
diff --git a/tracer/src/Datadog.Trace/Util/SkipStringCaseInterceptionAttribute.cs b/tracer/src/Datadog.Trace/Util/SkipStringCaseInterceptionAttribute.cs
new file mode 100644
index 000000000000..6e730602c0d9
--- /dev/null
+++ b/tracer/src/Datadog.Trace/Util/SkipStringCaseInterceptionAttribute.cs
@@ -0,0 +1,26 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using System;
+
+namespace Datadog.Trace.Util;
+
+///
+/// Opts a method, or every method in a type, out of the StringCaseInterception source generator - the
+/// generator that, on .NET Framework only, rewrites /
+/// call sites in this compilation to call
+/// instead, avoiding the allocation those methods otherwise always incur on that TFM.
+///
+///
+/// Known gap: applying this to a method does not cover call sites inside a lambda or local function
+/// declared within that method - the generator resolves the enclosing symbol for those to the lambda/local
+/// function itself, not the attributed outer method.
+///
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
+internal sealed class SkipStringCaseInterceptionAttribute : Attribute
+{
+}
diff --git a/tracer/test/Datadog.Trace.SourceGenerators.Tests/StringCaseInterceptorGeneratorTests.cs b/tracer/test/Datadog.Trace.SourceGenerators.Tests/StringCaseInterceptorGeneratorTests.cs
new file mode 100644
index 000000000000..2e65bfe10f48
--- /dev/null
+++ b/tracer/test/Datadog.Trace.SourceGenerators.Tests/StringCaseInterceptorGeneratorTests.cs
@@ -0,0 +1,228 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+using System.Text.RegularExpressions;
+using Datadog.Trace.SourceGenerators.Helpers;
+using Datadog.Trace.SourceGenerators.StringCaseInterception;
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Xunit;
+
+namespace Datadog.Trace.SourceGenerators.Tests;
+
+public class StringCaseInterceptorGeneratorTests
+{
+ private static readonly Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider NetFrameworkOptions =
+ TestHelpers.CreateOptionsProvider(("build_property.TargetFrameworkIdentifier", ".NETFramework"));
+
+ private static readonly Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider NetCoreOptions =
+ TestHelpers.CreateOptionsProvider(("build_property.TargetFrameworkIdentifier", ".NETCoreApp"));
+
+ private static readonly string[] NetFrameworkPreprocessorSymbols = ["NETFRAMEWORK"];
+
+ [Fact]
+ public void InterceptsStringCasingCallsOnNetFramework()
+ {
+ const string input = """
+ public class MyClass
+ {
+ public void DoWork(string value)
+ {
+ var upper = value.ToUpperInvariant();
+ var lower = value.ToLowerInvariant();
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetFrameworkOptions, preprocessorSymbols: NetFrameworkPreprocessorSymbols);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().HaveCount(1);
+
+ var source = output[0];
+ source.Should().Contain("Datadog.Trace.Generated.Interceptors");
+ source.Should().Contain("StringUtil.ToUpperInvariant");
+ source.Should().Contain("StringUtil.ToLowerInvariant");
+ CountInterceptsLocationAttributeUsages(source).Should().Be(2);
+ }
+
+ [Fact]
+ public void InterceptsConditionalAccessCasingCalls()
+ {
+ const string input = """
+ public class MyClass
+ {
+ public void DoWork(string value)
+ {
+ var upper = value?.ToUpperInvariant();
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetFrameworkOptions, preprocessorSymbols: NetFrameworkPreprocessorSymbols);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().HaveCount(1);
+ CountInterceptsLocationAttributeUsages(output[0]).Should().Be(1);
+ }
+
+ [Fact]
+ public void DoesNotEmitOnNonNetFrameworkTfm()
+ {
+ const string input = """
+ public class MyClass
+ {
+ public void DoWork(string value)
+ {
+ var upper = value.ToUpperInvariant();
+ var lower = value.ToLowerInvariant();
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetCoreOptions);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void IgnoresNonStringToUpperInvariant()
+ {
+ const string input = """
+ public class Foo
+ {
+ public string ToUpperInvariant() => "x";
+ }
+
+ public class MyClass
+ {
+ public void DoWork()
+ {
+ var result = new Foo().ToUpperInvariant();
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetFrameworkOptions, preprocessorSymbols: NetFrameworkPreprocessorSymbols);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void SkipsCallSitesMarkedWithSkipAttributeOnMethod()
+ {
+ const string input = """
+ namespace Datadog.Trace.Util
+ {
+ internal sealed class SkipStringCaseInterceptionAttribute : System.Attribute
+ {
+ }
+ }
+
+ public class MyClass
+ {
+ [Datadog.Trace.Util.SkipStringCaseInterception]
+ public void DoWork(string value)
+ {
+ var upper = value.ToUpperInvariant();
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetFrameworkOptions, preprocessorSymbols: NetFrameworkPreprocessorSymbols);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void SkipsCallSitesMarkedWithSkipAttributeOnContainingType()
+ {
+ const string input = """
+ namespace Datadog.Trace.Util
+ {
+ internal sealed class SkipStringCaseInterceptionAttribute : System.Attribute
+ {
+ }
+ }
+
+ [Datadog.Trace.Util.SkipStringCaseInterception]
+ public class MyClass
+ {
+ public void DoWork(string value)
+ {
+ var upper = value.ToUpperInvariant();
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetFrameworkOptions, preprocessorSymbols: NetFrameworkPreprocessorSymbols);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void SkipsSelfRecursiveCallsInsideTheHelperItself()
+ {
+ // Belt-and-braces guard: even without the [SkipStringCaseInterception] attribute, a call inside
+ // the type the interceptor delegates to must never be intercepted, or it would recurse forever.
+ // The real StringUtil lives in namespace System (see StringUtil.cs), which is what the generator's
+ // guard matches against, so the mock below must too.
+ const string input = """
+ namespace System
+ {
+ internal static class StringUtil
+ {
+ public static string ToUpperInvariant(string value) => value.ToUpperInvariant();
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetFrameworkOptions, preprocessorSymbols: NetFrameworkPreprocessorSymbols);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void DoesNotEmitWhenThereAreNoCallSites()
+ {
+ const string input = """
+ public class MyClass
+ {
+ public void DoWork(string value)
+ {
+ }
+ }
+ """;
+
+ var (diagnostics, output) = TestHelpers.GetGeneratedTrees(
+ new[] { input }, assertOutput: true, additionalFiles: null, optionsProvider: NetFrameworkOptions, preprocessorSymbols: NetFrameworkPreprocessorSymbols);
+
+ using var s = new AssertionScope();
+ diagnostics.Should().BeEmpty();
+ output.Should().BeEmpty();
+ }
+
+ private static int CountInterceptsLocationAttributeUsages(string source)
+ => Regex.Matches(source, Regex.Escape("[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(")).Count;
+}
diff --git a/tracer/test/Datadog.Trace.SourceGenerators.Tests/TestHelpers.cs b/tracer/test/Datadog.Trace.SourceGenerators.Tests/TestHelpers.cs
index 88c2566e6152..a98ab98904a6 100644
--- a/tracer/test/Datadog.Trace.SourceGenerators.Tests/TestHelpers.cs
+++ b/tracer/test/Datadog.Trace.SourceGenerators.Tests/TestHelpers.cs
@@ -14,6 +14,7 @@
using FluentAssertions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace Datadog.Trace.SourceGenerators.Tests
@@ -39,7 +40,7 @@ public static (ImmutableArray Diagnostics, string[] Output) GetGener
where TGenerator : IIncrementalGenerator, new()
=> GetGeneratedTrees(sources, assertOutput: true);
- public static (ImmutableArray Diagnostics, string[] Output) GetGeneratedTrees(string[] sources, bool assertOutput, (string Path, string Content)[] additionalFiles = null)
+ public static (ImmutableArray Diagnostics, string[] Output) GetGeneratedTrees(string[] sources, bool assertOutput, (string Path, string Content)[] additionalFiles = null, AnalyzerConfigOptionsProvider optionsProvider = null, string[] preprocessorSymbols = null)
where TGenerator : IIncrementalGenerator, new()
{
// get all the const string fields
@@ -50,13 +51,16 @@ public static (ImmutableArray Diagnostics, string[] Output) GetGener
.Where(x => !string.IsNullOrEmpty(x))
.ToArray();
- return GetGeneratedTrees(sources, trackingNames, assertOutput: assertOutput, additionalFiles: additionalFiles);
+ return GetGeneratedTrees(sources, trackingNames, additionalFiles: additionalFiles, assertOutput: assertOutput, optionsProvider: optionsProvider, preprocessorSymbols: preprocessorSymbols);
}
- public static (ImmutableArray Diagnostics, string[] Output) GetGeneratedTrees(string[] source, string[] stages, (string Path, string Content)[] additionalFiles = null, bool assertOutput = true)
+ public static (ImmutableArray Diagnostics, string[] Output) GetGeneratedTrees(string[] source, string[] stages, (string Path, string Content)[] additionalFiles = null, bool assertOutput = true, AnalyzerConfigOptionsProvider optionsProvider = null, string[] preprocessorSymbols = null)
where T : IIncrementalGenerator, new()
{
- var syntaxTrees = source.Select(static x => CSharpSyntaxTree.ParseText(x));
+ var parseOptions = preprocessorSymbols is null
+ ? CSharpParseOptions.Default
+ : CSharpParseOptions.Default.WithPreprocessorSymbols(preprocessorSymbols);
+ var syntaxTrees = source.Select(x => CSharpSyntaxTree.ParseText(x, parseOptions));
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location))
.Select(_ => MetadataReference.CreateFromFile(_.Location))
@@ -68,12 +72,20 @@ public static (ImmutableArray Diagnostics, string[] Output) GetGener
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
- GeneratorDriverRunResult runResult = RunGeneratorAndAssertOutput(compilation, stages, additionalFiles ?? Array.Empty<(string, string)>(), assertOutput);
+ GeneratorDriverRunResult runResult = RunGeneratorAndAssertOutput(compilation, stages, additionalFiles ?? Array.Empty<(string, string)>(), assertOutput, optionsProvider);
return (runResult.Diagnostics, runResult.GeneratedTrees.Select(x => x.ToString()).ToArray());
}
- private static GeneratorDriverRunResult RunGeneratorAndAssertOutput(CSharpCompilation compilation, string[] trackingNames, (string Path, string Content)[] additionalFiles = null, bool assertOutput = true)
+ ///
+ /// Builds an exposing the given key/value pairs as
+ /// global options (i.e. AnalyzerConfigOptionsProvider.GlobalOptions), for generators that read
+ /// MSBuild properties exposed via CompilerVisibleProperty (e.g. build_property.TargetFrameworkIdentifier).
+ ///
+ public static AnalyzerConfigOptionsProvider CreateOptionsProvider(params (string Key, string Value)[] globalOptions)
+ => new TestAnalyzerConfigOptionsProvider(new TestAnalyzerConfigOptions(globalOptions));
+
+ private static GeneratorDriverRunResult RunGeneratorAndAssertOutput(CSharpCompilation compilation, string[] trackingNames, (string Path, string Content)[] additionalFiles = null, bool assertOutput = true, AnalyzerConfigOptionsProvider optionsProvider = null)
where T : IIncrementalGenerator, new()
{
ISourceGenerator generator = new T().AsSourceGenerator();
@@ -84,7 +96,7 @@ private static GeneratorDriverRunResult RunGeneratorAndAssertOutput(CSharpCom
var additionalTexts = additionalFiles?.Select(f => (AdditionalText)new TestAdditionalText(f.Path, f.Content)).ToImmutableArray();
- GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], additionalTexts: additionalTexts, driverOptions: opts);
+ GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], additionalTexts: additionalTexts, parseOptions: null, optionsProvider: optionsProvider, driverOptions: opts);
var clone = compilation.Clone();
// Run twice, once with a clone of the compilation
@@ -250,5 +262,31 @@ public override SourceText GetText(CancellationToken cancellationToken = default
return SourceText.From(_text);
}
}
+
+ private class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
+ {
+ public TestAnalyzerConfigOptionsProvider(AnalyzerConfigOptions globalOptions)
+ {
+ GlobalOptions = globalOptions;
+ }
+
+ public override AnalyzerConfigOptions GlobalOptions { get; }
+
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GlobalOptions;
+
+ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => GlobalOptions;
+ }
+
+ private class TestAnalyzerConfigOptions : AnalyzerConfigOptions
+ {
+ private readonly Dictionary _values;
+
+ public TestAnalyzerConfigOptions((string Key, string Value)[] values)
+ {
+ _values = values.ToDictionary(x => x.Key, x => x.Value);
+ }
+
+ public override bool TryGetValue(string key, out string value) => _values.TryGetValue(key, out value);
+ }
}
}
diff --git a/tracer/test/Datadog.Trace.Tests/Util/StringCaseInterceptionTests.cs b/tracer/test/Datadog.Trace.Tests/Util/StringCaseInterceptionTests.cs
new file mode 100644
index 000000000000..f594a77cb6bb
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/Util/StringCaseInterceptionTests.cs
@@ -0,0 +1,35 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+using System.Linq;
+using System.Reflection;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests.Util;
+
+public class StringCaseInterceptionTests
+{
+ [Fact]
+ public void InterceptorTypeExistsOnlyOnNetFramework()
+ {
+ var type = typeof(Tracer).Assembly.GetType("Datadog.Trace.Generated.Interceptors.StringCaseInterceptors");
+
+#if NETFRAMEWORK
+ type.Should().NotBeNull();
+
+ // The generated InterceptsLocationAttribute is `file`-scoped, so the compiler mangles its
+ // metadata name (e.g. "XXX__InterceptsLocationAttribute") - match
+ // on the suffix rather than the exact name.
+ type!.GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .SelectMany(m => m.GetCustomAttributesData())
+ .Count(a => a.AttributeType.Name.EndsWith("InterceptsLocationAttribute"))
+ .Should()
+ .BeGreaterThan(50, "the generator should have intercepted every ToUpperInvariant()/ToLowerInvariant() call site in Datadog.Trace");
+#else
+ type.Should().BeNull("the interceptor is only ever generated for the .NET Framework build");
+#endif
+ }
+}