Skip to content

Commit d43927a

Browse files
committed
Handle remaining interpolated-string forms adjacent to '=' (#16696)
Extend the merged '=$"' fix to the verbatim (=$@", =@$") and extended multi-dollar (=$$""") interpolated-string forms, by matching '=' plus the opener and rewinding so the regular interpolated-string lexer handles the rest. Operators containing '$' remain rejected (FS0035), unchanged.
1 parent 2f9c6d8 commit d43927a

9 files changed

Lines changed: 118 additions & 5 deletions

docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* 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))
1515
* Fix `[<return: X>]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[<X>]` and `[<return: X>]` 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))
1616
* 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))
17+
* 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))
1718
* 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))
1819
* 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))
1920
* 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))

src/Compiler/lex.fsl

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -975,11 +975,14 @@ rule token (args: LexArgs) (skip: bool) = parse
975975

976976
| ignored_op_char* ('@'|'^') op_char* { checkExprOp lexbuf; INFIX_AT_HAT_OP(lexeme lexbuf) }
977977

978-
// For '=$"' (property/named-arg initialization with an interpolated string, e.g. C(Name=$"123")):
979-
// match the 3 chars, but consume only the '=' and rewind so the next scan begins at '$"',
980-
// letting the regular interpolated-string lexer process it (including any '{...}' holes).
981-
// See https://github.com/dotnet/fsharp/issues/16696.
982-
| '=' '$' '"' {
978+
// For '=' immediately followed (no space) by an interpolated-string opener, e.g. C(Name=$"123").
979+
// Match the '=' together with the opener, but consume only the '=' and rewind so the next scan
980+
// begins at the opener, letting the regular interpolated-string lexer process it (including any
981+
// '{...}' holes). This covers the single ($"), extended/multi-dollar ($$"""), and verbatim
982+
// ($@" / @$") interpolated-string forms. See https://github.com/dotnet/fsharp/issues/16696.
983+
| '=' '$' '"'
984+
| '=' ('$'+) '"' '"' '"'
985+
| '=' ("$@" | "@$") '"' {
983986
lexbuf.LexemeLength <- 1
984987
lexbuf.EndPos <- lexbuf.StartPos.ShiftColumnBy(1)
985988
EQUALS }

tests/FSharp.Compiler.ComponentTests/Language/InterpolatedStringsTests.fs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,34 @@ if c.Name <> "42" || r.Name <> "42" then failwith "expected 42"
393393
|> compileExeAndRun
394394
|> shouldSucceed
395395

396+
// The verbatim ($@" / @$") forms, also adjacent to '='.
397+
[<Fact>]
398+
let ``Issue 16696 - '=' adjacent to a verbatim interpolated string binds it`` () =
399+
Fsx """
400+
let n = 42
401+
let a =$@"{n}"
402+
let b =@$"{n}"
403+
if a <> "42" || b <> "42" then failwith "expected 42"
404+
"""
405+
|> compileExeAndRun
406+
|> shouldSucceed
407+
408+
[<Fact>]
409+
let ``Issue 16696 - '=' adjacent to a verbatim interpolated string in named-argument and record contexts`` () =
410+
Fsx """
411+
type C() = member val Name = "" with get, set
412+
type R = { Name: string }
413+
let n = 42
414+
let c = C(Name=$@"{n}")
415+
let r = { Name=@$"{n}" }
416+
if c.Name <> "42" || r.Name <> "42" then failwith "expected 42"
417+
"""
418+
|> compileExeAndRun
419+
|> shouldSucceed
420+
421+
// (The extended multi-dollar form '=$$"""..."""' is covered by the SyntaxTree baseline
422+
// SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs.)
423+
396424
// Operator lexing is unchanged: a '$' anywhere in an operator is still reserved (FS0035).
397425
// The only thing the fix changes is '=' directly before an interpolated-string opener;
398426
// everything below still lexes as an operator exactly as before.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let x =$$"""abc"""
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
ImplFile
2+
(ParsedImplFileInput
3+
("/root/String/SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar.fs",
4+
false,
5+
QualifiedNameOfFile
6+
SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar, [],
7+
[SynModuleOrNamespace
8+
([SynExprInterpolatedStringAdjacentEqualsExtendedMultiDollar], false,
9+
AnonModule,
10+
[Let
11+
(false,
12+
[SynBinding
13+
(None, Normal, false, false, [],
14+
PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector),
15+
SynValData
16+
(None, SynValInfo ([], SynArgInfo ([], false, None)), None),
17+
Named (SynIdent (x, None), false, None, (1,4--1,5)), None,
18+
InterpolatedString
19+
([String ("abc", (1,7--1,18))], TripleQuote, (1,7--1,18)),
20+
(1,4--1,5), Yes (1,0--1,18), { LeadingKeyword = Let (1,0--1,3)
21+
InlineKeyword = None
22+
EqualsRange = Some (1,6--1,7) })],
23+
(1,0--1,18), { InKeyword = None })], PreXmlDocEmpty, [], None,
24+
(1,0--2,0), { LeadingKeyword = None })], (true, true),
25+
{ ConditionalDirectives = []
26+
WarnDirectives = []
27+
CodeComments = [] }, set []))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let x =$@"abc"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
ImplFile
2+
(ParsedImplFileInput
3+
("/root/String/SynExprInterpolatedStringAdjacentEqualsVerbatim.fs", false,
4+
QualifiedNameOfFile SynExprInterpolatedStringAdjacentEqualsVerbatim, [],
5+
[SynModuleOrNamespace
6+
([SynExprInterpolatedStringAdjacentEqualsVerbatim], false, AnonModule,
7+
[Let
8+
(false,
9+
[SynBinding
10+
(None, Normal, false, false, [],
11+
PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector),
12+
SynValData
13+
(None, SynValInfo ([], SynArgInfo ([], false, None)), None),
14+
Named (SynIdent (x, None), false, None, (1,4--1,5)), None,
15+
InterpolatedString
16+
([String ("abc", (1,7--1,14))], Verbatim, (1,7--1,14)),
17+
(1,4--1,5), Yes (1,0--1,14), { LeadingKeyword = Let (1,0--1,3)
18+
InlineKeyword = None
19+
EqualsRange = Some (1,6--1,7) })],
20+
(1,0--1,14), { InKeyword = None })], PreXmlDocEmpty, [], None,
21+
(1,0--2,0), { LeadingKeyword = None })], (true, true),
22+
{ ConditionalDirectives = []
23+
WarnDirectives = []
24+
CodeComments = [] }, set []))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let x =@$"abc"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
ImplFile
2+
(ParsedImplFileInput
3+
("/root/String/SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar.fs",
4+
false,
5+
QualifiedNameOfFile
6+
SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar, [],
7+
[SynModuleOrNamespace
8+
([SynExprInterpolatedStringAdjacentEqualsVerbatimAtDollar], false,
9+
AnonModule,
10+
[Let
11+
(false,
12+
[SynBinding
13+
(None, Normal, false, false, [],
14+
PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector),
15+
SynValData
16+
(None, SynValInfo ([], SynArgInfo ([], false, None)), None),
17+
Named (SynIdent (x, None), false, None, (1,4--1,5)), None,
18+
InterpolatedString
19+
([String ("abc", (1,7--1,14))], Verbatim, (1,7--1,14)),
20+
(1,4--1,5), Yes (1,0--1,14), { LeadingKeyword = Let (1,0--1,3)
21+
InlineKeyword = None
22+
EqualsRange = Some (1,6--1,7) })],
23+
(1,0--1,14), { InKeyword = None })], PreXmlDocEmpty, [], None,
24+
(1,0--2,0), { LeadingKeyword = None })], (true, true),
25+
{ ConditionalDirectives = []
26+
WarnDirectives = []
27+
CodeComments = [] }, set []))

0 commit comments

Comments
 (0)