Skip to content

Commit c56be43

Browse files
Fix CancellationToken parameter ordering and name collision in v3 OperationCompiler (#341)
* Initial plan * fix: insert CT between required and optional params; generate unique CT name Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/b0c519de-0186-40ca-8174-42ed67a5316a * fix: add explicit restore + --no-restore to BuildTests to fix NETSDK1005 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/565d6633-576d-4587-b924-a29b0ea53c2c --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
1 parent ee4db30 commit c56be43

File tree

2 files changed

+52
-34
lines changed

2 files changed

+52
-34
lines changed

build.fsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ Target.createFinal "StopServer" (fun _ ->
105105
//Process.killAllByName "dotnet"
106106
)
107107

108-
Target.create "BuildTests" (fun _ -> dotnet "build" "SwaggerProvider.TestsAndDocs.sln -c Release")
108+
Target.create "BuildTests" (fun _ ->
109+
// Explicit restore ensures project.assets.json has all target frameworks before the build.
110+
// Without this, the inner-build restores triggered by Paket.Restore.targets may overwrite
111+
// the assets file with only one TFM, causing NETSDK1005 for the other TFM.
112+
dotnet "restore" "SwaggerProvider.TestsAndDocs.sln"
113+
dotnet "build" "SwaggerProvider.TestsAndDocs.sln -c Release --no-restore")
109114

110115
// --------------------------------------------------------------------------------------
111116
// Run the unit tests using test runner

src/SwaggerProvider.DesignTime/v3/OperationCompiler.fs

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
9696
let (|NoMediaType|_|)(content: IDictionary<string, OpenApiMediaType>) =
9797
if isNull content || content.Count = 0 then Some() else None
9898

99-
let payloadMime, parameters =
99+
let payloadMime, parameters, ctArgIndex =
100100
/// handles de-duplicating Swagger parameter names if the same parameter name
101101
/// appears in multiple locations in a given operation definition.
102102
let uniqueParamName usedNames (param: IOpenApiParameter) =
@@ -147,18 +147,15 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
147147

148148
let payloadTy = bodyFormatAndParam |> Option.map fst |> Option.defaultValue NoData
149149

150-
let orderedParameters =
151-
let required, optional =
152-
[ yield! openApiParameters
153-
if bodyFormatAndParam.IsSome then
154-
yield bodyFormatAndParam.Value |> snd ]
155-
|> List.distinctBy(fun op -> op.Name, op.In)
156-
|> List.partition(_.Required)
150+
let requiredOpenApiParams, optionalOpenApiParams =
151+
[ yield! openApiParameters
152+
if bodyFormatAndParam.IsSome then
153+
yield bodyFormatAndParam.Value |> snd ]
154+
|> List.distinctBy(fun op -> op.Name, op.In)
155+
|> List.partition(_.Required)
157156

158-
List.append required optional
159-
160-
let usedNames, providedParameters =
161-
((Set.empty, []), orderedParameters)
157+
let buildProvidedParameters usedNames (paramList: IOpenApiParameter list) =
158+
((usedNames, []), paramList)
162159
||> List.fold(fun (names, parameters) current ->
163160
let names, paramName = uniqueParamName names current
164161

@@ -173,26 +170,37 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
173170
ProvidedParameter(paramName, paramType, false, paramDefaultValue)
174171

175172
(names, providedParam :: parameters))
176-
|> fun (names, ps) -> names, List.rev ps
173+
|> fun (finalNames, ps) -> finalNames, List.rev ps
174+
175+
let namesAfterRequired, requiredProvidedParams =
176+
buildProvidedParameters Set.empty requiredOpenApiParams
177177

178-
let parameters =
178+
let _, optionalProvidedParams =
179+
buildProvidedParameters namesAfterRequired optionalOpenApiParams
180+
181+
let ctArgIndex, parameters =
179182
if includeCancellationToken then
180-
// Find a unique name for the CancellationToken parameter that doesn't
181-
// conflict with any OpenAPI parameter already in use.
182-
let ctParamName =
183-
Seq.initInfinite(fun i ->
184-
if i = 0 then
185-
"cancellationToken"
186-
else
187-
$"cancellationToken{i}")
188-
|> Seq.find(fun n -> not(Set.contains n usedNames))
183+
// Collect all used param names to generate a unique CT parameter name
184+
let usedNames =
185+
(requiredProvidedParams @ optionalProvidedParams)
186+
|> List.map(fun p -> p.Name)
187+
|> Set.ofList
188+
189+
let rec findUniqueName candidate n =
190+
if Set.contains candidate usedNames then
191+
findUniqueName $"cancellationToken{n}" (n + 1)
192+
else
193+
candidate
189194

190-
let ctParam = ProvidedParameter(ctParamName, typeof<Threading.CancellationToken>)
191-
providedParameters @ [ ctParam ]
195+
let ctName = findUniqueName "cancellationToken" 1
196+
let ctParam = ProvidedParameter(ctName, typeof<Threading.CancellationToken>)
197+
// CT is inserted after required params so it never follows optional params
198+
let ctArgIndex = List.length requiredProvidedParams
199+
ctArgIndex, requiredProvidedParams @ [ ctParam ] @ optionalProvidedParams
192200
else
193-
providedParameters
201+
-1, requiredProvidedParams @ optionalProvidedParams
194202

195-
payloadTy.ToMediaType(), parameters
203+
payloadTy.ToMediaType(), parameters, ctArgIndex
196204

197205
// find the inner type value
198206
let retMimeAndTy =
@@ -278,16 +286,21 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
278286
// Locates parameters matching the arguments
279287
let mutable payloadExp = None
280288

281-
// When the CancellationToken overload is generated, CancellationToken is always appended last.
282-
// Extract it by position to avoid name-collision issues and invalid Expr.Coerce
283-
// on a struct type (which generates an invalid castclass IL instruction).
289+
// When the CancellationToken overload is generated, CT is inserted at ctArgIndex
290+
// (after required params, before optional params). Extract it by that known index
291+
// to avoid name-collision issues and invalid Expr.Coerce on a struct type.
284292
let apiArgs, ct =
285293
let allArgs = List.tail args // skip `this`
286294

287295
if includeCancellationToken then
288-
match List.rev allArgs with
289-
| ctArg :: revApiArgs -> List.rev revApiArgs, Expr.Cast<Threading.CancellationToken>(ctArg)
290-
| [] -> failwith "Expected CancellationToken argument but argument list was empty"
296+
let ctArg = List.item ctArgIndex allArgs
297+
298+
let apiArgs =
299+
allArgs
300+
|> List.indexed
301+
|> List.choose(fun (i, a) -> if i = ctArgIndex then None else Some a)
302+
303+
apiArgs, Expr.Cast<Threading.CancellationToken>(ctArg)
291304
else
292305
allArgs, <@ Threading.CancellationToken.None @>
293306

0 commit comments

Comments
 (0)