Skip to content

Commit

Permalink
Add Diagnostic for Unsupported Version
Browse files Browse the repository at this point in the history
Make Create follow same pattern
  • Loading branch information
justindbaur committed Nov 27, 2023
1 parent d01a3a1 commit 73ba385
Show file tree
Hide file tree
Showing 18 changed files with 475 additions and 280 deletions.
4 changes: 1 addition & 3 deletions src/Pretender.SourceGenerator/CreateEntrypoint.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -86,7 +85,6 @@ public MethodDeclarationSyntax GetMethodDeclaration(int index)

public class CreateEntryPointComparer : IEqualityComparer<CreateEntrypoint>
{
public static CreateEntryPointComparer Instance = new();

bool IEqualityComparer<CreateEntrypoint>.Equals(CreateEntrypoint x, CreateEntrypoint y)
{
Expand Down
131 changes: 131 additions & 0 deletions src/Pretender.SourceGenerator/CreateInvocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using Pretender.SourceGenerator.Parser;

namespace Pretender.SourceGenerator
{
internal class CreateInvocation
{
public CreateInvocation(IInvocationOperation operation, ImmutableArray<ITypeSymbol>? typeArguments, InterceptsLocationInfo location)
{
Operation = operation;
TypeArguments = typeArguments;
Location = location;
}

public IInvocationOperation Operation { get; }
public ImmutableArray<ITypeSymbol>? TypeArguments { get; }
public InterceptsLocationInfo Location { get; }

public static bool IsCandidateSyntaxNode(SyntaxNode node)
{
return node is InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax
{
Name.Identifier.ValueText: "Create"
},
};
}

public static CreateInvocation? Create(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
Debug.Assert(IsCandidateSyntaxNode(context.Node));
return context.SemanticModel.GetOperation(context.Node, cancellationToken) is IInvocationOperation operation
&& IsCreateOperation(operation, out var typeArguments)
? new CreateInvocation(operation, typeArguments,new InterceptsLocationInfo(operation))
: null;
}

private static bool IsCreateOperation(IInvocationOperation operation, out ImmutableArray<ITypeSymbol>? typeArguments)
{
typeArguments = null;
if (operation.Instance is null
|| operation.Instance.Type is not INamedTypeSymbol namedType
|| !KnownTypeSymbols.IsPretend(namedType))
{
return false;
}

// Are they in the params overload?
if (operation.TargetMethod.Parameters.Length == 1
&& operation.TargetMethod.Parameters[0].IsParams)
{
return true;
}

typeArguments = operation.TargetMethod.TypeArguments;
return true;
}
}

public class CreateInvocationComparer : IEqualityComparer<CreateInvocation>
{
public static CreateInvocationComparer Instance = new();
bool IEqualityComparer<CreateInvocation>.Equals(CreateInvocation x, CreateInvocation y)
{
return SymbolEqualityComparer.Default.Equals(x.Operation.TargetMethod.ReturnType, y.Operation.TargetMethod.ReturnType)
&& CompareTypeArguments(x.TypeArguments, y.TypeArguments);
}

private static bool CompareTypeArguments(ImmutableArray<ITypeSymbol>? x, ImmutableArray<ITypeSymbol>? y)
{
if (!x.HasValue)
{
return !y.HasValue;
}

if (!y.HasValue)
{
// We've established x does have a value so y not having one is a non-match
return false;
}

var xArray = x.Value;
var yArray = y.Value;

if (xArray.Length != yArray.Length)
{
return false;
}

for (int i = 0; i < xArray.Length; i++)
{
if (!SymbolEqualityComparer.IncludeNullability.Equals(xArray[i], yArray[i]))
{
return false;
}
}

return true;
}

int IEqualityComparer<CreateInvocation>.GetHashCode(CreateInvocation obj)
{
unchecked
{
var hash = 17;
hash = hash * 31 + SymbolEqualityComparer.Default.GetHashCode(obj.Operation.TargetMethod.ReturnType);

if (!obj.TypeArguments.HasValue)
{
// TODO: Is 5 an okay value?
hash = hash * 31 + 5;
}
else
{
var typeArguments = obj.TypeArguments.Value;
foreach (var typeArgument in typeArguments)
{
hash = hash * 31 + SymbolEqualityComparer.Default.GetHashCode(typeArgument);
}
}

return hash;
}
}
}
}
12 changes: 10 additions & 2 deletions src/Pretender.SourceGenerator/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ namespace Pretender.SourceGenerator
{
internal static class DiagnosticDescriptors
{
public static DiagnosticDescriptor UnsupportedLanguageVersion { get; } = new (
"PRTND001",
"Unsupported language version",
"",
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor UnableToPretendSealedType { get; } = new(
"PRTD001",
"PRTND002",
"Unable to Pretend Sealed Types",
"Sealed types cannot be Pretended, did you mean to use an interface?",
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor InvalidSetupArgument { get; } = new(
"PRTD002",
"PRTND003",
"Invalid Setup Argument",
"We don't support operation type {0} as a setup argument.",
"Usage",
Expand Down
108 changes: 108 additions & 0 deletions src/Pretender.SourceGenerator/Emitter/CreateEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;

namespace Pretender.SourceGenerator.Emitter
{
internal class CreateEmitter
{
private readonly IInvocationOperation _originalOperation;
private readonly ImmutableArray<ITypeSymbol>? _typeArguments;
private readonly ImmutableArray<InterceptsLocationInfo> _locations;
private readonly int _index;

public CreateEmitter(IInvocationOperation originalOperation, ImmutableArray<ITypeSymbol>? typeArguments, ImmutableArray<InterceptsLocationInfo> locations, int index)
{
_originalOperation = originalOperation;
_typeArguments = typeArguments;
_locations = locations;
_index = index;
}

public IInvocationOperation Operation => _originalOperation;

public MethodDeclarationSyntax Emit(CancellationToken cancellationToken)
{
var returnType = _originalOperation.TargetMethod.ReturnType;

var returnTypeSyntax = returnType.AsUnknownTypeSyntax();

TypeParameterSyntax[] typeParameters;
ParameterSyntax[] methodParameters;
ArgumentSyntax[] constructorArguments;

if (_typeArguments.HasValue)
{
typeParameters = new TypeParameterSyntax[_typeArguments.Value.Length];

// We always take the Pretend<T> argument first as a this parameter
methodParameters = new ParameterSyntax[_typeArguments.Value.Length + 1];
constructorArguments = new ArgumentSyntax[_typeArguments.Value.Length + 1];

for (var i = 0; i < _typeArguments.Value.Length; i++)
{
var typeName = $"T{i}";
var argName = $"arg{i}";

typeParameters[i] = TypeParameter(typeName);
methodParameters[i + 1] = Parameter(Identifier(argName))
.WithType(ParseTypeName(typeName));
constructorArguments[i + 1] = Argument(IdentifierName(argName));
}
}
else
{
typeParameters = [];
methodParameters = new ParameterSyntax[1];
constructorArguments = new ArgumentSyntax[1];
}

methodParameters[0] = Parameter(Identifier("pretend"))
.WithType(GenericName("Pretend")
.AddTypeArgumentListArguments(returnTypeSyntax))
.WithModifiers(TokenList(Token(SyntaxKind.ThisKeyword))
);

constructorArguments[0] = Argument(IdentifierName("pretend"));

var objectCreation = ObjectCreationExpression(ParseTypeName(returnType.ToPretendName()))
.WithArgumentList(ArgumentList(SeparatedList(constructorArguments)));

var method = MethodDeclaration(returnTypeSyntax, $"Create{_index}")
.WithBody(Block(ReturnStatement(objectCreation)))
.WithParameterList(ParameterList(SeparatedList(methodParameters)))
.WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword)));

method = method.WithAttributeLists(List(CreateInterceptsAttributes()));

if (typeParameters.Length > 0)
{
return method
.WithTypeParameterList(TypeParameterList(SeparatedList(typeParameters)));
}

return method;
}

private ImmutableArray<AttributeListSyntax> CreateInterceptsAttributes()
{
var builder = ImmutableArray.CreateBuilder<AttributeListSyntax>(_locations.Length);

foreach (var location in _locations)
{
var attribute = Create(location.ToAttributeSyntax());
builder.Add(attribute);
}

return builder.MoveToImmutable();

static AttributeListSyntax Create(AttributeSyntax attribute)
{
return AttributeList(SingletonSeparatedList(attribute));
}
}
}
}
43 changes: 43 additions & 0 deletions src/Pretender.SourceGenerator/Parser/CreateParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Pretender.SourceGenerator.Emitter;
using static Pretender.SourceGenerator.PretenderSourceGenerator;

namespace Pretender.SourceGenerator.Parser
{
internal class CreateParser
{
private readonly CreateInvocation _createInvocation;
private readonly ImmutableArray<InterceptsLocationInfo> _locations;
private readonly int _index;
private readonly bool _isLanguageVersionSupported;
private readonly KnownTypeSymbols _knownTypeSymbols;

public CreateParser(CreateInvocation createInvocation, ImmutableArray<InterceptsLocationInfo> locations, int index, CompilationData compilationData)
{
_createInvocation = createInvocation;
_locations = locations;
_index = index;
_isLanguageVersionSupported = compilationData.LanguageVersionIsSupported;
_knownTypeSymbols = compilationData.TypeSymbols!;
}

public (CreateEmitter? Emitter, ImmutableArray<Diagnostic>? Diagnostics) Parse(CancellationToken cancellationToken)
{
if (!_isLanguageVersionSupported)
{
return (null, ImmutableArray.Create(Diagnostic.Create(DiagnosticDescriptors.UnsupportedLanguageVersion, null)));
}

// TODO:Do deeper introspection to make sure the supplied args match with supplied type arguments
// and we should provide the constructor to use to the emitter maybe
var emitter = new CreateEmitter(
_createInvocation.Operation,
_createInvocation.TypeArguments,
_locations,
_index);

return (emitter, null);
}
}
}
6 changes: 4 additions & 2 deletions src/Pretender.SourceGenerator/Parser/SetupActionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ internal class SetupActionParser
private readonly IOperation _setupActionArgument;
private readonly ITypeSymbol _pretendType;
private readonly bool _forcePropertySetter;
private readonly KnownTypeSymbols _knownTypeSymbols;

// TODO: Should I have a higher IOperation kind here? Like InvocationOperation?
public SetupActionParser(IOperation setupActionArgument, ITypeSymbol pretendType, bool forcePropertySetter)
public SetupActionParser(IOperation setupActionArgument, ITypeSymbol pretendType, bool forcePropertySetter, KnownTypeSymbols knownTypeSymbols)
{
_setupActionArgument = setupActionArgument;
_pretendType = pretendType;
_forcePropertySetter = forcePropertySetter;
_knownTypeSymbols = knownTypeSymbols;
}

public (SetupActionEmitter? Emitter, ImmutableArray<Diagnostic>? Diagnostics) Parse(CancellationToken cancellationToken)
Expand Down Expand Up @@ -45,7 +47,7 @@ public SetupActionParser(IOperation setupActionArgument, ITypeSymbol pretendType
builder.Add(SetupArgumentSpec.Create(arguments[i], i));
}

return (new SetupActionEmitter(_pretendType, candidate.Method, builder.MoveToImmutable()), null);
return (new SetupActionEmitter(_pretendType, candidate.Method, builder.MoveToImmutable(), _knownTypeSymbols), null);
}

private ImmutableArray<InvocationCandidate> GetInvocationCandidates(CancellationToken cancellationToken)
Expand Down
5 changes: 2 additions & 3 deletions src/Pretender.SourceGenerator/Parser/SetupParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public SetupParser(SetupInvocation setupInvocation, CompilationData compilationD
{
if (!_isLanguageVersionSupported)
{
// TODO: Create error diagnostic
return (null, null);
return (null, ImmutableArray.Create(Diagnostic.Create(DiagnosticDescriptors.UnsupportedLanguageVersion, null)));
}

var operation = _setupInvocation.Operation;
Expand All @@ -39,7 +38,7 @@ public SetupParser(SetupInvocation setupInvocation, CompilationData compilationD

var useSetMethod = operation.TargetMethod.Name == "SetupSet";

var parser = new SetupActionParser(setupArgument.Value, pretendType, useSetMethod);
var parser = new SetupActionParser(setupArgument.Value, pretendType, useSetMethod, _knownTypeSymbols);

var (setupActionEmitter, setupActionDiagnostics) = parser.Parse(cancellationToken);

Expand Down
5 changes: 2 additions & 3 deletions src/Pretender.SourceGenerator/Parser/VerifyParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public VerifyParser(VerifyInvocation verifyInvocation, CompilationData compilati
{
if (!_isLanguageVersionSupported)
{
// TODO: Create error diagnostic
return (null, null);
return (null, ImmutableArray.Create(Diagnostic.Create(DiagnosticDescriptors.UnsupportedLanguageVersion, null)));
}

cancellationToken.ThrowIfCancellationRequested();
Expand All @@ -41,7 +40,7 @@ public VerifyParser(VerifyInvocation verifyInvocation, CompilationData compilati
// TODO: This doesn't exist yet
var useSetMethod = operation.TargetMethod.Name == "VerifySet";

var parser = new SetupActionParser(setupArgument.Value, pretendType, useSetMethod);
var parser = new SetupActionParser(setupArgument.Value, pretendType, useSetMethod, _knownTypeSymbols);

var (setupActionEmitter, setupActionDiagnostics) = parser.Parse(cancellationToken);

Expand Down
Loading

0 comments on commit 73ba385

Please sign in to comment.