Skip to content

Add a refactoring between F# and .NET-compatible optional parameters - #20547

Open
xperiandri wants to merge 4 commits into
dotnet:mainfrom
xperiandri:feature/optional-parameter-default-value-refactoring
Open

xperiandri wants to merge 4 commits into
dotnet:mainfrom
xperiandri:feature/optional-parameter-default-value-refactoring

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Offers Ctrl+. to switch between ?x: T with defaultArg x c in the body and [<Optional; DefaultParameterValue(c)>] x: T, which C# and VB callers can omit too. Converting back offers ?x with defaultArg and, from F# 10, [<Struct>] ?x with defaultValueArg. F# call sites compile against both forms, so only the member is edited.

Not offered unless every use is defaultArg x c with one constant of the parameter's type, or for overrides, abstract slots, constructors, or a file with a paired .fsi.

🤖 Generated with Claude Code

…meters

?x: T is optional for F# callers and gives the body an option;
[<Optional; DefaultParameterValue(c)>] x: T is optional for C# and VB callers
too and gives the body the value. The refactoring moves the default between
defaultArg x c in the body and the attribute, adds the InteropServices open
when it is missing, and leaves call sites alone: F# accepts both forms the
same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xperiandri and others added 2 commits September 14, 2026 20:03
…form

[<Optional; DefaultParameterValue(c)>] x: T now converts back to either
?x: T with let x = defaultArg x c, or, from F# 10, [<Struct>] ?x: T with
let x = defaultValueArg x c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e of its section

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xperiandri
xperiandri marked this pull request as ready for review September 14, 2026 19:40
@xperiandri
xperiandri requested a review from a team as a code owner September 14, 2026 19:40
@github-actions github-actions Bot added the ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager label Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Modifies Visual Studio code executed while projects are open.

Generated by PR Tooling Safety Check · gpt56 1.8M ·

@xperiandri xperiandri changed the title Refactoring between F# and .NET-compatible optional parameters Add a refactoring between F# and .NET-compatible optional parameters Sep 15, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

Covered cases

Converts

  • Shadowing default, adding the missing open: ?greeting: string with let greeting = defaultArg greeting "Hello"[<Optional; DefaultParameterValue("Hello")>] greeting: string, the let line removed, open System.Runtime.InteropServices inserted; calls Greet("Ada") and greeting = "Hi" stay as they are
  • Shadowing default round trip, both ways, when the open already exists: ?step: int + let step = defaultArg step 1[<Optional; DefaultParameterValue(1)>] step: int; the same for a second parameter ?step: float with 0.5
  • Inline default: value + defaultArg step 1[<Optional; DefaultParameterValue(1)>] step: int) = value + step
  • Back with a body on the member line: step: int) = value + step?step: int) =, then let step = defaultArg step 1 and value + step on their own lines
  • Parameter with no default use: ?flag: bool (unused) → [<Optional>] flag: bool
  • Back with no DefaultParameterValue: [<Optional>] step: int?step: int + let step = defaultArg step Unchecked.defaultof<_>
  • Back to a struct optional parameter: [<Optional; DefaultParameterValue(1)>] step: int[<Struct>] ?step: int + let step = defaultValueArg step 1; [<Optional>] step: int… defaultValueArg step Unchecked.defaultof<_>
  • Action titles: the F# form offers only Use [<Optional; DefaultParameterValue>] for optional parameter; the .NET form offers Use F# '?' optional parameter, then Use F# '[<Struct>] ?' optional parameter
  • Language version gate: before F# 10 (--langversion:9.0), the .NET form offers only Use F# '?' optional parameter
  • Each converted document type-checks without errors

Not offered

  • Different defaults: defaultArg x 0 + defaultArg x 1
  • A use other than defaultArg: static member M(?x: int) = x.IsSome
  • Default that is not a constant: ?x: obj with defaultArg x (box 1)
  • Constant whose type does not match the parameter: ?x: int with defaultArg x 1.0
  • Parameter with no type annotation: static member M(?x) = defaultArg x 0
  • Struct optional parameter: [<Struct>] ?x: int with defaultValueArg x 0
  • Extra attribute in the list: [<Optional; In>] x: int
  • Override of an abstract slot: abstract M: ?x: int -> int with default _.M(?x: int) = defaultArg x 0
  • Ordinary parameter: static member M(x: int) = x
  • File that has a signature file: static member M: ?x: int -> int

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ AI review — verify independently.

let defaultText =
match defaultValue with
| ValueSome value -> sourceText.ToString(spanOf sourceText value.Range)
| ValueNone -> "Unchecked.defaultof<_>"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ [P2] Converting x to F# ?x changes C.M() from true to false — an omitted obj receives null instead of Missing.Value.

open System.Runtime.InteropServices

type C() =
    static member M([<Optional>] x: obj) =
        obj.ReferenceEquals(x, System.Reflection.Missing.Value)

printfn "%b" (C.M())

| Some(node,
SyntaxNode.SynExpr(SynExpr.App(isInfix = false; funcExpr = SingleIdent func; argExpr = arg) as inner) :: SyntaxNode.SynExpr(SynExpr.App(
isInfix = false; funcExpr = funcExpr; argExpr = (SynExpr.Const _ as defaultValue)) as application) :: rest) when
isSame arg node && hasText "defaultArg" func && isSame funcExpr inner

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ [P2] Converting ?x to .NET optional changes C.M() from 99 to 0 — matching defaultArg by spelling removes a call to a different function.

let defaultArg (_: int option) (_: int) = 99

type C() =
    static member M(?x: int) = defaultArg x 0

printfn "%d" (C.M())

match found, attributeText with
| ValueSome found, ValueSome attributeText ->
[
TextChange(TextSpan((spanOf sourceText optionalValRange).Start, 1), attributeText)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ [P2] Converting only ?y to .NET optional produces FS1212 — the remaining F# optional ?x precedes the converted parameter.

type C() =
    static member M(?x: int, ?y: int) =
        defaultArg x 1 + defaultArg y 2


let struct (prefix, defaultFunction) =
if asStruct then
struct ("[<Struct>] ?", "defaultValueArg")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ [P2] Converting x to [<Struct>] ?x leaves a formerly valid caller failing with FS0001 — Some 1 supplies option, but the parameter now requires voption.

open System.Runtime.InteropServices

type C =
    static member M([<Optional; DefaultParameterValue(0)>] x: int) = x

let result = C.M(?x = Some 1)

/// The whole line of `let x = defaultArg x c` when that line does nothing but rebind the parameter.
let private tryShadowingLine (sourceText: SourceText) (name: string) (application: SynExpr) (path: SyntaxVisitorPath) =
match path with
| SyntaxNode.SynBinding(SynBinding(headPat = SynPat.Named(ident = SynIdent(ident, _)); expr = rhs; trivia = trivia)) :: SyntaxNode.SynExpr(SynExpr.LetOrUse letOrUse) :: _ when

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ [P2] Converting ?x to .NET optional deletes let mutable x, leaving x <- x + 1 failing with FS0027 because the parameter is immutable.

type C() =
    static member M(?x: int) =
        let mutable x = defaultArg x 0
        x <- x + 1
        x

found
|| match node with
| SyntaxNode.SynModule(SynModuleDecl.Open(target = SynOpenDeclTarget.ModuleOrNamespace(longId = SynLongIdent(id = ids)))) ->
(ids |> List.map _.idText) = interopServices

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ [P2] Converting ?x to .NET optional produces FS0039 for both attributes — an open in another module suppresses the import needed at C.M.

module Other =
    open System.Runtime.InteropServices
    let x = 1

type C() =
    static member M(?x: int) = defaultArg x 0

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 18, 2026
@T-Gro
T-Gro self-requested a review September 18, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager AI-reviewed PR reviewed by AI review council

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants