22using System . Text ;
33using Microsoft . CodeAnalysis ;
44using Microsoft . CodeAnalysis . CSharp . Syntax ;
5- using Microsoft . CodeAnalysis . Text ;
65
76namespace Bunit . Web . Stubs ;
87
@@ -12,70 +11,124 @@ namespace Bunit.Web.Stubs;
1211[ Generator ]
1312public 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 ( $ "\t static 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 \t public static global::Bunit.ComponentFactoryCollection AddGeneratedStubInterceptor<TComponent>(this global::Bunit.ComponentFactoryCollection factories)" ) ;
114+ interceptorSource . AppendLine ( "\t \t \t where TComponent : Microsoft.AspNetCore.Components.IComponent" ) ;
115+ interceptorSource . AppendLine ( "\t \t {" ) ;
116+ interceptorSource . AppendLine ( $ "\t \t \t return 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
116171internal 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}
0 commit comments