-
Notifications
You must be signed in to change notification settings - Fork 1
Incremental generation #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
45268b2
chore: readme formatting
thygrrr a6e673d
feat: incremental generation, work in progress
thygrrr a82a697
feat: incremental generation tests
thygrrr dc9a36c
feat: pizza example, maybe needs work
thygrrr db0c944
fix: automated code review feedback
thygrrr cf6a4e1
fix: nitpicks
thygrrr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
|
|
@@ -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 | ||
|
|
@@ -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; | ||
| } | ||
| 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
|
||
|
|
||
| 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); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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(...)'.