Skip to content

Commit b627a6b

Browse files
authored
Fix explicit interface method emission and diagnostics (#2017)
* Fix explicit interface method emission and diagnostics Improves handling of explicit interface implementations in the source generator, ensuring that methods explicitly implemented from base interfaces are not redundantly emitted and that diagnostics are not incorrectly reported. Updates method model and emitter logic to track explicitness, normalizes method lookup keys, and adds polyfills for Index/Range for legacy targets. Updates tests and snapshots to reflect correct code generation. * Fix typo in workflow input and add CODECOV_TOKEN secret Corrects 'installWorkflows' to 'installWorkloads' in both ci-build and release workflows. Also adds the CODECOV_TOKEN secret to the ci-build workflow for code coverage reporting. * Remove installWorkloads parameter from workflows The installWorkloads parameter was removed from both ci-build.yml and release.yml GitHub Actions workflows, simplifying the configuration.
1 parent 1199c62 commit b627a6b

File tree

61 files changed

+912
-698
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+912
-698
lines changed

.github/workflows/ci-build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ jobs:
1515
with:
1616
productNamespacePrefix: "Refit"
1717
srcFolder: "./"
18-
installWorkflows: true
18+
secrets:
19+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ jobs:
1414
configuration: Release
1515
productNamespacePrefix: "Refit"
1616
srcFolder: "./"
17-
installWorkflows: false
1817
secrets:
1918
SIGN_ACCOUNT_NAME: ${{ secrets.SIGN_ACCOUNT_NAME }}
2019
SIGN_PROFILE_NAME: ${{ secrets.SIGN_PROFILE_NAME }}

Directory.Build.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
<PublishRepositoryUrl>true</PublishRepositoryUrl>
2424
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)buildtask.snk</AssemblyOriginatorKeyFile>
2525
<!--<SignAssembly>true</SignAssembly>-->
26-
<RefitTargets>net462;netstandard2.0;net8.0;net9.0</RefitTargets>
26+
<RefitTargets Condition="$([MSBuild]::IsOsPlatform('Windows'))">net462</RefitTargets>
27+
<RefitTestTargets>net8.0;net9.0;net10.0</RefitTestTargets>
28+
<RefitTargets>$(RefitTargets);netstandard2.0;$(RefitTestTargets)</RefitTargets>
2729
<NoWarn>IDE0040;CA1054;CA1510</NoWarn>
2830
</PropertyGroup>
2931

InterfaceStubGenerator.Shared/Emitter.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ UniqueNameBuilder uniqueNames
195195
),
196196
};
197197

198-
WriteMethodOpening(source, methodModel, !isTopLevel, isAsync);
198+
var isExplicit = methodModel.IsExplicitInterface || !isTopLevel;
199+
WriteMethodOpening(source, methodModel, isExplicit, isExplicit, isAsync);
199200

200201
// Build the list of args for the array
201202
var argArray = methodModel
@@ -219,10 +220,18 @@ UniqueNameBuilder uniqueNames
219220
? $", new global::System.Type[] {{ {string.Join(", ", genericArray)} }}"
220221
: string.Empty;
221222

223+
// Normalize method lookup key: strip explicit interface prefix if present (e.g. IFoo.Bar -> Bar)
224+
var lookupName = methodModel.Name;
225+
var lastDotIndex = lookupName.LastIndexOf('.');
226+
if (lastDotIndex >= 0 && lastDotIndex < lookupName.Length - 1)
227+
{
228+
lookupName = lookupName.Substring(lastDotIndex + 1);
229+
}
230+
222231
source.WriteLine(
223232
$"""
224233
var ______arguments = {argumentsArrayString};
225-
var ______func = requestBuilder.BuildRestResultFuncForMethod("{methodModel.Name}", {parameterTypesExpression}{genericString} );
234+
var ______func = requestBuilder.BuildRestResultFuncForMethod("{lookupName}", {parameterTypesExpression}{genericString} );
226235
227236
{@return}({returnType})______func(this.Client, ______arguments){configureAwait};
228237
"""
@@ -233,7 +242,8 @@ UniqueNameBuilder uniqueNames
233242

234243
private static void WriteNonRefitMethod(SourceWriter source, MethodModel methodModel)
235244
{
236-
WriteMethodOpening(source, methodModel, true);
245+
var isExplicit = methodModel.IsExplicitInterface;
246+
WriteMethodOpening(source, methodModel, isExplicit, isExplicit);
237247

238248
source.WriteLine(
239249
@"throw new global::System.NotImplementedException(""Either this method has no Refit HTTP method attribute or you've used something other than a string literal for the 'path' argument."");"
@@ -242,9 +252,6 @@ private static void WriteNonRefitMethod(SourceWriter source, MethodModel methodM
242252
WriteMethodClosing(source);
243253
}
244254

245-
// TODO: This assumes that the Dispose method is a void that takes no parameters.
246-
// The previous version did not.
247-
// Does the bool overload cause an issue here.
248255
private static void WriteDisposableMethod(SourceWriter source)
249256
{
250257
source.WriteLine(
@@ -293,6 +300,7 @@ UniqueNameBuilder uniqueNames
293300
private static void WriteMethodOpening(
294301
SourceWriter source,
295302
MethodModel methodModel,
303+
bool isDerivedExplicitImpl,
296304
bool isExplicitInterface,
297305
bool isAsync = false
298306
)
@@ -308,7 +316,12 @@ private static void WriteMethodOpening(
308316

309317
if (isExplicitInterface)
310318
{
311-
builder.Append(@$"{methodModel.ContainingType}.");
319+
var ct = methodModel.ContainingType;
320+
if (!ct.StartsWith("global::"))
321+
{
322+
ct = "global::" + ct;
323+
}
324+
builder.Append(@$"{ct}.");
312325
}
313326
builder.Append(@$"{methodModel.DeclaredMethod}(");
314327

@@ -318,7 +331,6 @@ private static void WriteMethodOpening(
318331
foreach (var param in methodModel.Parameters)
319332
{
320333
var annotation = param.Annotation;
321-
322334
list.Add($@"{param.Type}{(annotation ? '?' : string.Empty)} @{param.MetadataName}");
323335
}
324336

@@ -330,7 +342,7 @@ private static void WriteMethodOpening(
330342
source.WriteLine();
331343
source.WriteLine(builder.ToString());
332344
source.Indentation++;
333-
GenerateConstraints(source, methodModel.Constraints, isExplicitInterface);
345+
GenerateConstraints(source, methodModel.Constraints, isDerivedExplicitImpl || isExplicitInterface);
334346
source.Indentation--;
335347
source.WriteLine("{");
336348
source.Indentation++;

InterfaceStubGenerator.Shared/ImmutableEquatableArray.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ static int Combine(int h1, int h2)
6464

6565
IEnumerator IEnumerable.GetEnumerator() => _values.GetEnumerator();
6666

67-
public struct Enumerator
67+
public record struct Enumerator
6868
{
6969
private readonly T[] _values;
7070
private int _index;

InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="$(MSBuildThisFileDirectory)Models\TypeConstraint.cs" />
2424
<Compile Include="$(MSBuildThisFileDirectory)Models\WellKnownTypes.cs" />
2525
<Compile Include="$(MSBuildThisFileDirectory)Parser.cs" />
26+
<Compile Include="$(MSBuildThisFileDirectory)Polyfills\IndexRange.cs" />
2627
<Compile Include="$(MSBuildThisFileDirectory)SourceWriter.cs" />
2728
<Compile Include="$(MSBuildThisFileDirectory)UniqueNameBuilder.cs" />
2829
</ItemGroup>

InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@
1010

1111
namespace Refit.Generator
1212
{
13-
// * Search for all Interfaces, find the method definitions
14-
// and make sure there's at least one Refit attribute on one
15-
// * Generate the data we need for the template based on interface method
16-
// defn's
17-
1813
/// <summary>
19-
/// InterfaceStubGeneratorV2.
14+
/// InterfaceStubGenerator.
2015
/// </summary>
2116
[Generator]
2217
#if ROSLYN_4
@@ -28,7 +23,6 @@ public class InterfaceStubGenerator : ISourceGenerator
2823
private const string TypeParameterVariableName = "______typeParameters";
2924

3025
#if !ROSLYN_4
31-
3226
/// <summary>
3327
/// Executes the specified context.
3428
/// </summary>
@@ -51,13 +45,11 @@ out var refitInternalNamespace
5145
context.CancellationToken
5246
);
5347

54-
// Emit diagnostics
5548
foreach (var diagnostic in parseStep.diagnostics)
5649
{
5750
context.ReportDiagnostic(diagnostic);
5851
}
5952

60-
// Emit interface stubs
6153
foreach (var interfaceModel in parseStep.contextGenerationSpec.Interfaces)
6254
{
6355
var interfaceText = Emitter.EmitInterface(interfaceModel);
@@ -67,7 +59,6 @@ out var refitInternalNamespace
6759
);
6860
}
6961

70-
// Emit PreserveAttribute and Generated.Initialize
7162
Emitter.EmitSharedCode(
7263
parseStep.contextGenerationSpec,
7364
(name, code) => context.AddSource(name, code)
@@ -76,15 +67,9 @@ out var refitInternalNamespace
7667
#endif
7768

7869
#if ROSLYN_4
79-
80-
/// <summary>
81-
/// Initializes the specified context.
82-
/// </summary>
83-
/// <param name="context">The context.</param>
84-
/// <returns></returns>
70+
/// <inheritdoc/>
8571
public void Initialize(IncrementalGeneratorInitializationContext context)
8672
{
87-
// We're looking for methods with an attribute that are in an interface
8873
var candidateMethodsProvider = context.SyntaxProvider.CreateSyntaxProvider(
8974
(syntax, cancellationToken) =>
9075
syntax
@@ -96,8 +81,6 @@ is MethodDeclarationSyntax
9681
(context, cancellationToken) => (MethodDeclarationSyntax)context.Node
9782
);
9883

99-
// We also look for interfaces that derive from others, so we can see if any base methods contain
100-
// Refit methods
10184
var candidateInterfacesProvider = context.SyntaxProvider.CreateSyntaxProvider(
10285
(syntax, cancellationToken) =>
10386
syntax is InterfaceDeclarationSyntax { BaseList: not null },
@@ -146,9 +129,6 @@ out var refitInternalNamespace
146129
}
147130
);
148131

149-
// output the diagnostics
150-
// use `ImmutableEquatableArray` to cache cases where there are no diagnostics
151-
// otherwise the subsequent steps will always rerun.
152132
var diagnostics = parseStep
153133
.Select(static (x, _) => x.diagnostics.ToImmutableEquatableArray())
154134
.WithTrackingName(RefitGeneratorStepName.ReportDiagnostics);
@@ -168,14 +148,11 @@ out var refitInternalNamespace
168148
}
169149
);
170150
}
171-
172151
#else
173-
174152
/// <summary>
175153
/// Initializes the specified context.
176154
/// </summary>
177155
/// <param name="context">The context.</param>
178-
/// <returns></returns>
179156
public void Initialize(GeneratorInitializationContext context)
180157
{
181158
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
@@ -189,7 +166,6 @@ class SyntaxReceiver : ISyntaxReceiver
189166

190167
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
191168
{
192-
// We're looking for methods with an attribute that are in an interfaces
193169
if (
194170
syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax
195171
&& methodDeclarationSyntax.Parent is InterfaceDeclarationSyntax
@@ -199,15 +175,12 @@ syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax
199175
CandidateMethods.Add(methodDeclarationSyntax);
200176
}
201177

202-
// We also look for interfaces that derive from others, so we can see if any base methods contain
203-
// Refit methods
204178
if (syntaxNode is InterfaceDeclarationSyntax iface && iface.BaseList is not null)
205179
{
206180
CandidateInterfaces.Add(iface);
207181
}
208182
}
209183
}
210-
211184
#endif
212185
}
213186

InterfaceStubGenerator.Shared/Models/MethodModel.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Refit.Generator;
1+
using System.Collections.Immutable;
2+
3+
namespace Refit.Generator;
24

35
internal sealed record MethodModel(
46
string Name,
@@ -7,7 +9,8 @@ internal sealed record MethodModel(
79
string DeclaredMethod,
810
ReturnTypeInfo ReturnTypeMetadata,
911
ImmutableEquatableArray<ParameterModel> Parameters,
10-
ImmutableEquatableArray<TypeConstraint> Constraints
12+
ImmutableEquatableArray<TypeConstraint> Constraints,
13+
bool IsExplicitInterface
1114
);
1215

1316
internal enum ReturnTypeInfo : byte

InterfaceStubGenerator.Shared/Models/WellKnownTypes.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,41 @@
22

33
namespace Refit.Generator;
44

5+
/// <summary>
6+
/// WellKnownTypes.
7+
/// </summary>
58
public class WellKnownTypes(Compilation compilation)
69
{
7-
readonly Dictionary<string, INamedTypeSymbol?> cachedTypes = new();
10+
readonly Dictionary<string, INamedTypeSymbol?> cachedTypes = [];
811

12+
/// <summary>
13+
/// Gets this instance.
14+
/// </summary>
15+
/// <typeparam name="T"></typeparam>
16+
/// <returns></returns>
917
public INamedTypeSymbol Get<T>() => Get(typeof(T));
18+
19+
/// <summary>
20+
/// Gets the specified type.
21+
/// </summary>
22+
/// <param name="type">The type.</param>
23+
/// <returns></returns>
24+
/// <exception cref="InvalidOperationException">Could not get name of type " + type</exception>
1025
public INamedTypeSymbol Get(Type type)
1126
{
27+
if (type is null)
28+
{
29+
throw new ArgumentNullException(nameof(type));
30+
}
31+
1232
return Get(type.FullName ?? throw new InvalidOperationException("Could not get name of type " + type));
1333
}
1434

35+
/// <summary>
36+
/// Tries the get.
37+
/// </summary>
38+
/// <param name="typeFullName">Full name of the type.</param>
39+
/// <returns></returns>
1540
public INamedTypeSymbol? TryGet(string typeFullName)
1641
{
1742
if (cachedTypes.TryGetValue(typeFullName, out var typeSymbol))

0 commit comments

Comments
 (0)