Skip to content

Commit

Permalink
Add source gen support for explicitly opted in private members. (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
eiriktsarpalis authored Oct 30, 2024
1 parent 4be993e commit 1413476
Show file tree
Hide file tree
Showing 15 changed files with 524 additions and 126 deletions.
14 changes: 12 additions & 2 deletions src/TypeShape.Roslyn/Model/PropertyDataModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,25 @@ public PropertyDataModel(IFieldSymbol field)
/// </summary>
public bool IsField => PropertySymbol is IFieldSymbol;

/// <summary>
/// Whether the getter is part of the data model.
/// </summary>
public required bool IncludeGetter { get; init; }

/// <summary>
/// Whether the setter is part of the data model.
/// </summary>
public required bool IncludeSetter { get; init; }

/// <summary>
/// Whether we can access the property getter.
/// </summary>
public required bool CanRead { get; init; }
public required bool IsGetterAccessible { get; init; }

/// <summary>
/// Whether we can access the property setter.
/// </summary>
public required bool CanWrite { get; init; }
public required bool IsSetterAccessible { get; init; }

/// <summary>
/// Whether the getter method output is non-nullable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,30 @@ protected virtual IEnumerable<IMethodSymbol> ResolveConstructors(ITypeSymbol typ
}

/// <summary>
/// Determines whether the given property or field should be ignored from the data model.
/// Determines whether the given field should be included in the object data model.
/// </summary>
/// <remarks>Defaults to non-public properties being skipped.</remarks>
protected virtual bool IgnorePropertyOrField(ISymbol propertyOrField)
=> propertyOrField.DeclaredAccessibility is not Accessibility.Public;
/// <remarks>Defaults to including public fields only.</remarks>
protected virtual bool IncludeField(IFieldSymbol field)
=> field.DeclaredAccessibility is Accessibility.Public;

/// <summary>
/// Determines whether the given property should be included in the object data model.
/// </summary>
/// <remarks>Defaults to including public getters and setters only.</remarks>
protected virtual bool IncludeProperty(IPropertySymbol property, out bool includeGetter, out bool includeSetter)
{
if (property.DeclaredAccessibility is Accessibility.Public)
{
// Use the signature of the base property to determine shape and accessibility.
property = property.GetBaseProperty();
includeGetter = property.GetMethod is { } getter && IsAccessibleSymbol(getter);
includeSetter = property.SetMethod is { } setter && IsAccessibleSymbol(setter);
return true;
}

includeGetter = includeSetter = false;
return false;
}

private bool TryMapObject(ITypeSymbol type, ref TypeDataModelGenerationContext ctx, out TypeDataModel? model, out TypeDataModelGenerationStatus status)
{
Expand Down Expand Up @@ -92,15 +111,15 @@ private ImmutableArray<PropertyDataModel> MapProperties(INamedTypeSymbol type, r
foreach (ISymbol member in members)
{
if (member is IPropertySymbol { IsStatic: false, Parameters: [] } ps &&
IsAccessibleSymbol(ps) && !IsOverriddenOrShadowed(ps) && !IgnorePropertyOrField(ps) &&
!IsOverriddenOrShadowed(ps) && IncludeProperty(ps, out bool includeGetter, out bool includeSetter) &&
IncludeNestedType(ps.Type, ref ctx) is TypeDataModelGenerationStatus.Success)
{
PropertyDataModel propertyModel = MapProperty(ps);
PropertyDataModel propertyModel = MapProperty(ps, includeGetter, includeSetter);
properties.Add(propertyModel);
}
else if (
member is IFieldSymbol { IsStatic: false, IsConst: false } fs &&
IsAccessibleSymbol(fs) && !IsOverriddenOrShadowed(fs) && !IgnorePropertyOrField(fs) &&
!IsOverriddenOrShadowed(fs) && IncludeField(fs) &&
IncludeNestedType(fs.Type, ref ctx) is TypeDataModelGenerationStatus.Success)
{
PropertyDataModel fieldModel = MapField(fs);
Expand All @@ -114,9 +133,11 @@ private ImmutableArray<PropertyDataModel> MapProperties(INamedTypeSymbol type, r
return properties.ToImmutableArray();
}

private PropertyDataModel MapProperty(IPropertySymbol property)
private PropertyDataModel MapProperty(IPropertySymbol property, bool includeGetter, bool includeSetter)
{
Debug.Assert(property is { IsStatic: false, IsIndexer: false });
Debug.Assert(!includeGetter || property.GetBaseProperty().GetMethod is not null);
Debug.Assert(!includeSetter || property.GetBaseProperty().SetMethod is not null);

// Property symbol represents the most derived declaration in the current hierarchy.
// Need to use the base symbol to determine the actual signature, but use the derived
Expand All @@ -126,21 +147,26 @@ private PropertyDataModel MapProperty(IPropertySymbol property)

return new PropertyDataModel(property)
{
CanRead = baseProperty.GetMethod is { } getter && IsAccessibleSymbol(getter),
CanWrite = baseProperty.SetMethod is { } setter && IsAccessibleSymbol(setter),
IncludeGetter = includeGetter,
IncludeSetter = includeSetter,
IsGetterAccessible = baseProperty.GetMethod is { } getter && IsAccessibleSymbol(getter),
IsSetterAccessible = baseProperty.SetMethod is { } setter && IsAccessibleSymbol(setter),
IsGetterNonNullable = isGetterNonNullable,
IsSetterNonNullable = isSetterNonNullable,
};
}

private static PropertyDataModel MapField(IFieldSymbol field)
private PropertyDataModel MapField(IFieldSymbol field)
{
Debug.Assert(!field.IsStatic);
field.ResolveNullableAnnotation(out bool isGetterNonNullable, out bool isSetterNonNullable);
bool isAccessible = IsAccessibleSymbol(field);
return new PropertyDataModel(field)
{
CanRead = true,
CanWrite = !field.IsReadOnly,
IncludeGetter = true,
IncludeSetter = !field.IsReadOnly,
IsGetterAccessible = isAccessible,
IsSetterAccessible = isAccessible && !field.IsReadOnly,
IsGetterNonNullable = isGetterNonNullable,
IsSetterNonNullable = isSetterNonNullable,
};
Expand All @@ -164,7 +190,6 @@ private ImmutableArray<ConstructorDataModel> MapConstructors(INamedTypeSymbol ty
private ConstructorDataModel? MapConstructor(IMethodSymbol constructor, ImmutableArray<PropertyDataModel> properties, ref TypeDataModelGenerationContext ctx)
{
Debug.Assert(constructor.MethodKind is MethodKind.Constructor || constructor.IsStatic);
Debug.Assert(IsAccessibleSymbol(constructor));

var parameters = new List<ConstructorParameterDataModel>();
TypeDataModelGenerationContext scopedCtx = ctx;
Expand Down Expand Up @@ -194,7 +219,7 @@ private ImmutableArray<ConstructorDataModel> MapConstructors(INamedTypeSymbol ty
{
PropertyDataModel property = properties[i];

if (!property.CanWrite && !property.IsInitOnly)
if (!property.IncludeSetter && !property.IsInitOnly)
{
// We're only interested in settable properties.
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private bool TryMapTuple(ITypeSymbol type, ref TypeDataModelGenerationContext ct
return true;
}

PropertyDataModel propertyModel = MapProperty(elementProp);
PropertyDataModel propertyModel = MapProperty(elementProp, includeGetter: true, includeSetter: false);
elements.Add(propertyModel);
}

Expand Down
10 changes: 10 additions & 0 deletions src/TypeShape.SourceGenerator/Helpers/RoslynHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ public static bool IsGenericTypeDefinition(this ITypeSymbol type)
=> type is INamedTypeSymbol { IsGenericType: true } namedType &&
SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, type);

public static IPropertySymbol GetBaseProperty(this IPropertySymbol property)
{
while (property.OverriddenProperty is { } baseProp)
{
property = baseProp;
}

return property;
}

public static bool MatchesNamespace(this ISymbol? symbol, ImmutableArray<string> namespaceTokens)
{
for (int i = namespaceTokens.Length - 1; i >= 0; i--)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,8 @@ public sealed record ConstructorParameterShapeModel
public required bool IsRequired { get; init; }
public required bool IsInitOnlyProperty { get; init; }
public required bool IsNonNullable { get; init; }
public required bool IsAccessible { get; init; }
public required bool ParameterTypeContainsNullabilityAnnotations { get; init; }

/// <summary>
/// If an init-only property initializer, determines if a workaround
/// for https://github.com/dotnet/runtime/issues/89439 should be applied.
/// </summary>
public required bool PropertyTypeIsGenericInstantiation { get; init; }

public required bool IsPublic { get; init; }
public required bool IsField { get; init; }
public required bool HasDefaultValue { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public sealed record ConstructorShapeModel
{
public required TypeId DeclaringType { get; init; }
public required bool IsPublic { get; init; }
public required bool IsAccessible { get; init; }
public required ImmutableEquatableArray<ConstructorParameterShapeModel> Parameters { get; init; }
public required ImmutableEquatableArray<ConstructorParameterShapeModel> RequiredMembers { get; init; }
public required ImmutableEquatableArray<ConstructorParameterShapeModel> OptionalMembers { get; init; }
Expand Down
10 changes: 10 additions & 0 deletions src/TypeShape.SourceGenerator/Model/PropertyShapeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ public sealed record PropertyShapeModel
public required TypeId PropertyType { get; init; }

public required bool IsField { get; init; }
public required bool IsInitOnly { get; init; }

public required bool EmitGetter { get; init; }
public required bool EmitSetter { get; init; }

public required bool IsGetterAccessible { get; init; }
public required bool IsSetterAccessible { get; init; }

public required bool IsGetterPublic { get; init; }
public required bool IsSetterPublic { get; init; }

Expand All @@ -24,5 +28,11 @@ public sealed record PropertyShapeModel
/// </summary>
public required bool PropertyTypeContainsNullabilityAnnotations { get; init; }

/// <summary>
/// If an init-only property initializer, determines if a workaround
/// for https://github.com/dotnet/runtime/issues/89439 should be applied.
/// </summary>
public required bool IsGenericPropertyType { get; init; }

public required int Order { get; init; }
}
20 changes: 14 additions & 6 deletions src/TypeShape.SourceGenerator/Parser/Parser.ModelMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ static bool IsFactoryAcceptingIEnumerable(IMethodSymbol? method)
private PropertyShapeModel MapProperty(ITypeSymbol parentType, TypeId parentTypeId, PropertyDataModel property, bool isClassTupleType = false, int tupleElementIndex = -1)
{
ParsePropertyShapeAttribute(property.PropertySymbol, out string propertyName, out int order);
bool emitGetter = property.CanRead;
bool emitSetter = property.CanWrite && !property.IsInitOnly;

bool emitGetter = property.IncludeGetter;
bool emitSetter = property.IncludeSetter && !property.IsInitOnly;

return new PropertyShapeModel
{
Expand All @@ -168,14 +168,18 @@ private PropertyShapeModel MapProperty(ITypeSymbol parentType, TypeId parentType
: property.Name,

DeclaringType = SymbolEqualityComparer.Default.Equals(parentType, property.DeclaringType) ? parentTypeId : CreateTypeId(property.DeclaringType),
IsGenericPropertyType = !SymbolEqualityComparer.Default.Equals(property.PropertyType, property.PropertySymbol.OriginalDefinition.GetMemberType()),
PropertyType = CreateTypeId(property.PropertyType),
IsGetterNonNullable = emitGetter && property.IsGetterNonNullable,
IsSetterNonNullable = emitSetter && property.IsSetterNonNullable,
PropertyTypeContainsNullabilityAnnotations = property.PropertyType.ContainsNullabilityAnnotations(),
EmitGetter = emitGetter,
EmitSetter = emitSetter,
IsGetterAccessible = property.IsGetterAccessible,
IsSetterAccessible = property.IsSetterAccessible,
IsGetterPublic = emitGetter && property.BaseSymbol is IPropertySymbol { GetMethod.DeclaredAccessibility: Accessibility.Public } or IFieldSymbol { DeclaredAccessibility: Accessibility.Public },
IsSetterPublic = emitSetter && property.BaseSymbol is IPropertySymbol { SetMethod.DeclaredAccessibility: Accessibility.Public } or IFieldSymbol { DeclaredAccessibility: Accessibility.Public },
IsInitOnly = property.IsInitOnly,
IsField = property.IsField,
Order = order,
};
Expand All @@ -187,6 +191,7 @@ private ConstructorShapeModel MapConstructor(ObjectDataModel objectModel, TypeId
List<ConstructorParameterShapeModel>? requiredMembers = null;
List<ConstructorParameterShapeModel>? optionalMembers = null;

bool isAccessibleConstructor = IsAccessibleSymbol(constructor.Constructor);
bool isParameterizedConstructor = position > 0 || constructor.MemberInitializers.Any(p => p.IsRequired || p.IsInitOnly);
IEnumerable<PropertyDataModel> memberInitializers = isParameterizedConstructor
// Include all settable members but process required members first.
Expand All @@ -209,8 +214,8 @@ private ConstructorShapeModel MapConstructor(ObjectDataModel objectModel, TypeId
UnderlyingMemberName = propertyModel.Name,
Position = position++,
IsRequired = propertyModel.IsRequired,
IsAccessible = propertyModel.IsSetterAccessible,
IsInitOnlyProperty = propertyModel.IsInitOnly,
PropertyTypeIsGenericInstantiation = !SymbolEqualityComparer.Default.Equals(propertyModel.PropertyType, propertyModel.PropertySymbol.OriginalDefinition.GetMemberType()),
Kind = propertyModel.IsRequired ? ParameterKind.RequiredMember : ParameterKind.OptionalMember,
RefKind = RefKind.None,
IsNonNullable = propertyModel.IsSetterNonNullable,
Expand Down Expand Up @@ -254,6 +259,7 @@ private ConstructorShapeModel MapConstructor(ObjectDataModel objectModel, TypeId

StaticFactoryName = constructor.Constructor.IsStatic ? constructor.Constructor.GetFullyQualifiedName() : null,
IsPublic = constructor.Constructor.DeclaredAccessibility is Accessibility.Public,
IsAccessible = isAccessibleConstructor,
};
}

Expand Down Expand Up @@ -294,8 +300,8 @@ private ConstructorParameterShapeModel MapConstructorParameter(ObjectDataModel o
Kind = ParameterKind.ConstructorParameter,
RefKind = parameter.Parameter.RefKind,
IsRequired = !parameter.HasDefaultValue,
IsAccessible = true,
IsInitOnlyProperty = false,
PropertyTypeIsGenericInstantiation = false,
IsNonNullable = parameter.IsNonNullable,
ParameterTypeContainsNullabilityAnnotations = parameter.Parameter.Type.ContainsNullabilityAnnotations(),
IsPublic = true,
Expand All @@ -318,6 +324,7 @@ private static ConstructorShapeModel MapTupleConstructor(TypeId typeId, TupleDat
OptionalMembers = [],
OptionalMemberFlagsType = OptionalMemberFlagsType.None,
StaticFactoryName = null,
IsAccessible = true,
IsPublic = true,
};
}
Expand All @@ -332,6 +339,7 @@ private static ConstructorShapeModel MapTupleConstructor(TypeId typeId, TupleDat
OptionalMembers = [],
OptionalMemberFlagsType = OptionalMemberFlagsType.None,
StaticFactoryName = null,
IsAccessible = true,
IsPublic = true,
};
}
Expand All @@ -350,8 +358,8 @@ static ConstructorParameterShapeModel MapTupleConstructorParameter(TypeId typeId
Kind = ParameterKind.ConstructorParameter,
RefKind = RefKind.None,
IsRequired = true,
IsAccessible = true,
IsInitOnlyProperty = false,
PropertyTypeIsGenericInstantiation = false,
IsPublic = true,
IsField = true,
IsNonNullable = tupleElement.IsSetterNonNullable,
Expand Down
Loading

0 comments on commit 1413476

Please sign in to comment.