From 7faa613eafd3aef90497748dc36a67279a0da77a Mon Sep 17 00:00:00 2001
From: tvenclovas96_bigblackc <144885265+tvenclovas96@users.noreply.github.com>
Date: Sun, 8 Feb 2026 23:17:40 +0200
Subject: [PATCH 1/2] constraints
---
NewType.Generator/AliasAttributeSource.cs | 26 +++++++-
NewType.Generator/AliasCodeGenerator.cs | 65 ++++++++++++++++--
NewType.Generator/AliasGenerator.cs | 66 ++++++++++++++-----
NewType.Generator/AliasModel.cs | 19 ++++--
NewType.Generator/AliasModelExtractor.cs | 43 +++++++++---
NewType.Generator/AnalyzerReleases.Shipped.md | 3 +
.../AnalyzerReleases.Unshipped.md | 8 +++
NewType.Generator/NewType.Generator.csproj | 4 ++
NewType.Tests/ConstraintValidationTests.cs | 62 +++++++++++++++++
NewType.Tests/Types.cs | 14 +++-
10 files changed, 270 insertions(+), 40 deletions(-)
create mode 100644 NewType.Generator/AnalyzerReleases.Shipped.md
create mode 100644 NewType.Generator/AnalyzerReleases.Unshipped.md
create mode 100644 NewType.Tests/ConstraintValidationTests.cs
diff --git a/NewType.Generator/AliasAttributeSource.cs b/NewType.Generator/AliasAttributeSource.cs
index debf22e..dc04e74 100644
--- a/NewType.Generator/AliasAttributeSource.cs
+++ b/NewType.Generator/AliasAttributeSource.cs
@@ -35,7 +35,26 @@ internal enum NewtypeOptions
/// Suppress implicit conversions and constructor forwarding.
Opaque = NoImplicitConversions | NoConstructorForwarding,
}
-
+
+ ///
+ /// Controls which constraint-related features the newtype generator emits. If enabled,
+ /// will automatically call a user-defined 'bool IsValid(AliasedType value)' method on the newtype
+ /// to verify it is valid
+ ///
+ [global::System.Flags]
+ internal enum NewtypeConstraintOptions
+ {
+ /// Constraints disabled(default).
+ Disabled = 0,
+ /// Enable constraints. Debug builds only by default
+ Enabled = 1,
+ /// Include constraint code in release builds, if constraints are enabled
+ IncludeInRelease = 2,
+
+ /// Enable constraints and include in release builds.
+ ReleaseEnabled = Enabled | IncludeInRelease,
+ }
+
///
/// Marks a partial type as a type alias for the specified type.
/// The source generator will generate implicit conversions, operator forwarding,
@@ -54,7 +73,10 @@ public newtypeAttribute() { }
/// Controls which features the generator emits.
public NewtypeOptions Options { get; set; }
-
+
+ /// Controls which constraint features the generator emits.
+ public NewtypeConstraintOptions ConstraintOptions { 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..ff9b5a2 100644
--- a/NewType.Generator/AliasCodeGenerator.cs
+++ b/NewType.Generator/AliasCodeGenerator.cs
@@ -18,6 +18,9 @@ internal class AliasCodeGenerator
private readonly AliasModel _model;
private readonly StringBuilder _sb = new();
+ const string SingleIndent = " ";
+
+
public AliasCodeGenerator(AliasModel model)
{
_model = model;
@@ -137,12 +140,28 @@ 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.IncludeConstraints)
+ {
+ _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 +169,7 @@ private void AppendConstructors()
{
foreach (var ctor in _model.ForwardedConstructors)
{
- AppendForwardedConstructor(indent, ctor);
+ AppendForwardedConstructor(memberIndent, ctor);
}
}
}
@@ -668,7 +687,22 @@ 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.IncludeConstraints)
+ {
+ 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 +725,23 @@ private void AppendMethodImplAttribute(string indent)
_sb.AppendLine(line);
}
+ private void AppendConstraintChecker(string indent, string valueName)
+ {
+ if (!_model.validValidationMethod) return;
+
+ if (_model.DebugOnlyConstraints)
+ _sb.AppendLine("#if DEBUG");
+
+ _sb.Append(indent).Append(indent).Append(indent)
+ .AppendLine($"if (!IsValid({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.DebugOnlyConstraints)
+ _sb.AppendLine("#endif");
+ }
+
private static string FormatConstructorParameters(ConstructorInfo ctor)
{
return string.Join(", ", ctor.Parameters.Array.Select(p =>
@@ -728,7 +779,7 @@ 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)
{
diff --git a/NewType.Generator/AliasGenerator.cs b/NewType.Generator/AliasGenerator.cs
index 684aa30..a1e7379 100644
--- a/NewType.Generator/AliasGenerator.cs
+++ b/NewType.Generator/AliasGenerator.cs
@@ -12,30 +12,42 @@ namespace newtype.generator;
[Generator(LanguageNames.CSharp)]
public class AliasGenerator : IIncrementalGenerator
{
+ private static readonly DiagnosticDescriptor MissingIsValidMethodDiagnostic =
+ new DiagnosticDescriptor(
+ id: "NEWTYPE001",
+ title: "Missing validation method",
+ messageFormat: $"Type '{{0}}' uses constraints but does not define a compatible validation method. Expected signature: 'bool {AliasModel.ConstraintValidationMethodSymbol}({{1}})'.",
+ category: "Unknown",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "A constraint-enabled wrapped type must define a validation method."
+ );
+
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));
+ });
// 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 +57,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,21 +76,26 @@ 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;
}
// Mirrors MethodImplOptions.AggressiveInlining — the generator can't reference
// the injected NewtypeOptions enum, and we want readable defaults.
private const int DefaultOptions = 0;
+ private const int DefaultConstraintOptions = 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 constraintOptions = DefaultConstraintOptions;
int methodImpl = DefaultMethodImplAggressiveInlining;
foreach (var arg in attributeData.NamedArguments)
@@ -87,23 +105,39 @@ private static (int options, int methodImpl) ExtractNamedArguments(AttributeData
case "Options":
options = (int)arg.Value.Value!;
break;
+ case "ConstraintOptions":
+ constraintOptions = (int)arg.Value.Value!;
+ break;
case "MethodImpl":
methodImpl = (int)arg.Value.Value!;
break;
}
}
- return (options, methodImpl);
+ return new ExtractedOptions(options, constraintOptions, methodImpl);
}
private static void GenerateAliasCode(
SourceProductionContext context,
AliasModel model)
{
+ if (model.IncludeConstraints && !model.validValidationMethod)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ MissingIsValidMethodDiagnostic,
+ model.Location,
+ model.TypeName,
+ model.AliasedTypeFullName
+ ));
+
+ 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));
}
-}
+}
\ No newline at end of file
diff --git a/NewType.Generator/AliasModel.cs b/NewType.Generator/AliasModel.cs
index 2584186..7279467 100644
--- a/NewType.Generator/AliasModel.cs
+++ b/NewType.Generator/AliasModel.cs
@@ -8,7 +8,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,8 +16,10 @@ internal readonly record struct AliasModel(
bool IsReadonly,
bool IsClass,
bool IsRecord,
- bool IsRecordStruct,
-
+
+ // Location for messages
+ Location? Location,
+
// Aliased type
string AliasedTypeFullName,
string AliasedTypeMinimalName,
@@ -41,6 +43,10 @@ internal readonly record struct AliasModel(
bool SuppressImplicitUnwrap,
bool SuppressConstructorForwarding,
int MethodImplValue,
+
+ bool IncludeConstraints,
+ bool DebugOnlyConstraints,
+ bool validValidationMethod,
// Members
EquatableArray BinaryOperators,
@@ -50,7 +56,12 @@ internal readonly record struct AliasModel(
EquatableArray InstanceProperties,
EquatableArray InstanceMethods,
EquatableArray ForwardedConstructors
-);
+)
+{
+ public const string ConstraintValidationMethodSymbol = "IsValid";
+};
+
+internal readonly record struct ExtractedOptions(int Options, int ConstraintOptions, int MethodImpl);
internal readonly record struct BinaryOperatorInfo(
string Name,
diff --git a/NewType.Generator/AliasModelExtractor.cs b/NewType.Generator/AliasModelExtractor.cs
index da34af1..9660e07 100644
--- a/NewType.Generator/AliasModelExtractor.cs
+++ b/NewType.Generator/AliasModelExtractor.cs
@@ -16,16 +16,17 @@ internal static class AliasModelExtractor
private const int OptionsNoImplicitWrap = 1;
private const int OptionsNoImplicitUnwrap = 2;
private const int OptionsNoConstructorForwarding = 4;
+ private const int OptionsUseConstraints = 1;
+ private const int OptionsConstraintsInRelease = 2;
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() : "";
@@ -35,7 +36,6 @@ internal static class AliasModelExtractor
|| (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);
@@ -55,7 +55,11 @@ internal static class AliasModelExtractor
var constructors = ExtractForwardableConstructors(typeSymbol, aliasedType);
var typeDisplayString = typeSymbol.ToDisplayString();
+ var validIsValid = HasValidIsValid(typeSymbol, aliasedType);
+ var useConstraints = (allOptions.ConstraintOptions & OptionsUseConstraints) != 0;
+
+
return new AliasModel(
TypeName: typeName,
Namespace: namespaceName,
@@ -63,7 +67,7 @@ internal static class AliasModelExtractor
IsReadonly: isReadonly,
IsClass: isClass,
IsRecord: isRecord,
- IsRecordStruct: isRecordStruct,
+ Location: typeSymbol.Locations.FirstOrDefault(),
AliasedTypeFullName: aliasedTypeFullName,
AliasedTypeMinimalName: aliasedTypeMinimalName,
AliasedTypeSpecialType: aliasedType.SpecialType,
@@ -73,10 +77,13 @@ 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,
+ IncludeConstraints: useConstraints,
+ DebugOnlyConstraints: (allOptions.ConstraintOptions & OptionsConstraintsInRelease) == 0, // inverse
+ validValidationMethod: validIsValid,
BinaryOperators: binaryOperators,
UnaryOperators: unaryOperators,
StaticMembers: staticMembers,
@@ -355,6 +362,24 @@ private static string GetConstructorSignature(IMethodSymbol ctor)
}));
}
+ private static bool HasValidIsValid(ITypeSymbol targetType, ITypeSymbol aliasedType)
+ {
+ // does not have to be static, even it is more "hygienic"
+ // seems like it would a be a little annoying to enforce if not strictly needed
+ IMethodSymbol? isValidMethod =
+ targetType
+ .GetMembers(AliasModel.ConstraintValidationMethodSymbol)
+ .OfType()
+ .FirstOrDefault(m =>
+ m.ReturnType.SpecialType == SpecialType.System_Boolean &&
+ m.Parameters.Length == 1 &&
+ SymbolEqualityComparer.Default.Equals(
+ m.Parameters[0].Type,
+ aliasedType));
+
+ return isValidMethod != null;
+ }
+
private static string FormatDefaultValue(IParameterSymbol param)
{
var value = param.ExplicitDefaultValue;
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..cf30ff2
--- /dev/null
+++ b/NewType.Generator/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,8 @@
+; 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 |
\ No newline at end of file
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..2049999 100644
--- a/NewType.Tests/Types.cs
+++ b/NewType.Tests/Types.cs
@@ -8,6 +8,17 @@ namespace newtype.tests;
[newtype]
public readonly partial struct Velocity;
+[newtype(ConstraintOptions = NewtypeConstraintOptions.ReleaseEnabled)]
+public readonly partial struct Direction
+{
+ private static bool IsValid(Vector2 direction)
+ {
+ // enforce normalized
+ const float epsilon = 0.0001f;
+ return Math.Abs(direction.LengthSquared() - 1f) < epsilon;
+ }
+}
+
[newtype]
public readonly partial struct Scale;
@@ -103,6 +114,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
From 38b359e3bf8d1f43c8ecd496f58ee60bfd304841 Mon Sep 17 00:00:00 2001
From: tvenclovas96_bigblackc <144885265+tvenclovas96@users.noreply.github.com>
Date: Sat, 28 Feb 2026 21:02:48 +0200
Subject: [PATCH 2/2] constraints with attribute
---
NewType.Generator/AliasAttributeSource.cs | 24 +--
NewType.Generator/AliasCodeGenerator.cs | 155 +++++++++++-------
NewType.Generator/AliasGenerator.cs | 60 ++++---
NewType.Generator/AliasModel.cs | 28 ++--
NewType.Generator/AliasModelExtractor.cs | 101 ++++++++----
.../AliasValidationAttributeSource.cs | 33 ++++
.../AnalyzerReleases.Unshipped.md | 3 +-
NewType.Tests/Types.cs | 5 +-
8 files changed, 260 insertions(+), 149 deletions(-)
create mode 100644 NewType.Generator/AliasValidationAttributeSource.cs
diff --git a/NewType.Generator/AliasAttributeSource.cs b/NewType.Generator/AliasAttributeSource.cs
index dc04e74..237f7b8 100644
--- a/NewType.Generator/AliasAttributeSource.cs
+++ b/NewType.Generator/AliasAttributeSource.cs
@@ -35,26 +35,7 @@ internal enum NewtypeOptions
/// Suppress implicit conversions and constructor forwarding.
Opaque = NoImplicitConversions | NoConstructorForwarding,
}
-
- ///
- /// Controls which constraint-related features the newtype generator emits. If enabled,
- /// will automatically call a user-defined 'bool IsValid(AliasedType value)' method on the newtype
- /// to verify it is valid
- ///
- [global::System.Flags]
- internal enum NewtypeConstraintOptions
- {
- /// Constraints disabled(default).
- Disabled = 0,
- /// Enable constraints. Debug builds only by default
- Enabled = 1,
- /// Include constraint code in release builds, if constraints are enabled
- IncludeInRelease = 2,
-
- /// Enable constraints and include in release builds.
- ReleaseEnabled = Enabled | IncludeInRelease,
- }
-
+
///
/// Marks a partial type as a type alias for the specified type.
/// The source generator will generate implicit conversions, operator forwarding,
@@ -74,9 +55,6 @@ public newtypeAttribute() { }
/// Controls which features the generator emits.
public NewtypeOptions Options { get; set; }
- /// Controls which constraint features the generator emits.
- public NewtypeConstraintOptions ConstraintOptions { get; set; }
-
///
/// Overrides the MethodImplOptions applied to generated members.
/// Default is .
diff --git a/NewType.Generator/AliasCodeGenerator.cs b/NewType.Generator/AliasCodeGenerator.cs
index ff9b5a2..6fabeb5 100644
--- a/NewType.Generator/AliasCodeGenerator.cs
+++ b/NewType.Generator/AliasCodeGenerator.cs
@@ -20,7 +20,6 @@ internal class AliasCodeGenerator
const string SingleIndent = " ";
-
public AliasCodeGenerator(AliasModel model)
{
_model = model;
@@ -143,25 +142,27 @@ private void AppendConstructors()
var memberIndent = GetMemberIndent();
// Constructor from aliased type (always emitted)
- _sb.AppendLine($"{memberIndent}/// Creates a new {_model.TypeName} from a {_model.AliasedTypeMinimalName}.");
+ _sb.AppendLine(
+ $"{memberIndent}/// Creates a new {_model.TypeName} from a {_model.AliasedTypeMinimalName}.");
AppendMethodImplAttribute(memberIndent);
-
-
- if (_model.IncludeConstraints)
+
+
+ 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(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(
+ $"{memberIndent}public {_model.TypeName}({_model.AliasedTypeFullName} value) => _value = value;");
}
-
+
_sb.AppendLine();
// Forward constructors from the aliased type (conditionally)
@@ -193,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();
}
}
@@ -233,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
@@ -243,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
@@ -255,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
@@ -267,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();
}
}
@@ -285,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();
}
}
@@ -326,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();
}
@@ -340,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();
}
}
@@ -384,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();
}
@@ -396,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();
}
}
@@ -436,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();
}
}
@@ -513,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} ");
@@ -523,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();
}
@@ -537,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");
@@ -663,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();
}
}
@@ -687,22 +716,25 @@ private void AppendForwardedConstructor(string indent, ConstructorInfo ctor)
_sb.AppendLine($"{indent}/// Forwards {_model.AliasedTypeMinimalName} constructor.");
AppendMethodImplAttribute(indent);
- if (_model.IncludeConstraints)
+ 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});");
-
+ _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(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(
+ $"{indent}public {_model.TypeName}({parameters}) => _value = new {_model.AliasedTypeFullName}({arguments});");
}
+
_sb.AppendLine();
}
@@ -727,21 +759,23 @@ private void AppendMethodImplAttribute(string indent)
private void AppendConstraintChecker(string indent, string valueName)
{
- if (!_model.validValidationMethod) return;
-
- if (_model.DebugOnlyConstraints)
+ if (!_model.ConstraintModel.Valid) return;
+
+ if (!_model.ConstraintModel.InRelease)
_sb.AppendLine("#if DEBUG");
-
+
_sb.Append(indent).Append(indent).Append(indent)
- .AppendLine($"if (!IsValid({valueName}))");
+ .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.DebugOnlyConstraints)
+ .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 =>
@@ -779,7 +813,8 @@ private static string FormatConstructorArguments(ConstructorInfo ctor)
}));
}
- private string GetMemberIndent() => string.IsNullOrEmpty(_model.Namespace) ? SingleIndent : $"{SingleIndent}{SingleIndent}";
+ private string GetMemberIndent() =>
+ string.IsNullOrEmpty(_model.Namespace) ? SingleIndent : $"{SingleIndent}{SingleIndent}";
private static string? GetOperatorSymbol(string operatorName)
{
@@ -887,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 a1e7379..893f7b7 100644
--- a/NewType.Generator/AliasGenerator.cs
+++ b/NewType.Generator/AliasGenerator.cs
@@ -12,23 +12,14 @@ namespace newtype.generator;
[Generator(LanguageNames.CSharp)]
public class AliasGenerator : IIncrementalGenerator
{
- private static readonly DiagnosticDescriptor MissingIsValidMethodDiagnostic =
- new DiagnosticDescriptor(
- id: "NEWTYPE001",
- title: "Missing validation method",
- messageFormat: $"Type '{{0}}' uses constraints but does not define a compatible validation method. Expected signature: 'bool {AliasModel.ConstraintValidationMethodSymbol}({{1}})'.",
- category: "Unknown",
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- description: "A constraint-enabled wrapped type must define a validation method."
- );
-
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register the attribute source
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
@@ -88,14 +79,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// Mirrors MethodImplOptions.AggressiveInlining — the generator can't reference
// the injected NewtypeOptions enum, and we want readable defaults.
private const int DefaultOptions = 0;
- private const int DefaultConstraintOptions = 0;
private const int DefaultMethodImplAggressiveInlining = 256;
private static ExtractedOptions ExtractNamedArguments(
AttributeData attributeData)
{
int options = DefaultOptions;
- int constraintOptions = DefaultConstraintOptions;
int methodImpl = DefaultMethodImplAggressiveInlining;
foreach (var arg in attributeData.NamedArguments)
@@ -105,39 +94,64 @@ private static ExtractedOptions ExtractNamedArguments(
case "Options":
options = (int)arg.Value.Value!;
break;
- case "ConstraintOptions":
- constraintOptions = (int)arg.Value.Value!;
- break;
case "MethodImpl":
methodImpl = (int)arg.Value.Value!;
break;
}
}
- return new ExtractedOptions(options, constraintOptions, methodImpl);
+ return new ExtractedOptions(options, methodImpl);
}
private static void GenerateAliasCode(
SourceProductionContext context,
AliasModel model)
{
- if (model.IncludeConstraints && !model.validValidationMethod)
+ if (!model.ConstraintModel.Valid)
{
context.ReportDiagnostic(
Diagnostic.Create(
- MissingIsValidMethodDiagnostic,
- model.Location,
+ ValidatorInvalidDiagnostic, model.ConstraintModel.LocationInfo?.ToLocation(),
model.TypeName,
- model.AliasedTypeFullName
+ 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 7279467..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;
@@ -16,10 +17,10 @@ internal record AliasModel(
bool IsReadonly,
bool IsClass,
bool IsRecord,
-
+
// Location for messages
- Location? Location,
-
+ LocationInfo? LocationInfo,
+
// Aliased type
string AliasedTypeFullName,
string AliasedTypeMinimalName,
@@ -43,10 +44,7 @@ internal record AliasModel(
bool SuppressImplicitUnwrap,
bool SuppressConstructorForwarding,
int MethodImplValue,
-
- bool IncludeConstraints,
- bool DebugOnlyConstraints,
- bool validValidationMethod,
+ ConstraintModel ConstraintModel,
// Members
EquatableArray BinaryOperators,
@@ -56,12 +54,9 @@ internal record AliasModel(
EquatableArray InstanceProperties,
EquatableArray InstanceMethods,
EquatableArray ForwardedConstructors
-)
-{
- public const string ConstraintValidationMethodSymbol = "IsValid";
-};
+);
-internal readonly record struct ExtractedOptions(int Options, int ConstraintOptions, int MethodImpl);
+internal readonly record struct ExtractedOptions(int Options, int MethodImpl);
internal readonly record struct BinaryOperatorInfo(
string Name,
@@ -127,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 9660e07..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;
@@ -16,20 +17,18 @@ internal static class AliasModelExtractor
private const int OptionsNoImplicitWrap = 1;
private const int OptionsNoImplicitUnwrap = 2;
private const int OptionsNoConstructorForwarding = 4;
- private const int OptionsUseConstraints = 1;
- private const int OptionsConstraintsInRelease = 2;
- public static AliasModel? Extract(
+ public static AliasModel? Extract(
GeneratorAttributeSyntaxContext context,
ITypeSymbol aliasedType,
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
@@ -55,11 +54,9 @@ internal static class AliasModelExtractor
var constructors = ExtractForwardableConstructors(typeSymbol, aliasedType);
var typeDisplayString = typeSymbol.ToDisplayString();
- var validIsValid = HasValidIsValid(typeSymbol, aliasedType);
- var useConstraints = (allOptions.ConstraintOptions & OptionsUseConstraints) != 0;
-
-
+ var constraintModel = ExtractValidationMethod(typeSymbol, aliasedType);
+
return new AliasModel(
TypeName: typeName,
Namespace: namespaceName,
@@ -67,7 +64,7 @@ internal static class AliasModelExtractor
IsReadonly: isReadonly,
IsClass: isClass,
IsRecord: isRecord,
- Location: typeSymbol.Locations.FirstOrDefault(),
+ LocationInfo: ToLocationStruct(typeSymbol.Locations.FirstOrDefault()),
AliasedTypeFullName: aliasedTypeFullName,
AliasedTypeMinimalName: aliasedTypeMinimalName,
AliasedTypeSpecialType: aliasedType.SpecialType,
@@ -81,9 +78,7 @@ internal static class AliasModelExtractor
SuppressImplicitUnwrap: (allOptions.Options & OptionsNoImplicitUnwrap) != 0,
SuppressConstructorForwarding: (allOptions.Options & OptionsNoConstructorForwarding) != 0,
MethodImplValue: allOptions.MethodImpl,
- IncludeConstraints: useConstraints,
- DebugOnlyConstraints: (allOptions.ConstraintOptions & OptionsConstraintsInRelease) == 0, // inverse
- validValidationMethod: validIsValid,
+ ConstraintModel: constraintModel,
BinaryOperators: binaryOperators,
UnaryOperators: unaryOperators,
StaticMembers: staticMembers,
@@ -362,22 +357,52 @@ private static string GetConstructorSignature(IMethodSymbol ctor)
}));
}
- private static bool HasValidIsValid(ITypeSymbol targetType, ITypeSymbol aliasedType)
+ private static ConstraintModel ExtractValidationMethod(ITypeSymbol targetType,
+ ITypeSymbol aliasedType)
{
- // does not have to be static, even it is more "hygienic"
- // seems like it would a be a little annoying to enforce if not strictly needed
- IMethodSymbol? isValidMethod =
- targetType
- .GetMembers(AliasModel.ConstraintValidationMethodSymbol)
- .OfType()
- .FirstOrDefault(m =>
- m.ReturnType.SpecialType == SpecialType.System_Boolean &&
- m.Parameters.Length == 1 &&
- SymbolEqualityComparer.Default.Equals(
- m.Parameters[0].Type,
- aliasedType));
-
- return isValidMethod != null;
+ 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)
@@ -441,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.Unshipped.md b/NewType.Generator/AnalyzerReleases.Unshipped.md
index cf30ff2..20b0ef7 100644
--- a/NewType.Generator/AnalyzerReleases.Unshipped.md
+++ b/NewType.Generator/AnalyzerReleases.Unshipped.md
@@ -5,4 +5,5 @@
| Rule ID | Category | Severity | Notes |
|------------|-----------|----------|----------------|
-| NEWTYPE001 | `Unknown` | Error | AliasGenerator |
\ No newline at end of file
+| NEWTYPE001 | `Unknown` | Error | AliasGenerator |
+| NEWTYPE002 | `Unknown` | Error | AliasGenerator |
diff --git a/NewType.Tests/Types.cs b/NewType.Tests/Types.cs
index 2049999..d9597f0 100644
--- a/NewType.Tests/Types.cs
+++ b/NewType.Tests/Types.cs
@@ -8,10 +8,11 @@ namespace newtype.tests;
[newtype]
public readonly partial struct Velocity;
-[newtype(ConstraintOptions = NewtypeConstraintOptions.ReleaseEnabled)]
+[newtype]
public readonly partial struct Direction
{
- private static bool IsValid(Vector2 direction)
+ [newtypeValidation]
+ private static bool IsNormalized(Vector2 direction)
{
// enforce normalized
const float epsilon = 0.0001f;