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
}
 
    