From 0cb051e4028849812a9afd5a03276dff6c3b2c2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:58:18 +0000 Subject: [PATCH 1/8] Initial plan From 5af597906ab967a6c58391ac0d60c54ea7c4a1d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:10:16 +0000 Subject: [PATCH 2/8] Add warning FS3885 for let-in with multi-line body in sequence expressions Agent-Logs-Url: https://github.com/dotnet/fsharp/sessions/740bb677-5347-48ad-9001-278dfc1631e3 Co-authored-by: abonie <20281641+abonie@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + .../Checking/Expressions/CheckExpressions.fs | 10 +++ src/Compiler/FSComp.txt | 2 + src/Compiler/Facilities/LanguageFeatures.fs | 3 + src/Compiler/Facilities/LanguageFeatures.fsi | 1 + .../ErrorMessages/WarnExpressionTests.fs | 86 +++++++++++++++++++ 6 files changed, 103 insertions(+) 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 d5c2087765e..1388a605522 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -22,3 +22,4 @@ * Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289)) * Add `#version;;` directive to F# Interactive to display version and environment information. ([Issue #13307](https://github.com/dotnet/fsharp/issues/13307), [PR #19332](https://github.com/dotnet/fsharp/pull/19332)) +* Added warning FS3885 when `let ... in` with explicit `in` keyword is used in a sequence expression and the body extends to subsequent lines, causing unexpected scoping. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19526](https://github.com/dotnet/fsharp/pull/19526)) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 635a0dff045..91de1d8c442 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -6041,6 +6041,16 @@ and TcExprUndelayed (cenv: cenv) (overallTy: OverallTy) env tpenv (synExpr: SynE errorR(Error(FSComp.SR.tcConstructRequiresComputationExpression(), leadingKeyword.Range)) | _ -> () + // Warn when 'let ... in' has an explicit 'in' keyword and the body is a sequential expression + // spanning multiple lines. This indicates the user likely intended the 'let ... in' to scope only + // over the expression on the same line, but the parser greedily consumed subsequent lines as body. + match letOrUse with + | { Trivia = { InKeyword = Some inRange }; Body = SynExpr.Sequential(expr2 = expr2) } + when g.langVersion.SupportsFeature LanguageFeature.WarnOnLetInSequenceExpression + && expr2.Range.StartLine > inRange.StartLine -> + warning(Error(FSComp.SR.tcLetExpressionWithInHasMultiLineBody(), inRange)) + | _ -> () + TcLinearExprs (TcExprThatCanBeCtorBody cenv) cenv env overallTy tpenv false synExpr id | SynExpr.TryWith (synBodyExpr, synWithClauses, mTryToLast, spTry, spWith, trivia) -> diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index a4007147b9a..8835f841443 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1809,8 +1809,10 @@ featureWarnWhenFunctionValueUsedAsInterpolatedStringArg,"Warn when a function va featureMethodOverloadsCache,"Support for caching method overload resolution results for improved compilation performance." featureImplicitDIMCoverage,"Implicit dispatch slot coverage for default interface member implementations" featurePreprocessorElif,"#elif preprocessor directive" +featureWarnOnLetInSequenceExpression,"Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines" 3880,optsLangVersionOutOfSupport,"Language version '%s' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/%s" 3881,optsUnrecognizedLanguageFeature,"Unrecognized language feature name: '%s'. Use a valid feature name such as 'NameOf' or 'StringInterpolation'." 3882,lexHashElifMustBeFirst,"#elif directive must appear as the first non-whitespace character on a line" 3883,lexHashElifMustHaveIdent,"#elif directive should be immediately followed by an identifier" 3884,tcFunctionValueUsedAsInterpolatedStringArg,"This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments." +3885,tcLetExpressionWithInHasMultiLineBody,"The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope." diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index 1bc31837052..0cc80ce4541 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -108,6 +108,7 @@ type LanguageFeature = | MethodOverloadsCache | ImplicitDIMCoverage | PreprocessorElif + | WarnOnLetInSequenceExpression /// LanguageVersion management type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) = @@ -251,6 +252,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) // Put stabilized features here for F# 11.0 previews via .NET SDK preview channels LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg, languageVersion110 LanguageFeature.PreprocessorElif, languageVersion110 + LanguageFeature.WarnOnLetInSequenceExpression, languageVersion110 // Difference between languageVersion110 and preview - 11.0 gets turned on automatically by picking a preview .NET 11 SDK // previewVersion is only when "preview" is specified explicitly in project files and users also need a preview SDK @@ -453,6 +455,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) | LanguageFeature.MethodOverloadsCache -> FSComp.SR.featureMethodOverloadsCache () | LanguageFeature.ImplicitDIMCoverage -> FSComp.SR.featureImplicitDIMCoverage () | LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif () + | LanguageFeature.WarnOnLetInSequenceExpression -> FSComp.SR.featureWarnOnLetInSequenceExpression () /// Get a version string associated with the given feature. static member GetFeatureVersionString feature = diff --git a/src/Compiler/Facilities/LanguageFeatures.fsi b/src/Compiler/Facilities/LanguageFeatures.fsi index fd6182c9d3e..1a247cda15e 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -99,6 +99,7 @@ type LanguageFeature = | MethodOverloadsCache | ImplicitDIMCoverage | PreprocessorElif + | WarnOnLetInSequenceExpression /// LanguageVersion management type LanguageVersion = diff --git a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs index 942ab64dfcd..e07334ebdec 100644 --- a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs @@ -220,3 +220,89 @@ let main _argv = """ |> typecheck |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7091 + [] + let ``Warn when let-in has multi-line sequential body in do block``() = + FSharp """ +module Test +let x = 42 +do + let x = 1 in x + 1 + x + """ + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Warning 3885, Line 5, Col 15, Line 5, Col 17, + "The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope.") + (Warning 20, Line 5, Col 18, Line 5, Col 23, + "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") + (Warning 20, Line 5, Col 5, Line 6, Col 6, + "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") + ] + + // https://github.com/dotnet/fsharp/issues/7091 + [] + let ``No warning for single-line let-in``() = + FSharp """ +module Test +let result = let x = 1 in x + 1 + """ + |> withLangVersionPreview + |> typecheck + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7091 + [] + let ``No warning for let without in keyword``() = + FSharp """ +module Test +let x = 42 +do + let x = 1 + printfn "%d" x + """ + |> withLangVersionPreview + |> typecheck + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7091 + [] + let ``No warning for let-in without langversion preview``() = + FSharp """ +module Test +let x = 42 +do + let x = 1 in x + 1 + x + """ + |> withLangVersion "9.0" + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Warning 20, Line 5, Col 18, Line 5, Col 23, + "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") + (Warning 20, Line 5, Col 5, Line 6, Col 6, + "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") + ] + + // https://github.com/dotnet/fsharp/issues/7091 + [] + let ``Warn when let-in extends scope in function body``() = + FSharp """ +module Test +let f () = + let x = 1 in x + 1 + printfn "hello" + """ + |> withLangVersionPreview + |> typecheck + |> shouldFail + |> withDiagnostics [ + (Warning 3885, Line 4, Col 15, Line 4, Col 17, + "The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope.") + (Warning 20, Line 4, Col 18, Line 4, Col 23, + "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") + ] From 1046bc9993f69a2237f9a0c4642b97979e1716c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:18:33 +0000 Subject: [PATCH 3/8] Update release notes wording for FS3885 Agent-Logs-Url: https://github.com/dotnet/fsharp/sessions/740bb677-5347-48ad-9001-278dfc1631e3 Co-authored-by: abonie <20281641+abonie@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1388a605522..4161d0e6c95 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -22,4 +22,4 @@ * Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289)) * Add `#version;;` directive to F# Interactive to display version and environment information. ([Issue #13307](https://github.com/dotnet/fsharp/issues/13307), [PR #19332](https://github.com/dotnet/fsharp/pull/19332)) -* Added warning FS3885 when `let ... in` with explicit `in` keyword is used in a sequence expression and the body extends to subsequent lines, causing unexpected scoping. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19526](https://github.com/dotnet/fsharp/pull/19526)) +* Added warning FS3885 when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, which causes all subsequent lines to become part of the `let` body due to the parser's greedy behavior. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19526](https://github.com/dotnet/fsharp/pull/19526)) From d0a59326309648a337a1d2050a0861c8031a8980 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 9 Apr 2026 13:33:52 +0200 Subject: [PATCH 4/8] Fix CI failures: remove 'in' keyword triggering FS3885 and add XLF entries - Remove explicit 'in' keyword in HashMultiMap.fs and fsi.fs that triggered the new FS3885 warning (treated as error in CI builds) - Add missing XLF translation entries for featureWarnOnLetInSequenceExpression and tcLetExpressionWithInHasMultiLineBody to all 13 locale files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Interactive/fsi.fs | 2 +- src/Compiler/Utilities/HashMultiMap.fs | 2 +- src/Compiler/xlf/FSComp.txt.cs.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.de.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.es.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.fr.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.it.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.ja.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.ko.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.pl.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.ru.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.tr.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 10 ++++++++++ src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 10 ++++++++++ 15 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/Compiler/Interactive/fsi.fs b/src/Compiler/Interactive/fsi.fs index 1ff957e77a8..d1d78a19f65 100644 --- a/src/Compiler/Interactive/fsi.fs +++ b/src/Compiler/Interactive/fsi.fs @@ -1532,7 +1532,7 @@ type internal FsiConsoleInput /// Try to get the first line, if we snarfed it while probing. member _.TryGetFirstLine() = - let r = firstLine in + let r = firstLine firstLine <- None r diff --git a/src/Compiler/Utilities/HashMultiMap.fs b/src/Compiler/Utilities/HashMultiMap.fs index 2688869136e..e2e0832e2fa 100644 --- a/src/Compiler/Utilities/HashMultiMap.fs +++ b/src/Compiler/Utilities/HashMultiMap.fs @@ -169,7 +169,7 @@ type internal HashMultiMap<'Key, 'Value when 'Key: not null>(size: int, comparer | _ -> false member s.Remove(k: 'Key) = - let res = s.ContainsKey(k) in + let res = s.ContainsKey(k) s.Remove(k) res diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 6896c33a6a4..de788ad9d5b 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 3966a59a853..1bc0060ec22 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 08828606dd6..bccd16c9ca4 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 5a28ec15953..f05e7f960e7 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 5dead052c6a..4feeff4e636 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index f491fb0c4c5..b287d73b043 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index f8185fb2a22..b91da235c9c 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 7e81e135eeb..85f725c94a9 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 22a5db4504c..ae82e469b4d 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 8f5220ba47e..fc3715134de 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 17509827124..d48b90024c5 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index bf26625f8ec..b0e5c25315a 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 2a63e5ad753..5bfa69c98aa 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -8962,6 +8962,16 @@ This expression is a function value. When used in an interpolated string it will be formatted using its 'ToString' method, which is likely not the intended behavior. Consider applying the function to its arguments. + + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + Warn when 'let ... in' is used in a sequence expression and the body extends to subsequent lines + + + + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope. + + \ No newline at end of file From ad1ee3a383f2302732204a8943be163ed6d7c102 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 9 Apr 2026 14:59:43 +0200 Subject: [PATCH 5/8] Fix CI failures: remove 'in' keyword in RemoveUnnecessaryParenthesesTests.fs triggering FS3885, fix PR number in release notes, add .Language/preview.md entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- docs/release-notes/.Language/preview.md | 1 + .../CodeFixes/RemoveUnnecessaryParenthesesTests.fs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) 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 4161d0e6c95..2aad0b136cb 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -22,4 +22,4 @@ * Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289)) * Add `#version;;` directive to F# Interactive to display version and environment information. ([Issue #13307](https://github.com/dotnet/fsharp/issues/13307), [PR #19332](https://github.com/dotnet/fsharp/pull/19332)) -* Added warning FS3885 when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, which causes all subsequent lines to become part of the `let` body due to the parser's greedy behavior. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19526](https://github.com/dotnet/fsharp/pull/19526)) +* Added warning FS3885 when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, which causes all subsequent lines to become part of the `let` body due to the parser's greedy behavior. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19501](https://github.com/dotnet/fsharp/pull/19501)) diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index d97ef294125..7bfa7af4ce5 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -2,6 +2,7 @@ * Warn (FS3884) when a function or delegate value is used as an interpolated string argument, since it will be formatted via `ToString` rather than being applied. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289)) * Added `MethodOverloadsCache` language feature (preview) that caches overload resolution results for repeated method calls, significantly improving compilation performance. ([PR #19072](https://github.com/dotnet/fsharp/pull/19072)) +* Warn (FS3885) when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, causing unexpected scoping. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19501](https://github.com/dotnet/fsharp/pull/19501)) ### Fixed diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs index 7901df88801..eb5a2de9f5b 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/RemoveUnnecessaryParenthesesTests.fs @@ -2724,10 +2724,10 @@ module Patterns = let sb = StringBuilder() for original, expected in pairs -> - (let original = string (SynPat.fmt sb original) in + (let original = string (SynPat.fmt sb original) ignore <| sb.Clear() original), - (let expected = string (SynPat.fmt sb expected) in + (let expected = string (SynPat.fmt sb expected) ignore <| sb.Clear() expected) } From 1aae72dbf9219e993a792f77abc6751bb4247997 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 9 Apr 2026 16:31:52 +0200 Subject: [PATCH 6/8] Fix issue reference in release notes: #7091 -> #7741 The previous session referenced issue #7091 (Fix fsharp47 - test path fix) but the correct issue is #7741 (Wrong let expression parsing) which describes the let...in scoping problem this warning addresses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- docs/release-notes/.Language/preview.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 2aad0b136cb..fdec281a01a 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -22,4 +22,4 @@ * Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289)) * Add `#version;;` directive to F# Interactive to display version and environment information. ([Issue #13307](https://github.com/dotnet/fsharp/issues/13307), [PR #19332](https://github.com/dotnet/fsharp/pull/19332)) -* Added warning FS3885 when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, which causes all subsequent lines to become part of the `let` body due to the parser's greedy behavior. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19501](https://github.com/dotnet/fsharp/pull/19501)) +* Added warning FS3885 when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, which causes all subsequent lines to become part of the `let` body due to the parser's greedy behavior. ([Issue #7741](https://github.com/dotnet/fsharp/issues/7741), [PR #19501](https://github.com/dotnet/fsharp/pull/19501)) diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 7bfa7af4ce5..074af45dc5d 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -2,7 +2,7 @@ * Warn (FS3884) when a function or delegate value is used as an interpolated string argument, since it will be formatted via `ToString` rather than being applied. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289)) * Added `MethodOverloadsCache` language feature (preview) that caches overload resolution results for repeated method calls, significantly improving compilation performance. ([PR #19072](https://github.com/dotnet/fsharp/pull/19072)) -* Warn (FS3885) when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, causing unexpected scoping. ([Issue #7091](https://github.com/dotnet/fsharp/issues/7091), [PR #19501](https://github.com/dotnet/fsharp/pull/19501)) +* Warn (FS3885) when `let ... in` with explicit `in` keyword has a body that extends to subsequent lines, causing unexpected scoping. ([Issue #7741](https://github.com/dotnet/fsharp/issues/7741), [PR #19501](https://github.com/dotnet/fsharp/pull/19501)) ### Fixed From f37cbfce4361ee74dfd71e9cf115a42729283c4d Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Thu, 9 Apr 2026 18:17:34 +0200 Subject: [PATCH 7/8] Retrigger CI: FindReferences flaky test failure unrelated to PR changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From eaa7a23f1862503b10fb3a33f7404823bf999d15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:28:23 +0000 Subject: [PATCH 8/8] Move let-in handling from type-checking warning to LexFilter change When an explicit 'in' keyword is used with a block-scoped 'let' in light syntax and the body starts on the same line as 'in', push a new seq block context to limit the body scope based on indentation. This prevents the parser from greedily capturing subsequent lines as part of the let body. - Remove type-checking warning from CheckExpressions.fs - Add LexFilter logic: when blockLet=true and body is on same line as 'in', push CtxtSeqBlock with AddBlockEnd to scope the body - Update WarnExpressionTests to reflect new behavior (no FS3885, different parse tree produces different W20 ranges) - Update SyntaxTree baseline for minor range change Agent-Logs-Url: https://github.com/dotnet/fsharp/sessions/1c48edbc-5796-4d9b-a80d-3a3d423c2e3f Co-authored-by: abonie <20281641+abonie@users.noreply.github.com> --- .../Checking/Expressions/CheckExpressions.fs | 10 ---------- src/Compiler/SyntaxTree/LexFilter.fs | 20 +++++++++++++++++-- .../ErrorMessages/WarnExpressionTests.fs | 8 ++------ ...LetOrUseContainsTheRangeOfInKeyword.fs.bsl | 2 +- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 91de1d8c442..635a0dff045 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -6041,16 +6041,6 @@ and TcExprUndelayed (cenv: cenv) (overallTy: OverallTy) env tpenv (synExpr: SynE errorR(Error(FSComp.SR.tcConstructRequiresComputationExpression(), leadingKeyword.Range)) | _ -> () - // Warn when 'let ... in' has an explicit 'in' keyword and the body is a sequential expression - // spanning multiple lines. This indicates the user likely intended the 'let ... in' to scope only - // over the expression on the same line, but the parser greedily consumed subsequent lines as body. - match letOrUse with - | { Trivia = { InKeyword = Some inRange }; Body = SynExpr.Sequential(expr2 = expr2) } - when g.langVersion.SupportsFeature LanguageFeature.WarnOnLetInSequenceExpression - && expr2.Range.StartLine > inRange.StartLine -> - warning(Error(FSComp.SR.tcLetExpressionWithInHasMultiLineBody(), inRange)) - | _ -> () - TcLinearExprs (TcExprThatCanBeCtorBody cenv) cenv env overallTy tpenv false synExpr id | SynExpr.TryWith (synBodyExpr, synWithClauses, mTryToLast, spTry, spWith, trivia) -> diff --git a/src/Compiler/SyntaxTree/LexFilter.fs b/src/Compiler/SyntaxTree/LexFilter.fs index ed5da9fd043..ffa3072e21c 100644 --- a/src/Compiler/SyntaxTree/LexFilter.fs +++ b/src/Compiler/SyntaxTree/LexFilter.fs @@ -1680,8 +1680,24 @@ type LexFilterImpl ( if debug then dprintf "IN at %a (becomes %s)\n" outputPos tokenStartPos (if blockLet then "ODECLEND" else "IN") if tokenStartCol < offsidePos.Column then warn tokenTup (FSComp.SR.lexfltIncorrentIndentationOfIn()) popCtxt() - // Make sure we queue a dummy token at this position to check if any other pop rules apply - delayToken(pool.UseLocation(tokenTup, ODUMMY token)) + + if blockLet && lexbuf.SupportsFeature LanguageFeature.WarnOnLetInSequenceExpression then + let nextTokenTup = peekNextTokenTup() + let nextTokenStartPos = startPosOfTokenTup nextTokenTup + + if nextTokenStartPos.Line = tokenStartPos.Line then + // When the body expression starts on the same line as the 'in' keyword in light syntax, + // push a new seq block to limit the body scope to that line. This prevents the parser + // from greedily capturing all subsequent lines as part of the let body. + pushCtxtSeqBlock tokenTup AddBlockEnd + else + // Body starts on a new line after 'in' — the user intentionally placed the body + // on the next line, so use standard behavior. + delayToken(pool.UseLocation(tokenTup, ODUMMY token)) + else + // Make sure we queue a dummy token at this position to check if any other pop rules apply + delayToken(pool.UseLocation(tokenTup, ODUMMY token)) + returnToken tokenLexbufState (if blockLet then ODECLEND(mkSynRange tokenTup.StartPos tokenTup.EndPos, true) else token) // Balancing rule. Encountering a 'done' balances with a 'do'. i.e. even a non-offside 'done' closes a 'do' diff --git a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs index e07334ebdec..2c053b6ad41 100644 --- a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/WarnExpressionTests.fs @@ -235,9 +235,7 @@ do |> typecheck |> shouldFail |> withDiagnostics [ - (Warning 3885, Line 5, Col 15, Line 5, Col 17, - "The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope.") - (Warning 20, Line 5, Col 18, Line 5, Col 23, + (Warning 20, Line 5, Col 5, Line 5, Col 23, "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") (Warning 20, Line 5, Col 5, Line 6, Col 6, "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") @@ -301,8 +299,6 @@ let f () = |> typecheck |> shouldFail |> withDiagnostics [ - (Warning 3885, Line 4, Col 15, Line 4, Col 17, - "The use of 'in' in 'let ... in' on a single line followed by additional code on subsequent lines causes all subsequent lines to be part of the 'let' body. This may lead to unexpected scoping. Either remove the 'in' keyword and rely on indentation, or add parentheses to clarify the intended scope.") - (Warning 20, Line 4, Col 18, Line 4, Col 23, + (Warning 20, Line 4, Col 5, Line 4, Col 23, "The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.") ] diff --git a/tests/service/data/SyntaxTree/Expression/SynExprLetOrUseContainsTheRangeOfInKeyword.fs.bsl b/tests/service/data/SyntaxTree/Expression/SynExprLetOrUseContainsTheRangeOfInKeyword.fs.bsl index 4fb6595cbd2..b65ca5c4e11 100644 --- a/tests/service/data/SyntaxTree/Expression/SynExprLetOrUseContainsTheRangeOfInKeyword.fs.bsl +++ b/tests/service/data/SyntaxTree/Expression/SynExprLetOrUseContainsTheRangeOfInKeyword.fs.bsl @@ -23,7 +23,7 @@ ImplFile Range = (2,0--2,15) Trivia = { InKeyword = Some (2,10--2,12) } IsFromSource = true }, (2,0--2,15))], PreXmlDocEmpty, [], None, - (2,0--2,15), { LeadingKeyword = None })], (true, true), + (2,0--3,0), { LeadingKeyword = None })], (true, true), { ConditionalDirectives = [] WarnDirectives = [] CodeComments = [] }, set []))