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 + } +}