From bf71ec2908148fd6457b7219db8b7f648a63b936 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sat, 10 Sep 2022 23:29:26 +0800 Subject: [PATCH] First step to target fully source gen implementation I extract simple model for Refit client representation withing source gen That to allow facilitate process - Parse Syntax (done by Roslyn itslef) - Build internal model - Write based on model only, so logic would be as simple as possible Also that allows write more robust test if we test just internal model extraction from symbols. Otherwise if we will capture generated output, if we decide improve generation it will produce a lot of churn. I do not agains churn like that, but not sure that's would be appropriate. --- .../InterfaceStubGenerator.Shared.projitems | 4 +- .../InterfaceStubGenerator.cs | 95 ++++------------- .../RefitClientModel.cs | 100 ++++++++++++++++++ .../RefitMetadata.cs | 22 ++++ 4 files changed, 145 insertions(+), 76 deletions(-) create mode 100644 InterfaceStubGenerator.Shared/RefitClientModel.cs create mode 100644 InterfaceStubGenerator.Shared/RefitMetadata.cs diff --git a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems index 5589f69c2..3c02c35ff 100644 --- a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems +++ b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems @@ -6,10 +6,12 @@ b591423d-f92d-4e00-b0eb-615c9853506c - InterfaceStubGenerator.Shared + Refit.Generator + + \ No newline at end of file diff --git a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs index e59526513..1538f1f1f 100644 --- a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs +++ b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs @@ -86,6 +86,7 @@ public void GenerateInterfaceStubs( return; } + var refitMetadata = new RefitMetadata(disposableInterfaceSymbol, httpMethodBaseAttributeSymbol); // Check the candidates and keep the ones we're actually interested in @@ -100,7 +101,7 @@ public void GenerateInterfaceStubs( { // Get the symbol being declared by the method var methodSymbol = model.GetDeclaredSymbol(method); - if (IsRefitMethod(methodSymbol, httpMethodBaseAttributeSymbol)) + if (refitMetadata.IsRefitMethod(methodSymbol)) { var isAnnotated = compilation.Options.NullableContextOptions == NullableContextOptions.Enable || model.GetNullableContext(method.SpanStart) == NullableContext.Enabled; @@ -131,7 +132,7 @@ public void GenerateInterfaceStubs( // The interface has no refit methods, but its base interfaces might var hasDerivedRefit = ifaceSymbol.AllInterfaces .SelectMany(i => i.GetMembers().OfType()) - .Where(m => IsRefitMethod(m, httpMethodBaseAttributeSymbol)) + .Where(refitMetadata.IsRefitMethod) .Any(); if (hasDerivedRefit) @@ -212,18 +213,15 @@ internal static partial class Generated // each group is keyed by the Interface INamedTypeSymbol and contains the members // with a refit attribute on them. Types may contain other members, without the attribute, which we'll // need to check for and error out on - + var model = new RefitClientModel(group.Key, group.Value, refitMetadata); var classSource = ProcessInterface(context, reportDiagnostic, - group.Key, - group.Value, + model, preserveAttributeSymbol, - disposableInterfaceSymbol, - httpMethodBaseAttributeSymbol, supportsNullable, - interfaceToNullableEnabledMap[group.Key]); + interfaceToNullableEnabledMap[model.RefitInterface]); - var keyName = group.Key.Name; + var keyName = model.FileName; if(keyCount.TryGetValue(keyName, out var value)) { keyName = $"{keyName}{++value}"; @@ -237,37 +235,13 @@ internal static partial class Generated string ProcessInterface(TContext context, Action reportDiagnostic, - INamedTypeSymbol interfaceSymbol, - List refitMethods, + RefitClientModel interfaceModel, ISymbol preserveAttributeSymbol, - ISymbol disposableInterfaceSymbol, - INamedTypeSymbol httpMethodBaseAttributeSymbol, bool supportsNullable, bool nullableEnabled) { - - // Get the class name with the type parameters, then remove the namespace - var className = interfaceSymbol.ToDisplayString(); - var lastDot = className.LastIndexOf('.'); - if(lastDot > 0) - { - className = className.Substring(lastDot+1); - } - var classDeclaration = $"{interfaceSymbol.ContainingType?.Name}{className}"; - - - // Get the class name itself - var classSuffix = $"{interfaceSymbol.ContainingType?.Name}{interfaceSymbol.Name}"; - var ns = interfaceSymbol.ContainingNamespace?.ToDisplayString(); - - // if it's the global namespace, our lookup rules say it should be the same as the class name - if(interfaceSymbol.ContainingNamespace != null && interfaceSymbol.ContainingNamespace.IsGlobalNamespace) - { - ns = string.Empty; - } - - // Remove dots - ns = ns!.Replace(".", ""); + INamedTypeSymbol interfaceSymbol = interfaceModel.RefitInterface; + List refitMethods = interfaceModel.RefitMethods; // See what the nullable context is @@ -301,8 +275,8 @@ partial class Generated [{preserveAttributeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}] [global::System.Reflection.Obfuscation(Exclude=true)] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - partial class {ns}{classDeclaration} - : {interfaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}{GenerateConstraints(interfaceSymbol.TypeParameters, false)} + partial class {interfaceModel.NamespacePrefix}{interfaceModel.ClassDeclaration} + : {interfaceModel.BaseInterfaceDeclaration}{GenerateConstraints(interfaceSymbol.TypeParameters, false)} {{ /// @@ -310,30 +284,13 @@ partial class {ns}{classDeclaration} readonly global::Refit.IRequestBuilder requestBuilder; /// - public {ns}{classSuffix}(global::System.Net.Http.HttpClient client, global::Refit.IRequestBuilder requestBuilder) + public {interfaceModel.NamespacePrefix}{interfaceModel.ClassSuffix}(global::System.Net.Http.HttpClient client, global::Refit.IRequestBuilder requestBuilder) {{ Client = client; this.requestBuilder = requestBuilder; }} "); - // Get any other methods on the refit interfaces. We'll need to generate something for them and warn - var nonRefitMethods = interfaceSymbol.GetMembers().OfType().Except(refitMethods, SymbolEqualityComparer.Default).Cast().ToList(); - - // get methods for all inherited - var derivedMethods = interfaceSymbol.AllInterfaces.SelectMany(i => i.GetMembers().OfType()).ToList(); - - // Look for disposable - var disposeMethod = derivedMethods.Find(m => m.ContainingType?.Equals(disposableInterfaceSymbol, SymbolEqualityComparer.Default) == true); - if(disposeMethod != null) - { - //remove it from the derived methods list so we don't process it with the rest - derivedMethods.Remove(disposeMethod); - } - - // Pull out the refit methods from the derived types - var derivedRefitMethods = derivedMethods.Where(m => IsRefitMethod(m, httpMethodBaseAttributeSymbol)).ToList(); - var derivedNonRefitMethods = derivedMethods.Except(derivedMethods, SymbolEqualityComparer.Default).Cast().ToList(); // Handle Refit Methods foreach(var method in refitMethods) @@ -341,28 +298,22 @@ partial class {ns}{classDeclaration} ProcessRefitMethod(source, method, true); } - foreach (var method in refitMethods.Concat(derivedRefitMethods)) + foreach (var method in interfaceModel.AllRefitMethods) { ProcessRefitMethod(source, method, false); } // Handle non-refit Methods that aren't static or properties or have a method body - foreach (var method in nonRefitMethods.Concat(derivedNonRefitMethods)) + foreach (var method in interfaceModel.NonRefitMethods) { - if (method.IsStatic || - method.MethodKind == MethodKind.PropertyGet || - method.MethodKind == MethodKind.PropertySet || - !method.IsAbstract) // If an interface method has a body, it won't be abstract - continue; - ProcessNonRefitMethod(context, reportDiagnostic, source, method); } // Handle Dispose - if(disposeMethod != null) + if (interfaceModel.DisposeMethod != null) { - ProcessDisposableMethod(source, disposeMethod); + ProcessDisposableMethod(source, interfaceModel.DisposeMethod); } source.Append(@" @@ -432,7 +383,7 @@ string GenerateConstraints(ImmutableArray typeParameters, { var source = new StringBuilder(); // Need to loop over the constraints and create them - foreach(var typeParameter in typeParameters) + foreach (var typeParameter in typeParameters) { WriteConstraitsForTypeParameter(source, typeParameter, isOverrideOrExplicitImplementation); } @@ -445,7 +396,7 @@ void WriteConstraitsForTypeParameter(StringBuilder source, ITypeParameterSymbol // Explicit interface implementations and ovverrides can only have class or struct constraints var parameters = new List(); - if(typeParameter.HasReferenceTypeConstraint) + if (typeParameter.HasReferenceTypeConstraint) { parameters.Add("class"); } @@ -468,7 +419,7 @@ void WriteConstraitsForTypeParameter(StringBuilder source, ITypeParameterSymbol parameters.Add(typeConstraint.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); } } - + // new constraint has to be last if (typeParameter.HasConstructorConstraint && !isOverrideOrExplicitImplementation) { @@ -534,12 +485,6 @@ void WriteMethodOpening(StringBuilder source, IMethodSymbol methodSymbol, bool i void WriteMethodClosing(StringBuilder source) => source.Append(@" }"); - - bool IsRefitMethod(IMethodSymbol? methodSymbol, INamedTypeSymbol httpMethodAttibute) - { - return methodSymbol?.GetAttributes().Any(ad => ad.AttributeClass?.InheritsFromOrEquals(httpMethodAttibute) == true) == true; - } - #if ROSLYN_4 public void Initialize(IncrementalGeneratorInitializationContext context) diff --git a/InterfaceStubGenerator.Shared/RefitClientModel.cs b/InterfaceStubGenerator.Shared/RefitClientModel.cs new file mode 100644 index 000000000..b3192d05f --- /dev/null +++ b/InterfaceStubGenerator.Shared/RefitClientModel.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Linq; + +using Microsoft.CodeAnalysis; + +namespace Refit.Generator; + +internal class RefitClientModel +{ + readonly RefitMetadata refitMetadata; + + public RefitClientModel(INamedTypeSymbol refitInterface, List refitMethods, RefitMetadata refitMetadata) + { + RefitInterface = refitInterface; + RefitMethods = refitMethods; + this.refitMetadata = refitMetadata; + + // Get any other methods on the refit interfaces. We'll need to generate something for them and warn + var nonRefitMethods = refitInterface.GetMembers().OfType().Except(refitMethods, SymbolEqualityComparer.Default).Cast().ToList(); + + // get methods for all refitInterface + var derivedMethods = refitInterface.AllInterfaces.SelectMany(i => i.GetMembers().OfType()).ToList(); + + // Look for disposable + DisposeMethod = derivedMethods.Find(m => m.ContainingType?.Equals(refitMetadata.DisposableInterfaceSymbol, SymbolEqualityComparer.Default) == true); + if (DisposeMethod != null) + { + //remove it from the derived methods list so we don't process it with the rest + derivedMethods.Remove(DisposeMethod); + } + + // Pull out the refit methods from the derived types + var derivedRefitMethods = derivedMethods.Where(refitMetadata.IsRefitMethod).ToList(); + var derivedNonRefitMethods = derivedMethods.Except(derivedMethods, SymbolEqualityComparer.Default).Cast().ToList(); + + AllRefitMethods = refitMethods.Concat(derivedRefitMethods); + NonRefitMethods = nonRefitMethods.Concat(derivedNonRefitMethods) + .Where(static method => + { + return !(method.IsStatic || + method.MethodKind == MethodKind.PropertyGet || + method.MethodKind == MethodKind.PropertySet || + !method.IsAbstract); + }); + } + + public INamedTypeSymbol RefitInterface { get; } + public List RefitMethods { get; } + public IEnumerable AllRefitMethods { get; } + public IEnumerable NonRefitMethods { get; } + + public string FileName => RefitInterface.Name; + + public string ClassDeclaration + { + get + { + // Get the class name with the type parameters, then remove the namespace + var className = RefitInterface.ToDisplayString(); + var lastDot = className.LastIndexOf('.'); + if (lastDot > 0) + { + className = className.Substring(lastDot + 1); + } + var classDeclaration = $"{RefitInterface.ContainingType?.Name}{className}"; + return classDeclaration; + } + } + + public string ClassSuffix + { + get + { + // Get the class name itself + var classSuffix = $"{RefitInterface.ContainingType?.Name}{RefitInterface.Name}"; + return classSuffix; + } + } + + public string NamespacePrefix + { + get + { + var ns = RefitInterface.ContainingNamespace?.ToDisplayString(); + + // if it's the global namespace, our lookup rules say it should be the same as the class name + if (RefitInterface.ContainingNamespace != null && RefitInterface.ContainingNamespace.IsGlobalNamespace) + { + return string.Empty; + } + + // Remove dots + ns = ns!.Replace(".", ""); + return ns; + } + } + public string BaseInterfaceDeclaration => $"{RefitInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}"; + + public IMethodSymbol DisposeMethod { get; } +} diff --git a/InterfaceStubGenerator.Shared/RefitMetadata.cs b/InterfaceStubGenerator.Shared/RefitMetadata.cs new file mode 100644 index 000000000..b6fa0bbd0 --- /dev/null +++ b/InterfaceStubGenerator.Shared/RefitMetadata.cs @@ -0,0 +1,22 @@ +using System.Linq; + +using Microsoft.CodeAnalysis; + +namespace Refit.Generator; + +internal class RefitMetadata +{ + public RefitMetadata(INamedTypeSymbol? disposableInterfaceSymbol, INamedTypeSymbol httpMethodBaseAttributeSymbol) + { + DisposableInterfaceSymbol = disposableInterfaceSymbol; + HttpMethodBaseAttributeSymbol = httpMethodBaseAttributeSymbol; + } + + public INamedTypeSymbol? DisposableInterfaceSymbol { get; } + public INamedTypeSymbol HttpMethodBaseAttributeSymbol { get; } + + public bool IsRefitMethod(IMethodSymbol? methodSymbol) + { + return methodSymbol?.GetAttributes().Any(ad => ad.AttributeClass?.InheritsFromOrEquals(HttpMethodBaseAttributeSymbol) == true) == true; + } +}