Skip to content
Merged
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
37 changes: 3 additions & 34 deletions src/Stunts.CodeAnalysis/PointerMemberAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
Expand All @@ -9,9 +8,8 @@
namespace Stunts
{
/// <summary>
/// Analyzes source code looking for method invocations to methods annotated with
/// the <see cref="StuntGeneratorAttribute"/> and reports unsupported pointer types
/// in method arguments for codegen.
/// Previously reported <see cref="StuntDiagnostics.PointerMember"/> for pointer signatures.
/// Those signatures are now generated, so the analyzer no longer reports.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class PointerMemberAnalyzer : DiagnosticAnalyzer
Expand Down Expand Up @@ -48,35 +46,6 @@ public override void Initialize(AnalysisContext context)
}

void AnalyzeOperation(OperationAnalysisContext context)
{
var invocation = (IInvocationOperation)context.Operation;

// Get the matching symbol for the given generator attribute from the current compilation.
var generator = context.Compilation.GetTypeByMetadataName(generatorAttribute.FullName);
if (generator == null)
return;

if (invocation.TargetMethod.GetAttributes().Any(x =>
SymbolEqualityComparer.Default.Equals(x.AttributeClass, generator)))
{
var args = invocation.TargetMethod.TypeArguments
.OfType<INamedTypeSymbol>()
.Where(t => t.GetMembers()
.OfType<IMethodSymbol>()
.SelectMany(method => method.Parameters)
.Any(parameter => parameter.Type.Kind == SymbolKind.PointerType))
.ToArray();

if (args.Length > 0)
{
var diagnostic = Diagnostic.Create(
StuntDiagnostics.PointerMember,
invocation.Syntax.GetLocation(),
string.Join(", ", args.Select(t => t.Name)));

context.ReportDiagnostic(diagnostic);
}
}
}
=> _ = context.Compilation.GetTypeByMetadataName(generatorAttribute.FullName);
}
}
5 changes: 0 additions & 5 deletions src/Stunts.StaticProxy/DefaultImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ namespace Stunts
/// </summary>
static class DefaultImplementation
{
/// <summary>
/// Annotation on the scaffolded expression that invokes the default implementation.
/// </summary>
public const string Annotation = "Stunts.DefaultImplementation";

/// <summary>
/// Name of the static property that exposes the instance typed as the interface.
/// </summary>
Expand Down
19 changes: 19 additions & 0 deletions src/Stunts.StaticProxy/Processors/Annotations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.CodeAnalysis;

namespace Stunts.Processors
{
/// <summary>
/// Annotations the scaffold leaves on syntax nodes for a later rewrite pass.
/// </summary>
static class Annotations
{
/// <summary>A <c>ref struct</c> parameter or return.</summary>
public static readonly SyntaxAnnotation StructRef = new("Stunts.StructRef");

/// <summary>A pointer or function-pointer parameter or return.</summary>
public static readonly SyntaxAnnotation PointerRef = new("Stunts.PointerRef");

/// <summary>A call that proceeds to a default interface implementation.</summary>
public static readonly SyntaxAnnotation DefaultImplementation = new("Stunts.DefaultImplementation");
}
}
287 changes: 277 additions & 10 deletions src/Stunts.StaticProxy/Processors/CSharpRewrite.cs

Large diffs are not rendered by default.

94 changes: 63 additions & 31 deletions src/Stunts.StaticProxy/Processors/MemberScaffold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ static MemberDeclarationSyntax Stub(ISymbol member, bool isOverride, INamedTypeS

static MethodDeclarationSyntax Method(IMethodSymbol method, bool isOverride, INamedTypeSymbol? explicitInterface, ExpressionSyntax? receiver)
{
var declaration = MethodDeclaration(TypeName(method.ReturnType), method.Name)
var declaration = MethodDeclaration(ReturnType(method.ReturnType, method), method.Name)
.WithModifiers(Modifiers(method, isOverride, explicitInterface != null))
.WithParameterList(ParameterList(SeparatedList(method.Parameters.Select(Parameter))))
.WithExpressionBody(ArrowExpressionClause(Body(method, receiver)))
Expand All @@ -181,31 +181,28 @@ static MethodDeclarationSyntax Method(IMethodSymbol method, bool isOverride, INa
.WithConstraintClauses(Constraints(method));
}

if (method.ReturnsByRef || method.ReturnsByRefReadonly)
declaration = declaration.WithReturnType(RefType(declaration.ReturnType));

return declaration;
return Annotate(declaration, method.ReturnType);
}

static PropertyDeclarationSyntax Property(IPropertySymbol property, bool isOverride, INamedTypeSymbol? explicitInterface, ExpressionSyntax? receiver, INamedTypeSymbol? stunt)
{
var declaration = PropertyDeclaration(TypeName(property.Type), property.Name)
var declaration = PropertyDeclaration(ReturnType(property.Type, property.GetMethod), property.Name)
.WithModifiers(Modifiers(property, isOverride, explicitInterface != null))
.WithAccessorList(AccessorList(List(Accessors(property, receiver, stunt))));
return explicitInterface == null
? declaration
: declaration.WithExplicitInterfaceSpecifier(ExplicitInterfaceSpecifier(ParseName(explicitInterface.ToDisplayString(TypeFormat))));
if (explicitInterface != null)
declaration = declaration.WithExplicitInterfaceSpecifier(ExplicitInterfaceSpecifier(ParseName(explicitInterface.ToDisplayString(TypeFormat))));
return Annotate(declaration, property.Type);
}

static IndexerDeclarationSyntax Indexer(IPropertySymbol property, bool isOverride, INamedTypeSymbol? explicitInterface, ExpressionSyntax? receiver, INamedTypeSymbol? stunt)
{
var declaration = IndexerDeclaration(TypeName(property.Type))
var declaration = IndexerDeclaration(ReturnType(property.Type, property.GetMethod))
.WithModifiers(Modifiers(property, isOverride, explicitInterface != null))
.WithParameterList(BracketedParameterList(SeparatedList(property.Parameters.Select(Parameter))))
.WithAccessorList(AccessorList(List(Accessors(property, receiver, stunt))));
return explicitInterface == null
? declaration
: declaration.WithExplicitInterfaceSpecifier(ExplicitInterfaceSpecifier(ParseName(explicitInterface.ToDisplayString(TypeFormat))));
if (explicitInterface != null)
declaration = declaration.WithExplicitInterfaceSpecifier(ExplicitInterfaceSpecifier(ParseName(explicitInterface.ToDisplayString(TypeFormat))));
return Annotate(declaration, property.Type);
}

static EventDeclarationSyntax Event(IEventSymbol ev, bool isOverride, INamedTypeSymbol? explicitInterface, ExpressionSyntax? defaultInstance)
Expand Down Expand Up @@ -363,40 +360,75 @@ static ExpressionSyntax Body(IMethodSymbol method, ExpressionSyntax? receiver)
// implementation are flagged with an annotation instead.
static TExpression Proceed<TExpression>(ExpressionSyntax receiver, TExpression expression) where TExpression : ExpressionSyntax
=> receiver is BaseExpressionSyntax ? expression :
expression.WithAdditionalAnnotations(new SyntaxAnnotation(DefaultImplementation.Annotation));
expression.WithAdditionalAnnotations(Annotations.DefaultImplementation);

static ElementAccessExpressionSyntax ElementAccess(ExpressionSyntax receiver, IPropertySymbol property)
=> ElementAccessExpression(
receiver,
BracketedArgumentList(SeparatedList(property.Parameters.Select(ArgumentFor))));

static ParameterSyntax Parameter(IParameterSymbol parameter)
static TypeSyntax ReturnType(ITypeSymbol type, IMethodSymbol? accessor)
{
var syntax = SyntaxFactory.Parameter(Identifier(parameter.Name)).WithType(TypeName(parameter.Type));
var kind = RefKind(parameter.RefKind);
if (kind != null)
syntax = syntax.WithModifiers(TokenList(Token(kind.Value)));
if (parameter.IsParams)
syntax = syntax.AddModifiers(Token(SyntaxKind.ParamsKeyword));
var syntax = TypeName(type);
if (accessor == null)
return syntax;
// RefKind.RefReadOnly and RefKind.In share the same value, so use the bools.
if (accessor.ReturnsByRefReadonly)
return RefType(syntax).WithReadOnlyKeyword(Token(SyntaxKind.ReadOnlyKeyword));
if (accessor.ReturnsByRef)
return RefType(syntax);
return syntax;
}

static ArgumentSyntax ArgumentFor(IParameterSymbol parameter)
static TNode Annotate<TNode>(TNode node, ITypeSymbol type) where TNode : SyntaxNode
{
var argument = Argument(IdentifierName(parameter.Name));
var kind = RefKind(parameter.RefKind);
return kind == null ? argument : argument.WithRefKindKeyword(Token(kind.Value));
if (type.IsRefLikeType)
return (TNode)node.WithAdditionalAnnotations(Annotations.StructRef);
if (type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer)
return (TNode)node.WithAdditionalAnnotations(Annotations.PointerRef);
return node;
}

static SyntaxKind? RefKind(RefKind kind)
static ParameterSyntax Parameter(IParameterSymbol parameter)
{
switch (kind)
var syntax = SyntaxFactory.Parameter(Identifier(parameter.Name)).WithType(TypeName(parameter.Type));
var modifiers = new List<SyntaxToken>();
switch (parameter.RefKind)
{
case Microsoft.CodeAnalysis.RefKind.Ref: return SyntaxKind.RefKeyword;
case Microsoft.CodeAnalysis.RefKind.Out: return SyntaxKind.OutKeyword;
case Microsoft.CodeAnalysis.RefKind.In: return SyntaxKind.InKeyword;
default: return null;
case Microsoft.CodeAnalysis.RefKind.Ref:
modifiers.Add(Token(SyntaxKind.RefKeyword));
break;
case Microsoft.CodeAnalysis.RefKind.Out:
modifiers.Add(Token(SyntaxKind.OutKeyword));
break;
case Microsoft.CodeAnalysis.RefKind.In:
modifiers.Add(Token(SyntaxKind.InKeyword));
break;
case Microsoft.CodeAnalysis.RefKind.RefReadOnlyParameter:
modifiers.Add(Token(SyntaxKind.RefKeyword));
modifiers.Add(Token(SyntaxKind.ReadOnlyKeyword));
break;
}

if (parameter.IsParams)
modifiers.Add(Token(SyntaxKind.ParamsKeyword));
if (modifiers.Count > 0)
syntax = syntax.WithModifiers(TokenList(modifiers));
return Annotate(syntax, parameter.Type);
}

static ArgumentSyntax ArgumentFor(IParameterSymbol parameter)
{
var argument = Argument(IdentifierName(parameter.Name));
SyntaxKind? kind = parameter.RefKind switch
{
Microsoft.CodeAnalysis.RefKind.Ref => SyntaxKind.RefKeyword,
Microsoft.CodeAnalysis.RefKind.Out => SyntaxKind.OutKeyword,
Microsoft.CodeAnalysis.RefKind.In => SyntaxKind.InKeyword,
Microsoft.CodeAnalysis.RefKind.RefReadOnlyParameter => SyntaxKind.InKeyword,
_ => null,
};
return kind == null ? argument : argument.WithRefKindKeyword(Token(kind.Value));
}

internal static TypeSyntax TypeName(ITypeSymbol type) => ParseTypeName(type.ToDisplayString(TypeFormat));
Expand Down
37 changes: 37 additions & 0 deletions src/Stunts.StaticProxy/SignatureRef.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma warning disable CS8500
using System;
using System.Runtime.CompilerServices;

namespace Stunts
{
/// <summary>Refers to a live <typeparamref name="T"/> ref struct.</summary>
public unsafe class StructRef<T> : StructRef
where T : allows ref struct
{
/// <summary>Refers to <paramref name="value"/>.</summary>
public StructRef(ref T value)
: base(typeof(T), Unsafe.AsPointer(ref value))
{
}

/// <summary>The ref struct this ref points at.</summary>
public ref T Value => ref Unsafe.AsRef<T>(GetPtr(typeof(T)));

/// <summary>Drops the address when it still points at <paramref name="value"/>.</summary>
public void Invalidate(ref T value) => Invalidate(Unsafe.AsPointer(ref value));
}

/// <summary>Refers to a live <see cref="Span{T}"/>.</summary>
public class SpanRef<T> : StructRef<Span<T>>
{
/// <summary>Refers to <paramref name="value"/>.</summary>
public SpanRef(ref Span<T> value) : base(ref value) { }
}

/// <summary>Refers to a live <see cref="ReadOnlySpan{T}"/>.</summary>
public class ReadOnlySpanRef<T> : StructRef<ReadOnlySpan<T>>
{
/// <summary>Refers to <paramref name="value"/>.</summary>
public ReadOnlySpanRef(ref ReadOnlySpan<T> value) : base(ref value) { }
}
}
42 changes: 29 additions & 13 deletions src/Stunts.StaticProxy/StuntGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ void OnExecute(ProcessorContext context, NamingConvention naming)
var factory = StuntSyntaxFactory.CreateFactory(context.Language);
var stunts = new HashSet<string>();
var defaults = new HashSet<string>();
var needsSignatureRef = false;

foreach (var (source, candidate) in context.SyntaxReceivers
.OfType<IStuntCandidatesReceiver>()
Expand All @@ -193,6 +194,9 @@ void OnExecute(ProcessorContext context, NamingConvention naming)
NestedTypeStunt.NonPartialContainer(nested) != null)
continue;

if (candidate.Any(UsesSignatureRef))
needsSignatureRef = true;

var syntax = factory.CreateSyntax(naming, candidate);
var stuntContext = context with { DefaultImplementations = new(SymbolEqualityComparer.Default) };
var updated = driver.Process(syntax, stuntContext);
Expand Down Expand Up @@ -222,8 +226,32 @@ void OnExecute(ProcessorContext context, NamingConvention naming)
DefaultImplementation.Driver.Process(DefaultImplementation.CreateSyntax(naming, iface), context));
}
}

if (needsSignatureRef)
context.AddSource("SignatureRef.cs", SourceText.From(ThisAssembly.Resources.SignatureRef.Text, Encoding.UTF8));
}

static bool UsesSignatureRef(INamedTypeSymbol type)
{
foreach (var member in type.GetMembers())
{
switch (member)
{
case IMethodSymbol method when IsHeld(method.ReturnType) || method.Parameters.Any(parameter => IsHeld(parameter.Type)):
case IPropertySymbol property when IsHeld(property.Type) || property.Parameters.Any(parameter => IsHeld(parameter.Type)):
return true;
}
}

if (type.BaseType != null && type.BaseType.SpecialType != SpecialType.System_Object && UsesSignatureRef(type.BaseType))
return true;

return type.AllInterfaces.Any(UsesSignatureRef);
}

static bool IsHeld(ITypeSymbol type)
=> type.IsRefLikeType || type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer;

static void AddSource(ProcessorContext context, string name, SyntaxNode updated)
{
var code = updated.NormalizeWhitespace().ToFullString();
Expand Down Expand Up @@ -330,19 +358,7 @@ class CSharpFormatter : CSharpSyntaxRewriter
}
}

static bool CanGenerateFor(INamedTypeSymbol? symbol)
{
if (symbol == null)
return false;

// Cannot generate for types using pointer types
var usesPointers = symbol.GetMembers()
.OfType<IMethodSymbol>()
.SelectMany(method => method.Parameters)
.Any(parameter => parameter.Type.Kind == SymbolKind.PointerType);

return !usesPointers;
}
static bool CanGenerateFor(INamedTypeSymbol? symbol) => symbol != null;

class AggregateSyntaxReceiver : ISyntaxReceiver, IEnumerable
{
Expand Down
6 changes: 6 additions & 0 deletions src/Stunts.StaticProxy/Stunts.StaticProxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,10 @@

<Import Project="Stunts.StaticProxy.targets"/>

<ItemGroup>
<!-- Emitted into compilations that proxy a ref struct or pointer. netstandard2.0 cannot compile it. -->
<Compile Remove="SignatureRef.cs" />
<EmbeddedResource Include="SignatureRef.cs" />
</ItemGroup>

</Project>
12 changes: 7 additions & 5 deletions src/Stunts.UnitTests/.Scenarios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,11 @@ static Scenarios()
d.Severity == DiagnosticSeverity.Hidden ||
d.Severity == DiagnosticSeverity.Info;

var diagnostics = compilation.GetDiagnostics().RemoveAll(ignored);
if (diagnostics.Any())
return (diagnostics, compilation);
// Warnings in the scenario itself still fail here. Errors may name types
// the generator emits, so those are checked on the compilation after generation.
var before = compilation.GetDiagnostics().RemoveAll(ignored);
if (before.Any(d => d.Severity != DiagnosticSeverity.Error))
return (before, compilation);

GeneratorDriver driver = CSharpGeneratorDriver.Create(
new[] { new StuntGenerator() },
Expand All @@ -186,8 +188,8 @@ static Scenarios()
// Don't timeout if we're debugging.
var token = Debugger.IsAttached ? default : new CancellationTokenSource(5000).Token;

driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out diagnostics, token);
diagnostics = diagnostics.RemoveAll(ignored);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out var diagnostics, token);
diagnostics = output.GetDiagnostics().RemoveAll(ignored).RemoveAll(d => d.Severity != DiagnosticSeverity.Error);

if (bool.TryParse(ThisAssembly.Project.EmitCompilerGeneratedFiles, out var emit) && emit)
{
Expand Down
Loading
Loading