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
72 changes: 64 additions & 8 deletions src/DependencyModules.SourceGenerator.Impl/BaseSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,6 @@ typeDefinition is GenericTypeDefinition
}

var dependencyFlags = GetDependencyFlags(context);
var implementsEqualsFlag = GetEqualsFlag(context);
var modelInfo = AttributeModelHelper.GetAttributeClassInfo(context, cancellation);

if (dependencyFlags.OnlyRealm)
Expand All @@ -339,9 +338,9 @@ typeDefinition is GenericTypeDefinition
{
features |= ModuleEntryPointFeatures.IsRecord;
}
else if (!implementsEqualsFlag)
else
{
features |= ModuleEntryPointFeatures.ShouldImplementEquals;
features |= GetEqualityFeatures(context, typeDeclarationSyntax, cancellation);
}

if (!typeDeclarationSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
Expand Down Expand Up @@ -439,12 +438,69 @@ syntax is GlobalStatementSyntax
);
}

private bool GetEqualsFlag(GeneratorSyntaxContext context)
/// <summary>
/// Which equality members the generator writes for a module.
/// </summary>
/// <remarks>
/// Read from the symbol, so a member in any partial declaration counts. Only
/// <c>Equals(object)</c> is what the load operation calls, so a module that declares only
/// <c>Equals</c> for its own type still gets a generated <c>Equals(object)</c>, which calls it.
/// </remarks>
private static ModuleEntryPointFeatures GetEqualityFeatures(
GeneratorSyntaxContext context,
TypeDeclarationSyntax typeDeclarationSyntax,
CancellationToken cancellation
)
{
return context
.Node.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Any(m => m.Identifier.ToString().Equals("Equals"));
if (
context.SemanticModel.GetDeclaredSymbol(typeDeclarationSyntax, cancellation)
is not { } module
)
{
return ModuleEntryPointFeatures.ShouldImplementEquals
| ModuleEntryPointFeatures.ShouldImplementGetHashCode;
}

var features = ModuleEntryPointFeatures.None;

var declaresEqualsObject = false;

foreach (var member in module.GetMembers("Equals"))
{
if (member is not IMethodSymbol { IsStatic: false, Parameters.Length: 1 } equals)
{
continue;
}

var parameterType = equals.Parameters[0].Type;

if (parameterType.SpecialType == SpecialType.System_Object)
{
declaresEqualsObject = true;
}
else if (SymbolEqualityComparer.Default.Equals(parameterType, module))
{
features |= ModuleEntryPointFeatures.DeclaresTypedEquals;
}
}

if (declaresEqualsObject)
{
return features;
}

features |= ModuleEntryPointFeatures.ShouldImplementEquals;

var declaresGetHashCode = module
.GetMembers("GetHashCode")
.Any(member => member is IMethodSymbol { IsStatic: false, Parameters.Length: 0 });

if (!declaresGetHashCode)
{
features |= ModuleEntryPointFeatures.ShouldImplementGetHashCode;
}

return features;
}

private record DependencyFlags(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,14 @@ private void GenerateModuleClass(ModuleEntryPointModel model, CSharpFileDefiniti

FeatureMethod(classDefinition, model);

if (
(model.ModuleFeatures & ModuleEntryPointFeatures.ShouldImplementEquals)
== ModuleEntryPointFeatures.ShouldImplementEquals
)
if (model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementEquals))
{
EqualMethod(classDefinition, model);

HashMethod(classDefinition, model);
if (model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementGetHashCode))
{
HashMethod(classDefinition, model);
}
}
}

Expand Down Expand Up @@ -388,7 +388,11 @@ private void EqualMethod(ClassDefinition classDefinition, ModuleEntryPointModel

equalMethod.AddParameter(TypeDefinition.Get(typeof(object)).MakeNullable(), "obj");

equalMethod.Return($"obj is {model.EntryPointType.Name}");
equalMethod.Return(
model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.DeclaresTypedEquals)
? $"obj is {model.EntryPointType.Name} other && Equals(other)"
: $"obj is {model.EntryPointType.Name}"
);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ public enum ModuleEntryPointFeatures
/// namespace level, the nested declaration never became a module, and the build stayed green.
/// </remarks>
NestedInType = 32,

/// <summary>
/// The module declares no <c>GetHashCode()</c>. Read only with <see cref="ShouldImplementEquals"/>.
/// </summary>
ShouldImplementGetHashCode = 64,

/// <summary>
/// The module declares <c>Equals</c> for its own type, which the generated
/// <c>Equals(object)</c> calls.
/// </summary>
DeclaresTypedEquals = 128,
}

public record ModuleEntryPointModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ namespace DependencyModules.SourceGenerator.Impl.Models;
/// <param name="IsReadOnly">No set accessor. Nothing to configure, so not a parameter.</param>
/// <param name="IsStatic">Belongs to the type rather than the instance, so not a parameter.</param>
/// <param name="IsVisibleToAttribute">
/// Reachable from another type in the same assembly, which is where the generated attribute sits.
/// <c>public</c>, <c>internal</c> and <c>protected internal</c> are; <c>private</c>,
/// <c>protected</c>, <c>private protected</c> and an unmodified declaration are not.
/// The property and its set accessor are reachable from another type in the same assembly, which
/// is where the generated attribute sits. <c>public</c>, <c>internal</c> and
/// <c>protected internal</c> are; <c>private</c>, <c>protected</c>, <c>private protected</c> and an
/// unmodified declaration are not.
/// </param>
public record PropertyInfoModel(
ITypeDefinition PropertyType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public static bool IsNestedInType(ModuleEntryPointModel model) =>
/// </remarks>
public static bool ReliesOnGeneratedEquality(ModuleEntryPointModel model) =>
model.PropertyInfoModels.Any(p => p.IsModuleParameter)
&& model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementEquals);
&& model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.ShouldImplementEquals)
&& !model.ModuleFeatures.HasFlag(ModuleEntryPointFeatures.DeclaresTypedEquals);

public static void Report(
SourceProductionContext context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,38 @@ CancellationToken cancellationToken
{
var propertyList = new List<PropertyInfoModel>();

foreach (var syntax in context.Node.DescendantNodes())
// The module's own members only. A property of a nested type is not a module parameter.
var members = context.Node is TypeDeclarationSyntax typeDeclaration
? typeDeclaration.Members
: default;

foreach (var propertyDeclarationSyntax in members.OfType<PropertyDeclarationSyntax>())
{
cancellationToken.ThrowIfCancellationRequested();

if (syntax is PropertyDeclarationSyntax propertyDeclarationSyntax)
{
var setter = propertyDeclarationSyntax.AccessorList?.Accessors.FirstOrDefault(x =>
x.IsKind(SyntaxKind.SetAccessorDeclaration)
);
var setter = propertyDeclarationSyntax.AccessorList?.Accessors.FirstOrDefault(x =>
x.IsKind(SyntaxKind.SetAccessorDeclaration)
);

var propertyType = propertyDeclarationSyntax.Type.GetTypeDefinition(context);
var propertyType = propertyDeclarationSyntax.Type.GetTypeDefinition(context);

if (propertyType != null)
{
propertyList.Add(
new PropertyInfoModel(
propertyType,
propertyDeclarationSyntax.Identifier.ToString(),
setter == null,
propertyDeclarationSyntax.Modifiers.Any(m =>
m.IsKind(SyntaxKind.StaticKeyword)
),
IsVisibleToAttribute(propertyDeclarationSyntax.Modifiers)
)
);
}
if (propertyType != null)
{
propertyList.Add(
new PropertyInfoModel(
propertyType,
propertyDeclarationSyntax.Identifier.ToString(),
setter == null,
propertyDeclarationSyntax.Modifiers.Any(m =>
m.IsKind(SyntaxKind.StaticKeyword)
),
IsVisibleToAttribute(propertyDeclarationSyntax.Modifiers)
&& (
setter is not { Modifiers.Count: > 0 }
|| IsVisibleToAttribute(setter.Modifiers)
)
)
);
}
}

Expand All @@ -80,8 +86,9 @@ CancellationToken cancellationToken
}

/// <summary>
/// Whether a property with these modifiers can be read and written from another type in the
/// same assembly — which is what the generated attribute is.
/// Whether a property or a set accessor with these modifiers can be used from another type in
/// the same assembly — which is what the generated attribute is. A set accessor with no
/// modifier has the access of its property.
/// </summary>
/// <remarks>
/// Read from syntax rather than from the symbol, because everything else in this transform is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ IEnumerable<ModuleEntryPointModel> allEntryPoints
/// <remarks>
/// Every writer that emits registrations, decorations or interceptions into a module filters
/// through this, so an auto-generated module that defers to a declared one is skipped by all of
/// them rather than by whichever ones remembered to.
/// them rather than by whichever ones remembered to. So is a module that DM0003 or DM0017
/// rejects. A partial class written for a module that is not partial is CS0260, and one written
/// for a nested module is a second type at namespace level.
/// </remarks>
public static IList<ModuleEntryPointModel> RegistrationTargets(
IList<ModuleEntryPointModel> entryPoints
Expand All @@ -92,7 +94,11 @@ IList<ModuleEntryPointModel> entryPoints

for (var i = 0; i < entryPoints.Count; i++)
{
if (DelegateTargetFor(entryPoints[i], entryPoints) == null)
if (
DelegateTargetFor(entryPoints[i], entryPoints) == null
&& !ModuleEntryPointDiagnostics.IsNotPartial(entryPoints[i])
&& !ModuleEntryPointDiagnostics.IsNestedInType(entryPoints[i])
)
{
filtered?.Add(entryPoints[i]);
continue;
Expand Down
Loading
Loading