From 941d90a1193efc231e1e51f1b8e42bffc32bdc22 Mon Sep 17 00:00:00 2001 From: kerams Date: Sun, 21 Jun 2026 23:36:40 +0200 Subject: [PATCH 1/6] Support NotNullIfNotNullAttribute --- src/Compiler/AbstractIL/il.fs | 1 + src/Compiler/AbstractIL/il.fsi | 1 + .../Checking/Expressions/CheckExpressions.fs | 68 +++++- src/Compiler/Checking/MethodCalls.fs | 8 + src/Compiler/FSComp.txt | 1 + src/Compiler/Facilities/LanguageFeatures.fs | 3 + src/Compiler/Facilities/LanguageFeatures.fsi | 1 + .../TypedTree/TypedTreeOps.Attributes.fs | 6 + src/Compiler/TypedTree/WellKnownAttribs.fs | 1 + src/Compiler/TypedTree/WellKnownAttribs.fsi | 1 + src/Compiler/xlf/FSComp.txt.cs.xlf | 5 + src/Compiler/xlf/FSComp.txt.de.xlf | 5 + src/Compiler/xlf/FSComp.txt.es.xlf | 5 + src/Compiler/xlf/FSComp.txt.fr.xlf | 5 + src/Compiler/xlf/FSComp.txt.it.xlf | 5 + src/Compiler/xlf/FSComp.txt.ja.xlf | 5 + src/Compiler/xlf/FSComp.txt.ko.xlf | 5 + src/Compiler/xlf/FSComp.txt.pl.xlf | 5 + src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 + src/Compiler/xlf/FSComp.txt.ru.xlf | 5 + src/Compiler/xlf/FSComp.txt.tr.xlf | 5 + src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 + src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 + .../FSharp.Compiler.ComponentTests.fsproj | 1 + .../Nullness/NotNullIfNotNullTests.fs | 221 ++++++++++++++++++ 25 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 629a3d260bb..b0a900b4550 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -1257,6 +1257,7 @@ type WellKnownILAttributes = | RequiredMemberAttribute = (1u <<< 22) | NullableContextAttribute = (1u <<< 23) | AttributeUsageAttribute = (1u <<< 24) + | NotNullIfNotNullAttribute = (1u <<< 25) | NotComputed = (1u <<< 31) type internal ILAttributesStoredRepr = diff --git a/src/Compiler/AbstractIL/il.fsi b/src/Compiler/AbstractIL/il.fsi index dc22d5bc803..1f6296bf1be 100644 --- a/src/Compiler/AbstractIL/il.fsi +++ b/src/Compiler/AbstractIL/il.fsi @@ -907,6 +907,7 @@ type WellKnownILAttributes = | RequiredMemberAttribute = (1u <<< 22) | NullableContextAttribute = (1u <<< 23) | AttributeUsageAttribute = (1u <<< 24) + | NotNullIfNotNullAttribute = (1u <<< 25) | NotComputed = (1u <<< 31) /// Represents the efficiency-oriented storage of ILAttributes in another item. diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 070ce6b3e8e..ed9ebc3152f 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -3330,6 +3330,41 @@ let GetMethodArgs arg = unnamedCallerArgs, namedCallerArgs +let NotNullIfNotNullParamNames g (minfo: MethInfo) = + match minfo with + | ILMeth(ilMethInfo = ilminfo) when ilminfo.RawMetadata.Return.CustomAttrsStored.HasWellKnownAttribute (g, WellKnownILAttributes.NotNullIfNotNullAttribute) -> + ilminfo.RawMetadata.Return.CustomAttrs.AsArray() + |> Array.toList + |> List.choose (fun attr -> + if classifyILAttrib attr &&& WellKnownILAttributes.NotNullIfNotNullAttribute <> WellKnownILAttributes.None then + match decodeILAttribData attr with + | [ ILAttribElem.String (Some paramName) ], _ -> Some paramName + | _ -> None + else + None) + | FSMeth(valRef = vref) -> + match vref.ValReprInfo with + | Some (ValReprInfo(result = retInfo)) when ArgReprInfoHasWellKnownAttribute g WellKnownValAttributes.NotNullIfNotNullAttribute retInfo -> + retInfo.Attribs.AsList() + |> List.choose (fun attrib -> + if classifyValAttrib g attrib &&& WellKnownValAttributes.NotNullIfNotNullAttribute <> WellKnownValAttributes.None then + match attrib with + | Attrib(unnamedArgs = [ AttribStringArg paramName ]) -> Some paramName + | _ -> None + else + None) + | _ -> [] + | _ -> [] + +// Resolve the caller argument bound to 'paramName' (named arguments first, then by position) and return the type of its type-checked expression. +let TryGetCallerArgType g (minfo: MethInfo) (callerArgs: CallerArgs<_>) paramName = + match callerArgs.Named |> List.tryPick (List.tryPick (fun (CallerNamedArg(id, arg)) -> if id.idText = paramName then Some (tyOfExpr g arg.Expr) else None)) with + | None -> + match minfo.GetParamNames() |> List.tryPick (List.tryFindIndex (fun nm -> match nm with Some nm -> nm = paramName | _ -> false)) with + | Some idx -> callerArgs.Unnamed |> List.tryPick (List.tryItem idx) |> Option.map (fun arg -> tyOfExpr g arg.Expr) + | None -> None + | x -> x + //------------------------------------------------------------------------- // Helpers dealing with sequence expressions //------------------------------------------------------------------------- @@ -10270,12 +10305,26 @@ and TcMethodApplication_UniqueOverloadInference let arityFilteredCandidates = candidateMethsAndProps - let makeOneCalledMeth (minfo, pinfoOpt, usesParamArrayConversion) = + let makeOneCalledMeth (minfo: MethInfo, pinfoOpt, usesParamArrayConversion) = let minst = FreshenMethInfo mItem minfo let callerTyArgs = match tyArgsOpt with | Some tyargs -> minfo.AdjustUserTypeInstForFSharpStyleIndexedExtensionMembers tyargs | None -> minst + + // If the return value is [], give the return a fresh nullness inference variable here so that + // unique-overload inference does not prematurely commit the result to the declared (nullable) nullness. The real + // nullness is resolved post argument type-checking (see below), once the argument types are known. + let minfo = + if not minfo.IsConstructor && g.checkNullness && g.langVersion.SupportsFeature LanguageFeature.NotNullIfNotNull then + match NotNullIfNotNullParamNames g minfo with + | [ _ ] -> + let retTy = minfo.GetFSharpReturnType(cenv.amap, mMethExpr, callerTyArgs) + MethInfoWithModifiedReturnType(minfo, replaceNullnessOfTy (NewNullnessVar()) retTy) + | _ -> minfo + else + minfo + CalledMeth(cenv.infoReader, Some(env.NameEnv), isCheckingAttributeCall, FreshenMethInfo, mMethExpr, ad, minfo, minst, callerTyArgs, pinfoOpt, callerObjArgTys, callerArgs, usesParamArrayConversion, true, objTyOpt, staticTyOpt) let preArgumentTypeCheckingCalledMethGroup = @@ -10505,6 +10554,23 @@ and TcMethodApplication match tyArgsOpt with | Some tyargs -> minfo.AdjustUserTypeInstForFSharpStyleIndexedExtensionMembers tyargs | None -> minst + + let minfo = + if not minfo.IsConstructor && g.checkNullness && g.langVersion.SupportsFeature LanguageFeature.NotNullIfNotNull then + // 'minfo' may already carry a placeholder return nullness from unique-overload inference (phase 1); + // strip it back to the base method before applying the real (argument-derived) nullness. + let baseMinfo = match minfo with MethInfoWithModifiedReturnType(inner, _) -> inner | _ -> minfo + match NotNullIfNotNullParamNames g baseMinfo with + | [ paramName ] -> + match TryGetCallerArgType g baseMinfo callerArgs paramName with + | Some callerArgTy -> + let retTy = baseMinfo.GetFSharpReturnType(cenv.amap, mMethExpr, callerTyArgs) + MethInfoWithModifiedReturnType(baseMinfo, replaceNullnessOfTy (nullnessOfTy g callerArgTy) retTy) + | None -> baseMinfo + | _ -> baseMinfo + else + minfo + CalledMeth(cenv.infoReader, Some(env.NameEnv), isCheckingAttributeCall, FreshenMethInfo, mMethExpr, ad, minfo, minst, callerTyArgs, pinfoOpt, callerObjArgTys, callerArgs, usesParamArrayConversion, true, objTyOpt, staticTyOpt)) // Commit unassociated constraints prior to member overload resolution where there is ambiguity diff --git a/src/Compiler/Checking/MethodCalls.fs b/src/Compiler/Checking/MethodCalls.fs index a9c394ee4e9..9fdf22a3a1a 100644 --- a/src/Compiler/Checking/MethodCalls.fs +++ b/src/Compiler/Checking/MethodCalls.fs @@ -1250,6 +1250,14 @@ let rec BuildMethodCall tcVal g amap isMutable m isProp minfo valUseFlags minst let expr = mkCoerceExpr (expr, retTy, m, exprTy) expr, retTy + | MethInfoWithModifiedReturnType((FSMeth(_, _, vref, _) as innerMeth), retTy) -> + // Build the inner call directly, without re-invoking TakeObjAddrForMethodCall. + let vExpr, vExprTy = tcVal vref valUseFlags (innerMeth.DeclaringTypeInst @ minst) m + let expr, exprTy = BuildFSharpMethodApp g m vref vExpr vExprTy allArgs + + let expr = mkCoerceExpr (expr, retTy, m, exprTy) + expr, retTy + | MethInfoWithModifiedReturnType _ -> failwith "MethInfoWithModifiedReturnType: unexpected inner method kind" diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index dc27f32bae9..c35c5ca3dd6 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1821,3 +1821,4 @@ featurePreprocessorElif,"#elif preprocessor directive" 3888,implAttributeMissingFromSignature,"The attribute '%s' is present on '%s' in the implementation but not in the signature, which takes precedence for tooling and consumers. Add the attribute to the signature, to ensure the attribute is not ignored by the compiler." featureExceptionFieldSerializationSupport,"emit GetObjectData and field-restoring deserialization constructor for exception types" featureErrorOnMissingSignatureAttribute,"error (rather than warning) when an enforced compiler-semantic attribute is present in the .fs but missing from the .fsi" +featureNotNullIfNotNull,"honor the 'NotNullIfNotNull' attribute on a method's return value" diff --git a/src/Compiler/Facilities/LanguageFeatures.fs b/src/Compiler/Facilities/LanguageFeatures.fs index da4ef690311..3312cb9a146 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fs +++ b/src/Compiler/Facilities/LanguageFeatures.fs @@ -110,6 +110,7 @@ type LanguageFeature = | PreprocessorElif | ExceptionFieldSerializationSupport | ErrorOnMissingSignatureAttribute + | NotNullIfNotNull /// LanguageVersion management type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) = @@ -254,6 +255,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg, languageVersion110 LanguageFeature.PreprocessorElif, languageVersion110 LanguageFeature.ExceptionFieldSerializationSupport, languageVersion110 + LanguageFeature.NotNullIfNotNull, 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 @@ -459,6 +461,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array) | LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif () | LanguageFeature.ExceptionFieldSerializationSupport -> FSComp.SR.featureExceptionFieldSerializationSupport () | LanguageFeature.ErrorOnMissingSignatureAttribute -> FSComp.SR.featureErrorOnMissingSignatureAttribute () + | LanguageFeature.NotNullIfNotNull -> FSComp.SR.featureNotNullIfNotNull () /// 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 5ba352191af..1be8275de35 100644 --- a/src/Compiler/Facilities/LanguageFeatures.fsi +++ b/src/Compiler/Facilities/LanguageFeatures.fsi @@ -101,6 +101,7 @@ type LanguageFeature = | PreprocessorElif | ExceptionFieldSerializationSupport | ErrorOnMissingSignatureAttribute + | NotNullIfNotNull /// LanguageVersion management type LanguageVersion = diff --git a/src/Compiler/TypedTree/TypedTreeOps.Attributes.fs b/src/Compiler/TypedTree/TypedTreeOps.Attributes.fs index d011c2d231f..21e3bda323c 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Attributes.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Attributes.fs @@ -183,6 +183,7 @@ module internal ILExtensions = WellKnownILAttributes.SetsRequiredMembersAttribute | "System.ObsoleteAttribute" -> WellKnownILAttributes.ObsoleteAttribute | "System.Diagnostics.CodeAnalysis.ExperimentalAttribute" -> WellKnownILAttributes.ExperimentalAttribute + | "System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute" -> WellKnownILAttributes.NotNullIfNotNullAttribute | "System.AttributeUsageAttribute" -> WellKnownILAttributes.AttributeUsageAttribute | _ -> WellKnownILAttributes.None @@ -592,6 +593,11 @@ module internal AttributeHelpers = | "ConditionalAttribute" -> WellKnownValAttributes.ConditionalAttribute | _ -> WellKnownValAttributes.None + | [| "System"; "Diagnostics"; "CodeAnalysis"; name |] -> + match name with + | "NotNullIfNotNullAttribute" -> WellKnownValAttributes.NotNullIfNotNullAttribute + | _ -> WellKnownValAttributes.None + | [| "System"; name |] -> match name with | "ThreadStaticAttribute" -> WellKnownValAttributes.ThreadStaticAttribute diff --git a/src/Compiler/TypedTree/WellKnownAttribs.fs b/src/Compiler/TypedTree/WellKnownAttribs.fs index fac3508a56e..748f525b89c 100644 --- a/src/Compiler/TypedTree/WellKnownAttribs.fs +++ b/src/Compiler/TypedTree/WellKnownAttribs.fs @@ -116,6 +116,7 @@ type internal WellKnownValAttributes = | NoEagerConstraintApplicationAttribute = (1uL <<< 38) | ValueAsStaticPropertyAttribute = (1uL <<< 39) | TailCallAttribute = (1uL <<< 40) + | NotNullIfNotNullAttribute = (1uL <<< 41) | NotComputed = (1uL <<< 63) module internal Flags = diff --git a/src/Compiler/TypedTree/WellKnownAttribs.fsi b/src/Compiler/TypedTree/WellKnownAttribs.fsi index da7a7b67f33..4939f94aaa8 100644 --- a/src/Compiler/TypedTree/WellKnownAttribs.fsi +++ b/src/Compiler/TypedTree/WellKnownAttribs.fsi @@ -114,6 +114,7 @@ type internal WellKnownValAttributes = | NoEagerConstraintApplicationAttribute = (1uL <<< 38) | ValueAsStaticPropertyAttribute = (1uL <<< 39) | TailCallAttribute = (1uL <<< 40) + | NotNullIfNotNullAttribute = (1uL <<< 41) | NotComputed = (1uL <<< 63) module internal Flags = diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 84e5e3cc50c..32eb6598139 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -527,6 +527,11 @@ neproměnné vzory napravo od vzorů typu „jako“ + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop nepovinný zprostředkovatel komunikace s možnou hodnotou null diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index d6edecb4def..35dac025ce1 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -527,6 +527,11 @@ Nicht-Variablenmuster rechts neben as-Mustern + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop Interop, NULL-Werte zulassend, optional diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 46573e801f1..281f8dd69bc 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -527,6 +527,11 @@ patrones no variables a la derecha de los patrones "as" + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop interoperabilidad opcional que admite valores NULL diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 1acfc469b5b..0cd33a35441 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -527,6 +527,11 @@ modèles non variables à droite de modèles « as » + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop interopérabilité facultative pouvant accepter une valeur null diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index deaca46fad0..f27b686b2e0 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -527,6 +527,11 @@ modelli non variabili a destra dei modelli 'as' + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop Interop facoltativo nullable diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 10029fab134..7977bede1f8 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -527,6 +527,11 @@ 'as' パターンの右側の非変数パターン + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop Null 許容のオプションの相互運用 diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 1d7a19af957..cfe29218dae 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -527,6 +527,11 @@ 'as' 패턴의 오른쪽에 있는 변수가 아닌 패턴 + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop nullable 선택적 interop diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 170e6e37718..e28c23a1c59 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -527,6 +527,11 @@ stałe wzorce po prawej stronie wzorców typu „as” + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop opcjonalna międzyoperacyjność dopuszczająca wartość null diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index 87ef255a7e8..67cf82ad478 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -527,6 +527,11 @@ padrões não-variáveis à direita dos padrões 'as'. + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop interoperabilidade opcional anulável diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 57f29e98517..3bd062a0e57 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -527,6 +527,11 @@ шаблоны без переменных справа от шаблонов "as" + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop необязательное взаимодействие, допускающее значение NULL diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 08753e47a27..cee8392e86f 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -527,6 +527,11 @@ 'as' desenlerinin sağındaki değişken olmayan desenler + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop null atanabilir isteğe bağlı birlikte çalışma diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index e117ab0ec5d..f14491cfce7 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -527,6 +527,11 @@ "as" 模式右侧的非变量模式 + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop 可以为 null 的可选互操作 diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 31c5596a098..dcc74bd974c 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -527,6 +527,11 @@ 'as' 模式右邊的非變數模式 + + honor the 'NotNullIfNotNull' attribute on a method's return value + honor the 'NotNullIfNotNull' attribute on a method's return value + + nullable optional interop 可為 Null 的選擇性 Interop diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 3fc031a525f..f2c74fefd3d 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -372,6 +372,7 @@ + diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs new file mode 100644 index 00000000000..d1bfbd437e2 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs @@ -0,0 +1,221 @@ +module Language.NotNullIfNotNull + +open FSharp.Test +open FSharp.Test.Compiler + + +let typeCheckWithStrictNullness cu = + cu + |> withCheckNulls + |> withLangVersionPreview + |> withWarnOn 3261 + |> withOptions ["--warnaserror+"] + |> typecheck + +let csNotNullLib = + CSharp """ +#nullable enable +using System.Diagnostics.CodeAnalysis; +namespace NotNullLib { + public class C { + [return: NotNullIfNotNull(nameof(input))] + public static string? Echo(string? input) => input; + + // The result is non-null when the SECOND parameter is non-null. + [return: NotNullIfNotNull(nameof(second))] + public static string? DependsOnSecond(string? first, string? second) => second; + } +}""" |> withName "csNotNullLib" + +let private nullableExpected = "was expected but this expression is nullable" + +[] +let ``BCL Path.GetExtension - non-null input yields non-null result`` () = + FSharp """module MyLibrary +open System.IO + +let nonNull : string = "file.txt" +let ext : string = Path.GetExtension(nonNull) +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``BCL Path.GetExtension - nullable input yields nullable result`` () = + FSharp """module MyLibrary +open System.IO + +let maybeNull : string | null = "file.txt" +let ext : string = Path.GetExtension(maybeNull) +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Multiple NotNullIfNotNull attributes are not supported - Delegate.Combine stays nullable`` () = + // Delegate.Combine carries two [return: NotNullIfNotNull] attributes. Making the result depend on more than one + // parameter (logical OR) is not supported, so the declared nullable return type is kept even though an argument + // is non-null. + FSharp """module MyLibrary +open System + +let d1 : Delegate = Action(fun () -> ()) :> Delegate +let dMaybe : Delegate | null = null + +let combined : Delegate = Delegate.Combine(d1, dMaybe) +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Csharp NotNullIfNotNull - non-null propagation works positionally`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let maybeNull : string | null = "y" + +// Single referenced parameter, passed positionally +let r1 : string = C.Echo(notNull) + +// Referenced parameter is the second one; nullable first, non-null second -> non-null. +// Arguments are positional (no named arguments), so this proves the parameter is identified by name. +let r2 : string = C.DependsOnSecond(maybeNull, notNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``Csharp NotNullIfNotNull - non-null propagation works with named arguments`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let maybeNull : string | null = "y" + +let r : string = C.DependsOnSecond(second = notNull, first = maybeNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``Csharp NotNullIfNotNull - Echo stays nullable for nullable input`` () = + FSharp """module MyLibrary +open NotNullLib + +let maybeNull : string | null = "y" +let r : string = C.Echo(maybeNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Csharp NotNullIfNotNull - depends on second parameter, not the first`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let maybeNull : string | null = "y" + +// Non-null first but nullable referenced (second) parameter -> result stays nullable +let r : string = C.DependsOnSecond(notNull, maybeNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Local F# method with NotNullIfNotNull - non-null propagation`` () = + FSharp """module MyLibrary +open System.Diagnostics.CodeAnalysis + +type C = + [] + static member Echo(x: string | null) : string | null = x + +let notNull : string = "a" +let ok : string = C.Echo(notNull) +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``Local F# method with NotNullIfNotNull - stays nullable for nullable input`` () = + FSharp """module MyLibrary +open System.Diagnostics.CodeAnalysis + +type C = + [] + static member Echo(x: string | null) : string | null = x + +let maybeNull : string | null = "a" +let bad : string = C.Echo(maybeNull) +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Referenced F# method with NotNullIfNotNull - non-null propagation`` () = + let fsharpLib = + FSharp """module NotNullFSharpLib +open System.Diagnostics.CodeAnalysis + +type C = + [] + static member Echo(x: string | null) : string | null = x +""" + |> withCheckNulls + |> withName "NotNullFSharpLib" + + FSharp """module MyLibrary +open NotNullFSharpLib + +let notNull : string = "a" +let ok : string = C.Echo(notNull) +""" + |> asLibrary + |> withReferences [fsharpLib] + |> typeCheckWithStrictNullness + |> shouldSucceed + +[] +let ``Referenced F# method with NotNullIfNotNull - stays nullable for nullable input`` () = + let fsharpLib = + FSharp """module NotNullFSharpLib +open System.Diagnostics.CodeAnalysis + +type C = + [] + static member Echo(x: string | null) : string | null = x +""" + |> withCheckNulls + |> withName "NotNullFSharpLib" + + FSharp """module MyLibrary +open NotNullFSharpLib + +let maybeNull : string | null = "a" +let bad : string = C.Echo(maybeNull) +""" + |> asLibrary + |> withReferences [fsharpLib] + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected From 8061f9620ca229c4f6afe6f2ac567059b2800998 Mon Sep 17 00:00:00 2001 From: kerams Date: Mon, 22 Jun 2026 00:12:11 +0200 Subject: [PATCH 2/6] Stuff --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 + docs/release-notes/.Language/preview.md | 1 + src/Compiler/DependencyManager/AssemblyResolveHandler.fs | 4 ++-- src/Compiler/Utilities/range.fs | 2 +- 4 files changed, 5 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 dd63aa988bd..8a54e13855f 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -96,6 +96,7 @@ * Debug: rework for expressions stepping ([PR #19894](https://github.com/dotnet/fsharp/pull/19894)) * Debug: rework conditional erasure, fix stepping over literals ([PR #19897](https://github.com/dotnet/fsharp/pull/19897)) * Debug: fix if and match condition sequence points ([PR #19932](https://github.com/dotnet/fsharp/pull/19932)) +* Support common types of `NotNullIfNotNullAttribute` usage. If a method parameter is marked with `NotNullIfNotNullAttribute`, the compiler will now honor this attribute and mark the return type as non-null. ([PR #19977](https://github.com/dotnet/fsharp/pull/19977)) ### Changed diff --git a/docs/release-notes/.Language/preview.md b/docs/release-notes/.Language/preview.md index 90c1aa2faa6..cb9d639c163 100644 --- a/docs/release-notes/.Language/preview.md +++ b/docs/release-notes/.Language/preview.md @@ -3,6 +3,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)) * Added `ErrorOnMissingSignatureAttribute` preview language feature: makes FS3888 (compiler-semantic attribute on the `.fs` but not on the `.fsi`) an error instead of a warning. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880)) +* Support common types of `NotNullIfNotNullAttribute` usage. If a method parameter is marked with `NotNullIfNotNullAttribute`, the compiler will now honor this attribute and mark the return type as non-null. ([PR #19977](https://github.com/dotnet/fsharp/pull/19977)) ### Fixed diff --git a/src/Compiler/DependencyManager/AssemblyResolveHandler.fs b/src/Compiler/DependencyManager/AssemblyResolveHandler.fs index 6daf749f87f..d59b65d835e 100644 --- a/src/Compiler/DependencyManager/AssemblyResolveHandler.fs +++ b/src/Compiler/DependencyManager/AssemblyResolveHandler.fs @@ -54,7 +54,7 @@ type AssemblyResolveHandlerCoreclr(assemblyProbingPaths: AssemblyResolutionProbe let assemblyPathOpt = assemblyPaths - |> Seq.tryFind (fun path -> Path.GetFileNameWithoutExtension(path) = simpleName) + |> Seq.tryFind (fun path -> String.Equals(Path.GetFileNameWithoutExtension(path), simpleName)) match assemblyPathOpt with | Some path -> loadAssembly path @@ -84,7 +84,7 @@ type AssemblyResolveHandlerDeskTop(assemblyProbingPaths: AssemblyResolutionProbe let assemblyPathOpt = assemblyPaths - |> Seq.tryFind (fun path -> Path.GetFileNameWithoutExtension(path) = simpleName) + |> Seq.tryFind (fun path -> String.Equals(Path.GetFileNameWithoutExtension(path), simpleName)) match assemblyPathOpt with | Some path -> Assembly.LoadFrom path diff --git a/src/Compiler/Utilities/range.fs b/src/Compiler/Utilities/range.fs index 3a22199c32f..2a05fa74c75 100755 --- a/src/Compiler/Utilities/range.fs +++ b/src/Compiler/Utilities/range.fs @@ -334,7 +334,7 @@ type Range(code1: int64, code2: int64) = member m.FileName = fileOfFileIndex m.FileIndex member internal m.ShortFileName = - Path.GetFileName(fileOfFileIndex m.FileIndex) |> nonNull + Path.GetFileName(fileOfFileIndex m.FileIndex) |> Unchecked.nonNull member m.ApplyLineDirectives() = match LineDirectives.store.TryFind m.FileIndex with From 3acded4fd04252b0b883fabf59d8f22c5af8a0c5 Mon Sep 17 00:00:00 2001 From: kerams Date: Mon, 22 Jun 2026 01:00:45 +0200 Subject: [PATCH 3/6] Stuff --- .../Language/Nullness/NotNullIfNotNullTests.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs index d1bfbd437e2..343f4e20363 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs @@ -18,11 +18,11 @@ let csNotNullLib = using System.Diagnostics.CodeAnalysis; namespace NotNullLib { public class C { - [return: NotNullIfNotNull(nameof(input))] + [return: NotNullIfNotNull("input")] public static string? Echo(string? input) => input; // The result is non-null when the SECOND parameter is non-null. - [return: NotNullIfNotNull(nameof(second))] + [return: NotNullIfNotNull("second")] public static string? DependsOnSecond(string? first, string? second) => second; } }""" |> withName "csNotNullLib" From 8c7aceab7ba171b6b0fcf55742346a8d35731c47 Mon Sep 17 00:00:00 2001 From: kerams Date: Mon, 22 Jun 2026 07:55:07 +0200 Subject: [PATCH 4/6] Stuff --- src/Compiler/Checking/NicePrint.fs | 2 +- .../Nullness/NotNullIfNotNullTests.fs | 27 ++++++++++++------- ...iler.Service.SurfaceArea.netstandard20.bsl | 1 + 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index de8daffa7c5..6bbf4e09644 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -1723,7 +1723,7 @@ module InfoMemberPrinting = let layout,paramLayouts = match denv.showCsharpCodeAnalysisAttributes, minfo with - | true, ILMeth(_g,mi,_e) -> + | true, (ILMeth(_, mi, _) | MethInfoWithModifiedReturnType(ILMeth(_, mi, _), _)) -> let methodLayout = // Render Method attributes and [return:..] attributes on separate lines above (@@) the method definition PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (minfo.GetCustomAttrs()) (squareAngleL >> (@@)) layout diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs index 343f4e20363..e2ec34a7b5a 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs @@ -3,13 +3,16 @@ module Language.NotNullIfNotNull open FSharp.Test open FSharp.Test.Compiler - -let typeCheckWithStrictNullness cu = +let withStrictNullness cu = cu - |> withCheckNulls |> withLangVersionPreview + |> withCheckNulls |> withWarnOn 3261 |> withOptions ["--warnaserror+"] + +let typeCheckWithStrictNullness cu = + cu + |> withStrictNullness |> typecheck let csNotNullLib = @@ -89,7 +92,8 @@ let r2 : string = C.DependsOnSecond(maybeNull, notNull) """ |> asLibrary |> withReferences [csNotNullLib] - |> typeCheckWithStrictNullness + |> withStrictNullness + |> compile |> shouldSucceed [] @@ -104,7 +108,8 @@ let r : string = C.DependsOnSecond(second = notNull, first = maybeNull) """ |> asLibrary |> withReferences [csNotNullLib] - |> typeCheckWithStrictNullness + |> withStrictNullness + |> compile |> shouldSucceed [] @@ -117,7 +122,8 @@ let r : string = C.Echo(maybeNull) """ |> asLibrary |> withReferences [csNotNullLib] - |> typeCheckWithStrictNullness + |> withStrictNullness + |> compile |> shouldFail |> withDiagnosticMessageMatches nullableExpected @@ -134,7 +140,8 @@ let r : string = C.DependsOnSecond(notNull, maybeNull) """ |> asLibrary |> withReferences [csNotNullLib] - |> typeCheckWithStrictNullness + |> withStrictNullness + |> compile |> shouldFail |> withDiagnosticMessageMatches nullableExpected @@ -192,7 +199,8 @@ let ok : string = C.Echo(notNull) """ |> asLibrary |> withReferences [fsharpLib] - |> typeCheckWithStrictNullness + |> withStrictNullness + |> compile |> shouldSucceed [] @@ -216,6 +224,7 @@ let bad : string = C.Echo(maybeNull) """ |> asLibrary |> withReferences [fsharpLib] - |> typeCheckWithStrictNullness + |> withStrictNullness + |> compile |> shouldFail |> withDiagnosticMessageMatches nullableExpected diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 4034ba8064d..8067be778fc 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -1833,6 +1833,7 @@ FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes IsUnm FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes NoEagerConstraintApplicationAttribute FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes None FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes NotComputed +FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes NotNullIfNotNullAttribute FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes NullableAttribute FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes NullableContextAttribute FSharp.Compiler.AbstractIL.IL+WellKnownILAttributes: WellKnownILAttributes ObsoleteAttribute From 6daa50ae57688ab771d572bcda7b967323a372e3 Mon Sep 17 00:00:00 2001 From: kerams Date: Mon, 22 Jun 2026 09:15:59 +0200 Subject: [PATCH 5/6] Correctness, refactor --- .../Checking/Expressions/CheckExpressions.fs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index ed9ebc3152f..675d29a1e27 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -3356,14 +3356,19 @@ let NotNullIfNotNullParamNames g (minfo: MethInfo) = | _ -> [] | _ -> [] -// Resolve the caller argument bound to 'paramName' (named arguments first, then by position) and return the type of its type-checked expression. +// Resolve the caller argument bound to 'paramName' and return the type of its type-checked expression. let TryGetCallerArgType g (minfo: MethInfo) (callerArgs: CallerArgs<_>) paramName = - match callerArgs.Named |> List.tryPick (List.tryPick (fun (CallerNamedArg(id, arg)) -> if id.idText = paramName then Some (tyOfExpr g arg.Expr) else None)) with - | None -> - match minfo.GetParamNames() |> List.tryPick (List.tryFindIndex (fun nm -> match nm with Some nm -> nm = paramName | _ -> false)) with - | Some idx -> callerArgs.Unnamed |> List.tryPick (List.tryItem idx) |> Option.map (fun arg -> tyOfExpr g arg.Expr) - | None -> None - | x -> x + // First try to find a named argument with the given name + callerArgs.Named + |> List.tryPick (List.tryPick (fun (CallerNamedArg(id, arg)) -> if id.idText = paramName then Some arg else None)) + |> Option.orElseWith (fun () -> + // If there is no matching named argument, find the argument in the same position as the parameter with the given name + minfo.GetParamNames() + |> Seq.concat + |> Seq.tryFindIndex (fun nm -> match nm with Some nm -> nm = paramName | _ -> false) + |> Option.bind (fun idx -> Seq.concat callerArgs.Unnamed |> Seq.tryItem idx) + ) + |> Option.map (fun arg -> tyOfExpr g arg.Expr) //------------------------------------------------------------------------- // Helpers dealing with sequence expressions From b65a49bcf7dc84859d6a30889519eaca6e2c3894 Mon Sep 17 00:00:00 2001 From: kerams Date: Mon, 22 Jun 2026 14:38:20 +0200 Subject: [PATCH 6/6] Fix, add tests --- .../Checking/Expressions/CheckExpressions.fs | 6 ++- .../Nullness/NotNullIfNotNullTests.fs | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 675d29a1e27..a0c84439716 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -10570,7 +10570,11 @@ and TcMethodApplication match TryGetCallerArgType g baseMinfo callerArgs paramName with | Some callerArgTy -> let retTy = baseMinfo.GetFSharpReturnType(cenv.amap, mMethExpr, callerTyArgs) - MethInfoWithModifiedReturnType(baseMinfo, replaceNullnessOfTy (nullnessOfTy g callerArgTy) retTy) + let argNullness = + match GetTyparTyIfSupportsNull g callerArgTy with + | ValueSome _ -> g.knownWithNull + | ValueNone -> nullnessOfTy g callerArgTy + MethInfoWithModifiedReturnType(baseMinfo, replaceNullnessOfTy argNullness retTy) | None -> baseMinfo | _ -> baseMinfo else diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs index e2ec34a7b5a..36764471f1a 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs @@ -145,6 +145,21 @@ let r : string = C.DependsOnSecond(notNull, maybeNull) |> shouldFail |> withDiagnosticMessageMatches nullableExpected +[] +let ``Csharp NotNullIfNotNull - unannotated parameter with non-null return annotation fails`` () = + FSharp """module MyLibrary +open NotNullLib + +let f x : string = C.Echo(x) +let _ : string = f null +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches "Nullness warning: The type 'string' does not support 'null'." + [] let ``Local F# method with NotNullIfNotNull - non-null propagation`` () = FSharp """module MyLibrary @@ -228,3 +243,39 @@ let bad : string = C.Echo(maybeNull) |> compile |> shouldFail |> withDiagnosticMessageMatches nullableExpected + +[] +let ``BCL Path.GetExtension - null literal input yields nullable result`` () = + FSharp """module MyLibrary +open System.IO + +let ext : string = Path.GetExtension(null) +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``BCL Path.GetExtension - null-bound variable input yields nullable result`` () = + FSharp """module MyLibrary +open System.IO + +let maybeNull = null +let ext : string = Path.GetExtension(maybeNull) +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``BCL Path.GetExtension - explicit non-null parameter annotation yields non-null result`` () = + FSharp """module MyLibrary +open System.IO + +let f (x: string) : string = Path.GetExtension x +""" + |> asLibrary + |> typeCheckWithStrictNullness + |> shouldSucceed \ No newline at end of file