Skip to content
Open
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
10 changes: 10 additions & 0 deletions InterfaceStubGenerator.Shared/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Text;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -43,7 +44,7 @@
// TODO: we should allow source generators to provide source during initialize, so that this step isn't required.
var options = (CSharpParseOptions)compilation.SyntaxTrees[0].Options;

var disposableInterfaceSymbol = wellKnownTypes.Get(typeof(IDisposable));

Check warning on line 47 in InterfaceStubGenerator.Shared/Parser.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

Prefer the generic overload 'Refit.Generator.WellKnownTypes.Get<T>()' instead of 'Refit.Generator.WellKnownTypes.Get(System.Type)' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)

Check warning on line 47 in InterfaceStubGenerator.Shared/Parser.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

Prefer the generic overload 'Refit.Generator.WellKnownTypes.Get<T>()' instead of 'Refit.Generator.WellKnownTypes.Get(System.Type)' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)
var httpMethodBaseAttributeSymbol = wellKnownTypes.TryGet(
"Refit.HttpMethodAttribute"
);
Expand Down Expand Up @@ -304,9 +305,18 @@
var derivedRefitMethods = derivedMethods
.Where(m => IsRefitMethod(m, httpMethodBaseAttributeSymbol))
.ToArray();

var derivedNonRefitMethods = derivedMethods
.Except(derivedRefitMethods, SymbolEqualityComparer.Default)
.Cast<IMethodSymbol>()
.Where(m =>
// Only flag methods from interfaces that have at least one Refit method.
Comment on lines 309 to +313
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new filter on derivedNonRefitMethods removes inherited non‑Refit members from processing entirely. derivedNonRefitMethods is later used to generate explicit interface implementations (via ParseNonRefitMethod / WriteNonRefitMethod) that are required for the generated client class to compile when the Refit interface inherits a base interface containing non‑Refit members. If the intent is only to suppress RF001 for methods coming from “plain” base interfaces, keep generating the stub methods but conditionally suppress the diagnostic emission (e.g., add a flag to ParseNonRefitMethod to skip DiagnosticDescriptors.InvalidRefitMember for those members).

Copilot uses AI. Check for mistakes.
// Methods from plain non-Refit interfaces (e.g. IBaseApi with GetBaseUri())
// should be silently ignored — they are not HTTP endpoints.
m.ContainingType.GetMembers()
.OfType<IMethodSymbol>()
.Any(im => IsRefitMethod(im, httpMethodBaseAttributeSymbol))
)
.ToArray();

// Exclude base interface methods that the current interface explicitly implements.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ public RefitGeneratorTestIGeneratedInterface(global::System.Net.Http.HttpClient
return await ((global::System.Threading.Tasks.Task<string>)______func(this.Client, ______arguments)).ConfigureAwait(false);
}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated snapshot no longer includes an explicit implementation for IBaseInterface.NonRefitMethod(). Since IGeneratedInterface : IBaseInterface, the generated client must still implement that member (even if it just throws) to compile and be castable to IGeneratedInterface. If the goal is to stop emitting RF001 for non‑Refit base-interface members, suppress the diagnostic but still emit a stub implementation to satisfy the interface contract.

Suggested change
void global::RefitGeneratorTest.IBaseInterface.NonRefitMethod()
{
throw new global::System.NotImplementedException();
}

Copilot uses AI. Check for mistakes.
/// <inheritdoc />
void global::RefitGeneratorTest.IBaseInterface.NonRefitMethod()
{
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.");
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions Refit.Tests/IInterfaceInheritingNonRefit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Refit.Tests
{
public interface IBaseApi
{
string GetBaseUri();
}

public interface IMyRefitApi : IBaseApi
{
[Get("/users")]
Task<List<string>> GetUsers();
}
Comment on lines +8 to +12
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new fixture is not self-contained for the generator test compilation: Task and List are unqualified and there are no using System.Threading.Tasks; / using System.Collections.Generic; directives. In VerifyGenerator only this file is compiled, so these become error symbols and can cause the generator to produce invalid output (and can hide real regressions). Add the required using directives or fully-qualify the types so the compilation is successful and the snapshot represents real generated code.

Copilot uses AI. Check for mistakes.
}
8 changes: 8 additions & 0 deletions Refit.Tests/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ public async Task GenerateInterfaceStubsWithoutNamespaceSmokeTest()
var path = IntegrationTestHelper.GetPath("IServiceWithoutNamespace.cs");
await VerifyGenerator(path);
}

[Fact]
public async Task InheritingFromNonRefitInterfaceDoesNotGenerateDiagnostic()
{
var path = IntegrationTestHelper.GetPath("IInterfaceInheritingNonRefit.cs");
await VerifyGenerator(path);
}
}

public static class ThisIsDumbButMightHappen
Expand Down Expand Up @@ -210,3 +217,4 @@ Task PostMessage<T, U, V>([Body] T message, U param1, V param2)
}

public interface IMessage;

6 changes: 6 additions & 0 deletions Refit.Tests/Refit.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,10 @@
<ItemGroup>
<Folder Include="_snapshots\" />
</ItemGroup>

<ItemGroup>
<Compile Remove="IInterfaceInheritingNonRefit.cs" />
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excluding IInterfaceInheritingNonRefit.cs from compilation looks like a workaround for the fixture not compiling as-is. If you add the missing using directives / fully-qualified type names in the fixture, consider removing this <Compile Remove> (or instead include the fixture as <None> if the intent is to keep it out of the test assembly) so the project doesn’t silently diverge from the actual scenario being tested.

Suggested change
<Compile Remove="IInterfaceInheritingNonRefit.cs" />
<None Include="IInterfaceInheritingNonRefit.cs" />

Copilot uses AI. Check for mistakes.
</ItemGroup>


</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//HintName: Generated.g.cs

#pragma warning disable
namespace Refit.Implementation
{

/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[global::RefitInternalGenerated.PreserveAttribute]
[global::System.Reflection.Obfuscation(Exclude=true)]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class Generated
{
#if NET5_0_OR_GREATER
[System.Runtime.CompilerServices.ModuleInitializer]
[System.Diagnostics.CodeAnalysis.DynamicDependency(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All, typeof(global::Refit.Implementation.Generated))]
public static void Initialize()
{
}
#endif
}
}
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//HintName: IMyRefitApi.g.cs
#nullable disable
#pragma warning disable
namespace Refit.Implementation
{

partial class Generated
{

/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[global::RefitInternalGenerated.PreserveAttribute]
[global::System.Reflection.Obfuscation(Exclude=true)]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
partial class RefitTestsIMyRefitApi
: global::Refit.Tests.IMyRefitApi
{
/// <inheritdoc />
public global::System.Net.Http.HttpClient Client { get; }
readonly global::Refit.IRequestBuilder requestBuilder;

/// <inheritdoc />
public RefitTestsIMyRefitApi(global::System.Net.Http.HttpClient client, global::Refit.IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}


/// <inheritdoc />
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snapshot shows the generated implementation of IMyRefitApi does not provide any implementation for the inherited IBaseApi.GetBaseUri() member. If IBaseApi is part of the consumer compilation (real-world case), this would make the generated client fail to compile (or fail to implement the interface contract). If the intention is only to avoid RF001 for “plain” base interfaces, the generator should still emit an explicit stub for GetBaseUri() (without reporting RF001) so the generated type remains a valid IMyRefitApi implementation.

Suggested change
/// <inheritdoc />
/// <inheritdoc />
public global::System.Uri GetBaseUri()
{
return this.Client.BaseAddress;
}
/// <inheritdoc />

Copilot uses AI. Check for mistakes.
public async Task<List<string>> GetUsers()
{
var ______arguments = global::System.Array.Empty<object>();
var ______func = requestBuilder.BuildRestResultFuncForMethod("GetUsers", global::System.Array.Empty<global::System.Type>() );

return await ((Task<List<string>>)______func(this.Client, ______arguments)).ConfigureAwait(false);
}
}
}
}

#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//HintName: PreserveAttribute.g.cs

#pragma warning disable
namespace RefitInternalGenerated
{
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
[global::System.AttributeUsage (global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct | global::System.AttributeTargets.Enum | global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Method | global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Event | global::System.AttributeTargets.Interface | global::System.AttributeTargets.Delegate)]
sealed class PreserveAttribute : global::System.Attribute
{
//
// Fields
//
public bool AllMembers;

public bool Conditional;
}
}
#pragma warning restore
Loading