Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <copyright file="CallSite.cs" company="Datadog">
// 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.
// </copyright>

namespace Datadog.Trace.SourceGenerators.StringCaseInterception;

/// <summary>
/// A single <see cref="string.ToUpperInvariant()"/>/<see cref="string.ToLowerInvariant()"/> call site to intercept.
/// </summary>
internal sealed record CallSite
{
public CallSite(string methodName, string interceptsLocationAttribute)
{
MethodName = methodName;
InterceptsLocationAttribute = interceptsLocationAttribute;
}

/// <summary>
/// Gets either <c>ToUpperInvariant</c> or <c>ToLowerInvariant</c>.
/// </summary>
public string MethodName { get; }

/// <summary>
/// Gets the full <c>[InterceptsLocation(...)]</c> attribute syntax for this call site, as returned by
/// <see cref="Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetInterceptsLocationAttributeSyntax"/>.
/// </summary>
public string InterceptsLocationAttribute { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// <copyright file="Sources.cs" company="Datadog">
// 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.
// </copyright>

using System.Collections.Generic;
using System.Text;

namespace Datadog.Trace.SourceGenerators.StringCaseInterception;

internal static class Sources
{
public static string GenerateInterceptors(IReadOnlyList<string> upperAttributes, IReadOnlyList<string> 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<string> 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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// <copyright file="StringCaseInterceptorGenerator.cs" company="Datadog">
// 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.
// </copyright>

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;

/// <summary>
/// On .NET Framework only, intercepts every <see cref="string.ToUpperInvariant()"/>/
/// <see cref="string.ToLowerInvariant()"/> 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).
/// </summary>
[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";

/// <inheritdoc />
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<CallSite> 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<CallSite> 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());
}

/// <summary>
/// Opts a call site out when the containing method/type carries <see cref="SkipAttributeFullName"/>,
/// or when the call is inside the helper or the interceptor stub themselves
/// </summary>
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<CallSite> callSites, bool isNetFramework, SourceProductionContext context)
{
if (!isNetFramework || callSites.IsDefaultOrEmpty)
{
return;
}

var upper = new List<string>();
var lower = new List<string>();

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));
}
}
9 changes: 9 additions & 0 deletions tracer/src/Datadog.Trace/Datadog.Trace.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@
<ProjectReference Include="..\Datadog.Trace.SourceGenerators\Datadog.Trace.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<!-- Required by StringCaseInterception -->
<CompilerVisibleProperty Include="TargetFrameworkIdentifier" />
</ItemGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
<InterceptorsNamespaces>$(InterceptorsNamespaces);Datadog.Trace.Generated.Interceptors</InterceptorsNamespaces>
</PropertyGroup>

<ItemGroup Condition=" $(TargetFramework.StartsWith('net4')) ">
<Reference Include="System.Configuration" />
<Reference Include="System.Web" />
Expand Down
Loading
Loading