diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 0d3a441b291..7eacbe886ab 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -15,6 +15,7 @@ * Honor `--nowarn` and `--warnaserror` for warnings emitted during command-line option parsing ([Issue #19576](https://github.com/dotnet/fsharp/issues/19576), [PR #19776](https://github.com/dotnet/fsharp/pull/19776)) * Fix `[]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[]` and `[]` are applied to the same binding. ([Issue #17904](https://github.com/dotnet/fsharp/issues/17904), [Issue #19020](https://github.com/dotnet/fsharp/issues/19020), [PR #19738](https://github.com/dotnet/fsharp/pull/19738)) * Fix `=` adjacent to an interpolated string (e.g. `C(Name=$"value")`) being lexed as the invalid operator `=$` instead of an assignment followed by an interpolated string. ([Issue #16696](https://github.com/dotnet/fsharp/issues/16696)) +* Extend the `=` adjacent to an interpolated string fix to the verbatim (`=$@"…"`, `=@$"…"`) and extended multi-dollar (`=$$"""…"""`) interpolated-string forms. ([Issue #16696](https://github.com/dotnet/fsharp/issues/16696), [PR #19984](https://github.com/dotnet/fsharp/pull/19984)) * Preserve type abbreviations (`string`, user-defined aliases) in the refined type of bindings introduced after a `| null` pattern in a `match` expression. ([Issue #19646](https://github.com/dotnet/fsharp/issues/19646), [PR #19745](https://github.com/dotnet/fsharp/pull/19745)) * Fix attributes on return type of unparenthesized tuple methods being silently dropped from IL. ([Issue #462](https://github.com/dotnet/fsharp/issues/462), [PR #19714](https://github.com/dotnet/fsharp/pull/19714)) * Fix false-positive nullness warning (FS3261) when pattern matching narrows nullness inside seq/list/array comprehensions. ([Issue #19644](https://github.com/dotnet/fsharp/issues/19644), [PR #19743](https://github.com/dotnet/fsharp/pull/19743)) diff --git a/src/Compiler/lex.fsl b/src/Compiler/lex.fsl index 87c1b269ae4..ce4dd5955a6 100644 --- a/src/Compiler/lex.fsl +++ b/src/Compiler/lex.fsl @@ -975,11 +975,14 @@ rule token (args: LexArgs) (skip: bool) = parse | ignored_op_char* ('@'|'^') op_char* { checkExprOp lexbuf; INFIX_AT_HAT_OP(lexeme lexbuf) } - // For '=$"' (property/named-arg initialization with an interpolated string, e.g. C(Name=$"123")): - // match the 3 chars, but consume only the '=' and rewind so the next scan begins at '$"', - // letting the regular interpolated-string lexer process it (including any '{...}' holes). - // See https://github.com/dotnet/fsharp/issues/16696. - | '=' '$' '"' { + // For '=' immediately followed (no space) by an interpolated-string opener, e.g. C(Name=$"123"). + // Match the '=' together with the opener, but consume only the '=' and rewind so the next scan + // begins at the opener, letting the regular interpolated-string lexer process it (including any + // '{...}' holes). This covers the single ($"), extended/multi-dollar ($$"""), and verbatim + // ($@" / @$") interpolated-string forms. See https://github.com/dotnet/fsharp/issues/16696. + | '=' '$' '"' + | '=' ('$'+) '"' '"' '"' + | '=' ("$@" | "@$") '"' { lexbuf.LexemeLength <- 1 lexbuf.EndPos <- lexbuf.StartPos.ShiftColumnBy(1) EQUALS } diff --git a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs index 9894fa68126..4db7b63ad4b 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs @@ -393,6 +393,40 @@ if c.Name <> "42" || r.Name <> "42" then failwith "expected 42" |> compileExeAndRun |> shouldSucceed + // The verbatim ($@" / @$") forms, also adjacent to '='. + [] + let ``Issue 16696 - '=' adjacent to a verbatim interpolated string binds it`` () = + Fsx """ +let n = 42 +let a =$@"{n}" +let b =@$"{n}" +if a <> "42" || b <> "42" then failwith "expected 42" + """ + |> compileExeAndRun + |> shouldSucceed + + [] + let ``Issue 16696 - '=' adjacent to a verbatim interpolated string in named-argument and record contexts`` () = + Fsx """ +type C() = member val Name = "" with get, set +type R = { Name: string } +let n = 42 +let c = C(Name=$@"{n}") +let r = { Name=@$"{n}" } +if c.Name <> "42" || r.Name <> "42" then failwith "expected 42" + """ + |> compileExeAndRun + |> shouldSucceed + + // The extended multi-dollar ($$) form, also adjacent to '='. Note that a $$ string uses double + // braces for holes ({{n}}), so {n} would be literal text. Uses an escaped string literal because + // the source contains """, which cannot be embedded in an F# """...""" string. + [] + let ``Issue 16696 - '=' adjacent to an extended multi-dollar interpolated string binds it`` () = + Fsx "let n = 42\nlet x =$$\"\"\"{{n}}\"\"\"\nif x <> \"42\" then failwith \"expected 42\"" + |> compileExeAndRun + |> shouldSucceed + // Operator lexing is unchanged: a '$' anywhere in an operator is still reserved (FS0035). // The only thing the fix changes is '=' directly before an interpolated-string opener; // everything below still lexes as an operator exactly as before. diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs new file mode 100644 index 00000000000..2b3ad22a496 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs @@ -0,0 +1 @@ +let x =$$"""abc""" diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs.bsl new file mode 100644 index 00000000000..dd9e8e1841d --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs.bsl @@ -0,0 +1,27 @@ +ImplFile + (ParsedImplFileInput + ("/root/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs", + false, + QualifiedNameOfFile + SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar, [], + [SynModuleOrNamespace + ([SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar], false, + AnonModule, + [Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None), + Named (SynIdent (x, None), false, None, (1,4--1,5)), None, + InterpolatedString + ([String ("abc", (1,7--1,18))], TripleQuote, (1,7--1,18)), + (1,4--1,5), Yes (1,0--1,18), { LeadingKeyword = Let (1,0--1,3) + InlineKeyword = None + EqualsRange = Some (1,6--1,7) })], + (1,0--1,18), { InKeyword = None })], PreXmlDocEmpty, [], None, + (1,0--2,0), { LeadingKeyword = None })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs new file mode 100644 index 00000000000..2f93d9471c0 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs @@ -0,0 +1 @@ +let x =$@"abc" diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs.bsl new file mode 100644 index 00000000000..fc9329b25d7 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs.bsl @@ -0,0 +1,24 @@ +ImplFile + (ParsedImplFileInput + ("/root/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs", false, + QualifiedNameOfFile SynExprInterpolatedStringAdjacentEqualsVerbatim, [], + [SynModuleOrNamespace + ([SynExprInterpolatedStringAdjacentEqualsVerbatim], false, AnonModule, + [Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None), + Named (SynIdent (x, None), false, None, (1,4--1,5)), None, + InterpolatedString + ([String ("abc", (1,7--1,14))], Verbatim, (1,7--1,14)), + (1,4--1,5), Yes (1,0--1,14), { LeadingKeyword = Let (1,0--1,3) + InlineKeyword = None + EqualsRange = Some (1,6--1,7) })], + (1,0--1,14), { InKeyword = None })], PreXmlDocEmpty, [], None, + (1,0--2,0), { LeadingKeyword = None })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set [])) diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs new file mode 100644 index 00000000000..4ae5463f272 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs @@ -0,0 +1 @@ +let x =@$"abc" diff --git a/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs.bsl b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs.bsl new file mode 100644 index 00000000000..258e0d900e2 --- /dev/null +++ b/tests/service/data/SyntaxTree/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs.bsl @@ -0,0 +1,27 @@ +ImplFile + (ParsedImplFileInput + ("/root/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs", + false, + QualifiedNameOfFile + SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar, [], + [SynModuleOrNamespace + ([SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar], false, + AnonModule, + [Let + (false, + [SynBinding + (None, Normal, false, false, [], + PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector), + SynValData + (None, SynValInfo ([], SynArgInfo ([], false, None)), None), + Named (SynIdent (x, None), false, None, (1,4--1,5)), None, + InterpolatedString + ([String ("abc", (1,7--1,14))], Verbatim, (1,7--1,14)), + (1,4--1,5), Yes (1,0--1,14), { LeadingKeyword = Let (1,0--1,3) + InlineKeyword = None + EqualsRange = Some (1,6--1,7) })], + (1,0--1,14), { InKeyword = None })], PreXmlDocEmpty, [], None, + (1,0--2,0), { LeadingKeyword = None })], (true, true), + { ConditionalDirectives = [] + WarnDirectives = [] + CodeComments = [] }, set []))