diff --git a/NewType.Generator/AliasAttributeSource.cs b/NewType.Generator/AliasAttributeSource.cs
index debf22e..237f7b8 100644
--- a/NewType.Generator/AliasAttributeSource.cs
+++ b/NewType.Generator/AliasAttributeSource.cs
@@ -35,7 +35,7 @@ internal enum NewtypeOptions
/// Suppress implicit conversions and constructor forwarding.
Opaque = NoImplicitConversions | NoConstructorForwarding,
}
-
+
///
/// Marks a partial type as a type alias for the specified type.
/// The source generator will generate implicit conversions, operator forwarding,
@@ -54,7 +54,7 @@ public newtypeAttribute() { }
/// Controls which features the generator emits.
public NewtypeOptions Options { get; set; }
-
+
///
/// Overrides the MethodImplOptions applied to generated members.
/// Default is .
diff --git a/NewType.Generator/AliasCodeGenerator.cs b/NewType.Generator/AliasCodeGenerator.cs
index cc75f93..6fabeb5 100644
--- a/NewType.Generator/AliasCodeGenerator.cs
+++ b/NewType.Generator/AliasCodeGenerator.cs
@@ -18,6 +18,8 @@ internal class AliasCodeGenerator
private readonly AliasModel _model;
private readonly StringBuilder _sb = new();
+ const string SingleIndent = " ";
+
public AliasCodeGenerator(AliasModel model)
{
_model = model;
@@ -137,12 +139,30 @@ private void AppendField()
private void AppendConstructors()
{
- var indent = GetMemberIndent();
+ var memberIndent = GetMemberIndent();
// Constructor from aliased type (always emitted)
- _sb.AppendLine($"{indent}/// Creates a new {_model.TypeName} from a {_model.AliasedTypeMinimalName}.");
- AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public {_model.TypeName}({_model.AliasedTypeFullName} value) => _value = value;");
+ _sb.AppendLine(
+ $"{memberIndent}/// Creates a new {_model.TypeName} from a {_model.AliasedTypeMinimalName}.");
+ AppendMethodImplAttribute(memberIndent);
+
+
+ if (_model.ConstraintModel.UseConstraints)
+ {
+ _sb.AppendLine($"{memberIndent}public {_model.TypeName}({_model.AliasedTypeFullName} value)");
+ _sb.Append(memberIndent).Append('{').AppendLine();
+
+ AppendConstraintChecker(SingleIndent, "value");
+ _sb.Append(SingleIndent).Append(SingleIndent).Append(SingleIndent).AppendLine("_value = value;");
+
+ _sb.Append(memberIndent).Append('}').AppendLine();
+ }
+ else
+ {
+ _sb.AppendLine(
+ $"{memberIndent}public {_model.TypeName}({_model.AliasedTypeFullName} value) => _value = value;");
+ }
+
_sb.AppendLine();
// Forward constructors from the aliased type (conditionally)
@@ -150,7 +170,7 @@ private void AppendConstructors()
{
foreach (var ctor in _model.ForwardedConstructors)
{
- AppendForwardedConstructor(indent, ctor);
+ AppendForwardedConstructor(memberIndent, ctor);
}
}
}
@@ -174,18 +194,22 @@ private void AppendImplicitOperators()
// Implicit from aliased type to alias (T → Alias)
if (!_model.SuppressImplicitWrap)
{
- _sb.AppendLine($"{indent}/// Implicitly converts from {_model.AliasedTypeMinimalName} to {_model.TypeName}.");
+ _sb.AppendLine(
+ $"{indent}/// Implicitly converts from {_model.AliasedTypeMinimalName} to {_model.TypeName}.");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static implicit operator {_model.TypeName}({_model.AliasedTypeFullName} value) => new {_model.TypeName}(value);");
+ _sb.AppendLine(
+ $"{indent}public static implicit operator {_model.TypeName}({_model.AliasedTypeFullName} value) => new {_model.TypeName}(value);");
_sb.AppendLine();
}
// Implicit from alias to aliased type (Alias → T)
if (!_model.SuppressImplicitUnwrap)
{
- _sb.AppendLine($"{indent}/// Implicitly converts from {_model.TypeName} to {_model.AliasedTypeMinimalName}.");
+ _sb.AppendLine(
+ $"{indent}/// Implicitly converts from {_model.TypeName} to {_model.AliasedTypeMinimalName}.");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static implicit operator {_model.AliasedTypeFullName}({_model.TypeName} value) => value._value;");
+ _sb.AppendLine(
+ $"{indent}public static implicit operator {_model.AliasedTypeFullName}({_model.TypeName} value) => value._value;");
_sb.AppendLine();
}
}
@@ -214,7 +238,8 @@ private void AppendBinaryOperators()
var expr1 = WrapIfAlias(op.ReturnIsAliasedType, $"left._value {opSymbol} right._value");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} left, {_model.TypeName} right) => {expr1};");
+ _sb.AppendLine(
+ $"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} left, {_model.TypeName} right) => {expr1};");
_sb.AppendLine();
// Also generate alias op T for cross-type interop
@@ -224,7 +249,8 @@ private void AppendBinaryOperators()
// The implicit conversion to T already covers the T op alias case.
var expr2 = WrapIfAlias(op.ReturnIsAliasedType, $"left._value {opSymbol} right");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} left, {_model.AliasedTypeFullName} right) => {expr2};");
+ _sb.AppendLine(
+ $"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} left, {_model.AliasedTypeFullName} right) => {expr2};");
_sb.AppendLine();
}
// Operator with aliased type on left only — also emit T op Alias
@@ -236,7 +262,8 @@ private void AppendBinaryOperators()
var expr = WrapIfAlias(op.ReturnIsAliasedType, $"left._value {opSymbol} right");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} left, {op.RightTypeFullName} right) => {expr};");
+ _sb.AppendLine(
+ $"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} left, {op.RightTypeFullName} right) => {expr};");
_sb.AppendLine();
}
// Operator with aliased type on right only — also emit Alias op T
@@ -248,7 +275,8 @@ private void AppendBinaryOperators()
var expr = WrapIfAlias(op.ReturnIsAliasedType, $"left {opSymbol} right._value");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {returnTypeStr} operator {opSymbol}({op.LeftTypeFullName} left, {_model.TypeName} right) => {expr};");
+ _sb.AppendLine(
+ $"{indent}public static {returnTypeStr} operator {opSymbol}({op.LeftTypeFullName} left, {_model.TypeName} right) => {expr};");
_sb.AppendLine();
}
}
@@ -266,24 +294,28 @@ private void AppendBinaryOperators()
if (IsShiftOperator(opName))
{
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} left, int right) => new {_model.TypeName}(left._value {opSymbol} right);");
+ _sb.AppendLine(
+ $"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} left, int right) => new {_model.TypeName}(left._value {opSymbol} right);");
_sb.AppendLine();
}
else
{
// Alias op Alias
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} left, {_model.TypeName} right) => new {_model.TypeName}(left._value {opSymbol} right._value);");
+ _sb.AppendLine(
+ $"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} left, {_model.TypeName} right) => new {_model.TypeName}(left._value {opSymbol} right._value);");
_sb.AppendLine();
// Alias op T
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} left, {_model.AliasedTypeFullName} right) => new {_model.TypeName}(left._value {opSymbol} right);");
+ _sb.AppendLine(
+ $"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} left, {_model.AliasedTypeFullName} right) => new {_model.TypeName}(left._value {opSymbol} right);");
_sb.AppendLine();
// T op Alias
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {_model.TypeName} operator {opSymbol}({_model.AliasedTypeFullName} left, {_model.TypeName} right) => new {_model.TypeName}(left {opSymbol} right._value);");
+ _sb.AppendLine(
+ $"{indent}public static {_model.TypeName} operator {opSymbol}({_model.AliasedTypeFullName} left, {_model.TypeName} right) => new {_model.TypeName}(left {opSymbol} right._value);");
_sb.AppendLine();
}
}
@@ -307,7 +339,8 @@ private void AppendUnaryOperators()
var expr = WrapIfAlias(op.ReturnIsAliasedType, $"{opSymbol}value._value");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} value) => {expr};");
+ _sb.AppendLine(
+ $"{indent}public static {returnTypeStr} operator {opSymbol}({_model.TypeName} value) => {expr};");
_sb.AppendLine();
}
@@ -321,7 +354,8 @@ private void AppendUnaryOperators()
if (opSymbol == null) continue;
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} value) => new {_model.TypeName}({opSymbol}value._value);");
+ _sb.AppendLine(
+ $"{indent}public static {_model.TypeName} operator {opSymbol}({_model.TypeName} value) => new {_model.TypeName}({opSymbol}value._value);");
_sb.AppendLine();
}
}
@@ -365,7 +399,8 @@ private void AppendComparisonOperators()
foreach (var op in ops)
{
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator {op}({_model.TypeName} left, {_model.TypeName} right) => left._value {op} right._value;");
+ _sb.AppendLine(
+ $"{indent}public static bool operator {op}({_model.TypeName} left, {_model.TypeName} right) => left._value {op} right._value;");
_sb.AppendLine();
}
@@ -377,15 +412,17 @@ private void AppendComparisonOperators()
{
var isRefType = !_model.AliasedTypeIsValueType;
- string CompareExpr(string op) => isRefType
- ? $"(left._value is null ? (right._value is null ? 0 : -1) : left._value.CompareTo(right._value)) {op} 0"
- : $"left._value.CompareTo(right._value) {op} 0";
+ string CompareExpr(string op) =>
+ isRefType
+ ? $"(left._value is null ? (right._value is null ? 0 : -1) : left._value.CompareTo(right._value)) {op} 0"
+ : $"left._value.CompareTo(right._value) {op} 0";
string[] ops = ["<", ">", "<=", ">="];
foreach (var op in ops)
{
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator {op}({_model.TypeName} left, {_model.TypeName} right) => {CompareExpr(op)};");
+ _sb.AppendLine(
+ $"{indent}public static bool operator {op}({_model.TypeName} left, {_model.TypeName} right) => {CompareExpr(op)};");
_sb.AppendLine();
}
}
@@ -417,39 +454,46 @@ private void AppendEqualityMembers()
// Object.Equals override
_sb.AppendLine($"{indent}/// ");
- _sb.AppendLine($"{indent}public override bool Equals(object? obj) => obj is {_model.TypeName} other && Equals(other);");
+ _sb.AppendLine(
+ $"{indent}public override bool Equals(object? obj) => obj is {_model.TypeName} other && Equals(other);");
_sb.AppendLine();
if (_model.IsClass)
{
// Class types need null-safe equality operators
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator ==({_model.TypeName}? left, {_model.TypeName}? right) => ReferenceEquals(left, right) || (left is not null && left.Equals(right));");
+ _sb.AppendLine(
+ $"{indent}public static bool operator ==({_model.TypeName}? left, {_model.TypeName}? right) => ReferenceEquals(left, right) || (left is not null && left.Equals(right));");
_sb.AppendLine();
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator !=({_model.TypeName}? left, {_model.TypeName}? right) => !(left == right);");
+ _sb.AppendLine(
+ $"{indent}public static bool operator !=({_model.TypeName}? left, {_model.TypeName}? right) => !(left == right);");
_sb.AppendLine();
}
else if (_model.HasNativeEqualityOperator)
{
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator ==({_model.TypeName} left, {_model.TypeName} right) => left._value == right._value;");
+ _sb.AppendLine(
+ $"{indent}public static bool operator ==({_model.TypeName} left, {_model.TypeName} right) => left._value == right._value;");
_sb.AppendLine();
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator !=({_model.TypeName} left, {_model.TypeName} right) => left._value != right._value;");
+ _sb.AppendLine(
+ $"{indent}public static bool operator !=({_model.TypeName} left, {_model.TypeName} right) => left._value != right._value;");
_sb.AppendLine();
}
else
{
// Fallback: route through Equals
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator ==({_model.TypeName} left, {_model.TypeName} right) => left.Equals(right);");
+ _sb.AppendLine(
+ $"{indent}public static bool operator ==({_model.TypeName} left, {_model.TypeName} right) => left.Equals(right);");
_sb.AppendLine();
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public static bool operator !=({_model.TypeName} left, {_model.TypeName} right) => !left.Equals(right);");
+ _sb.AppendLine(
+ $"{indent}public static bool operator !=({_model.TypeName} left, {_model.TypeName} right) => !left.Equals(right);");
_sb.AppendLine();
}
}
@@ -494,7 +538,8 @@ private void AppendStaticMembers()
if (member.IsProperty)
{
- _sb.AppendLine($"{indent}/// Forwards {_model.AliasedTypeMinimalName}.{member.Name}.");
+ _sb.AppendLine(
+ $"{indent}/// Forwards {_model.AliasedTypeMinimalName}.{member.Name}.");
_sb.AppendLine($"{indent}public static {returnTypeStr} {member.Name}");
_sb.AppendLine($"{indent}{{");
AppendMethodImplAttribute($"{indent} ");
@@ -504,7 +549,8 @@ private void AppendStaticMembers()
}
else if (member.IsReadonlyField)
{
- _sb.AppendLine($"{indent}/// Forwards {_model.AliasedTypeMinimalName}.{member.Name}.");
+ _sb.AppendLine(
+ $"{indent}/// Forwards {_model.AliasedTypeMinimalName}.{member.Name}.");
_sb.AppendLine($"{indent}public static {returnTypeStr} {member.Name} => {valueExpr};");
_sb.AppendLine();
}
@@ -518,7 +564,8 @@ private void AppendInstanceMembers()
{
var indent = GetMemberIndent();
- if (_model.InstanceFields.Length == 0 && _model.InstanceProperties.Length == 0 && _model.InstanceMethods.Length == 0)
+ if (_model.InstanceFields.Length == 0 && _model.InstanceProperties.Length == 0
+ && _model.InstanceMethods.Length == 0)
return;
_sb.AppendLine($"{indent}#region Instance Members");
@@ -644,7 +691,8 @@ private void AppendToString()
if (_model.ImplementsIFormattable)
{
_sb.AppendLine($"{indent}/// ");
- _sb.AppendLine($"{indent}public string ToString(string? format, IFormatProvider? formatProvider) => _value.ToString(format, formatProvider);");
+ _sb.AppendLine(
+ $"{indent}public string ToString(string? format, IFormatProvider? formatProvider) => _value.ToString(format, formatProvider);");
_sb.AppendLine();
}
}
@@ -668,7 +716,25 @@ private void AppendForwardedConstructor(string indent, ConstructorInfo ctor)
_sb.AppendLine($"{indent}/// Forwards {_model.AliasedTypeMinimalName} constructor.");
AppendMethodImplAttribute(indent);
- _sb.AppendLine($"{indent}public {_model.TypeName}({parameters}) => _value = new {_model.AliasedTypeFullName}({arguments});");
+ if (_model.ConstraintModel.UseConstraints)
+ {
+ const string valueName = "newValue";
+ _sb.AppendLine($"{indent}public {_model.TypeName}({parameters})");
+ _sb.Append(indent).Append('{').AppendLine();
+ _sb.Append(indent).Append(SingleIndent)
+ .AppendLine($"var {valueName} = new {_model.AliasedTypeFullName}({arguments});");
+
+ AppendConstraintChecker(SingleIndent, valueName);
+ _sb.Append(SingleIndent).Append(SingleIndent).Append(SingleIndent).AppendLine($"_value = {valueName};");
+
+ _sb.Append(indent).Append('}').AppendLine();
+ }
+ else
+ {
+ _sb.AppendLine(
+ $"{indent}public {_model.TypeName}({parameters}) => _value = new {_model.AliasedTypeFullName}({arguments});");
+ }
+
_sb.AppendLine();
}
@@ -691,6 +757,25 @@ private void AppendMethodImplAttribute(string indent)
_sb.AppendLine(line);
}
+ private void AppendConstraintChecker(string indent, string valueName)
+ {
+ if (!_model.ConstraintModel.Valid) return;
+
+ if (!_model.ConstraintModel.InRelease)
+ _sb.AppendLine("#if DEBUG");
+
+ _sb.Append(indent).Append(indent).Append(indent)
+ .AppendLine($"if (!{_model.ConstraintModel.ValidationSymbolName}({valueName}))");
+ _sb.Append(indent).Append(indent).Append(indent).Append(indent)
+ .AppendLine(
+ $"throw new InvalidOperationException($\"Failed validation check when trying to create '{_model.TypeName}' with '{_model.AliasedTypeMinimalName}' value: {{{valueName}}}\");"); // we heard you like interpolation
+
+ if (!_model.ConstraintModel.InRelease)
+ _sb.AppendLine("#endif");
+ else
+ _sb.AppendLine();
+ }
+
private static string FormatConstructorParameters(ConstructorInfo ctor)
{
return string.Join(", ", ctor.Parameters.Array.Select(p =>
@@ -728,7 +813,8 @@ private static string FormatConstructorArguments(ConstructorInfo ctor)
}));
}
- private string GetMemberIndent() => string.IsNullOrEmpty(_model.Namespace) ? " " : " ";
+ private string GetMemberIndent() =>
+ string.IsNullOrEmpty(_model.Namespace) ? SingleIndent : $"{SingleIndent}{SingleIndent}";
private static string? GetOperatorSymbol(string operatorName)
{
@@ -836,4 +922,4 @@ SpecialType.System_Int64 or SpecialType.System_UInt64 or
SpecialType.System_Single or SpecialType.System_Double or
SpecialType.System_Decimal or SpecialType.System_Char;
}
-}
+}
\ No newline at end of file
diff --git a/NewType.Generator/AliasGenerator.cs b/NewType.Generator/AliasGenerator.cs
index 684aa30..893f7b7 100644
--- a/NewType.Generator/AliasGenerator.cs
+++ b/NewType.Generator/AliasGenerator.cs
@@ -15,27 +15,30 @@ public class AliasGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register the attribute source
- context.RegisterPostInitializationOutput(ctx => { ctx.AddSource("newtypeAttribute.g.cs", SourceText.From(NewtypeAttributeSource.Source, Encoding.UTF8)); });
+ context.RegisterPostInitializationOutput(ctx =>
+ {
+ ctx.AddSource("newtypeAttribute.g.cs", SourceText.From(NewtypeAttributeSource.Source, Encoding.UTF8));
+ ctx.AddSource("newtypeConstraintAttribute.g.cs",
+ SourceText.From(ConstraintAttributeSource.Source, Encoding.UTF8));
+ });
// Pipeline for generic [newtype] attribute
- var genericPipeline = context.SyntaxProvider
+ IncrementalValuesProvider genericPipeline = context.SyntaxProvider
.ForAttributeWithMetadataName(
"newtype.newtypeAttribute`1",
predicate: static (node, _) => node is TypeDeclarationSyntax,
transform: static (ctx, _) => ExtractGenericModel(ctx))
- .Where(static model => model is not null)
- .Select(static (model, _) => model!.Value);
+ .Where(static model => model is not null)!;
context.RegisterSourceOutput(genericPipeline, static (spc, model) => GenerateAliasCode(spc, model));
// Pipeline for non-generic [newtype(typeof(T))] attribute
- var nonGenericPipeline = context.SyntaxProvider
+ IncrementalValuesProvider nonGenericPipeline = context.SyntaxProvider
.ForAttributeWithMetadataName(
"newtype.newtypeAttribute",
predicate: static (node, _) => node is TypeDeclarationSyntax,
transform: static (ctx, _) => ExtractNonGenericModel(ctx))
- .Where(static model => model is not null)
- .Select(static (model, _) => model!.Value);
+ .Where(static model => model is not null)!;
context.RegisterSourceOutput(nonGenericPipeline, static (spc, model) => GenerateAliasCode(spc, model));
}
@@ -45,14 +48,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
foreach (var attributeData in context.Attributes)
{
var attributeClass = attributeData.AttributeClass;
- if (attributeClass is {IsGenericType: true} &&
+ if (attributeClass is { IsGenericType: true } &&
attributeClass.TypeArguments.Length == 1)
{
var aliasedType = attributeClass.TypeArguments[0];
- var (options, methodImpl) = ExtractNamedArguments(attributeData);
- return AliasModelExtractor.Extract(context, aliasedType, options, methodImpl);
+ var options = ExtractNamedArguments(attributeData);
+ return AliasModelExtractor.Extract(context, aliasedType, options);
}
}
+
return null;
}
@@ -63,10 +67,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
if (attributeData.ConstructorArguments.Length > 0 &&
attributeData.ConstructorArguments[0].Value is ITypeSymbol aliasedType)
{
- var (options, methodImpl) = ExtractNamedArguments(attributeData);
- return AliasModelExtractor.Extract(context, aliasedType, options, methodImpl);
+ var options = ExtractNamedArguments(attributeData);
+
+ return AliasModelExtractor.Extract(context, aliasedType, options);
}
}
+
return null;
}
@@ -75,7 +81,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
private const int DefaultOptions = 0;
private const int DefaultMethodImplAggressiveInlining = 256;
- private static (int options, int methodImpl) ExtractNamedArguments(AttributeData attributeData)
+ private static ExtractedOptions ExtractNamedArguments(
+ AttributeData attributeData)
{
int options = DefaultOptions;
int methodImpl = DefaultMethodImplAggressiveInlining;
@@ -93,17 +100,58 @@ private static (int options, int methodImpl) ExtractNamedArguments(AttributeData
}
}
- return (options, methodImpl);
+ return new ExtractedOptions(options, methodImpl);
}
private static void GenerateAliasCode(
SourceProductionContext context,
AliasModel model)
{
+ if (!model.ConstraintModel.Valid)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ ValidatorInvalidDiagnostic, model.ConstraintModel.LocationInfo?.ToLocation(),
+ model.TypeName,
+ model.ConstraintModel.ValidationSymbolName ?? "Method",
+ model.AliasedTypeMinimalName
+ ));
+
+ return;
+ }
+
+ if (model.ConstraintModel.Multiple)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(ValidatorMultipleDiagnostic, model.LocationInfo?.ToLocation())
+ );
+ return;
+ }
+
var generator = new AliasCodeGenerator(model);
var source = generator.Generate();
var fileName = $"{model.TypeDisplayString.Replace(".", "_").Replace("<", "_").Replace(">", "_")}.g.cs";
context.AddSource(fileName, SourceText.From(source, Encoding.UTF8));
}
-}
+
+ private static readonly DiagnosticDescriptor ValidatorInvalidDiagnostic =
+ new(
+ id: "NEWTYPE001",
+ title: "Malformed validation method",
+ messageFormat: "Incorrectly formed validation method for type '{0}'. Expected signature: 'bool {1}({2})'.",
+ category: "Unknown",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ private static readonly DiagnosticDescriptor ValidatorMultipleDiagnostic =
+ new(
+ id: "NEWTYPE002",
+ title: "Multiple validators",
+ messageFormat: "Only a single validation method should be used",
+ category: "Unknown",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+}
\ No newline at end of file
diff --git a/NewType.Generator/AliasModel.cs b/NewType.Generator/AliasModel.cs
index 2584186..231291c 100644
--- a/NewType.Generator/AliasModel.cs
+++ b/NewType.Generator/AliasModel.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
namespace newtype.generator;
@@ -8,7 +9,7 @@ namespace newtype.generator;
/// Fully-extracted, equatable model representing a newtype alias.
/// Contains only strings, bools, plain enums, and EquatableArrays — no Roslyn symbols.
///
-internal readonly record struct AliasModel(
+internal record AliasModel(
// Type being declared
string TypeName,
string Namespace,
@@ -16,7 +17,9 @@ internal readonly record struct AliasModel(
bool IsReadonly,
bool IsClass,
bool IsRecord,
- bool IsRecordStruct,
+
+ // Location for messages
+ LocationInfo? LocationInfo,
// Aliased type
string AliasedTypeFullName,
@@ -41,6 +44,7 @@ internal readonly record struct AliasModel(
bool SuppressImplicitUnwrap,
bool SuppressConstructorForwarding,
int MethodImplValue,
+ ConstraintModel ConstraintModel,
// Members
EquatableArray BinaryOperators,
@@ -52,6 +56,8 @@ internal readonly record struct AliasModel(
EquatableArray ForwardedConstructors
);
+internal readonly record struct ExtractedOptions(int Options, int MethodImpl);
+
internal readonly record struct BinaryOperatorInfo(
string Name,
string LeftTypeFullName,
@@ -116,3 +122,12 @@ internal readonly record struct ConstructorParameterInfo(
bool IsParams,
string? DefaultValueLiteral
) : IEquatable;
+
+internal readonly record struct LocationInfo(
+ string FilePath,
+ TextSpan TextSpan,
+ LinePositionSpan LineSpan
+) : IEquatable
+{
+ public Location ToLocation() => Location.Create(FilePath, TextSpan, LineSpan);
+}
\ No newline at end of file
diff --git a/NewType.Generator/AliasModelExtractor.cs b/NewType.Generator/AliasModelExtractor.cs
index da34af1..e066c01 100644
--- a/NewType.Generator/AliasModelExtractor.cs
+++ b/NewType.Generator/AliasModelExtractor.cs
@@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
namespace newtype.generator;
@@ -17,25 +18,23 @@ internal static class AliasModelExtractor
private const int OptionsNoImplicitUnwrap = 2;
private const int OptionsNoConstructorForwarding = 4;
- public static AliasModel? Extract(
+ public static AliasModel? Extract(
GeneratorAttributeSyntaxContext context,
ITypeSymbol aliasedType,
- int options,
- int methodImpl)
+ ExtractedOptions allOptions)
{
var typeDecl = (TypeDeclarationSyntax)context.TargetNode;
var typeSymbol = (INamedTypeSymbol)context.TargetSymbol;
var typeName = typeSymbol.Name;
var ns = typeSymbol.ContainingNamespace;
- var namespaceName = ns is {IsGlobalNamespace: false} ? ns.ToDisplayString() : "";
+ var namespaceName = ns is { IsGlobalNamespace: false } ? ns.ToDisplayString() : "";
var isReadonly = typeDecl.Modifiers.Any(SyntaxKind.ReadOnlyKeyword);
var isClass = typeDecl is ClassDeclarationSyntax
|| (typeDecl is RecordDeclarationSyntax rds
&& !rds.ClassOrStructKeyword.IsKind(SyntaxKind.StructKeyword));
var isRecord = typeDecl is RecordDeclarationSyntax;
- var isRecordStruct = isRecord && !isClass;
var aliasedTypeFullName = aliasedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var aliasedTypeMinimalName = aliasedType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
@@ -56,6 +55,8 @@ internal static class AliasModelExtractor
var typeDisplayString = typeSymbol.ToDisplayString();
+ var constraintModel = ExtractValidationMethod(typeSymbol, aliasedType);
+
return new AliasModel(
TypeName: typeName,
Namespace: namespaceName,
@@ -63,7 +64,7 @@ internal static class AliasModelExtractor
IsReadonly: isReadonly,
IsClass: isClass,
IsRecord: isRecord,
- IsRecordStruct: isRecordStruct,
+ LocationInfo: ToLocationStruct(typeSymbol.Locations.FirstOrDefault()),
AliasedTypeFullName: aliasedTypeFullName,
AliasedTypeMinimalName: aliasedTypeMinimalName,
AliasedTypeSpecialType: aliasedType.SpecialType,
@@ -73,10 +74,11 @@ internal static class AliasModelExtractor
HasNativeEqualityOperator: hasNativeEquality,
TypeDisplayString: typeDisplayString,
HasStaticMemberCandidates: hasStaticMemberCandidates,
- SuppressImplicitWrap: (options & OptionsNoImplicitWrap) != 0,
- SuppressImplicitUnwrap: (options & OptionsNoImplicitUnwrap) != 0,
- SuppressConstructorForwarding: (options & OptionsNoConstructorForwarding) != 0,
- MethodImplValue: methodImpl,
+ SuppressImplicitWrap: (allOptions.Options & OptionsNoImplicitWrap) != 0,
+ SuppressImplicitUnwrap: (allOptions.Options & OptionsNoImplicitUnwrap) != 0,
+ SuppressConstructorForwarding: (allOptions.Options & OptionsNoConstructorForwarding) != 0,
+ MethodImplValue: allOptions.MethodImpl,
+ ConstraintModel: constraintModel,
BinaryOperators: binaryOperators,
UnaryOperators: unaryOperators,
StaticMembers: staticMembers,
@@ -355,6 +357,54 @@ private static string GetConstructorSignature(IMethodSymbol ctor)
}));
}
+ private static ConstraintModel ExtractValidationMethod(ITypeSymbol targetType,
+ ITypeSymbol aliasedType)
+ {
+ IMethodSymbol? validationMethod = null;
+ Location? location = null;
+ bool invalid = false;
+ bool multiple = false;
+ bool inRelease = false;
+
+ foreach (var method in targetType.GetMembers().OfType())
+ {
+ foreach (var attributeData in method.GetAttributes().Where(x => x is not null))
+ {
+ if (attributeData.AttributeClass!.Name == ConstraintAttributeSource.AttributeName)
+ {
+ foreach (var arg in attributeData.NamedArguments)
+ {
+ if (arg.Key == "IncludeInRelease")
+ {
+ inRelease = (bool)arg.Value.Value!;
+ }
+ }
+
+ // doesn't have to be static
+ var methodValid = method.ReturnType.SpecialType == SpecialType.System_Boolean &&
+ method.Parameters.Length == 1 &&
+ SymbolEqualityComparer.Default.Equals(
+ method.Parameters[0].Type,
+ aliasedType);
+
+ invalid |= !methodValid;
+
+ if (validationMethod == null)
+ {
+ validationMethod = method;
+ location = method.Locations[0];
+ }
+ else
+ {
+ multiple = true;
+ }
+ }
+ }
+ }
+
+ return new ConstraintModel(validationMethod?.Name, inRelease, !invalid, multiple, ToLocationStruct(location));
+ }
+
private static string FormatDefaultValue(IParameterSymbol param)
{
var value = param.ExplicitDefaultValue;
@@ -416,4 +466,24 @@ private static bool ImplementsInterface(ITypeSymbol type, string interfaceFullNa
return type.AllInterfaces.Any(i => i.ToDisplayString() == interfaceFullName);
}
+
+ private static LocationInfo? ToLocationStruct(Location? location) =>
+ location is not null && location.IsInSource ?
+ new LocationInfo(
+ location.SourceTree.FilePath,
+ location.SourceSpan,
+ new LinePositionSpan(
+ location.GetLineSpan().StartLinePosition,
+ location.GetLineSpan().EndLinePosition))
+ :null;
}
+
+internal record ConstraintModel(
+ string? ValidationSymbolName,
+ bool InRelease,
+ bool Valid,
+ bool Multiple,
+ LocationInfo? LocationInfo)
+{
+ public bool UseConstraints => ValidationSymbolName is not null && Valid && !Multiple;
+};
\ No newline at end of file
diff --git a/NewType.Generator/AliasValidationAttributeSource.cs b/NewType.Generator/AliasValidationAttributeSource.cs
new file mode 100644
index 0000000..f24749c
--- /dev/null
+++ b/NewType.Generator/AliasValidationAttributeSource.cs
@@ -0,0 +1,33 @@
+namespace newtype.generator;
+
+///
+/// Source text for the [Alias] attribute that gets injected into user compilations.
+///
+internal static class ConstraintAttributeSource
+{
+ public const string AttributeName = "newtypeValidationAttribute";
+
+ public const string Source = """
+ //
+ #nullable enable
+
+ ///
+ /// Used to mark validation methods for aliased types that are called upon construction.
+ ///
+ namespace newtype
+ {
+ [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+ [global::System.Diagnostics.Conditional("newtype_GENERATOR")]
+ internal sealed class newtypeValidationAttribute : global::System.Attribute
+ {
+ ///
+ /// Creates a new validation attribute.
+ ///
+ public newtypeValidationAttribute() { }
+
+ /// Whether the validation method should remain in release or be stripped.
+ public bool IncludeInRelease { get; set; }
+ }
+ }
+ """;
+}
\ No newline at end of file
diff --git a/NewType.Generator/AnalyzerReleases.Shipped.md b/NewType.Generator/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..60b59dd
--- /dev/null
+++ b/NewType.Generator/AnalyzerReleases.Shipped.md
@@ -0,0 +1,3 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/NewType.Generator/AnalyzerReleases.Unshipped.md b/NewType.Generator/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..20b0ef7
--- /dev/null
+++ b/NewType.Generator/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,9 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+| Rule ID | Category | Severity | Notes |
+|------------|-----------|----------|----------------|
+| NEWTYPE001 | `Unknown` | Error | AliasGenerator |
+| NEWTYPE002 | `Unknown` | Error | AliasGenerator |
diff --git a/NewType.Generator/NewType.Generator.csproj b/NewType.Generator/NewType.Generator.csproj
index 60e8b63..4de361b 100644
--- a/NewType.Generator/NewType.Generator.csproj
+++ b/NewType.Generator/NewType.Generator.csproj
@@ -49,5 +49,9 @@
+
+
+
+
diff --git a/NewType.Tests/ConstraintValidationTests.cs b/NewType.Tests/ConstraintValidationTests.cs
new file mode 100644
index 0000000..e1296b0
--- /dev/null
+++ b/NewType.Tests/ConstraintValidationTests.cs
@@ -0,0 +1,62 @@
+using System.Numerics;
+using newtype.tests;
+using Xunit;
+
+public class ConstraintValidationTests
+{
+ [Fact]
+ public void Direction_Valid()
+ {
+ //doesn't throw
+ var dir = new Direction(new Vector2(0, 1));
+ }
+
+ [Fact]
+ public void Direction_ValidImplicit()
+ {
+ //doesn't throw
+ Direction dir = new Vector2(0, 1);
+ }
+
+ [Fact]
+ public void Direction_Valid_Forwarded()
+ {
+ //doesn't throw
+ var dir = new Direction(0, 1);
+ }
+
+ [Fact]
+ public void Direction_CreateInvalid_Throws()
+ {
+ Assert.Throws(() => new Direction(new Vector2(999, 999)));
+ }
+
+ [Fact]
+ public void Direction_CreateInvalidImplicit_Throws()
+ {
+ Assert.Throws(() => (Direction)new Vector2(999, 999));
+ }
+
+ [Fact]
+ public void Direction_CreateInvalidForwarded_Throws()
+ {
+ Assert.Throws(() => new Direction(999, 999));
+ }
+
+ [Fact]
+ public void Direction_ForwardedOperation_ValidResult()
+ {
+ var dir1 = new Direction(new Vector2(0, 1f));
+ var dir2 = new Direction(new Vector2(0, 1f));
+ var dir3 = dir1 * dir2;
+ }
+
+ [Fact]
+ public void Direction_ForwardedOperation_InvalidResult_Throws()
+ {
+ var dir1 = new Direction(new Vector2(0, 1f));
+ var dir2 = new Direction(new Vector2(0, 1f));
+
+ Assert.Throws(() => dir1 + dir2);
+ }
+}
\ No newline at end of file
diff --git a/NewType.Tests/Types.cs b/NewType.Tests/Types.cs
index 7f3673a..d9597f0 100644
--- a/NewType.Tests/Types.cs
+++ b/NewType.Tests/Types.cs
@@ -8,6 +8,18 @@ namespace newtype.tests;
[newtype]
public readonly partial struct Velocity;
+[newtype]
+public readonly partial struct Direction
+{
+ [newtypeValidation]
+ private static bool IsNormalized(Vector2 direction)
+ {
+ // enforce normalized
+ const float epsilon = 0.0001f;
+ return Math.Abs(direction.LengthSquared() - 1f) < epsilon;
+ }
+}
+
[newtype]
public readonly partial struct Scale;
@@ -103,6 +115,5 @@ public partial class DisplayName;
// "Extending Your Types" example — custom cross-type operator
public readonly partial struct Position
{
- public static Position operator +(Position p, Velocity v)
- => new(p.Value + v.Value);
+ public static Position operator +(Position p, Velocity v) => new(p.Value + v.Value);
}
\ No newline at end of file