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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.1.0</VersionPrefix>
<VersionPrefix>1.1.1</VersionPrefix>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/layeredcraft/optimized-enums</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,22 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) =>

cancellationToken.ThrowIfCancellationRequested();

// OE0101: Warn about non-private constructors
foreach (var ctor in classSymbol.Constructors)
// OE0101: Warn about non-private constructors.
// Skip for abstract classes: a protected constructor is idiomatic and required
// so that concrete subclasses can invoke base(...).
if (!classSymbol.IsAbstract)
{
if (ctor.IsImplicitlyDeclared)
continue;
if (ctor.DeclaredAccessibility != Accessibility.Private)
foreach (var ctor in classSymbol.Constructors)
{
diagnostics.Add(new DiagnosticInfo(
DiagnosticDescriptors.NonPrivateConstructor,
ctor.CreateLocationInfo(),
className));
if (ctor.IsImplicitlyDeclared)
continue;
if (ctor.DeclaredAccessibility != Accessibility.Private)
{
diagnostics.Add(new DiagnosticInfo(
DiagnosticDescriptors.NonPrivateConstructor,
ctor.CreateLocationInfo(),
className));
}
}
}

Expand Down Expand Up @@ -121,12 +126,20 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) =>
DetectDuplicateValues(classSymbol, context.SemanticModel, validMembers, diagnostics, className, cancellationToken);

// OE0004: no valid members
// Abstract classes with no eligible members and no diagnostics are intermediate base
// classes — skip silently so they produce no output and no noise.
// If diagnostics were collected (e.g. OE0101, OE0102) we still surface them by falling
// through to the EnumInfo return; the generator skips code emission for empty member lists.
if (validMembers.Count == 0)
{
diagnostics.Add(new DiagnosticInfo(
DiagnosticDescriptors.NoMembersFound,
location,
className));
if (classSymbol.IsAbstract && diagnostics.Count == 0)
return null;

if (!classSymbol.IsAbstract)
diagnostics.Add(new DiagnosticInfo(
DiagnosticDescriptors.NoMembersFound,
location,
className));
}

return new EnumInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,118 @@ private OrderStatus(int value, string name) : base(value, name) { }
},
TestContext.Current.CancellationToken);

[Fact]
public async Task AbstractBase_WithCRTP() =>
await GeneratorTestHelpers.Verify(
new VerifyTestOptions
{
SourceCode = """
using LayeredCraft.OptimizedEnums;

namespace MyApp.Domain;

public abstract partial class DomainEnum<TSelf> : OptimizedEnum<TSelf, int>
where TSelf : DomainEnum<TSelf>
{
public string Description { get; }

protected DomainEnum(int value, string name, string description)
: base(value, name)
{
Description = description;
}
}

public sealed partial class OrderStatus : DomainEnum<OrderStatus>
{
public static readonly OrderStatus Pending = new(1, nameof(Pending), "Order is pending");
public static readonly OrderStatus Paid = new(2, nameof(Paid), "Payment received");

private OrderStatus(int value, string name, string description)
: base(value, name, description) { }
}
""",
ExpectedTrees = 1,
},
TestContext.Current.CancellationToken);

[Fact]
public async Task AbstractBase_Alone_ProducesNoOutput()
{
var options = new VerifyTestOptions
{
SourceCode = """
using LayeredCraft.OptimizedEnums;

namespace MyApp.Domain;

public abstract partial class DomainEnum<TSelf> : OptimizedEnum<TSelf, int>
where TSelf : DomainEnum<TSelf>
{
public string Description { get; }

protected DomainEnum(int value, string name, string description)
: base(value, name)
{
Description = description;
}
}
""",
ExpectedTrees = 0,
};

var (driver, _) = GeneratorTestHelpers.GenerateFromSource(options, TestContext.Current.CancellationToken);

var result = driver.GetRunResult();

result.Diagnostics.Should().BeEmpty("abstract intermediate base class should produce no diagnostics");
result.GeneratedTrees.Length.Should().Be(0, "abstract intermediate base class should produce no generated output");
}

[Fact]
public async Task AbstractEnum_WithMembers_StillGenerates() =>
await GeneratorTestHelpers.Verify(
new VerifyTestOptions
{
SourceCode = """
using LayeredCraft.OptimizedEnums;

namespace MyApp.Domain;

public abstract partial class Status : OptimizedEnum<Status, int>
{
public static readonly Status Active = null!;
public static readonly Status Inactive = null!;

protected Status(int value, string name) : base(value, name) { }
}
""",
ExpectedTrees = 1,
},
TestContext.Current.CancellationToken);

[Fact]
public async Task Warning_AbstractBase_WithOnlyInvalidMembers_StillSurfacesDiagnostics() =>
await GeneratorTestHelpers.VerifyFailure(
new VerifyTestOptions
{
SourceCode = """
using LayeredCraft.OptimizedEnums;

namespace MyApp.Domain;

public abstract partial class DomainEnum<TSelf> : OptimizedEnum<TSelf, int>
where TSelf : DomainEnum<TSelf>
{
public static DomainEnum<TSelf> BadField = null!;

protected DomainEnum(int value, string name) : base(value, name) { }
}
""",
ExpectedDiagnosticId = "OE0102",
},
TestContext.Current.CancellationToken);

[Fact]
public async Task Error_OE0005_DuplicateValue_IsEmitted() =>
await GeneratorTestHelpers.VerifyFailure(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//HintName: MyApp.Domain.OrderStatus.g.cs
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

#nullable enable

namespace MyApp.Domain;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
partial class OrderStatus
{
private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<global::MyApp.Domain.OrderStatus> s_all =
global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[]
{
Pending,
Paid
});

private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<string> s_names =
global::System.Array.AsReadOnly(new string[]
{
Pending.Name,
Paid.Name
});

private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<int> s_values =
global::System.Array.AsReadOnly(new int[]
{
Pending.Value,
Paid.Value
});

private static readonly global::System.Collections.Generic.Dictionary<string, global::MyApp.Domain.OrderStatus> s_byName =
new global::System.Collections.Generic.Dictionary<string, global::MyApp.Domain.OrderStatus>(2, global::System.StringComparer.Ordinal)
{
[Pending.Name] = Pending,
[Paid.Name] = Paid
};

private static readonly global::System.Collections.Generic.Dictionary<int, global::MyApp.Domain.OrderStatus> s_byValue =
new global::System.Collections.Generic.Dictionary<int, global::MyApp.Domain.OrderStatus>(2)
{
[Pending.Value] = Pending,
[Paid.Value] = Paid
};

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::System.Collections.Generic.IReadOnlyList<global::MyApp.Domain.OrderStatus> All => s_all;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::System.Collections.Generic.IReadOnlyList<string> Names => s_names;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::System.Collections.Generic.IReadOnlyList<int> Values => s_values;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public const int Count = 2;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::MyApp.Domain.OrderStatus FromName(string name)
{
if (!s_byName.TryGetValue(name, out var result))
throw new global::System.Collections.Generic.KeyNotFoundException(
$"'{name}' is not a valid name for OrderStatus");

return result;
}

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) =>
s_byName.TryGetValue(name, out result);

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::MyApp.Domain.OrderStatus FromValue(int value)
{
if (!s_byValue.TryGetValue(value, out var result))
throw new global::System.Collections.Generic.KeyNotFoundException(
$"'{value}' is not a valid value for OrderStatus");

return result;
}

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) =>
s_byValue.TryGetValue(value, out result);

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool ContainsName(string name) => s_byName.ContainsKey(name);

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool ContainsValue(int value) => s_byValue.ContainsKey(value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//HintName: MyApp.Domain.Status.g.cs
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

#nullable enable

namespace MyApp.Domain;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
partial class Status
{
private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<global::MyApp.Domain.Status> s_all =
global::System.Array.AsReadOnly(new global::MyApp.Domain.Status[]
{
Active,
Inactive
});

private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<string> s_names =
global::System.Array.AsReadOnly(new string[]
{
Active.Name,
Inactive.Name
});

private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<int> s_values =
global::System.Array.AsReadOnly(new int[]
{
Active.Value,
Inactive.Value
});

private static readonly global::System.Collections.Generic.Dictionary<string, global::MyApp.Domain.Status> s_byName =
new global::System.Collections.Generic.Dictionary<string, global::MyApp.Domain.Status>(2, global::System.StringComparer.Ordinal)
{
[Active.Name] = Active,
[Inactive.Name] = Inactive
};

private static readonly global::System.Collections.Generic.Dictionary<int, global::MyApp.Domain.Status> s_byValue =
new global::System.Collections.Generic.Dictionary<int, global::MyApp.Domain.Status>(2)
{
[Active.Value] = Active,
[Inactive.Value] = Inactive
};

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::System.Collections.Generic.IReadOnlyList<global::MyApp.Domain.Status> All => s_all;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::System.Collections.Generic.IReadOnlyList<string> Names => s_names;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::System.Collections.Generic.IReadOnlyList<int> Values => s_values;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public const int Count = 2;

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::MyApp.Domain.Status FromName(string name)
{
if (!s_byName.TryGetValue(name, out var result))
throw new global::System.Collections.Generic.KeyNotFoundException(
$"'{name}' is not a valid name for Status");

return result;
}

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool TryFromName(string name, out global::MyApp.Domain.Status? result) =>
s_byName.TryGetValue(name, out result);

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static global::MyApp.Domain.Status FromValue(int value)
{
if (!s_byValue.TryGetValue(value, out var result))
throw new global::System.Collections.Generic.KeyNotFoundException(
$"'{value}' is not a valid value for Status");

return result;
}

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool TryFromValue(int value, out global::MyApp.Domain.Status? result) =>
s_byValue.TryGetValue(value, out result);

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool ContainsName(string name) => s_byName.ContainsKey(name);

[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
public static bool ContainsValue(int value) => s_byValue.ContainsKey(value);
}
Loading