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
552 changes: 161 additions & 391 deletions NewType.Generator/AliasCodeGenerator.cs

Large diffs are not rendered by default.

128 changes: 41 additions & 87 deletions NewType.Generator/AliasGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
Expand All @@ -8,6 +7,7 @@ namespace newtype.generator;

/// <summary>
/// Incremental source generator that creates type alias implementations.
/// Uses ForAttributeWithMetadataName for efficient attribute-based incremental generation.
/// </summary>
[Generator(LanguageNames.CSharp)]
public class AliasGenerator : IIncrementalGenerator
Expand All @@ -17,111 +17,65 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// Register the attribute source
context.RegisterPostInitializationOutput(ctx => { ctx.AddSource("newtypeAttribute.g.cs", SourceText.From(NewtypeAttributeSource.Source, Encoding.UTF8)); });

// Find all type declarations with our attribute
var aliasDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => IsCandidateType(node),
transform: static (ctx, _) => GetAliasInfo(ctx))
.Where(static info => info is not null)
.Select(static (info, _) => info!.Value);

// Combine with compilation
var compilationAndAliases = context.CompilationProvider.Combine(aliasDeclarations.Collect());
// Pipeline for generic [newtype<T>] attribute
var 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);

context.RegisterSourceOutput(genericPipeline, static (spc, model) => GenerateAliasCode(spc, model));

// Pipeline for non-generic [newtype(typeof(T))] attribute
var 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);

context.RegisterSourceOutput(nonGenericPipeline, static (spc, model) => GenerateAliasCode(spc, model));
}

// Generate the source
context.RegisterSourceOutput(compilationAndAliases, static (spc, source) =>
private static AliasModel? ExtractGenericModel(GeneratorAttributeSyntaxContext context)
{
foreach (var attributeData in context.Attributes)
{
var (compilation, aliases) = source;
foreach (var alias in aliases)
var attributeClass = attributeData.AttributeClass;
if (attributeClass is {IsGenericType: true} &&
attributeClass.TypeArguments.Length == 1)
{
GenerateAliasCode(spc, compilation, alias);
var aliasedType = attributeClass.TypeArguments[0];
return AliasModelExtractor.Extract(context, aliasedType);
}
});
}

private static bool IsCandidateType(SyntaxNode node)
{
if (node is StructDeclarationSyntax {AttributeLists.Count: > 0} structDecl)
return structDecl.Modifiers.Any(SyntaxKind.PartialKeyword);

if (node is ClassDeclarationSyntax {AttributeLists.Count: > 0} classDecl)
return classDecl.Modifiers.Any(SyntaxKind.PartialKeyword);

if (node is RecordDeclarationSyntax {AttributeLists.Count: > 0} recordDecl)
return recordDecl.Modifiers.Any(SyntaxKind.PartialKeyword);

return false;
}
Comment on lines +45 to +54
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
return null;
}

private static AliasInfo? GetAliasInfo(GeneratorSyntaxContext context)
private static AliasModel? ExtractNonGenericModel(GeneratorAttributeSyntaxContext context)
{
var typeDecl = (TypeDeclarationSyntax) context.Node;
var semanticModel = context.SemanticModel;

foreach (var attributeList in typeDecl.AttributeLists)
foreach (var attributeData in context.Attributes)
{
foreach (var attribute in attributeList.Attributes)
if (attributeData.ConstructorArguments.Length > 0 &&
attributeData.ConstructorArguments[0].Value is ITypeSymbol aliasedType)
{
var symbolInfo = semanticModel.GetSymbolInfo(attribute);
if (symbolInfo.Symbol is not IMethodSymbol attributeConstructor)
continue;

var attributeType = attributeConstructor.ContainingType;
var fullName = attributeType.ToDisplayString();

// Check for generic Alias<T>
if (attributeType.IsGenericType &&
attributeType.OriginalDefinition.ToDisplayString() == "newtype.newtypeAttribute<T>")
{
var aliasedType = attributeType.TypeArguments[0];
var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl);
if (typeSymbol is null) continue;

return new AliasInfo(
typeDecl,
typeSymbol,
aliasedType);
}

// Check for non-generic Alias(typeof(T))
if (fullName == "newtype.newtypeAttribute")
{
var attributeData = semanticModel.GetDeclaredSymbol(typeDecl)?
.GetAttributes()
.FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "newtype.newtypeAttribute");

if (attributeData?.ConstructorArguments.Length > 0 &&
attributeData.ConstructorArguments[0].Value is ITypeSymbol aliasedType)
{
var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl);
if (typeSymbol is null) continue;

return new AliasInfo(
typeDecl,
typeSymbol,
aliasedType);
}
}
return AliasModelExtractor.Extract(context, aliasedType);
}
}
Comment on lines +60 to 67
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.

return null;
}

private static void GenerateAliasCode(
SourceProductionContext context,
Compilation compilation,
AliasInfo alias)
AliasModel model)
{
var generator = new AliasCodeGenerator(compilation, alias);
var generator = new AliasCodeGenerator(model);
var source = generator.Generate();

var fileName = $"{alias.TypeSymbol.ToDisplayString().Replace(".", "_").Replace("<", "_").Replace(">", "_")}.g.cs";
var fileName = $"{model.TypeDisplayString.Replace(".", "_").Replace("<", "_").Replace(">", "_")}.g.cs";
context.AddSource(fileName, SourceText.From(source, Encoding.UTF8));
}
}

internal readonly record struct AliasInfo(
TypeDeclarationSyntax TypeDeclaration,
INamedTypeSymbol TypeSymbol,
ITypeSymbol AliasedType);
112 changes: 112 additions & 0 deletions NewType.Generator/AliasModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;

namespace newtype.generator;

/// <summary>
/// Fully-extracted, equatable model representing a newtype alias.
/// Contains only strings, bools, plain enums, and EquatableArrays — no Roslyn symbols.
/// </summary>
internal readonly record struct AliasModel(
// Type being declared
string TypeName,
string Namespace,
Accessibility DeclaredAccessibility,
bool IsReadonly,
bool IsClass,
bool IsRecord,
bool IsRecordStruct,

// Aliased type
string AliasedTypeFullName,
string AliasedTypeMinimalName,
SpecialType AliasedTypeSpecialType,
bool AliasedTypeIsValueType,

// Interface flags
bool ImplementsIComparable,
bool ImplementsIFormattable,
bool HasNativeEqualityOperator,

// Pre-computed file name
string TypeDisplayString,

// Whether the aliased type has any public static non-operator members
// (used to emit the #region even when no property/readonly-field members survive filtering)
bool HasStaticMemberCandidates,

// Members
EquatableArray<BinaryOperatorInfo> BinaryOperators,
EquatableArray<UnaryOperatorInfo> UnaryOperators,
EquatableArray<StaticMemberInfo> StaticMembers,
EquatableArray<InstanceFieldInfo> InstanceFields,
EquatableArray<InstancePropertyInfo> InstanceProperties,
EquatableArray<InstanceMethodInfo> InstanceMethods,
EquatableArray<ConstructorInfo> ForwardedConstructors
);

internal readonly record struct BinaryOperatorInfo(
string Name,
string LeftTypeFullName,
string RightTypeFullName,
string ReturnTypeFullName,
bool LeftIsAliasedType,
bool RightIsAliasedType,
bool ReturnIsAliasedType
) : IEquatable<BinaryOperatorInfo>;

internal readonly record struct UnaryOperatorInfo(
string Name,
string ReturnTypeFullName,
bool ReturnIsAliasedType
) : IEquatable<UnaryOperatorInfo>;

internal readonly record struct StaticMemberInfo(
string Name,
string TypeFullName,
bool TypeIsAliasedType,
bool IsProperty,
bool IsReadonlyField
) : IEquatable<StaticMemberInfo>;

internal readonly record struct InstanceFieldInfo(
string Name,
string TypeFullName,
bool TypeIsAliasedType
) : IEquatable<InstanceFieldInfo>;

internal readonly record struct InstancePropertyInfo(
string Name,
string TypeFullName,
bool TypeIsAliasedType,
bool HasGetter
) : IEquatable<InstancePropertyInfo>;

internal readonly record struct InstanceMethodInfo(
string Name,
string ReturnTypeFullName,
bool ReturnsVoid,
bool ReturnIsAliasedType,
bool SkipReturnWrapping,
EquatableArray<MethodParameterInfo> Parameters
) : IEquatable<InstanceMethodInfo>;

internal readonly record struct MethodParameterInfo(
string Name,
string TypeFullName,
RefKind RefKind,
bool IsAliasedType
) : IEquatable<MethodParameterInfo>;

internal readonly record struct ConstructorInfo(
EquatableArray<ConstructorParameterInfo> Parameters
) : IEquatable<ConstructorInfo>;

internal readonly record struct ConstructorParameterInfo(
string Name,
string TypeFullName,
RefKind RefKind,
bool IsParams,
string? DefaultValueLiteral
) : IEquatable<ConstructorParameterInfo>;
Loading