Skip to content

Commit a599146

Browse files
linkdotnetegil
authored andcommitted
feat: use a new function to invoke the generator
1 parent 9028354 commit a599146

6 files changed

Lines changed: 128 additions & 91 deletions

File tree

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

Lines changed: 0 additions & 31 deletions
This file was deleted.

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

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Text;
33
using Microsoft.CodeAnalysis;
44
using Microsoft.CodeAnalysis.CSharp.Syntax;
5-
using Microsoft.CodeAnalysis.Text;
65

76
namespace Bunit.Web.Stubs;
87

@@ -12,70 +11,124 @@ namespace Bunit.Web.Stubs;
1211
[Generator]
1312
public class StubGenerator : IIncrementalGenerator
1413
{
15-
private const string AttributeFullQualifiedName = "Bunit.StubAttribute";
16-
1714
/// <inheritdoc/>
1815
public void Initialize(IncrementalGeneratorInitializationContext context)
1916
{
20-
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
21-
"StubAttribute.g.cs",
22-
SourceText.From(StubAttributeGenerator.StubAttribute, Encoding.UTF8)));
23-
2417
var classesToStub = context.SyntaxProvider
25-
.ForAttributeWithMetadataName(
26-
AttributeFullQualifiedName,
27-
predicate: static (s, _) => s is ClassDeclarationSyntax,
18+
.CreateSyntaxProvider(
19+
predicate: static (s, _) => s is InvocationExpressionSyntax,
2820
transform: static (ctx, _) => GetStubClassInfo(ctx))
2921
.Where(static m => m is not null);
3022

31-
3223
context.RegisterSourceOutput(
3324
classesToStub,
3425
static (spc, source) => Execute(source, spc));
3526
}
3627

37-
private static StubClassInfo GetStubClassInfo(GeneratorAttributeSyntaxContext context)
28+
private static StubClassInfo GetStubClassInfo(GeneratorSyntaxContext context)
3829
{
39-
foreach (var attribute in context.TargetSymbol.GetAttributes())
30+
var invocation = context.Node as InvocationExpressionSyntax;
31+
if (!IsComponentFactoryStubMethod(invocation, context.SemanticModel))
4032
{
41-
if (context.TargetSymbol is not ITypeSymbol stubbedType ||
42-
!ImplementsInterface(stubbedType, "Microsoft.AspNetCore.Components.IComponent"))
33+
return null;
34+
}
35+
36+
if (invocation?.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax { TypeArgumentList.Arguments.Count: 1 } genericName })
37+
{
38+
var typeArgument = genericName.TypeArgumentList.Arguments[0];
39+
if (context.SemanticModel.GetSymbolInfo(typeArgument).Symbol is ITypeSymbol symbol)
4340
{
44-
continue;
41+
var path = GetInterceptorFilePath(context.Node.SyntaxTree, context.SemanticModel.Compilation);
42+
var line = context.SemanticModel.SyntaxTree.GetLineSpan(context.Node.Span).StartLinePosition.Line + 1;
43+
// Find then column for "AddGeneratedStub" in the invocation expression
44+
var column = context.SemanticModel.SyntaxTree.GetLineSpan(context.Node.Span).StartLinePosition.Character + 1;
45+
return new StubClassInfo
46+
{
47+
StubClassName = $"{symbol.Name}Stub",
48+
TargetTypeNamespace = symbol.ContainingNamespace.ToDisplayString(),
49+
TargetType = symbol,
50+
Path = path,
51+
Line = line,
52+
Column = column + 19, // Yeah - no - well - it works (Future Steven can take care of it)
53+
};
4554
}
55+
}
4656

47-
var namespaceName = stubbedType.ContainingNamespace.ToDisplayString();
48-
var className = context.TargetSymbol.Name;
57+
return null;
4958

50-
// TODO: Check for the name not the first
51-
var originalTypeToStub = attribute.ConstructorArguments.FirstOrDefault().Value;
52-
if (originalTypeToStub is not ITypeSymbol originalType)
59+
static bool IsComponentFactoryStubMethod(InvocationExpressionSyntax invocation, SemanticModel semanticModel)
60+
{
61+
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
5362
{
54-
continue;
63+
return false;
5564
}
5665

57-
return new StubClassInfo { ClassName = className, Namespace = namespaceName, TargetType = originalType };
58-
}
66+
if (memberAccess.Name.Identifier.Text != "AddGeneratedStub" ||
67+
invocation.ArgumentList.Arguments.Count != 0)
68+
{
69+
return false;
70+
}
5971

60-
return null;
72+
return semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol { IsExtensionMethod: true, ReceiverType.Name: "ComponentFactoryCollection" };
73+
}
6174

62-
static bool ImplementsInterface(ITypeSymbol typeSymbol, string interfaceName)
75+
static string GetInterceptorFilePath(SyntaxTree tree, Compilation compilation)
6376
{
64-
return typeSymbol.AllInterfaces.Any(i => i.ToDisplayString() == interfaceName);
77+
return compilation.Options.SourceReferenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath;
6578
}
6679
}
6780

6881
private static void Execute(StubClassInfo classInfo, SourceProductionContext context)
82+
{
83+
var didStubComponent = GenerateStubComponent(classInfo, context);
84+
if (didStubComponent is false)
85+
{
86+
return;
87+
}
88+
89+
// Generate the attribute
90+
const string attribute = @"namespace System.Runtime.CompilerServices
91+
{
92+
sealed file class InterceptsLocationAttribute : Attribute
93+
{
94+
public InterceptsLocationAttribute(string filePath, int line, int column)
95+
{
96+
_ = filePath;
97+
_ = line;
98+
_ = column;
99+
}
100+
}
101+
}";
102+
103+
// Generate the interceptor
104+
var interceptorSource = new StringBuilder();
105+
interceptorSource.AppendLine(attribute);
106+
interceptorSource.AppendLine();
107+
interceptorSource.AppendLine("namespace Bunit");
108+
interceptorSource.AppendLine("{");
109+
interceptorSource.AppendLine($"\tstatic class Interceptor{classInfo.StubClassName}");
110+
interceptorSource.AppendLine("\t{");
111+
interceptorSource.AppendLine(
112+
$"\t\t[System.Runtime.CompilerServices.InterceptsLocationAttribute(\"{classInfo.Path}\", {classInfo.Line}, {classInfo.Column})]");
113+
interceptorSource.AppendLine("\t\tpublic static global::Bunit.ComponentFactoryCollection AddGeneratedStubInterceptor<TComponent>(this global::Bunit.ComponentFactoryCollection factories)");
114+
interceptorSource.AppendLine("\t\t\twhere TComponent : Microsoft.AspNetCore.Components.IComponent");
115+
interceptorSource.AppendLine("\t\t{");
116+
interceptorSource.AppendLine($"\t\t\treturn factories.Add<global::{classInfo.TargetType.ToDisplayString()}, {classInfo.TargetTypeNamespace}.{classInfo.StubClassName}>();");
117+
interceptorSource.AppendLine("\t\t}");
118+
interceptorSource.AppendLine("\t}");
119+
interceptorSource.AppendLine("}");
120+
121+
context.AddSource($"Interceptor{classInfo.StubClassName}.g.cs", interceptorSource.ToString());
122+
}
123+
124+
private static bool GenerateStubComponent(StubClassInfo classInfo, SourceProductionContext context)
69125
{
70126
var hasSomethingToStub = false;
71127
var targetTypeSymbol = (INamedTypeSymbol)classInfo!.TargetType;
72128
var sourceBuilder = new StringBuilder();
73129

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}");
130+
sourceBuilder.AppendLine($"namespace {classInfo.TargetTypeNamespace};");
131+
sourceBuilder.AppendLine($"public class {classInfo.StubClassName} : Microsoft.AspNetCore.Components.ComponentBase");
79132
sourceBuilder.Append("{");
80133

81134
foreach (var member in targetTypeSymbol
@@ -108,14 +161,19 @@ private static void Execute(StubClassInfo classInfo, SourceProductionContext con
108161

109162
if (hasSomethingToStub)
110163
{
111-
context.AddSource($"{classInfo.ClassName}Stub.g.cs", sourceBuilder.ToString());
164+
context.AddSource($"{classInfo.StubClassName}Stub.g.cs", sourceBuilder.ToString());
112165
}
166+
167+
return hasSomethingToStub;
113168
}
114169
}
115170

116171
internal sealed class StubClassInfo
117172
{
118-
public string ClassName { get; set; }
119-
public string Namespace { get; set; }
173+
public string StubClassName { get; set; }
174+
public string TargetTypeNamespace { get; set; }
120175
public ITypeSymbol TargetType { get; set; }
176+
public string Path { get; set; }
177+
public int Line { get; set; }
178+
public int Column { get; set; }
121179
}

src/bunit.web/Extensions/StubComponentFactoryCollectionExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,20 @@ public static ComponentFactoryCollection AddStub(
161161
factories.Add(new StubComponentFactory(componentTypePredicate, replacementFragment));
162162
return factories;
163163
}
164+
165+
#if NET8_0_OR_GREATER
166+
/// <summary>
167+
/// TODO.
168+
/// </summary>
169+
#pragma warning disable S2326
170+
public static ComponentFactoryCollection AddGeneratedStub<TComponent>(this ComponentFactoryCollection factories) where TComponent : IComponent
171+
#pragma warning restore S2326
172+
{
173+
if (factories is null)
174+
throw new ArgumentNullException(nameof(factories));
175+
176+
return factories;
177+
}
178+
#endif
164179
}
165180
#endif

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
using Microsoft.AspNetCore.Components;
2+
using Microsoft.AspNetCore.Components.Rendering;
23

34
namespace Bunit.Web.Stub;
45

6+
public class ParentComponent : ComponentBase
7+
{
8+
protected override void BuildRenderTree(RenderTreeBuilder builder)
9+
{
10+
builder.OpenComponent<CounterComponent>(1);
11+
builder.AddAttribute(2, "Count", 2);
12+
builder.CloseComponent();
13+
}
14+
}
15+
516
public class CounterComponent : ComponentBase
617
{
718
[Parameter] public int Count { get; set; }
Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,15 @@
1-
using System.Reflection;
2-
using Microsoft.AspNetCore.Components;
3-
41
namespace Bunit.Web.Stub;
52

6-
public class StubTests
3+
public class StubTests : TestContext
74
{
85
[Fact]
96
public void Stubbed_component_has_same_parameters()
107
{
11-
var counterComponentStubProperties = typeof(CounterComponentStub).GetProperties();
12-
13-
foreach (var prop in typeof(CounterComponent).GetProperties())
14-
{
15-
var matchingProp = counterComponentStubProperties.FirstOrDefault(p => p.Name == prop.Name);
16-
17-
Assert.NotNull(matchingProp);
8+
ComponentFactories.AddGeneratedStub<CounterComponent>();
9+
10+
var cut = RenderComponent<ParentComponent>();
1811

19-
var isParameter = prop.GetCustomAttribute(typeof(ParameterAttribute)) is not null;
20-
var stubIsParameter = matchingProp.GetCustomAttribute(typeof(ParameterAttribute)) is not null;
21-
Assert.Equal(isParameter, stubIsParameter);
22-
23-
var isCascadingParameter = prop.GetCustomAttribute(typeof(CascadingParameterAttribute)) is not null;
24-
var stubIsCascadingParameter = matchingProp.GetCustomAttribute(typeof(CascadingParameterAttribute)) is not null;
25-
Assert.Equal(isCascadingParameter, stubIsCascadingParameter);
26-
}
12+
var child = cut.FindComponent<CounterComponentStub>();
13+
Assert.Equal(2, child.Instance.Count);
2714
}
28-
}
29-
30-
[Stub(typeof(CounterComponent))]
31-
public partial class CounterComponentStub : ComponentBase
32-
{
33-
}
15+
}

tests/bunit.generators.tests/bunit.generators.tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<IsPackable>false</IsPackable>
1010
<IsTestProject>true</IsTestProject>
1111
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
12+
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Bunit</InterceptorsPreviewNamespaces>
1213
</PropertyGroup>
1314

1415
<ItemGroup>
@@ -34,6 +35,7 @@
3435
<ItemGroup>
3536
<ProjectReference Include="..\..\src\bunit.generators.internal\bunit.generators.internal.csproj" />
3637
<ProjectReference Include="..\..\src\bunit.generators\bunit.generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
38+
<ProjectReference Include="..\..\src\bunit.web\bunit.web.csproj" />
3739
</ItemGroup>
3840

3941
</Project>

0 commit comments

Comments
 (0)