Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Roslyn 3.8 and Roslyn 4.0 source generator scenarios #1216

Merged
merged 6 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>InterfaceStubGeneratorV1</AssemblyName>
<RootNamespace>Refit.Generator</RootNamespace>
<IsPackable>false</IsPackable>
<AssemblyOriginatorKeyFile>..\buildtask.snk</AssemblyOriginatorKeyFile>
Expand All @@ -21,4 +22,6 @@
</PropertyGroup>
</Target>

<Import Project="..\InterfaceStubGenerator.Shared\InterfaceStubGenerator.Shared.projitems" Label="Shared" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>InterfaceStubGeneratorV2</AssemblyName>
<RootNamespace>Refit.Generator</RootNamespace>
<IsPackable>false</IsPackable>
<AssemblyOriginatorKeyFile>..\buildtask.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<IsRoslynComponent>true</IsRoslynComponent>
<Nullable>enable</Nullable>
<DefineConstants>$(DefineConstants);ROSLYN_4</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.0-2.final" PrivateAssets="all" />
</ItemGroup>

<Target Name="SetBuildVer" AfterTargets="GetBuildVersion" BeforeTargets="SetCloudBuildVersionVars;SetCloudBuildNumberWithVersion">
<PropertyGroup>
<Version>$(BuildVersion)</Version>
<AssemblyVersion>$(BuildVersionSimple)</AssemblyVersion>
</PropertyGroup>
</Target>

<Import Project="..\InterfaceStubGenerator.Shared\InterfaceStubGenerator.Shared.projitems" Label="Shared" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>b591423d-f92d-4e00-b0eb-615c9853506c</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>InterfaceStubGenerator.Shared</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)InterfaceStubGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ITypeSymbolExtensions.cs" />
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.shproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>b591423d-f92d-4e00-b0eb-615c9853506c</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="InterfaceStubGenerator.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ namespace Refit.Generator
// defn's

[Generator]
#if ROSLYN_4
public class InterfaceStubGeneratorV2 : IIncrementalGenerator
#else
public class InterfaceStubGenerator : ISourceGenerator
#endif
{
#pragma warning disable RS2008 // Enable analyzer release tracking
static readonly DiagnosticDescriptor InvalidRefitMember = new(
"RF001",
"Refit types must have Refit HTTP method attributes",
"Method {0}.{1} either has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument.",
"Method {0}.{1} either has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument",
"Refit",
DiagnosticSeverity.Warning,
true);
Expand All @@ -37,40 +41,59 @@ public class InterfaceStubGenerator : ISourceGenerator
true);
#pragma warning restore RS2008 // Enable analyzer release tracking

public void Execute(GeneratorExecutionContext context)
{
GenerateInterfaceStubs(context);
}
#if !ROSLYN_4

public void GenerateInterfaceStubs(GeneratorExecutionContext context)
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
return;

context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.RefitInternalNamespace", out var refitInternalNamespace);

GenerateInterfaceStubs(
context,
static (context, diagnostic) => context.ReportDiagnostic(diagnostic),
static (context, hintName, sourceText) => context.AddSource(hintName, sourceText),
(CSharpCompilation)context.Compilation,
refitInternalNamespace,
receiver.CandidateMethods.ToImmutableArray(),
receiver.CandidateInterfaces.ToImmutableArray());
}

#endif

public void GenerateInterfaceStubs<TContext>(
TContext context,
Action<TContext, Diagnostic> reportDiagnostic,
Action<TContext, string, SourceText> addSource,
CSharpCompilation compilation,
string? refitInternalNamespace,
ImmutableArray<MethodDeclarationSyntax> candidateMethods,
ImmutableArray<InterfaceDeclarationSyntax> candidateInterfaces)
{
refitInternalNamespace = $"{refitInternalNamespace ?? string.Empty}RefitInternalGenerated";

// we're going to create a new compilation that contains the attribute.
// TODO: we should allow source generators to provide source during initialize, so that this step isn't required.
var options = (context.Compilation as CSharpCompilation)!.SyntaxTrees[0].Options as CSharpParseOptions;
var compilation = context.Compilation;
var options = (CSharpParseOptions)compilation.SyntaxTrees[0].Options;

var disposableInterfaceSymbol = compilation.GetTypeByMetadataName("System.IDisposable")!;
var httpMethodBaseAttributeSymbol = compilation.GetTypeByMetadataName("Refit.HttpMethodAttribute");

if(httpMethodBaseAttributeSymbol == null)
{
context.ReportDiagnostic(Diagnostic.Create(RefitNotReferenced, null));
reportDiagnostic(context, Diagnostic.Create(RefitNotReferenced, null));
return;
}


// Check the candidates and keep the ones we're actually interested in

var interfaceToNullableEnabledMap = new Dictionary<INamedTypeSymbol, bool>();
#pragma warning disable RS1024 // Compare symbols correctly
var interfaceToNullableEnabledMap = new Dictionary<INamedTypeSymbol, bool>(SymbolEqualityComparer.Default);
#pragma warning restore RS1024 // Compare symbols correctly
var methodSymbols = new List<IMethodSymbol>();
foreach (var group in receiver.CandidateMethods.GroupBy(m => m.SyntaxTree))
foreach (var group in candidateMethods.GroupBy(m => m.SyntaxTree))
{
var model = compilation.GetSemanticModel(group.Key);
foreach (var method in group)
Expand All @@ -79,7 +102,7 @@ public void GenerateInterfaceStubs(GeneratorExecutionContext context)
var methodSymbol = model.GetDeclaredSymbol(method);
if (IsRefitMethod(methodSymbol, httpMethodBaseAttributeSymbol))
{
var isAnnotated = context.Compilation.Options.NullableContextOptions == NullableContextOptions.Enable ||
var isAnnotated = compilation.Options.NullableContextOptions == NullableContextOptions.Enable ||
model.GetNullableContext(method.SpanStart) == NullableContext.Enabled;
interfaceToNullableEnabledMap[methodSymbol!.ContainingType] = isAnnotated;

Expand All @@ -88,12 +111,12 @@ public void GenerateInterfaceStubs(GeneratorExecutionContext context)
}
}

var interfaces = methodSymbols.GroupBy(m => m.ContainingType)
var interfaces = methodSymbols.GroupBy<IMethodSymbol, INamedTypeSymbol>(m => m.ContainingType, SymbolEqualityComparer.Default)
.ToDictionary(g => g.Key, v => v.ToList());

// Look through the candidate interfaces
var interfaceSymbols = new List<INamedTypeSymbol>();
foreach(var group in receiver.CandidateInterfaces.GroupBy(i => i.SyntaxTree))
foreach(var group in candidateInterfaces.GroupBy(i => i.SyntaxTree))
{
var model = compilation.GetSemanticModel(group.Key);
foreach (var iface in group)
Expand Down Expand Up @@ -127,7 +150,7 @@ public void GenerateInterfaceStubs(GeneratorExecutionContext context)
if(!interfaces.Any()) return;


var supportsNullable = ((CSharpParseOptions)context.ParseOptions).LanguageVersion >= LanguageVersion.CSharp8;
var supportsNullable = options.LanguageVersion >= LanguageVersion.CSharp8;

var keyCount = new Dictionary<string, int>();

Expand All @@ -152,10 +175,10 @@ sealed class PreserveAttribute : global::System.Attribute
";


compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));
compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));

// add the attribute text
context.AddSource("PreserveAttribute.g.cs", SourceText.From(attributeText, Encoding.UTF8));
addSource(context, "PreserveAttribute.g.cs", SourceText.From(attributeText, Encoding.UTF8));

// get the newly bound attribute
var preserveAttributeSymbol = compilation.GetTypeByMetadataName($"{refitInternalNamespace}.PreserveAttribute")!;
Expand All @@ -177,7 +200,7 @@ internal static partial class Generated
}}
#pragma warning restore
";
context.AddSource("Generated.g.cs", SourceText.From(generatedClassText, Encoding.UTF8));
addSource(context, "Generated.g.cs", SourceText.From(generatedClassText, Encoding.UTF8));

compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(generatedClassText, Encoding.UTF8), options));

Expand All @@ -190,14 +213,15 @@ internal static partial class Generated
// 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 classSource = ProcessInterface(group.Key,
var classSource = ProcessInterface(context,
reportDiagnostic,
group.Key,
group.Value,
preserveAttributeSymbol,
disposableInterfaceSymbol,
httpMethodBaseAttributeSymbol,
supportsNullable,
interfaceToNullableEnabledMap[group.Key],
context);
interfaceToNullableEnabledMap[group.Key]);

var keyName = group.Key.Name;
if(keyCount.TryGetValue(keyName, out var value))
Expand All @@ -206,19 +230,20 @@ internal static partial class Generated
}
keyCount[keyName] = value;

context.AddSource($"{keyName}.g.cs", SourceText.From(classSource, Encoding.UTF8));
addSource(context, $"{keyName}.g.cs", SourceText.From(classSource, Encoding.UTF8));
}

}

string ProcessInterface(INamedTypeSymbol interfaceSymbol,
string ProcessInterface<TContext>(TContext context,
Action<TContext, Diagnostic> reportDiagnostic,
INamedTypeSymbol interfaceSymbol,
List<IMethodSymbol> refitMethods,
ISymbol preserveAttributeSymbol,
ISymbol disposableInterfaceSymbol,
INamedTypeSymbol httpMethodBaseAttributeSymbol,
bool supportsNullable,
bool nullableEnabled,
GeneratorExecutionContext context)
bool nullableEnabled)
{

// Get the class name with the type parameters, then remove the namespace
Expand Down Expand Up @@ -331,7 +356,7 @@ partial class {ns}{classDeclaration}
!method.IsAbstract) // If an interface method has a body, it won't be abstract
continue;

ProcessNonRefitMethod(source, method, context);
ProcessNonRefitMethod(context, reportDiagnostic, source, method);
}

// Handle Dispose
Expand Down Expand Up @@ -458,7 +483,7 @@ void WriteConstraitsForTypeParameter(StringBuilder source, ITypeParameterSymbol

}

void ProcessNonRefitMethod(StringBuilder source, IMethodSymbol methodSymbol, GeneratorExecutionContext context)
void ProcessNonRefitMethod<TContext>(TContext context, Action<TContext, Diagnostic> reportDiagnostic, StringBuilder source, IMethodSymbol methodSymbol)
{
WriteMethodOpening(source, methodSymbol, true);

Expand All @@ -471,7 +496,7 @@ void ProcessNonRefitMethod(StringBuilder source, IMethodSymbol methodSymbol, Gen
foreach(var location in methodSymbol.Locations)
{
var diagnostic = Diagnostic.Create(InvalidRefitMember, location, methodSymbol.ContainingType.Name, methodSymbol.Name);
context.ReportDiagnostic(diagnostic);
reportDiagnostic(context, diagnostic);
}
}

Expand Down Expand Up @@ -515,6 +540,48 @@ bool IsRefitMethod(IMethodSymbol? methodSymbol, INamedTypeSymbol httpMethodAttib
return methodSymbol?.GetAttributes().Any(ad => ad.AttributeClass?.InheritsFromOrEquals(httpMethodAttibute) == true) == true;
}

#if ROSLYN_4

public void Initialize(IncrementalGeneratorInitializationContext context)
{
// We're looking for methods with an attribute that are in an interface
var candidateMethodsProvider = context.SyntaxProvider.CreateSyntaxProvider(
(syntax, cancellationToken) => syntax is MethodDeclarationSyntax { Parent: InterfaceDeclarationSyntax, AttributeLists: { Count: > 0 } },
(context, cancellationToken) => (MethodDeclarationSyntax)context.Node);

// We also look for interfaces that derive from others, so we can see if any base methods contain
// Refit methods
var candidateInterfacesProvider = context.SyntaxProvider.CreateSyntaxProvider(
(syntax, cancellationToken) => syntax is InterfaceDeclarationSyntax { BaseList: not null },
(context, cancellationToken) => (InterfaceDeclarationSyntax)context.Node);

var refitInternalNamespace = context.AnalyzerConfigOptionsProvider.Select(
(analyzerConfigOptionsProvider, cancellationToken) => analyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.RefitInternalNamespace", out var refitInternalNamespace) ? refitInternalNamespace : null);

var inputs = candidateMethodsProvider.Collect()
.Combine(candidateInterfacesProvider.Collect())
.Select((combined, cancellationToken) => (candidateMethods: combined.Left, candidateInterfaces: combined.Right))
.Combine(refitInternalNamespace)
.Combine(context.CompilationProvider)
.Select((combined, cancellationToken) => (combined.Left.Left.candidateMethods, combined.Left.Left.candidateInterfaces, refitInternalNamespace: combined.Left.Right, compilation: combined.Right));

context.RegisterSourceOutput(
inputs,
(context, collectedValues) =>
{
GenerateInterfaceStubs(
context,
static (context, diagnostic) => context.ReportDiagnostic(diagnostic),
static (context, hintName, sourceText) => context.AddSource(hintName, sourceText),
(CSharpCompilation)collectedValues.compilation,
collectedValues.refitInternalNamespace,
collectedValues.candidateMethods,
collectedValues.candidateInterfaces);
});
}

#else

public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
Expand Down Expand Up @@ -544,5 +611,7 @@ methodDeclarationSyntax.Parent is InterfaceDeclarationSyntax &&
}
}
}

#endif
}
}
Loading