Skip to content

Commit

Permalink
Merge pull request #1216 from sharwell/multi-target
Browse files Browse the repository at this point in the history
Support Roslyn 3.8 and Roslyn 4.0 source generator scenarios
  • Loading branch information
clairernovotny authored Aug 19, 2021
2 parents 1a3890d + 482cf42 commit 150dd1f
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 35 deletions.
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

0 comments on commit 150dd1f

Please sign in to comment.