Skip to content

Commit 89779cd

Browse files
fix!: read problem details without a serializer, move reflection to Refit.Reflection, drop deprecated .NET 8/9 (#2374)
* fix: read problem details without the configured serializer - Read problem+json with Refit's own JsonElement reader, so it works with any resolver, when trimmed and under Native AOT - Drop the default converters and number handling so the default options keep the System.Text.Json fast path - Remove JsonExtensionData from ProblemDetails.Extensions and the unused ProblemDetailsJsonContext - Move ObjectToInferredTypesConverter to Refit.Reflection and add AddReflectionConverters for opt-in enum names and inferred objects Closes #2368 Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: move reflection entry points into the reflection packages - Move RestService.For and RequestBuilder to Refit.Reflection as extension members, keeping the call syntax - Add Refit.HttpClientFactory.Reflection for AddRefitClient and AddKeyedRefitClient - Make Refit.Testing use Refit.Reflection for CreateClient - Drop net8.0 and net9.0 targets and their API baselines Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: drop reflection attributes from the request builder contracts - Remove RequiresUnreferencedCode and RequiresDynamicCode from IRequestBuilder and GeneratedOnlyRequestBuilder Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * feat: generate serializer-specific form field names - Add FormFieldNames and IContentSerializerNameStyle so generated form fields pick the right compile-time name - Read Newtonsoft JsonProperty and XmlElement or XmlAttribute names in the generator - Use the generated path for the Newtonsoft and Xml serializers, not just System.Text.Json Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: move reflection form flattening into Refit.Reflection - Core flattens simple values, collections and dictionaries and calls a registered flattener only for run-time-typed complex values - Move the property walk, its metadata cache and ReflectionPropertyHelpers to Refit.Reflection - Add ReflectionSupport.EnsureRegistered, called by every reflection entry point Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: move serializer field-name lookup into Refit.Reflection - Remove IHttpContentSerializer.GetFieldNameForProperty and the PropertyInfo hook from the System.Text.Json, Newtonsoft and Xml serializers - Resolve names in Refit.Reflection from each serializer's name style, with IReflectionFieldNameProvider for custom serializers Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: move enum member lookup into Refit.Reflection - EnumHelpers keeps the numeric helpers; the run-time EnumMember attribute lookup is registered by Refit.Reflection Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: move the reflection method metadata types into Refit.Reflection - RestMethodInfo, RestMethodParameterInfo and RestMethodParameterProperty are only produced by the reflection pipeline Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: move the System.Text.Json reflection fallbacks into Refit.Reflection - The serializer always reads type metadata; options with no resolver get the reflection resolver Refit.Reflection registers, or a message explaining how to supply a context - Remove the per-method reflection fallbacks, the polymorphism attribute checks and the trim suppressions - StubHttp, the reflection request builder and RestService.For register the support Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * test: cover the reflection support and drop the last converter read - Remove the Converters read from WritesDeclaredTypeAsObject so no production code touches a fast-path-blocking option - Add converter and name-lookup tests for the types moved into Refit.Reflection Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: mark the Newtonsoft and XML serializers as not trim or AOT compatible - Remove the trim and AOT suppressions from the Newtonsoft and XML serializers, which wrap reflection-based libraries - Cover the generator's named attribute argument parsing Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * test: cover the enum lookup without a registered resolver Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor!: replace ICustomAttributeProvider with IParameterAttributeProvider in the formatter contract - Add Refit-owned IParameterAttributeProvider so IUrlParameterFormatter no longer exposes System.Reflection - Generated and core code pass an empty provider instead of a Type - Wrap reflection elements in CachedAttributeProvider in Refit.Reflection - Refresh public API baselines, snapshots, tests, examples and benchmarks Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: remove the DynamicDependency attribute from generated registration - Drop the emitted DynamicallyAccessedMemberTypes.All dependency and its polyfill - Refresh snapshots and the test that asserted on it Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * docs: ban LINQ in production code - Add the rule to CLAUDE.md - Drop an unused System.Reflection using from the Newtonsoft serializer Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: keep DefaultJsonTypeInfoResolver out of core Refit - Detect and copy modifiers of the reflection resolver through hooks registered by Refit.Reflection - Replace the uncovered loop in FallbackExplanation with an index check - Cover the unregistered hook paths Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * refactor: drop redundant null and range checks - Remove checks already implied by IsNullOrWhiteSpace and the earlier delay validation Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * test: cover a null global Newtonsoft settings factory Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * test: fix repeated literal in the Newtonsoft settings test Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * test: cover body model equality and interpolated brace escaping - Remove the dead AllowRelative key check, as it is the attribute's only member Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * test: cover more async disposal, route binding and query parsing branches - Exclude two compiler-generated async finally edges and one unreachable BCL fallback - Cover the retry ISO date with an empty value and the query string parser directly Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * test: reach full line and branch coverage - Cover generator edge cases: split partial interfaces, a missing Refit reference, three-argument returns and relative URLs on observables - Cover the analyzer location and path helpers, async disposal and route binding branches - Split the return type and delimiter classification into small testable helpers - Exclude only compiler-generated async finally edges and an unreachable BCL fallback Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> * fix: repair CI for .NET Framework, Native AOT and shared serializers - Register generated clients on .NET Framework by declaring ModuleInitializerAttribute when the target lacks it - Assign the default JSON resolver under a lock so a shared serializer is thread safe - Keep reflection registration out of the StubHttp constructor so AOT apps stay clean - Drop the stale Refit.Reflection trimmer root and pack Refit.Reflection for the package consumer - Update the ProblemDetails sample now that the metadata no longer fails Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com> --------- Co-authored-by: Atakan Ük <61142938+AtakanUk@users.noreply.github.com>
1 parent baf69ae commit 89779cd

305 files changed

Lines changed: 5945 additions & 7611 deletions

File tree

Some content is hidden

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

‎.github/workflows/native-aot.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
with:
2222
projectFile: src/examples/Documentation/Aot/NativeClient/NativeClient.csproj
2323

24-
# Packs Refit, Refit.HttpClientFactory and Refit.Testing once for the package consumer below.
24+
# Packs Refit, Refit.Reflection, Refit.HttpClientFactory and Refit.Testing once for the package consumer below.
2525
pack-for-package-consumer:
2626
runs-on: ubuntu-latest
2727
env:
@@ -46,7 +46,7 @@ jobs:
4646
shell: bash
4747
working-directory: src
4848
run: |
49-
for project in Refit Refit.HttpClientFactory Refit.Testing; do
49+
for project in Refit Refit.Reflection Refit.HttpClientFactory Refit.Testing; do
5050
dotnet pack "$project/$project.csproj" --configuration Release --output "$GITHUB_WORKSPACE/aot-feed"
5151
done
5252

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ dotnet run --project "tests/Refit.GeneratorTests/Refit.GeneratorTests.csproj" -f
165165
- Shared target frameworks and package versions are centralized in `src/Directory.Build.props` and `src/Directory.Packages.props`.
166166
- The repository uses `LangVersion=latest`; use modern C# features where they improve clarity or generated-code quality.
167167
- AOT and trim analyzer behavior is enabled for compatible TFMs; prefer APIs that avoid reflection, dynamic code, and runtime type lookup.
168+
- **No LINQ in production code.** Never use `System.Linq` (`Select`, `Where`, `OfType`, `FirstOrDefault`, `ToArray` and so on) in any shipping project, including the generator, analyzers and examples. Use plain loops. LINQ is allowed in test projects only.
168169
- Keep changes scoped. Do not rewrite unrelated files while touching generator/runtime paths.
169170
- Prefer focused tests that compile or execute the real generated output when changing source generator behavior.
170171

‎src/Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<PropertyGroup>
4949
<RefitGeneratorTargets>netstandard2.0</RefitGeneratorTargets>
5050
<RefitFrameworkTargets>net462;net470;net471;net472;net48;net481</RefitFrameworkTargets>
51-
<RefitModernTargets>net8.0;net9.0;net10.0;net11.0</RefitModernTargets>
51+
<RefitModernTargets>net10.0;net11.0</RefitModernTargets>
5252
<RefitShippingTargets>$(RefitFrameworkTargets);$(RefitModernTargets)</RefitShippingTargets>
5353
<RefitTestTargets>$(RefitModernTargets)</RefitTestTargets>
5454
<RefitBenchmarkTargets>$(RefitModernTargets)</RefitBenchmarkTargets>

‎src/InterfaceStubGenerator.Shared/BodyContentEmission.cs‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ internal static string BuildInlineFormUnroll(
174174
}
175175

176176
// CanUnrollForm rejects the null, HttpContent, Stream, string, and dictionary bodies the reflection path
177-
// special-cases; a non-System.Text.Json serializer resolves field names differently, so it falls back too.
177+
// special-cases; a serializer that does not report its name style resolves field names differently, so it falls back too.
178178
return $$"""
179-
{{bodyIndent}}if ({{settingsLocal}}.ContentSerializer is global::Refit.SystemTextJsonContentSerializer
179+
{{bodyIndent}}if ({{settingsLocal}}.ContentSerializer is global::Refit.IContentSerializerNameStyle
180180
{{inner}} && global::Refit.GeneratedRequestRunner.CanUnrollForm({{bodyExpr}}))
181181
{{bodyIndent}}{
182182
{{inner}}var {{entriesLocal}} = new global::System.Collections.Generic.List<{{kvpType}}>({{fields.Length}});
@@ -206,8 +206,7 @@ internal static void AppendFormFieldUnroll(
206206
var propertyNameLiteral = Emitter.ToCSharpStringLiteral(field.PropertyName);
207207
var explicitNameLiteral = LiteralEmissionHelpers.ToNullableCSharpStringLiteral(field.ExplicitName);
208208
var prefixSegmentLiteral = LiteralEmissionHelpers.ToNullableCSharpStringLiteral(field.PrefixSegment);
209-
var keyExpr =
210-
$"global::Refit.GeneratedRequestRunner.BuildQueryKey({emission.SettingsLocal}, {propertyNameLiteral}, {explicitNameLiteral}, {prefixSegmentLiteral})";
209+
var keyExpr = BuildFormKeyExpression(field, emission.SettingsLocal, propertyNameLiteral, explicitNameLiteral, prefixSegmentLiteral);
211210

212211
_ = sb.Append(indent).Append("var ").Append(valueLocal).Append(" = ").Append(site.BodyExpr).Append(".@").Append(field.PropertyName).AppendLine(";");
213212

@@ -239,6 +238,31 @@ internal static void AppendFormFieldUnroll(
239238
_ = sb.Append(indent).AppendLine("}");
240239
}
241240

241+
/// <summary>Builds the key expression for one unrolled form field, picking the serializer-aware helper when the field has serializer names.</summary>
242+
/// <param name="field">The form field descriptor.</param>
243+
/// <param name="settingsLocal">The settings local name.</param>
244+
/// <param name="propertyNameLiteral">The CLR property name literal.</param>
245+
/// <param name="explicitNameLiteral">The explicit name literal, or <c>null</c>.</param>
246+
/// <param name="prefixSegmentLiteral">The prefix segment literal, or <c>null</c>.</param>
247+
/// <returns>The key expression source.</returns>
248+
internal static string BuildFormKeyExpression(
249+
FormFieldModel field,
250+
string settingsLocal,
251+
string propertyNameLiteral,
252+
string explicitNameLiteral,
253+
string prefixSegmentLiteral)
254+
{
255+
if (field.JsonName is null && field.NewtonsoftName is null && field.XmlName is null)
256+
{
257+
return $"global::Refit.GeneratedRequestRunner.BuildQueryKey({settingsLocal}, {propertyNameLiteral}, {explicitNameLiteral}, {prefixSegmentLiteral})";
258+
}
259+
260+
var json = LiteralEmissionHelpers.ToNullableCSharpStringLiteral(field.JsonName);
261+
var newtonsoft = LiteralEmissionHelpers.ToNullableCSharpStringLiteral(field.NewtonsoftName);
262+
var xml = LiteralEmissionHelpers.ToNullableCSharpStringLiteral(field.XmlName);
263+
return $"global::Refit.GeneratedRequestRunner.BuildFormKey({settingsLocal}, {propertyNameLiteral}, {explicitNameLiteral}, {json}, {newtonsoft}, {xml}, {prefixSegmentLiteral})";
264+
}
265+
242266
/// <summary>Appends one entry-list <c>Add</c> using an explicit <c>KeyValuePair</c> construction (no C# 9 target-typed new).</summary>
243267
/// <param name="sb">The statement builder.</param>
244268
/// <param name="site">The shared locals and rendered fragments for the enclosing body.</param>

‎src/InterfaceStubGenerator.Shared/Emitter.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ internal static class Emitter
7575
/// <summary>Emits the shared preserve attribute and factory registration code.</summary>
7676
/// <param name="model">The context generation model describing the interfaces.</param>
7777
/// <param name="addSource">Callback used to add generated source files.</param>
78+
/// <param name="support">How the registration is hooked to assembly load in the consumer compilation.</param>
7879
internal static void EmitSharedCode(
7980
ContextGenerationModel model,
80-
Action<string, SourceText> addSource)
81+
Action<string, SourceText> addSource,
82+
ModuleInitializerSupport support = ModuleInitializerSupport.Guarded)
8183
{
8284
if (model.Interfaces.Length == 0)
8385
{
@@ -86,7 +88,7 @@ internal static void EmitSharedCode(
8688

8789
var generatedFileHeader = BuildSharedGeneratedFileHeader(model.Interfaces, model.EmitGeneratedCodeMarkers);
8890
SharedInfrastructureEmission.EmitPreserveAttribute(model, generatedFileHeader, addSource);
89-
SharedInfrastructureEmission.EmitGeneratedFactoryModule(model, generatedFileHeader, addSource);
91+
SharedInfrastructureEmission.EmitGeneratedFactoryModule(model, generatedFileHeader, addSource, support);
9092
}
9193

9294
/// <summary>Emits the generated implementation source for a single interface.</summary>

‎src/InterfaceStubGenerator.Shared/FormFieldEmission.cs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ namespace Refit.Generator;
77
/// <summary>Emits the cached HTTP method field and form-field descriptor array for the inline path.</summary>
88
internal static class FormFieldEmission
99
{
10+
/// <summary>The opening of the grouped compile-time field names argument.</summary>
11+
private const string FormFieldNamesNew = "new global::Refit.FormFieldNames(";
12+
13+
/// <summary>The closing of the grouped compile-time field names argument.</summary>
14+
private const string FormFieldNamesClose = ")";
15+
1016
/// <summary>Resolves the HTTP method expression, caching a custom verb in a static field.</summary>
1117
/// <param name="request">The parsed request model.</param>
1218
/// <param name="uniqueNames">The interface-scope unique name builder.</param>
@@ -117,8 +123,16 @@ internal static string BuildFormFieldElements(
117123
SpanBufferEmission.AppendText(destination, InlineRequestEmission.FormFieldNameOpen, ref position);
118124
SpanBufferEmission.AppendText(destination, field.PropertyName, ref position);
119125
SpanBufferEmission.AppendText(destination, InlineRequestEmission.FormFieldNameClose, ref position);
126+
SpanBufferEmission.AppendText(destination, FormFieldNamesNew, ref position);
120127
SpanBufferEmission.AppendLiteralOrNull(destination, field.ExplicitName, ref position);
121128
SpanBufferEmission.AppendText(destination, InlineRequestEmission.ArgumentSeparator, ref position);
129+
SpanBufferEmission.AppendLiteralOrNull(destination, field.JsonName, ref position);
130+
SpanBufferEmission.AppendText(destination, InlineRequestEmission.ArgumentSeparator, ref position);
131+
SpanBufferEmission.AppendLiteralOrNull(destination, field.NewtonsoftName, ref position);
132+
SpanBufferEmission.AppendText(destination, InlineRequestEmission.ArgumentSeparator, ref position);
133+
SpanBufferEmission.AppendLiteralOrNull(destination, field.XmlName, ref position);
134+
SpanBufferEmission.AppendText(destination, FormFieldNamesClose, ref position);
135+
SpanBufferEmission.AppendText(destination, InlineRequestEmission.ArgumentSeparator, ref position);
122136
SpanBufferEmission.AppendLiteralOrNull(destination, field.PrefixSegment, ref position);
123137
SpanBufferEmission.AppendText(destination, InlineRequestEmission.ArgumentSeparator, ref position);
124138
SpanBufferEmission.AppendLiteralOrNull(destination, field.Format, ref position);
@@ -155,8 +169,16 @@ internal static int MeasureFormFieldElement(FormFieldModel field, string bodyTyp
155169
+ InlineRequestEmission.FormFieldNameOpen.Length
156170
+ field.PropertyName.Length
157171
+ InlineRequestEmission.FormFieldNameClose.Length
172+
+ FormFieldNamesNew.Length
158173
+ LiteralEmissionHelpers.LiteralOrNullLength(field.ExplicitName)
159174
+ InlineRequestEmission.ArgumentSeparator.Length
175+
+ LiteralEmissionHelpers.LiteralOrNullLength(field.JsonName)
176+
+ InlineRequestEmission.ArgumentSeparator.Length
177+
+ LiteralEmissionHelpers.LiteralOrNullLength(field.NewtonsoftName)
178+
+ InlineRequestEmission.ArgumentSeparator.Length
179+
+ LiteralEmissionHelpers.LiteralOrNullLength(field.XmlName)
180+
+ FormFieldNamesClose.Length
181+
+ InlineRequestEmission.ArgumentSeparator.Length
160182
+ LiteralEmissionHelpers.LiteralOrNullLength(field.PrefixSegment)
161183
+ InlineRequestEmission.ArgumentSeparator.Length
162184
+ LiteralEmissionHelpers.LiteralOrNullLength(field.Format)

‎src/InterfaceStubGenerator.Shared/InlineRequestEmission.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,22 @@ internal static string BuildRelativeUriExpression(
743743
return null;
744744
}
745745

746+
/// <summary>Determines whether the method's <c>[Url]</c> parameter accepts relative URI references.</summary>
747+
/// <param name = "request">The request model to inspect.</param>
748+
/// <returns><see langword="true"/> when a <c>[Url]</c> parameter exists and allows relative URIs.</returns>
749+
internal static bool AllowsRelativeUrl(in RequestModel request)
750+
{
751+
foreach (var parameter in request.Bindings.Parameters)
752+
{
753+
if (parameter.Kind == RequestParameterKind.Url)
754+
{
755+
return parameter.AllowRelativeUrl;
756+
}
757+
}
758+
759+
return false;
760+
}
761+
746762
/// <summary>Builds the cancellation token expression for an inline generated method.</summary>
747763
/// <param name = "request">The request model to inspect.</param>
748764
/// <returns>The cancellation token expression.</returns>

‎src/InterfaceStubGenerator.Shared/InterfaceMemberParsing.cs‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,19 +248,43 @@ internal static MethodModel ParseNonRefitMethod(
248248
/// <c>Name`1</c> corresponds to <c>(Name, 1)</c>, so this classifies identically.</remarks>
249249
internal static ReturnTypeInfo GetReturnTypeInfo(IMethodSymbol methodSymbol) =>
250250
methodSymbol.ReturnType is INamedTypeSymbol named
251-
? (named.Name, named.Arity) switch
251+
? named.Arity switch
252252
{
253-
("Task", 0) => ReturnTypeInfo.AsyncVoid,
254-
("Task", 1) when InlineRequestClassification.IsHttpRequestMessageType(named.TypeArguments[0]) => ReturnTypeInfo.RequestMessage,
255-
("Task", 1) or ("ValueTask", 1) => ReturnTypeInfo.AsyncResult,
256-
("IAsyncEnumerable", 1) => ReturnTypeInfo.AsyncEnumerable,
257-
("IObservable", 1) => ReturnTypeInfo.Observable,
258-
(PagedEnumerableName, PagedEnumerableArity) when RequestParameterParsingHelpers.IsInNamespace(named, "Refit") => ReturnTypeInfo.Paged,
259-
("Void", 0) => ReturnTypeInfo.SyncVoid,
253+
0 => ClassifyNonGenericReturn(named.Name),
254+
1 => ClassifySingleArgumentReturn(named),
255+
PagedEnumerableArity => named.Name == PagedEnumerableName && RequestParameterParsingHelpers.IsInNamespace(named, "Refit")
256+
? ReturnTypeInfo.Paged
257+
: ReturnTypeInfo.Return,
260258
_ => ReturnTypeInfo.Return
261259
}
262260
: ReturnTypeInfo.Return;
263261

262+
/// <summary>Classifies a return type that has no type arguments.</summary>
263+
/// <param name="name">The type's name.</param>
264+
/// <returns>The classified return type information.</returns>
265+
internal static ReturnTypeInfo ClassifyNonGenericReturn(string name) =>
266+
name switch
267+
{
268+
"Task" => ReturnTypeInfo.AsyncVoid,
269+
"Void" => ReturnTypeInfo.SyncVoid,
270+
_ => ReturnTypeInfo.Return
271+
};
272+
273+
/// <summary>Classifies a return type that has one type argument.</summary>
274+
/// <param name="named">The return type.</param>
275+
/// <returns>The classified return type information.</returns>
276+
internal static ReturnTypeInfo ClassifySingleArgumentReturn(INamedTypeSymbol named) =>
277+
named.Name switch
278+
{
279+
"Task" => InlineRequestClassification.IsHttpRequestMessageType(named.TypeArguments[0])
280+
? ReturnTypeInfo.RequestMessage
281+
: ReturnTypeInfo.AsyncResult,
282+
"ValueTask" => ReturnTypeInfo.AsyncResult,
283+
"IAsyncEnumerable" => ReturnTypeInfo.AsyncEnumerable,
284+
"IObservable" => ReturnTypeInfo.Observable,
285+
_ => ReturnTypeInfo.Return
286+
};
287+
264288
/// <summary>Collects the distinct member names declared on the interface, preserving order.</summary>
265289
/// <param name="members">The directly declared members of the interface.</param>
266290
/// <returns>The distinct member names.</returns>

‎src/InterfaceStubGenerator.Shared/InterfaceSemanticParsing.cs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ internal static InterfaceModel[] ParseMethodInterface(
2222
MethodDeclarationSyntax method,
2323
CancellationToken cancellationToken)
2424
{
25-
var symbol = semanticModel.GetDeclaredSymbol(method, cancellationToken);
25+
// A method declaration in the model's own syntax tree always has a declared symbol.
26+
var symbol = semanticModel.GetDeclaredSymbol(method, cancellationToken)!;
2627
var attribute = semanticModel.Compilation.GetTypeByMetadataName("Refit.HttpMethodAttribute");
27-
if (symbol is null || attribute is null || !Parser.IsRefitMethod(symbol, attribute))
28+
if (attribute is null || !Parser.IsRefitMethod(symbol, attribute))
2829
{
2930
return [];
3031
}
@@ -289,9 +290,11 @@ private static InterfaceModel ParseInterface(
289290
}
290291

291292
methods.Add(method);
292-
if (!compilationNullableEnabled && method.DeclaringSyntaxReferences is { IsEmpty: false } references)
293+
294+
// A method declared in source always has at least one declaring syntax reference.
295+
if (!compilationNullableEnabled)
293296
{
294-
nullableEnabled = IsNullableEnabled(references[0], semanticModel);
297+
nullableEnabled = IsNullableEnabled(method.DeclaringSyntaxReferences[0], semanticModel);
295298
}
296299
}
297300

‎src/InterfaceStubGenerator.Shared/InterfaceStubGeneratorV2.cs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Runtime.CompilerServices;
88
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp;
910
using Microsoft.CodeAnalysis.CSharp.Syntax;
1011
using Microsoft.CodeAnalysis.Diagnostics;
1112

@@ -109,9 +110,28 @@ public void Initialize(
109110
.WithTrackingName(RefitGeneratorStepName.BuildRefit);
110111
context.EmitSource(interfaceModels);
111112

113+
var moduleInitializerSupport = context.CompilationProvider.Select(static (compilation, _) =>
114+
DetectModuleInitializerSupport(compilation));
115+
112116
context.RegisterImplementationSourceOutput(
113-
contextModel,
114-
static (spc, model) => Emitter.EmitSharedCode(model, spc.AddSource));
117+
contextModel.Combine(moduleInitializerSupport),
118+
static (spc, combined) => Emitter.EmitSharedCode(combined.Left, spc.AddSource, combined.Right));
119+
}
120+
121+
/// <summary>Decides how the generated factory registration can be hooked to assembly load in a compilation.</summary>
122+
/// <param name="compilation">The consumer compilation.</param>
123+
/// <returns>The module initializer support the compilation offers.</returns>
124+
internal static ModuleInitializerSupport DetectModuleInitializerSupport(Compilation compilation)
125+
{
126+
// The compiler only recognises [ModuleInitializer] from C# 9. The generator only runs on C# compilations.
127+
if (((CSharpCompilation)compilation).LanguageVersion < LanguageVersion.CSharp9)
128+
{
129+
return ModuleInitializerSupport.Guarded;
130+
}
131+
132+
return compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.ModuleInitializerAttribute") is null
133+
? ModuleInitializerSupport.UnguardedWithPolyfill
134+
: ModuleInitializerSupport.Unguarded;
115135
}
116136

117137
/// <summary>Combines built-in and potential custom HTTP method candidates.</summary>

0 commit comments

Comments
 (0)