I've taken a stab at writing an incremental source generator; it is generating the correct source code, but it's not doing so incrementally. I feel like it has to be something wrong with my Initialize
method or my custom return type (ClassInfo
) not being cache friendly. I've never written an IEquatable
either, so I really thing it has something to do with that.
ClassInfo
public readonly struct ClassInfo : IEquatable<ClassInfo>
{
public readonly string? Namespace { get; }
public readonly string Name { get; }
public readonly ImmutableArray<IPropertySymbol> PropertyNames { get; }
public ClassInfo(ITypeSymbol type)
{
Namespace = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToDisplayString();
Name = type.Name;
PropertyNames = GetPropertyNames(type);
}
private static ImmutableArray<IPropertySymbol> GetPropertyNames(ITypeSymbol type)
{
return type.GetMembers()
.Select(m =>
{
// Only properties
if (m is not IPropertySymbol prop || m.DeclaredAccessibility != Accessibility.Public)
return null;
// Without ignore attribute
if (GenHelper.IsPropsToStringIgnore(m))
return null;
return (IPropertySymbol)m;
//return SymbolEqualityComparer.Default.Equals(prop.Type, type) ? prop.Name : null;
})
.Where(m => m != null)!
.ToImmutableArray<IPropertySymbol>();
}
public override bool Equals(object? obj) => obj is ClassInfo other && Equals(other);
public bool Equals(ClassInfo other)
{
if (ReferenceEquals(null, other))
return false;
//if (ReferenceEquals(this, other))
// return true;
return Namespace == other.Namespace
&& Name == other.Name
&& PropertyNames.SequenceEqual(other.PropertyNames); // <-- Problem Line
}
public override int GetHashCode()
{
var hashCode = (Namespace != null ? Namespace.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ Name.GetHashCode();
hashCode = (hashCode * 397) ^ PropertyNames.GetHashCode(); // <-- Problem Line
return hashCode;
}
}
IncrementalGenerator.Initialize
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("PropsToStringAttribute.g.cs", SourceText.From(AttributeTexts.PropsToStringAttribute, Encoding.UTF8));
ctx.AddSource("PropToStringAttribute.g.cs", SourceText.From(AttributeTexts.PropToStringAttribute, Encoding.UTF8));
ctx.AddSource("PropsToStringIgnoreAttribute.g.cs", SourceText.From(AttributeTexts.PropsToStringIgnoreAttribute, Encoding.UTF8));
});
var classProvider = context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
static (ctx, ct) => GetClassInfoOrNull(ctx, ct)
)
.Where(type => type is not null)
.Collect()
.SelectMany((classes, _) => classes.Distinct());
context.RegisterSourceOutput(classProvider, Generate);
}
GetClassInfoOrNull
internal static ClassInfo? GetClassInfoOrNull(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// We know the node is a ClassDeclarationSyntax thanks to IsSyntaxTargetForGeneration
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
var type = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, classDeclarationSyntax, cancellationToken) as ITypeSymbol;
return IsPropsToString(type) ? new ClassInfo(type!) : null;
}
IsPropsToString
public static bool IsPropsToString(ISymbol? type)
{
return type is not null &&
type.GetAttributes()
.Any(a => a.AttributeClass is
{
Name: ClassAttributeName,
ContainingNamespace:
{
Name: PTSNamespace,
ContainingNamespace.IsGlobalNamespace: true
}
});
}
IsPropsToStringIgnore
public static bool IsPropsToStringIgnore(ISymbol type)
{
return type is not null &&
type.GetAttributes()
.Any(a => a.AttributeClass is
{
Name: PropertyIgnoreAttributeName,
ContainingNamespace:
{
Name: PTSNamespace,
ContainingNamespace.IsGlobalNamespace: true
}
});
}
As a side note, I mostly followed this https://www.thinktecture.com/en/net/roslyn-source-generators-performance/
Edit 9/2/22
I have narrowed down the problem to two lines of code noted above in ClassInfo.Equals
and ClassInfo.GetHashCode
; the two lines that deal with equating the array of names. I commented out those two lines and started to get incremental code generation. However, I wasn't getting new code generation when properties changes (as espected), I instead had to change the name of the class(es) to get new code generated (again, as expected).
Edit 9/7/22 Added project to GitHub
Edit 9/8/22
I tried not using SequenceEquals
to compare my PropertyNames
array, but it didnt work.
public bool Equals(ClassInfo other)
{
if (PropertyNames.Count() != other.PropertyNames.Count())
return false;
int i = 0;
bool propIsEqual = true;
while (propIsEqual && i < PropertyNames.Count())
{
propIsEqual &= SymbolEqualityComparer.Default.Equals(PropertyNames[i], other.PropertyNames[i]);
i++;
}
return Namespace == other.Namespace
&& Name == other.Name
&& propIsEqual;
//PropertyNames.SequenceEqual(other.PropertyNames); // <-- Problem Line
}