Skip to content

Commit e2095aa

Browse files
linkdotnetegil
authored andcommitted
feat: Automatically generate stub
1 parent 1595589 commit e2095aa

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#nullable enable
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using System.Text;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
9+
namespace Bunit.Web.Stubs;
10+
11+
[Generator]
12+
public class StubGenerator : IIncrementalGenerator
13+
{
14+
public void Initialize(IncrementalGeneratorInitializationContext context)
15+
{
16+
var classDeclarations = context.SyntaxProvider
17+
.CreateSyntaxProvider(
18+
predicate: static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
19+
transform: static (ctx, _) => GetStubClassInfo(ctx))
20+
.Where(static m => m is not null);
21+
22+
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
23+
24+
context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Item2, spc));
25+
}
26+
27+
private static StubClassInfo? GetStubClassInfo(GeneratorSyntaxContext context)
28+
{
29+
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
30+
31+
// Check if the class is partial
32+
if (!classDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword))
33+
{
34+
return null;
35+
}
36+
37+
// Find the StubAttribute on the class
38+
foreach (var attribute in classDeclarationSyntax.AttributeLists.SelectMany(a => a.Attributes))
39+
{
40+
var attributeSymbol =
41+
ModelExtensions.GetSymbolInfo(context.SemanticModel, attribute).Symbol as IMethodSymbol;
42+
if (attributeSymbol is null || !IsStubAttribute(attributeSymbol))
43+
{
44+
continue;
45+
}
46+
47+
if (attribute.ArgumentList?.Arguments is not [{ Expression: TypeOfExpressionSyntax typeOfExpression }])
48+
{
49+
continue;
50+
}
51+
52+
var typeSymbol = ModelExtensions.GetTypeInfo(context.SemanticModel, typeOfExpression.Type).Type;
53+
if (typeSymbol == null)
54+
{
55+
continue;
56+
}
57+
58+
var namespaceSyntax = classDeclarationSyntax.Parent as NamespaceDeclarationSyntax;
59+
var namespaceName = namespaceSyntax?.Name.ToString();
60+
var className = classDeclarationSyntax.Identifier.ValueText;
61+
62+
return new StubClassInfo { ClassName = className, Namespace = namespaceName, TargetType = typeSymbol };
63+
}
64+
65+
return null;
66+
}
67+
68+
private static bool IsStubAttribute(ISymbol attributeSymbol) => attributeSymbol.ContainingType.ToDisplayString() == "Bunit.StubAttribute";
69+
70+
private static void Execute(ImmutableArray<StubClassInfo?> classes, SourceProductionContext context)
71+
{
72+
if (classes.IsDefaultOrEmpty)
73+
{
74+
return;
75+
}
76+
77+
foreach (var classInfo in classes.Where(t => t?.TargetType is INamedTypeSymbol))
78+
{
79+
var targetTypeSymbol = (INamedTypeSymbol)classInfo!.TargetType;
80+
var sourceBuilder = new StringBuilder();
81+
82+
sourceBuilder.AppendLine($"namespace {classInfo.Namespace};");
83+
// TODO: Use same modifier (not public) as the original class
84+
sourceBuilder.AppendLine($"public partial class {classInfo.ClassName}");
85+
sourceBuilder.AppendLine("{");
86+
87+
foreach (var member in targetTypeSymbol
88+
.GetMembers()
89+
.OfType<IPropertySymbol>()
90+
.Where(p => p.GetAttributes()
91+
.Any(attr =>
92+
attr.AttributeClass?.ToDisplayString() ==
93+
"Microsoft.AspNetCore.Components.ParameterAttribute" ||
94+
attr.AttributeClass?.ToDisplayString() ==
95+
"Microsoft.AspNetCore.Components.CascadingParameterAttribute")))
96+
{
97+
var existingProperty = targetTypeSymbol?.GetMembers().OfType<IPropertySymbol>()
98+
.FirstOrDefault(p => p.Name == member.Name &&
99+
p.GetAttributes().Any(attr =>
100+
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterAttribute" ||
101+
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.CascadingParameterAttribute"));
102+
if (existingProperty is not null)
103+
{
104+
continue;
105+
}
106+
107+
var propertyType = $"global::{member.Type.ToDisplayString()}";
108+
var propertyName = member.Name;
109+
110+
var isParameterAttribute = member.GetAttributes().Any(attr =>
111+
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterAttribute");
112+
var attributeLine = isParameterAttribute
113+
? "\t[global::Microsoft.AspNetCore.Components.Parameter]"
114+
: "\t[global::Microsoft.AspNetCore.Components.CascadingParameter]";
115+
116+
sourceBuilder.AppendLine(attributeLine);
117+
sourceBuilder.AppendLine($"\tpublic {propertyType} {propertyName} {{ get; set; }}");
118+
sourceBuilder.AppendLine();
119+
}
120+
121+
sourceBuilder.AppendLine("}");
122+
context.AddSource($"{classInfo.ClassName}Stub.g.cs", sourceBuilder.ToString());
123+
}
124+
}
125+
}
126+
127+
internal sealed record StubClassInfo
128+
{
129+
public required string ClassName { get; init; }
130+
public required string? Namespace { get; init; }
131+
public required ITypeSymbol TargetType { get; init; }
132+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#if NET5_0_OR_GREATER
2+
namespace Bunit;
3+
4+
/// <summary>
5+
/// TODO.
6+
/// </summary>
7+
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
8+
public sealed class StubAttribute : Attribute
9+
{
10+
/// <summary>
11+
/// TODO.
12+
/// </summary>
13+
public Type TargetType { get; }
14+
15+
/// <summary>
16+
/// TODO.
17+
/// </summary>
18+
/// <param name="targetType"></param>
19+
public StubAttribute(Type targetType)
20+
{
21+
TargetType = targetType;
22+
}
23+
}
24+
#endif

0 commit comments

Comments
 (0)