Skip to content

Commit

Permalink
Fixes #9 - Disallow default(Foo) by emitting a build error
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveDunn committed Dec 3, 2021
1 parent 1619790 commit 9ae3006
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 108 deletions.
1 change: 1 addition & 0 deletions Vogen.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantSuppressNullableWarningExpression/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMember_002EGlobal/@EntryIndexedValue">WARNING</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Beckham/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Daves/@EntryIndexedValue">True</s:Boolean>
Expand Down
30 changes: 30 additions & 0 deletions src/Vogen.Examples/NoDefaulting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using Vogen;
#pragma warning disable CS0219

namespace Vogen.Examples.NoDefaulting
{
/*
You shouldn't be allowed to `default` a Value Object as it bypasses
any validation you might have added.
*/

public class Naughty
{
public Naughty()
{
// uncomment for - error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
// CustomerId c = default;
// var c2 = default(CustomerId);

// VendorId v = default;
// var v2 = default(VendorId);
}
}

[ValueObject(typeof(int))]
public partial struct CustomerId { }

[ValueObject(typeof(int))]
public partial class VendorId { }
}
3 changes: 2 additions & 1 deletion src/Vogen/Diagnostics/DiagnosticCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public enum DiagnosticCode
ValidationMustBeStatic = 5,
InstanceMethodCannotHaveNullArgumentName = 6,
InstanceMethodCannotHaveNullArgumentValue = 7,
CannotHaveUserConstructors = 8
CannotHaveUserConstructors = 8,
UsingDefaultProhibited = 9
}
23 changes: 22 additions & 1 deletion src/Vogen/Diagnostics/DiagnosticCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ internal class DiagnosticCollection : IEnumerable<Diagnostic>
"Types cannot be nested",
"Type '{0}' must specify an underlying type");

private static readonly DiagnosticDescriptor _usingDefaultProhibited = CreateDescriptor(
DiagnosticCode.UsingDefaultProhibited,
"Using default of Value Objects is prohibited",
"Type '{0}' cannot be constructed with default as it is prohibited.");

private static readonly DiagnosticDescriptor _cannotHaveUserConstructors = CreateDescriptor(
DiagnosticCode.CannotHaveUserConstructors,
"Cannot have user defined constructors",
Expand Down Expand Up @@ -70,6 +75,9 @@ public void AddValidationMustBeStatic(MethodDeclarationSyntax member) =>
public void AddMustSpecifyUnderlyingType(INamedTypeSymbol underlyingType) =>
AddDiagnostic(_mustSpecifyUnderlyingType, underlyingType.Locations, underlyingType.Name);

public void AddUsingDefaultProhibited(Location locationOfDefaultStatement, string voClassName) =>
AddDiagnostic(_usingDefaultProhibited, voClassName, locationOfDefaultStatement);

public void AddCannotHaveUserConstructors(IMethodSymbol constructor) =>
AddDiagnostic(_cannotHaveUserConstructors, constructor.Locations);

Expand All @@ -92,11 +100,24 @@ private static DiagnosticDescriptor CreateDescriptor(DiagnosticCode code, string
return new DiagnosticDescriptor(code.Format(), title, messageFormat, "RestEaseGeneration", severity, isEnabledByDefault: true, customTags: tags);
}

private void AddDiagnostic(DiagnosticDescriptor descriptor, string name, Location location)
{
var diagnostic = Diagnostic.Create(descriptor, location, name);

AddDiagnostic(diagnostic);
}

private void AddDiagnostic(DiagnosticDescriptor descriptor, IEnumerable<Location> locations, params object?[] args)
{
var locationsList = (locations as IReadOnlyList<Location>) ?? locations.ToList();

var diagnostic = Diagnostic.Create(
descriptor,
locationsList.Count == 0 ? Location.None : locationsList[0],
locationsList.Skip(1),
args);

AddDiagnostic(Diagnostic.Create(descriptor, locationsList.Count == 0 ? Location.None : locationsList[0], locationsList.Skip(1), args));
AddDiagnostic(diagnostic);
}

private void AddDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object?[] args) =>
Expand Down
255 changes: 170 additions & 85 deletions src/Vogen/ValueObjectReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,123 +22,208 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
try
{
if (context.Node is not TypeDeclarationSyntax typeDeclarationSyntax)
if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax)
{
return;
}
HandleType(context, typeDeclarationSyntax);

var voClass = (INamedTypeSymbol) context.SemanticModel.GetDeclaredSymbol(context.Node)!;

var attributes = voClass.GetAttributes();

if (attributes.Length == 0)
{
return;
}

AttributeData? voAttribute =
attributes.SingleOrDefault(a =>
{
var fullName = a.AttributeClass?.FullName();
return fullName is "Vogen.ValueObjectAttribute";
});

if (voAttribute is null)
if (context.Node is LiteralExpressionSyntax literalExpressionSyntax)
{
return;
}

if (voAttribute.ConstructorArguments.Length == 0)
{
DiagnosticMessages.AddMustSpecifyUnderlyingType(voClass);
HandleDefaultLiteralExpression(context, literalExpressionSyntax);
return;
}

foreach (var eachConstructor in voClass.Constructors)
{
// no need to check for default constructor as it's already defined
// and the user will see: error CS0111: Type 'Foo' already defines a member called 'Foo' with the same parameter type
if (eachConstructor.Parameters.Length > 0)
{
DiagnosticMessages.AddCannotHaveUserConstructors(eachConstructor);
}

}

var underlyingType = (INamedTypeSymbol?) voAttribute.ConstructorArguments[0].Value;

if (underlyingType is null)
if (context.Node is DefaultExpressionSyntax defaultExpressionSyntax)
{
DiagnosticMessages.AddMustSpecifyUnderlyingType(voClass);
HandleDefaultExpression(context, defaultExpressionSyntax);
return;
}
}
catch (Exception ex) when (LogException(ex))
{
}
}

private void HandleDefaultLiteralExpression(GeneratorSyntaxContext context, LiteralExpressionSyntax literalExpressionSyntax)
{
if (literalExpressionSyntax.Kind() != SyntaxKind.DefaultLiteralExpression)
{
return;
}

var ancestor = literalExpressionSyntax.Ancestors(false)
.FirstOrDefault(a => a.IsKind(SyntaxKind.VariableDeclaration));

if (ancestor is not VariableDeclarationSyntax variableDeclarationSyntax)
{
return;
}

if (!IsOneOfOurValueObjects(context, variableDeclarationSyntax.Type, out string name))
{
return;
}

DiagnosticMessages.AddUsingDefaultProhibited(literalExpressionSyntax.GetLocation(), name);
}

private void HandleDefaultExpression(GeneratorSyntaxContext context, DefaultExpressionSyntax defaultExpressionSyntax)
{
TypeSyntax? typeSyntax = defaultExpressionSyntax?.Type;

if (typeSyntax == null)
{
return;
}

if (!IsOneOfOurValueObjects(context, typeSyntax, out string name))
{
return;
}

DiagnosticMessages.AddUsingDefaultProhibited(typeSyntax.GetLocation(), name);
}

private bool IsOneOfOurValueObjects(GeneratorSyntaxContext context, TypeSyntax typeSyntax, out string name)
{
name = string.Empty;

SymbolInfo typeSymbolInfo = context.SemanticModel.GetSymbolInfo(typeSyntax);

var voClass = typeSymbolInfo.Symbol;

if (voClass == null)
{
return false;
}

var attributes = voClass.GetAttributes();

if (attributes.Length == 0)
{
return false;
}

var containingType = context.SemanticModel.GetDeclaredSymbol(context.Node)!.ContainingType;
if (containingType != null)
AttributeData? voAttribute =
attributes.SingleOrDefault(a => a.AttributeClass?.FullName() is "Vogen.ValueObjectAttribute");

if (voAttribute is null)
{
return false;
}

name = voClass.Name;

return true;
}

private void HandleType(GeneratorSyntaxContext context, TypeDeclarationSyntax typeDeclarationSyntax)
{
var voClass = (INamedTypeSymbol) context.SemanticModel.GetDeclaredSymbol(context.Node)!;

var attributes = voClass.GetAttributes();

if (attributes.Length == 0)
{
return;
}

AttributeData? voAttribute = attributes.SingleOrDefault(
a => a.AttributeClass?.FullName() is "Vogen.ValueObjectAttribute");

if (voAttribute is null)
{
return;
}

if (voAttribute.ConstructorArguments.Length == 0)
{
DiagnosticMessages.AddMustSpecifyUnderlyingType(voClass);
return;
}

foreach (var eachConstructor in voClass.Constructors)
{
// no need to check for default constructor as it's already defined
// and the user will see: error CS0111: Type 'Foo' already defines a member called 'Foo' with the same parameter type
if (eachConstructor.Parameters.Length > 0)
{
DiagnosticMessages.AddTypeCannotBeNested(voClass, containingType);
DiagnosticMessages.AddCannotHaveUserConstructors(eachConstructor);
}
}

var underlyingType = (INamedTypeSymbol?) voAttribute.ConstructorArguments[0].Value;

if (underlyingType is null)
{
DiagnosticMessages.AddMustSpecifyUnderlyingType(voClass);
return;
}

var containingType = context.SemanticModel.GetDeclaredSymbol(context.Node)!.ContainingType;
if (containingType != null)
{
DiagnosticMessages.AddTypeCannotBeNested(voClass, containingType);
}

var instanceProperties = TryBuildInstanceProperties(attributes, voClass);
var instanceProperties = TryBuildInstanceProperties(attributes, voClass);

MethodDeclarationSyntax? validateMethod = null;
MethodDeclarationSyntax? validateMethod = null;

// add any validator methods it finds
foreach (var memberDeclarationSyntax in typeDeclarationSyntax.Members)
// add any validator methods it finds
foreach (var memberDeclarationSyntax in typeDeclarationSyntax.Members)
{
if (memberDeclarationSyntax is MethodDeclarationSyntax mds)
{
if (memberDeclarationSyntax is MethodDeclarationSyntax mds)
if (!(mds.DescendantTokens().Any(t => t.IsKind(SyntaxKind.StaticKeyword))))
{
if (!(mds.DescendantTokens().Any(t => t.IsKind(SyntaxKind.StaticKeyword))))
{
DiagnosticMessages.AddValidationMustBeStatic(mds);
}
DiagnosticMessages.AddValidationMustBeStatic(mds);
}

object? value = mds.Identifier.Value;
object? value = mds.Identifier.Value;

Log.Add($" Found method named {value}");
Log.Add($" Found method named {value}");

if (value?.ToString() == "Validate")
if (value?.ToString() == "Validate")
{
TypeSyntax returnTypeSyntax = mds.ReturnType;
if (returnTypeSyntax.ToString() != "Validation")
{
TypeSyntax returnTypeSyntax = mds.ReturnType;
if (returnTypeSyntax.ToString() != "Validation")
{
DiagnosticMessages.AddValidationMustReturnValidationType(mds);
Log.Add($" Validate return type is {returnTypeSyntax}");

}
DiagnosticMessages.AddValidationMustReturnValidationType(mds);
Log.Add($" Validate return type is {returnTypeSyntax}");

Log.Add($" Added and will call {value}");

validateMethod = mds;
}
}
}

if (SymbolEqualityComparer.Default.Equals(voClass, underlyingType))
{
DiagnosticMessages.AddUnderlyingTypeMustNotBeSameAsValueObjectType(voClass);
}
Log.Add($" Added and will call {value}");

if (underlyingType.ImplementsInterfaceOrBaseClass(typeof(ICollection)))
{
DiagnosticMessages.AddUnderlyingTypeCannotBeCollection(voClass, underlyingType);
validateMethod = mds;
}
}

bool isValueType = underlyingType.IsValueType;
}

WorkItems.Add(new ValueObjectWorkItem
{
InstanceProperties = instanceProperties.ToList(),
TypeToAugment = typeDeclarationSyntax,
IsValueType = isValueType,
UnderlyingType = underlyingType,
ValidateMethod = validateMethod,
FullNamespace = voClass.FullNamespace()
});
if (SymbolEqualityComparer.Default.Equals(voClass, underlyingType))
{
DiagnosticMessages.AddUnderlyingTypeMustNotBeSameAsValueObjectType(voClass);
}
catch (Exception ex) when (LogException(ex))

if (underlyingType.ImplementsInterfaceOrBaseClass(typeof(ICollection)))
{
DiagnosticMessages.AddUnderlyingTypeCannotBeCollection(voClass, underlyingType);
}

bool isValueType = underlyingType.IsValueType;

WorkItems.Add(new ValueObjectWorkItem
{
InstanceProperties = instanceProperties.ToList(),
TypeToAugment = typeDeclarationSyntax,
IsValueType = isValueType,
UnderlyingType = underlyingType,
ValidateMethod = validateMethod,
FullNamespace = voClass.FullNamespace()
});
}

private bool LogException(Exception ex)
Expand Down Expand Up @@ -177,7 +262,7 @@ private IEnumerable<InstanceProperties> TryBuildInstanceProperties(
{
Log.Add($"name symbol for InstanceAttribute is null");
DiagnosticMessages.AddInstanceMethodCannotHaveNullArgumentName(voClass);
// continue;
// continue;
}

var value = constructorArguments[1].Value;
Expand Down
Loading

0 comments on commit 9ae3006

Please sign in to comment.