diff --git a/src/PatternKit.Generators/Composite/CompositeGenerator.cs b/src/PatternKit.Generators/Composite/CompositeGenerator.cs index a9f68c68..e6a971a2 100644 --- a/src/PatternKit.Generators/Composite/CompositeGenerator.cs +++ b/src/PatternKit.Generators/Composite/CompositeGenerator.cs @@ -131,7 +131,8 @@ private static string RenderComposite(INamedTypeSymbol component, string compone sb.AppendLine("{"); foreach (var property in GetContractProperties(component)) { - sb.Append(" public abstract ").Append(property.Type.ToDisplayString(TypeFormat)).Append(' ').Append(property.Name).Append(" { get; }").AppendLine(); + sb.Append(component.TypeKind == TypeKind.Class ? " public abstract override " : " public abstract ") + .Append(property.Type.ToDisplayString(TypeFormat)).Append(' ').Append(property.Name).Append(" { get; }").AppendLine(); sb.AppendLine(); } sb.AppendLine(" public virtual bool IsLeaf => true;"); diff --git a/test/PatternKit.Generators.Tests/CompositeGeneratorTests.cs b/test/PatternKit.Generators.Tests/CompositeGeneratorTests.cs index a19d3fba..cfd27235 100644 --- a/test/PatternKit.Generators.Tests/CompositeGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/CompositeGeneratorTests.cs @@ -51,6 +51,47 @@ public sealed class CategoryNode : CategoryCompositeBase ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); } + [Scenario("GeneratesCompositeBasesForAbstractClassWithCustomNames")] + [Fact] + public void GeneratesCompositeBasesForAbstractClassWithCustomNames() + { + const string source = """ + using PatternKit.Generators.Composite; + + namespace TestNamespace; + + [CompositeComponent( + ComponentBaseName = "MenuItemBase", + CompositeBaseName = "MenuGroupBase", + ChildrenPropertyName = "Items")] + public abstract partial class MenuItem + { + public abstract string Name { get; } + + [CompositeIgnore] + public abstract int SortOrder { get; } + } + """; + + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GeneratesCompositeBasesForAbstractClassWithCustomNames)); + var gen = new CompositeGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated); + + ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics)); + + var generated = ScenarioExpect.Single(result.Results.SelectMany(r => r.GeneratedSources)); + var text = generated.SourceText.ToString(); + ScenarioExpect.Equal("MenuItem.Composite.g.cs", generated.HintName); + ScenarioExpect.Contains("public abstract partial class MenuItemBase : global::TestNamespace.MenuItem", text); + ScenarioExpect.Contains("public virtual global::System.Collections.Generic.IReadOnlyList Items", text); + ScenarioExpect.Contains("public abstract override string Name { get; }", text); + ScenarioExpect.DoesNotContain("SortOrder", text); + ScenarioExpect.Contains("public abstract partial class MenuGroupBase : MenuItemBase", text); + + var emit = updated.Emit(Stream.Null); + ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); + } + [Scenario("ReportsDiagnosticWhenComponentIsNotPartial")] [Fact] public void ReportsDiagnosticWhenComponentIsNotPartial() @@ -97,4 +138,55 @@ public partial class Category var diags = result.Results.SelectMany(r => r.Diagnostics); ScenarioExpect.Contains(diags, d => d.Id == "PKCMP002"); } + + [Scenario("ReportsDiagnosticWhenCompositeContractHasEvent")] + [Fact] + public void ReportsDiagnosticWhenCompositeContractHasEvent() + { + const string source = """ + using System; + using PatternKit.Generators.Composite; + + namespace TestNamespace; + + [CompositeComponent] + public partial interface ICategory + { + event EventHandler? Changed; + } + """; + + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ReportsDiagnosticWhenCompositeContractHasEvent)); + var gen = new CompositeGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var result, out _); + + var diags = result.Results.SelectMany(r => r.Diagnostics); + ScenarioExpect.Contains(diags, d => d.Id == "PKCMP004"); + } + + [Scenario("ReportsDiagnosticWhenCompositeGeneratedNameConflicts")] + [Fact] + public void ReportsDiagnosticWhenCompositeGeneratedNameConflicts() + { + const string source = """ + using PatternKit.Generators.Composite; + + namespace TestNamespace; + + public abstract class CategoryComponentBase; + + [CompositeComponent] + public partial interface ICategory + { + string Name { get; } + } + """; + + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ReportsDiagnosticWhenCompositeGeneratedNameConflicts)); + var gen = new CompositeGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var result, out _); + + var diags = result.Results.SelectMany(r => r.Diagnostics); + ScenarioExpect.Contains(diags, d => d.Id == "PKCMP003"); + } } diff --git a/test/PatternKit.Generators.Tests/FlyweightGeneratorTests.cs b/test/PatternKit.Generators.Tests/FlyweightGeneratorTests.cs index f3ccfd98..d8b2fb96 100644 --- a/test/PatternKit.Generators.Tests/FlyweightGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/FlyweightGeneratorTests.cs @@ -37,6 +37,39 @@ public readonly partial record struct Glyph(char Value) ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); } + [Scenario("GeneratesFlyweightCacheWithoutTryGet")] + [Fact] + public void GeneratesFlyweightCacheWithoutTryGet() + { + const string source = """ + using PatternKit.Generators.Flyweight; + + namespace TestNamespace; + + [Flyweight(typeof(string), CacheTypeName = "TokenCache", GenerateTryGet = false)] + public sealed partial record class Token(string Value) + { + [FlyweightFactory] + private static Token Create(string key) => new(key); + } + """; + + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GeneratesFlyweightCacheWithoutTryGet)); + var gen = new FlyweightGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated); + + ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics)); + + var generated = result.Results.SelectMany(r => r.GeneratedSources).Single(s => s.HintName == "Token.Flyweight.g.cs").SourceText.ToString(); + ScenarioExpect.Contains("partial record class Token", generated); + ScenarioExpect.Contains("public sealed partial class TokenCache", generated); + ScenarioExpect.Contains("public global::TestNamespace.Token Get(string key)", generated); + ScenarioExpect.DoesNotContain("public bool TryGet(", generated); + + var emit = updated.Emit(Stream.Null); + ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); + } + [Scenario("ReportsMissingFactory")] [Fact] public void ReportsMissingFactory() @@ -83,6 +116,61 @@ public readonly partial record struct Glyph(char Value) ScenarioExpect.Contains(diags, d => d.Id == "PKFLY006"); } + [Scenario("ReportsMultipleFlyweightFactories")] + [Fact] + public void ReportsMultipleFlyweightFactories() + { + const string source = """ + using PatternKit.Generators.Flyweight; + + namespace TestNamespace; + + [Flyweight(typeof(string))] + public readonly partial record struct Glyph(char Value) + { + [FlyweightFactory] + private static Glyph Create(string key) => new(key[0]); + + [FlyweightFactory] + private static Glyph CreateAlternate(string key) => new(key[^1]); + } + """; + + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ReportsMultipleFlyweightFactories)); + var gen = new FlyweightGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var result, out _); + + var diags = result.Results.SelectMany(r => r.Diagnostics); + ScenarioExpect.Contains(diags, d => d.Id == "PKFLY003"); + } + + [Scenario("ReportsFlyweightCacheNameConflict")] + [Fact] + public void ReportsFlyweightCacheNameConflict() + { + const string source = """ + using PatternKit.Generators.Flyweight; + + namespace TestNamespace; + + public sealed class GlyphFlyweightCache; + + [Flyweight(typeof(string))] + public readonly partial record struct Glyph(char Value) + { + [FlyweightFactory] + private static Glyph Create(string key) => new(key[0]); + } + """; + + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ReportsFlyweightCacheNameConflict)); + var gen = new FlyweightGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var result, out _); + + var diags = result.Results.SelectMany(r => r.Diagnostics); + ScenarioExpect.Contains(diags, d => d.Id == "PKFLY005"); + } + [Scenario("ReportsNonPartialAndNonStaticFactory")] [Fact] public void ReportsNonPartialAndNonStaticFactory() @@ -115,4 +203,36 @@ public readonly partial record struct NonStaticFactoryGlyph(char Value) ScenarioExpect.Contains(diags, d => d.Id == "PKFLY001"); ScenarioExpect.Contains(diags, d => d.Id == "PKFLY004"); } + + [Scenario("ReportsInvalidFlyweightFactoryForWrongKeyAndReturnTypes")] + [Fact] + public void ReportsInvalidFlyweightFactoryForWrongKeyAndReturnTypes() + { + const string source = """ + using PatternKit.Generators.Flyweight; + + namespace TestNamespace; + + [Flyweight(typeof(string))] + public readonly partial record struct WrongKeyGlyph(char Value) + { + [FlyweightFactory] + private static WrongKeyGlyph Create(int key) => new((char)key); + } + + [Flyweight(typeof(string))] + public readonly partial record struct WrongReturnGlyph(char Value) + { + [FlyweightFactory] + private static string Create(string key) => key; + } + """; + + var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ReportsInvalidFlyweightFactoryForWrongKeyAndReturnTypes)); + var gen = new FlyweightGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var result, out _); + + var diags = result.Results.SelectMany(r => r.Diagnostics).ToArray(); + ScenarioExpect.Equal(2, diags.Count(d => d.Id == "PKFLY004")); + } }