Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
de88058
feat: added route generation, currently not emitting, don't know why
TimothyMakkison Jun 28, 2026
1a2ea21
fix: can run tests without error messages
TimothyMakkison Jun 28, 2026
ee7e465
feat: add exceptions and unmatched guard logic
TimothyMakkison Jun 28, 2026
b8ba68a
feat: move `subProperties` into `RefitModel`
TimothyMakkison Jun 29, 2026
2855074
feat: partially move route logic into `Parser`
TimothyMakkison Jun 29, 2026
4a11c27
chore: refactor and rename `BuildObjectParamValidationDict`
TimothyMakkison Jun 29, 2026
099c0b7
feat: enable inline generation for route parameters
TimothyMakkison Jun 29, 2026
b31c75a
feat: run tests and accept changes
TimothyMakkison Jun 29, 2026
c692ecb
feat: use route fragment based generation in `Emitter`
TimothyMakkison Jun 29, 2026
7102319
fix: correctly emit object property fragment
TimothyMakkison Jun 29, 2026
d157429
chore: remove unused code
TimothyMakkison Jun 29, 2026
afbaf61
chore: remove unneeded model
TimothyMakkison Jun 29, 2026
f992630
chore: reorder and try to fix some errors
TimothyMakkison Jun 29, 2026
43d592b
chore: refactor add summaries
TimothyMakkison Jun 29, 2026
f65dc62
chore: add summaries for `GeneratedRequestRunner`
TimothyMakkison Jun 29, 2026
af2c5e8
fix: use `ConcurrentDictionary` to fix concurrency issue
TimothyMakkison Jun 29, 2026
3636179
fix: constant path test
TimothyMakkison Jun 29, 2026
f853fd6
fix: update snapshot tests
TimothyMakkison Jun 29, 2026
5db6bf0
feat: add type parameter array and add @ to generated parameter names
TimothyMakkison Jun 30, 2026
ecfd0ce
chchore: remove unused property and summary
TimothyMakkison Jul 3, 2026
65ed90c
feat: pass `genericCount`
TimothyMakkison Jul 3, 2026
e9cb54b
feat: refactor `AddStandardParameter`
TimothyMakkison Jul 5, 2026
d2d2d89
feat: refactor `AppendObjectPropertyFragment` to `AddRouteObjectPrope…
TimothyMakkison Jul 5, 2026
2d1591f
feat: add `TryParseRouteParameter`
TimothyMakkison Jul 6, 2026
52328ae
fix: remove local `stackalloc` to support C# 7.3
TimothyMakkison Jul 6, 2026
4f5e515
feat: add `BuildInlineTypeParameterField`
TimothyMakkison Jul 6, 2026
9965b81
chore: rename `AddPathObjectProperty` and `AddPathParameter`
TimothyMakkison Jul 6, 2026
653e875
chore: make some analysers happy
TimothyMakkison Jul 6, 2026
ed2fdd5
feat: add `ParameterInfoKey` and `ParameterInfoCacheKeyComparer`
TimothyMakkison Jul 6, 2026
f281edb
feat: change generated code for `AddRouteParameter` and `AddPathObjec…
TimothyMakkison Jul 6, 2026
07b69fd
chore: code cleanup, add comments, minor refactor
TimothyMakkison Jul 6, 2026
b4c0d40
feat: add skeleton for framework `GetMethod`
TimothyMakkison Jul 6, 2026
38859d6
chore: remove debug code
TimothyMakkison Jul 6, 2026
5ed5793
feat: add `genericTypeArgument` emitter
TimothyMakkison Jul 7, 2026
bbd083e
chore: add generic argument, refactor and add documentation
TimothyMakkison Jul 7, 2026
0ccc2f5
chore: add comments, remove replaces references to `Route` to `Path`
TimothyMakkison Jul 8, 2026
ec972f8
feat: fix missing argument and invert generic emitter
TimothyMakkison Jul 8, 2026
02bce74
feat!: reflection-free source-generated request building
glennawatson Jul 12, 2026
3757b5d
perf(generator): span-formattable path parameter fast-write
glennawatson Jul 12, 2026
84b462e
refactor(generator): make InterfaceGenerationContext a reference type
glennawatson Jul 12, 2026
8803708
perf(generator): cut generator allocations ~17-18%
glennawatson Jul 12, 2026
35ae0d4
feat(generator): close reflection-fallback gaps and expand coverage
glennawatson Jul 12, 2026
5c8d478
feat(generator): bind path objects and flatten their residual query
glennawatson Jul 12, 2026
455f1f2
test: cover PR-touched files to 100% and narrow coverage exclusions
glennawatson Jul 12, 2026
a6a21f3
build: disable S4027 to match SonarCloud ruleset
glennawatson Jul 12, 2026
c741d7a
build: apply uniform analyzer standards to test code
glennawatson Jul 12, 2026
e86b686
test: drop the fragile synthetic-net10 span-escape generator test
glennawatson Jul 12, 2026
59f5385
build: disable analyzer rules that duplicate their SST equivalents
glennawatson Jul 12, 2026
9756ea6
perf(generator): span-format query values without an intermediate string
glennawatson Jul 12, 2026
b645535
feat(generator): source-generated multipart request building
glennawatson Jul 12, 2026
0983832
build: fix the SST reference in the CA1044 dedup comment
glennawatson Jul 12, 2026
7e2640d
perf(generator): drop per-call query-builder scan and empty attribute…
glennawatson Jul 12, 2026
dab08c8
fix(tests): update Verify.TUnit version and remove carriage return ha…
glennawatson Jul 12, 2026
a18a6f5
feat(generator): widen inline request coverage and unroll path builder
glennawatson Jul 13, 2026
cfc51d9
test(generator): pin the HTTP QUERY verb scenario
glennawatson Jul 13, 2026
3a561c0
perf(generator): span-format nullable value-type query parameters
glennawatson Jul 13, 2026
7f0f70f
feat(generator): source-generate serialized multipart parts
glennawatson Jul 13, 2026
f00cfd0
feat(generator): inline concrete-class path parameters
glennawatson Jul 13, 2026
d4fcb17
feat(generator): inline collection and array path parameters
glennawatson Jul 13, 2026
90b86c5
feat(generator): inline concrete (non-sealed) dict values and multipa…
glennawatson Jul 13, 2026
7ed0e7f
chore: untrack agent worktree and ignore .claude
glennawatson Jul 13, 2026
face0f5
feat(generator): inline IObservable return shape as cold observable
glennawatson Jul 13, 2026
e6c9f3d
feat(generator): inline no-leading-slash relative paths
glennawatson Jul 13, 2026
8066c73
feat: inline and reflect multi-level dotted path params
glennawatson Jul 13, 2026
bb7850a
fix: restore reflection parity for inlined multipart and slashless ro…
glennawatson Jul 13, 2026
398facb
perf(generator): cache custom-verb HttpMethod in a static field
glennawatson Jul 13, 2026
bc126da
perf(generator): pre-escape constant query keys at generation time
glennawatson Jul 13, 2026
13e7187
perf: percent-encode span-formatted values in place, extend to net8
glennawatson Jul 13, 2026
8013d9e
fix: use the net9 span overload of Uri.EscapeDataString in the escape…
glennawatson Jul 13, 2026
7239871
test(benchmarks): cover the verb-cache and span-escape query paths
glennawatson Jul 13, 2026
f0c1b01
test(benchmarks): use explicit new HttpMethod so the custom verb inlines
glennawatson Jul 13, 2026
5f131df
test(analyzers): treat IObservable as inline-supported, not a fallback
glennawatson Jul 13, 2026
ebca5b5
Merge remote-tracking branch 'origin/main' into feature/generated-req…
glennawatson Jul 13, 2026
212910e
Merge Tim's aot_route (#2209) into feature/generated-request-building
glennawatson Jul 13, 2026
a2657b2
refactor: extract attribute flattening to drop the SST1443 suppression
glennawatson Jul 13, 2026
3f128ff
build: move hot-path LINQ analyzer rules to PerformanceSharp
glennawatson Jul 13, 2026
fa5c27b
refactor: update severity levels for various diagnostics in .editorco…
glennawatson Jul 13, 2026
19fca32
test: cover source-generated request building to 100%
glennawatson Jul 13, 2026
a5e7415
feat: Add support for parsing query objects and generating request bo…
glennawatson Jul 13, 2026
166473c
fix: update SharpAnalyzersVersion to 3.21.5
glennawatson Jul 13, 2026
b39cae4
refactor: bring remaining projects into analyzer compliance
glennawatson Jul 13, 2026
4e9cb03
refactor: update SharpAnalyzersVersion to 3.21.13 and apply static la…
glennawatson Jul 14, 2026
3a424ac
test: cover reachable branches across the touched surface
glennawatson Jul 14, 2026
5cceb50
refactor: close the uncoverable branch residual to reach 100% per file
glennawatson Jul 14, 2026
bfcc1d5
build: adopt SST2307 in place of S4018 for uninferable generics
glennawatson Jul 14, 2026
1c0e4f6
build: clear suppressions targeting analyzer-disabled rules
glennawatson Jul 14, 2026
dec46db
build: finish Sonar->Sharp rule migration on analyzers 3.25.2
glennawatson Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
641 changes: 435 additions & 206 deletions .editorconfig

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ Refit-Tests/test-results
/InterfaceStubGenerator.Core/Properties/launchSettings.json
*.svclog
**/BenchmarkDotNet.Artifacts

# Local agent worktrees and scratch
.claude/
297 changes: 139 additions & 158 deletions README.md

Large diffs are not rendered by default.

273 changes: 273 additions & 0 deletions docs/breaking-changes.md

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<RunAnalyzers>true</RunAnalyzers>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<ReportAnalyzer>true</ReportAnalyzer>
<Nullable>enable</Nullable>

Expand Down Expand Up @@ -158,7 +157,7 @@
<MinVerAutoIncrement>minor</MinVerAutoIncrement>
<MinVerDefaultPreReleaseIdentifiers>alpha.0</MinVerDefaultPreReleaseIdentifiers>
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerMinimumMajorMinor>10.2</MinVerMinimumMajorMinor>
<MinVerMinimumMajorMinor>14.0</MinVerMinimumMajorMinor>
</PropertyGroup>

<ItemGroup>
Expand Down
10 changes: 5 additions & 5 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

<PropertyGroup Label="Shared Version Variables">
<PrimitivesVersion>6.0.0</PrimitivesVersion>
<TUnitVersion>1.58.0</TUnitVersion>
<TUnitVersion>1.59.0</TUnitVersion>

<!-- StyleSharp and PerformanceSharp ship from the same repo on a single version line. -->
<SharpAnalyzersVersion>3.17.7</SharpAnalyzersVersion>
<SharpAnalyzersVersion>3.25.2</SharpAnalyzersVersion>
</PropertyGroup>

<PropertyGroup Label="Framework-Aligned Versions">
Expand Down Expand Up @@ -47,7 +47,7 @@
<PackageVersion Include="StyleSharp.Analyzers" Version="$(SharpAnalyzersVersion)"/>
<PackageVersion Include="PerformanceSharp.Analyzers" Version="$(SharpAnalyzersVersion)"/>
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0"/>
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.28.0.143324"/>
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.29.0.143774"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="$(MicrosoftCodeAnalysisAnalyzersVersion)"/>
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0"/>
</ItemGroup>
Expand All @@ -69,7 +69,7 @@

<ItemGroup Label="Test Stack">
<PackageVersion Include="TUnit" Version="$(TUnitVersion)"/>
<PackageVersion Include="Verify.TUnit" Version="31.22.0"/>
<PackageVersion Include="Verify.TUnit" Version="31.24.1"/>
<PackageVersion Include="Verify.DiffPlex" Version="3.3.0"/>
<PackageVersion Include="Verify.SourceGenerators" Version="2.5.0"/>
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.9.0"/>
Expand All @@ -83,7 +83,7 @@
<ItemGroup Label="Examples">
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(AspNetVersion)"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(AspNetVersion)"/>
<PackageVersion Include="Serilog" Version="4.3.1"/>
<PackageVersion Include="Serilog" Version="4.4.0"/>
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1"/>
</ItemGroup>

Expand Down
12 changes: 12 additions & 0 deletions src/InterfaceStubGenerator.Shared/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Error,
true);

/// <summary>Diagnostic reported when a source-generation-only attribute is used on a method that cannot generate inline.</summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "RS2008", Justification = "Diagnostic IDs are stable and intentionally not tracked in an analyzer release-tracking file.")]
public static readonly DiagnosticDescriptor SourceGenOnlyAttributeRequiresInlineRequest =
new(
"RF007",
"Attribute requires generated request building",
"Method '{0}' uses [{1}], which is only honored by generated request building, but its request cannot be "
+ "generated inline. Make the method compatible with inline generation or remove the attribute.",
Category,
DiagnosticSeverity.Error,
true);

/// <summary>The diagnostic category for Refit generator diagnostics.</summary>
private const string Category = "Refit";
}
271 changes: 271 additions & 0 deletions src/InterfaceStubGenerator.Shared/Emitter.Buffers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
// Copyright (c) 2019-2026 ReactiveUI and Contributors. All rights reserved.
// ReactiveUI and Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;

namespace Refit.Generator;

/// <summary>Low-level primitives that assemble generated source into pre-sized character buffers.</summary>
internal static partial class Emitter
{
/// <summary>The cached indentation strings for the levels the emitter uses in its hot paths.</summary>
private static readonly string[] IndentCache = BuildIndentCache();

#if NETSTANDARD2_0
/// <summary>Delegate used to fill a generated string buffer.</summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <param name="destination">The target character buffer.</param>
/// <param name="state">The caller supplied state.</param>
private delegate void GeneratedStringAction<in TState>(char[] destination, TState state);
#else
/// <summary>Delegate used to fill a generated string buffer.</summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <param name="destination">The target character buffer.</param>
/// <param name="state">The caller supplied state.</param>
private delegate void GeneratedStringAction<in TState>(Span<char> destination, TState state);
#endif

/// <summary>Concatenates populated source fragments without allocating a trimmed array.</summary>
/// <param name="parts">The source fragments.</param>
/// <param name="count">The populated fragment count.</param>
/// <returns>The concatenated source.</returns>
private static string ConcatParts(string[] parts, int count)
{
var length = 0;
for (var i = 0; i < count; i++)
{
length += parts[i].Length;
}

return CreateGeneratedString(
length,
(Parts: parts, Count: count),
static (destination, state) =>
{
var position = 0;
for (var i = 0; i < state.Count; i++)
{
AppendText(destination, state.Parts[i], ref position);
}
});
}

/// <summary>Joins populated source fragments without allocating a trimmed array.</summary>
/// <param name="parts">The source fragments.</param>
/// <param name="count">The populated fragment count.</param>
/// <param name="separator">The separator text.</param>
/// <returns>The joined source.</returns>
private static string JoinParts(string[] parts, int count, string separator)
{
if (count == 0)
{
return string.Empty;
}

var length = separator.Length * (count - 1);
for (var i = 0; i < count; i++)
{
length += parts[i].Length;
}

return CreateGeneratedString(
length,
(Parts: parts, Count: count, Separator: separator),
static (destination, state) =>
{
var position = 0;
for (var i = 0; i < state.Count; i++)
{
if (i > 0)
{
AppendText(destination, state.Separator, ref position);
}

AppendText(destination, state.Parts[i], ref position);
}
});
}

/// <summary>Builds a generated indentation string.</summary>
/// <param name="level">The indentation level.</param>
/// <returns>The generated indentation.</returns>
/// <remarks>Indentation levels are compile-time constants, so the common levels are cached once and shared
/// instead of allocating an identical fresh string at every per-method and per-parameter call site. Levels beyond
/// the cache allocate a fresh string, a fallback reached only by nesting deeper than the shared test fixtures.</remarks>
[ExcludeFromCodeCoverage]
private static string Indent(int level) =>
(uint)level < (uint)IndentCache.Length
? IndentCache[level]
: new string(' ', level * CharsPerIndentation);

/// <summary>Precomputes the shared indentation strings for levels 0 through <see cref="MaxCachedIndentLevel"/>.</summary>
/// <returns>The cached indentation strings, indexed by level.</returns>
private static string[] BuildIndentCache()
{
var cache = new string[MaxCachedIndentLevel + 1];
for (var level = 0; level <= MaxCachedIndentLevel; level++)
{
cache[level] = new(' ', level * CharsPerIndentation);
}

return cache;
}

#if NETSTANDARD2_0
/// <summary>Writes a C# string literal or the <c>null</c> keyword into a generated string buffer.</summary>
/// <param name="destination">The target character buffer.</param>
/// <param name="value">The value to quote, or <see langword="null"/>.</param>
/// <param name="position">The current write position.</param>
private static void AppendLiteralOrNull(char[] destination, string? value, ref int position)
{
if (value is null)
{
AppendText(destination, NullLiteral, ref position);
return;
}

destination[position] = '"';
position++;
foreach (var character in value)
{
var escape = EscapeSequence(character);
if (escape is null)
{
destination[position] = character;
position++;
}
else
{
AppendText(destination, escape, ref position);
}
}

destination[position] = '"';
position++;
}

/// <summary>Writes the decimal rendering of a non-negative 32-bit integer into a generated string buffer.</summary>
/// <param name="destination">The target character buffer.</param>
/// <param name="value">The non-negative value to render (callers only pass <c>CollectionFormat</c> values).</param>
/// <param name="position">The current write position.</param>
private static void AppendInt32(char[] destination, int value, ref int position)
{
var end = position + Int32Length(value);
var write = end;
do
{
--write;
destination[write] = (char)('0' + (value % DecimalRadix));
value /= DecimalRadix;
}
while (value > 0);

position = end;
}
#else
/// <summary>Writes a C# string literal or the <c>null</c> keyword into a generated string buffer.</summary>
/// <param name="destination">The target character span.</param>
/// <param name="value">The value to quote, or <see langword="null"/>.</param>
/// <param name="position">The current write position.</param>
private static void AppendLiteralOrNull(Span<char> destination, string? value, ref int position)
{
if (value is null)
{
AppendText(destination, NullLiteral, ref position);
return;
}

destination[position] = '"';
position++;
foreach (var character in value)
{
var escape = EscapeSequence(character);
if (escape is null)
{
destination[position] = character;
position++;
}
else
{
AppendText(destination, escape, ref position);
}
}

destination[position] = '"';
position++;
}

/// <summary>Writes the decimal rendering of a non-negative 32-bit integer into a generated string buffer.</summary>
/// <param name="destination">The target character span.</param>
/// <param name="value">The non-negative value to render (callers only pass <c>CollectionFormat</c> values).</param>
/// <param name="position">The current write position.</param>
private static void AppendInt32(Span<char> destination, int value, ref int position)
{
var end = position + Int32Length(value);
var write = end;
do
{
--write;
destination[write] = (char)('0' + (value % DecimalRadix));
value /= DecimalRadix;
}
while (value > 0);

position = end;
}
#endif

#if NETSTANDARD2_0
/// <summary>Creates a generated string using a pre-sized buffer.</summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <param name="length">The string length.</param>
/// <param name="state">The caller supplied state.</param>
/// <param name="action">The buffer fill callback.</param>
/// <returns>The generated string.</returns>
private static string CreateGeneratedString<TState>(
int length,
TState state,
GeneratedStringAction<TState> action)
{
var destination = new char[length];
action(destination, state);
return new(destination);
}

/// <summary>Appends text into a generated string buffer.</summary>
/// <param name="destination">The target character buffer.</param>
/// <param name="text">The text to append.</param>
/// <param name="position">The current write position.</param>
private static void AppendText(char[] destination, string text, ref int position)
{
text.CopyTo(0, destination, position, text.Length);
position += text.Length;
}
#else
/// <summary>Creates a generated string using <see cref="string.Create{TState}(int, TState, System.Buffers.SpanAction{char, TState})"/>.</summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <param name="length">The string length.</param>
/// <param name="state">The caller supplied state.</param>
/// <param name="action">The buffer fill callback.</param>
/// <returns>The generated string.</returns>
private static string CreateGeneratedString<TState>(
int length,
TState state,
GeneratedStringAction<TState> action) =>
string.Create(
length,
(State: state, Action: action),
static (destination, context) => context.Action(destination, context.State));

/// <summary>Appends text into a generated string buffer.</summary>
/// <param name="destination">The target character buffer.</param>
/// <param name="text">The text to append.</param>
/// <param name="position">The current write position.</param>
private static void AppendText(Span<char> destination, string text, ref int position)
{
text.AsSpan().CopyTo(destination[position..]);
position += text.Length;
}
#endif
}
Loading
Loading