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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
([PR #19724](https://github.com/dotnet/fsharp/pull/19724))
* Emit debug points at a stack-empty position ([PR #19877](https://github.com/dotnet/fsharp/pull/19877))
* Fix spurious XmlDoc warnings (unknown parameter / no documentation for parameter) under `--warnon:3390` when a get/set property documents the full parameter set across both accessors. ([Issue #13684](https://github.com/dotnet/fsharp/issues/13684), [PR #19884](https://github.com/dotnet/fsharp/pull/19884))
* Stop F# Interactive from mutating script arguments that follow `--`. Abbreviated flags like `-d`, `-r`, `-I` after the `--` separator are no longer colon-joined with their next token in `fsi.CommandLineArgs`. ([Issue #10819](https://github.com/dotnet/fsharp/issues/10819), [PR #19926](https://github.com/dotnet/fsharp/pull/19926))

### Added

Expand Down
22 changes: 21 additions & 1 deletion src/Compiler/Interactive/fsi.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,27 @@ type internal FsiCommandLineOptions(fsi: FsiEvaluationSessionHostConfig, argv: s
@ fsiUsageSuffix tcConfigB

let abbrevArgs = GetAbbrevFlagSet tcConfigB false
ParseCompilerOptions(collect, fsiCompilerOptions, List.tail (PostProcessCompilerArgs abbrevArgs argv))
// PostProcessCompilerArgs rewrites abbreviated flags like `-d 5` to `-d:5`.
// We must NOT apply that rewrite to user-script arguments that follow `--`,
// otherwise fsi.CommandLineArgs leaks the rewrite to scripts (see #10819).
// Split argv at the first `--`, post-process only the compiler-args prefix,
// and pass the suffix (including the `--` separator itself) through unmodified.
// Keeping `--` in the suffix preserves both downstream handlers:
// * with a script preceding `--`, the OptionGeneral IsScript handler captures
// the suffix as script args (matching pre-fix behaviour for fsi.CommandLineArgs);
// * with no script (e.g. `dotnet fsi -- -d 5`), the `--` token reaches
// ParseCompilerOptions and fires its OptionRest recordExplicitArg handler,
// so `-d` and `5` are captured as explicit args instead of being parsed
// as compiler options.
let processedArgs =
match Array.tryFindIndex (fun (a: string) -> a = "--") argv with
| Some idx ->
let prefix = argv[0 .. idx - 1]
let suffix = argv[idx..]
PostProcessCompilerArgs abbrevArgs prefix @ List.ofArray suffix
| None -> PostProcessCompilerArgs abbrevArgs argv

ParseCompilerOptions(collect, fsiCompilerOptions, List.tail processedArgs)
with e ->
stopProcessingRecovery e range0
failwithf "Error creating evaluation session: %A" e
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace CompilerOptions.Fsi

open System
open System.IO
open Xunit
open FSharp.Test
open FSharp.Test.Compiler

/// Regression tests for https://github.com/dotnet/fsharp/issues/10819
/// fsi.CommandLineArgs must preserve every token after `--` byte-for-byte;
/// in particular abbreviated flags like -d / -r / -I that follow `--` must
/// NOT be colon-joined with their next argument.
module FsiCommandLineArgsTests =

let private writeProbeScript () : string =
// Prints one ARG=<value> line per element of fsi.CommandLineArgs so the
// test can parse exact contents from stdout. A leading empty `printfn`
// ensures the first ARG= line is never prefixed by an FSI `> ` prompt
// (which only matters when the probe runs interactively via --use,
// not when it is invoked as the script-file argument directly).
let body = """
printfn ""
for a in fsi.CommandLineArgs do
printfn "ARG=%s" a
"""
let path =
Path.Combine(
Path.GetTempPath(),
sprintf "fsi_cmdline_%s.fsx" (Guid.NewGuid().ToString("N")))
File.WriteAllText(path, body)
path

let private parseArgsFromStdOut (stdout: string) : string list =
stdout.Split([| '\r'; '\n' |], StringSplitOptions.RemoveEmptyEntries)
|> Array.choose (fun line ->
if line.StartsWith("ARG=") then Some (line.Substring(4)) else None)
|> Array.toList

/// Run fsi <script> <extra args> and return the parsed fsi.CommandLineArgs
/// (without index 0, which is the absolute script path and varies per run).
let private runAndGetTail (extraArgs: string list) : string list =
let scriptPath = writeProbeScript ()
try
let result = runFsiProcess (scriptPath :: extraArgs)
Assert.True(
result.ExitCode = 0,
sprintf "fsi exited %d. stdout=%s stderr=%s"
result.ExitCode result.StdOut result.StdErr)
match parseArgsFromStdOut result.StdOut with
| [] ->
failwithf "No ARG= lines in stdout. stdout=%s stderr=%s"
result.StdOut result.StdErr
| _ :: tail -> tail
finally
try File.Delete(scriptPath) with _ -> ()

// Primary regression theory for #10819: with `--`, abbreviated flags and
// arbitrary user tokens must reach the script verbatim. The `--` separator
// itself is included in fsi.CommandLineArgs (matching pre-fix behaviour);
// the GREEN fix only stops PostProcessCompilerArgs from colon-joining the
// abbreviated flags that follow it.
[<Theory>]
[<InlineData("-d,5", "--,-d,5")>]
[<InlineData("-r,5", "--,-r,5")>]
[<InlineData("-I,5", "--,-I,5")>]
[<InlineData("--foo,--bar=baz", "--,--foo,--bar=baz")>]
[<InlineData("a:b,c:d", "--,a:b,c:d")>]
let ``fsi.CommandLineArgs preserves args after -- verbatim``
(userArgsCsv: string) (expectedCsv: string) =
let userArgs = userArgsCsv.Split(',') |> Array.toList
let expected = expectedCsv.Split(',') |> Array.toList
// Script appears before `--`; the script-arg tail follows `--`.
let tail = runAndGetTail ("--" :: userArgs)
Assert.Equal<string list>(expected, tail)

// Baseline: -b is not an abbreviated flag, so it already round-trips through
// `--` today without colon-joining. Locks in the only piece of current
// behaviour that is correct, so the GREEN fix can't accidentally regress
// unrelated tokens.
[<Fact>]
let ``fsi.CommandLineArgs preserves non-abbreviated args after -- (baseline)`` () =
let tail = runAndGetTail ["--"; "-b"; "5"]
Assert.Equal<string list>(["--"; "-b"; "5"], tail)

// No-script regression for #10819: when `--` appears without a preceding
// script-file argument, ParseCompilerOptions must see `--` so its
// OptionRest recordExplicitArg handler fires. If the GREEN fix were to
// strip `--` from the suffix, `-d` and `5` would instead be parsed as
// compiler options (and `-d` would fail to bind without its joined
// value), so this row locks the OptionRest path.
[<Fact>]
let ``fsi.CommandLineArgs preserves args after -- with no script (OptionRest path)`` () =
let scriptPath = writeProbeScript ()
try
// --use:<script> + --exec loads the probe script then exits without
// making the script path the first non-option arg, so the IsScript
// OptionGeneral handler does NOT fire and the `--` token reaches
// ParseCompilerOptions' OptionRest handler instead. --nologo
// suppresses the banner so the FSI prompt prefix does not bleed
// into the parsed ARG= lines.
let result =
runFsiProcess
[ "--nologo"
"--use:" + scriptPath
"--exec"
"--"
"-d"
"5" ]
Assert.True(
result.ExitCode = 0,
sprintf "fsi exited %d. stdout=%s stderr=%s"
result.ExitCode result.StdOut result.StdErr)
let args = parseArgsFromStdOut result.StdOut
// With no script before `--`, OptionRest captured only the tokens
// AFTER `--`; the `--` itself is consumed by ParseCompilerOptions.
// args[0] is the fsi binary path; the tail must be exactly `-d 5`.
match args with
| [] ->
failwithf "No ARG= lines in stdout. stdout=%s stderr=%s"
result.StdOut result.StdErr
| _ :: tail ->
Assert.Equal<string list>([ "-d"; "5" ], tail)
finally
try File.Delete(scriptPath) with _ -> ()
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@
<Compile Include="CompilerOptions\Fsc\pdb\Pdb.fs" />
<Compile Include="CompilerOptions\fsc\tailcalls\tailcalls.fs" />
<Compile Include="CompilerOptions\fsi\FsiCliTests.fs" />
<Compile Include="CompilerOptions\fsi\FsiCommandLineArgsTests.fs" />
<Compile Include="CompilerOptions\fsi\Nologo.fs" />
<Compile Include="CompilerOptions\fsi\Langversion.fs" />
<Compile Include="CompilerService\RangeModule.fs" />
Expand Down
Loading