Skip to content

Commit

Permalink
feat: First working versions
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Nov 25, 2023
1 parent af8a56c commit 21249a5
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 82 deletions.
139 changes: 62 additions & 77 deletions src/bunit.generators/Web.Stubs/StubGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace Bunit.Web.Stubs;

Expand All @@ -13,61 +12,49 @@ namespace Bunit.Web.Stubs;
[Generator]
public class StubGenerator : IIncrementalGenerator
{
private const string AttributeFullQualifiedName = "Bunit.StubAttribute";

/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
"StubAttribute.g.cs",
SourceText.From(StubAttributeGenerator.StubAttribute, Encoding.UTF8)));

var classesToStub = context.SyntaxProvider
.ForAttributeWithMetadataName(
AttributeFullQualifiedName,
predicate: static (s, _) => s is ClassDeclarationSyntax,
transform: static (ctx, _) => GetStubClassInfo(ctx))
.Where(static m => m is not null);

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

context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Item2, spc));
context.RegisterSourceOutput(
classesToStub,
static (spc, source) => Execute(source, spc));
}

private static StubClassInfo GetStubClassInfo(GeneratorSyntaxContext context)
private static StubClassInfo GetStubClassInfo(GeneratorAttributeSyntaxContext context)
{
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;

// Check if the class is partial
if (!classDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword))
{
return null;
}

// Find the StubAttribute on the class
foreach (var attribute in classDeclarationSyntax.AttributeLists.SelectMany(a => a.Attributes))
foreach (var attribute in context.TargetSymbol.GetAttributes())
{
var attributeSymbol =
ModelExtensions.GetSymbolInfo(context.SemanticModel, attribute).Symbol as IMethodSymbol;
if (attributeSymbol is null || !IsStubAttribute(attributeSymbol))
if (context.TargetSymbol is not ITypeSymbol stubbedType ||
!ImplementsInterface(stubbedType, "Microsoft.AspNetCore.Components.IComponent"))
{
continue;
}

if (attribute.ArgumentList?.Arguments is not [{ Expression: TypeOfExpressionSyntax typeOfExpression }])
{
continue;
}

var typeSymbol = ModelExtensions.GetTypeInfo(context.SemanticModel, typeOfExpression.Type).Type;
if (typeSymbol == null)
{
continue;
}
var namespaceName = stubbedType.ContainingNamespace.ToDisplayString();
var className = context.TargetSymbol.Name;

if (!ImplementsInterface(typeSymbol, "Microsoft.AspNetCore.Components.IComponent"))
// TODO: Check for the name not the first
var originalTypeToStub = attribute.ConstructorArguments.FirstOrDefault().Value;
if (originalTypeToStub is not ITypeSymbol originalType)
{
continue;
}

var namespaceSyntax = classDeclarationSyntax.Parent as NamespaceDeclarationSyntax;
var namespaceName = namespaceSyntax?.Name.ToString();
var className = classDeclarationSyntax.Identifier.ValueText;

return new StubClassInfo { ClassName = className, Namespace = namespaceName, TargetType = typeSymbol };
return new StubClassInfo { ClassName = className, Namespace = namespaceName, TargetType = originalType };
}

return null;
Expand All @@ -78,51 +65,49 @@ static bool ImplementsInterface(ITypeSymbol typeSymbol, string interfaceName)
}
}

private static bool IsStubAttribute(ISymbol attributeSymbol) => attributeSymbol.ContainingType.ToDisplayString() == "Bunit.StubAttribute";

private static void Execute(ImmutableArray<StubClassInfo> classes, SourceProductionContext context)
private static void Execute(StubClassInfo classInfo, SourceProductionContext context)
{
context.AddSource("StubAttribute.g.cs", StubAttributeGenerator.StubAttribute);

if (classes.IsDefaultOrEmpty)
var hasSomethingToStub = false;
var targetTypeSymbol = (INamedTypeSymbol)classInfo!.TargetType;
var sourceBuilder = new StringBuilder();

// TODO: Shall we dictate file-scoped namespaces here?
sourceBuilder.AppendLine($"namespace {classInfo.Namespace};");

// TODO: If the class is a nested one, that approach does not work
sourceBuilder.AppendLine($"public partial class {classInfo.ClassName}");
sourceBuilder.Append("{");

foreach (var member in targetTypeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.GetAttributes()
.Any(attr =>
attr.AttributeClass?.ToDisplayString() ==
"Microsoft.AspNetCore.Components.ParameterAttribute" ||
attr.AttributeClass?.ToDisplayString() ==
"Microsoft.AspNetCore.Components.CascadingParameterAttribute")))
{
return;
sourceBuilder.AppendLine();

hasSomethingToStub = true;
var propertyType = member.Type.ToDisplayString();
var propertyName = member.Name;

var isParameterAttribute = member.GetAttributes().Any(attr =>
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterAttribute");
var attributeLine = isParameterAttribute
? "\t[global::Microsoft.AspNetCore.Components.Parameter]"
: "\t[global::Microsoft.AspNetCore.Components.CascadingParameter]";

sourceBuilder.AppendLine(attributeLine);
sourceBuilder.AppendLine($"\tpublic {propertyType} {propertyName} {{ get; set; }}");
}

foreach (var classInfo in classes.Where(t => t?.TargetType is INamedTypeSymbol))
{
var targetTypeSymbol = (INamedTypeSymbol)classInfo!.TargetType;
var sourceBuilder = new StringBuilder();

sourceBuilder.AppendLine($"namespace {classInfo.Namespace};");
sourceBuilder.AppendLine($"public partial class {classInfo.ClassName}");
sourceBuilder.AppendLine("{");

foreach (var member in targetTypeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.GetAttributes()
.Any(attr =>
attr.AttributeClass?.ToDisplayString() ==
"Microsoft.AspNetCore.Components.ParameterAttribute" ||
attr.AttributeClass?.ToDisplayString() ==
"Microsoft.AspNetCore.Components.CascadingParameterAttribute")))
{
var propertyType = $"global::{member.Type.ToDisplayString()}";
var propertyName = member.Name;

var isParameterAttribute = member.GetAttributes().Any(attr =>
attr.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterAttribute");
var attributeLine = isParameterAttribute
? "\t[global::Microsoft.AspNetCore.Components.Parameter]"
: "\t[global::Microsoft.AspNetCore.Components.CascadingParameter]";

sourceBuilder.AppendLine(attributeLine);
sourceBuilder.AppendLine($"\tpublic {propertyType} {propertyName} {{ get; set; }}");
sourceBuilder.AppendLine();
}
sourceBuilder.AppendLine("}");

sourceBuilder.AppendLine("}");
if (hasSomethingToStub)
{
context.AddSource($"{classInfo.ClassName}Stub.g.cs", sourceBuilder.ToString());
}
}
Expand Down
1 change: 1 addition & 0 deletions tests/bunit.generators.tests/Web.Stub/CounterComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public class CounterComponent : ComponentBase
{
[Parameter] public int Count { get; set; }
[CascadingParameter] public int CascadingCount { get; set; }
[Parameter] public EventCallback IncrementCount { get; set; }
}
10 changes: 5 additions & 5 deletions tests/bunit.generators.tests/Web.Stub/StubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public void Stubbed_component_has_same_parameters()
Assert.Equal(isCascadingParameter, stubIsCascadingParameter);
}
}
[Stub(typeof(CounterComponentStub))]
public partial class CounterComponentStub : ComponentBase
{
}
}

[Stub(typeof(CounterComponent))]
public partial class CounterComponentStub : ComponentBase
{
}

0 comments on commit 21249a5

Please sign in to comment.