Skip to content

Commit 9028354

Browse files
linkdotnetegil
authored andcommitted
feat: First working versions
1 parent 134723c commit 9028354

3 files changed

Lines changed: 68 additions & 82 deletions

File tree

src/bunit.generators/Web.Stubs/StubGenerator.cs

Lines changed: 62 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
using System.Collections.Immutable;
21
using System.Linq;
32
using System.Text;
43
using Microsoft.CodeAnalysis;
5-
using Microsoft.CodeAnalysis.CSharp;
64
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Text;
76

87
namespace Bunit.Web.Stubs;
98

@@ -13,61 +12,49 @@ namespace Bunit.Web.Stubs;
1312
[Generator]
1413
public class StubGenerator : IIncrementalGenerator
1514
{
15+
private const string AttributeFullQualifiedName = "Bunit.StubAttribute";
16+
1617
/// <inheritdoc/>
1718
public void Initialize(IncrementalGeneratorInitializationContext context)
1819
{
19-
var classDeclarations = context.SyntaxProvider
20-
.CreateSyntaxProvider(
21-
predicate: static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
20+
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
21+
"StubAttribute.g.cs",
22+
SourceText.From(StubAttributeGenerator.StubAttribute, Encoding.UTF8)));
23+
24+
var classesToStub = context.SyntaxProvider
25+
.ForAttributeWithMetadataName(
26+
AttributeFullQualifiedName,
27+
predicate: static (s, _) => s is ClassDeclarationSyntax,
2228
transform: static (ctx, _) => GetStubClassInfo(ctx))
2329
.Where(static m => m is not null);
2430

25-
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
2631

27-
context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Item2, spc));
32+
context.RegisterSourceOutput(
33+
classesToStub,
34+
static (spc, source) => Execute(source, spc));
2835
}
2936

30-
private static StubClassInfo GetStubClassInfo(GeneratorSyntaxContext context)
37+
private static StubClassInfo GetStubClassInfo(GeneratorAttributeSyntaxContext context)
3138
{
32-
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
33-
34-
// Check if the class is partial
35-
if (!classDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword))
36-
{
37-
return null;
38-
}
39-
40-
// Find the StubAttribute on the class
41-
foreach (var attribute in classDeclarationSyntax.AttributeLists.SelectMany(a => a.Attributes))
39+
foreach (var attribute in context.TargetSymbol.GetAttributes())
4240
{
43-
var attributeSymbol =
44-
ModelExtensions.GetSymbolInfo(context.SemanticModel, attribute).Symbol as IMethodSymbol;
45-
if (attributeSymbol is null || !IsStubAttribute(attributeSymbol))
41+
if (context.TargetSymbol is not ITypeSymbol stubbedType ||
42+
!ImplementsInterface(stubbedType, "Microsoft.AspNetCore.Components.IComponent"))
4643
{
4744
continue;
4845
}
4946

50-
if (attribute.ArgumentList?.Arguments is not [{ Expression: TypeOfExpressionSyntax typeOfExpression }])
51-
{
52-
continue;
53-
}
54-
55-
var typeSymbol = ModelExtensions.GetTypeInfo(context.SemanticModel, typeOfExpression.Type).Type;
56-
if (typeSymbol == null)
57-
{
58-
continue;
59-
}
47+
var namespaceName = stubbedType.ContainingNamespace.ToDisplayString();
48+
var className = context.TargetSymbol.Name;
6049

61-
if (!ImplementsInterface(typeSymbol, "Microsoft.AspNetCore.Components.IComponent"))
50+
// TODO: Check for the name not the first
51+
var originalTypeToStub = attribute.ConstructorArguments.FirstOrDefault().Value;
52+
if (originalTypeToStub is not ITypeSymbol originalType)
6253
{
6354
continue;
6455
}
6556

66-
var namespaceSyntax = classDeclarationSyntax.Parent as NamespaceDeclarationSyntax;
67-
var namespaceName = namespaceSyntax?.Name.ToString();
68-
var className = classDeclarationSyntax.Identifier.ValueText;
69-
70-
return new StubClassInfo { ClassName = className, Namespace = namespaceName, TargetType = typeSymbol };
57+
return new StubClassInfo { ClassName = className, Namespace = namespaceName, TargetType = originalType };
7158
}
7259

7360
return null;
@@ -78,51 +65,49 @@ static bool ImplementsInterface(ITypeSymbol typeSymbol, string interfaceName)
7865
}
7966
}
8067

81-
private static bool IsStubAttribute(ISymbol attributeSymbol) => attributeSymbol.ContainingType.ToDisplayString() == "Bunit.StubAttribute";
82-
83-
private static void Execute(ImmutableArray<StubClassInfo> classes, SourceProductionContext context)
68+
private static void Execute(StubClassInfo classInfo, SourceProductionContext context)
8469
{
85-
context.AddSource("StubAttribute.g.cs", StubAttributeGenerator.StubAttribute);
86-
87-
if (classes.IsDefaultOrEmpty)
70+
var hasSomethingToStub = false;
71+
var targetTypeSymbol = (INamedTypeSymbol)classInfo!.TargetType;
72+
var sourceBuilder = new StringBuilder();
73+
74+
// TODO: Shall we dictate file-scoped namespaces here?
75+
sourceBuilder.AppendLine($"namespace {classInfo.Namespace};");
76+
77+
// TODO: If the class is a nested one, that approach does not work
78+
sourceBuilder.AppendLine($"public partial class {classInfo.ClassName}");
79+
sourceBuilder.Append("{");
80+
81+
foreach (var member in targetTypeSymbol
82+
.GetMembers()
83+
.OfType<IPropertySymbol>()
84+
.Where(p => p.GetAttributes()
85+
.Any(attr =>
86+
attr.AttributeClass?.ToDisplayString() ==
87+
"Microsoft.AspNetCore.Components.ParameterAttribute" ||
88+
attr.AttributeClass?.ToDisplayString() ==
89+
"Microsoft.AspNetCore.Components.CascadingParameterAttribute")))
8890
{
89-
return;
91+
sourceBuilder.AppendLine();
92+
93+
hasSomethingToStub = true;
94+
var propertyType = member.Type.ToDisplayString();
95+
var propertyName = member.Name;
96+
97+
var isParameterAttribute = member.GetAttributes().Any(attr =>
98+
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterAttribute");
99+
var attributeLine = isParameterAttribute
100+
? "\t[global::Microsoft.AspNetCore.Components.Parameter]"
101+
: "\t[global::Microsoft.AspNetCore.Components.CascadingParameter]";
102+
103+
sourceBuilder.AppendLine(attributeLine);
104+
sourceBuilder.AppendLine($"\tpublic {propertyType} {propertyName} {{ get; set; }}");
90105
}
91106

92-
foreach (var classInfo in classes.Where(t => t?.TargetType is INamedTypeSymbol))
93-
{
94-
var targetTypeSymbol = (INamedTypeSymbol)classInfo!.TargetType;
95-
var sourceBuilder = new StringBuilder();
96-
97-
sourceBuilder.AppendLine($"namespace {classInfo.Namespace};");
98-
sourceBuilder.AppendLine($"public partial class {classInfo.ClassName}");
99-
sourceBuilder.AppendLine("{");
100-
101-
foreach (var member in targetTypeSymbol
102-
.GetMembers()
103-
.OfType<IPropertySymbol>()
104-
.Where(p => p.GetAttributes()
105-
.Any(attr =>
106-
attr.AttributeClass?.ToDisplayString() ==
107-
"Microsoft.AspNetCore.Components.ParameterAttribute" ||
108-
attr.AttributeClass?.ToDisplayString() ==
109-
"Microsoft.AspNetCore.Components.CascadingParameterAttribute")))
110-
{
111-
var propertyType = $"global::{member.Type.ToDisplayString()}";
112-
var propertyName = member.Name;
113-
114-
var isParameterAttribute = member.GetAttributes().Any(attr =>
115-
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterAttribute");
116-
var attributeLine = isParameterAttribute
117-
? "\t[global::Microsoft.AspNetCore.Components.Parameter]"
118-
: "\t[global::Microsoft.AspNetCore.Components.CascadingParameter]";
119-
120-
sourceBuilder.AppendLine(attributeLine);
121-
sourceBuilder.AppendLine($"\tpublic {propertyType} {propertyName} {{ get; set; }}");
122-
sourceBuilder.AppendLine();
123-
}
107+
sourceBuilder.AppendLine("}");
124108

125-
sourceBuilder.AppendLine("}");
109+
if (hasSomethingToStub)
110+
{
126111
context.AddSource($"{classInfo.ClassName}Stub.g.cs", sourceBuilder.ToString());
127112
}
128113
}

tests/bunit.generators.tests/Web.Stub/CounterComponent.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ public class CounterComponent : ComponentBase
66
{
77
[Parameter] public int Count { get; set; }
88
[CascadingParameter] public int CascadingCount { get; set; }
9+
[Parameter] public EventCallback IncrementCount { get; set; }
910
}

tests/bunit.generators.tests/Web.Stub/StubTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ public void Stubbed_component_has_same_parameters()
2525
Assert.Equal(isCascadingParameter, stubIsCascadingParameter);
2626
}
2727
}
28-
29-
[Stub(typeof(CounterComponentStub))]
30-
public partial class CounterComponentStub : ComponentBase
31-
{
32-
}
28+
}
29+
30+
[Stub(typeof(CounterComponent))]
31+
public partial class CounterComponentStub : ComponentBase
32+
{
3333
}

0 commit comments

Comments
 (0)