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 54f5a122d6d..4d9e529d78b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -103,6 +103,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)) * Checker: recover on checking language version ([PR ##19970](https://github.com/dotnet/fsharp/pull/19970)) ### Improved 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/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 95bb99d33e3..6058010f268 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 af41d86a6d2..bd71aec2d5f 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 e295bb7a6eb..7bc5a57b576 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -3331,6 +3331,46 @@ 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' and return the type of its type-checked expression. +let TryGetCallerArgType g (minfo: MethInfo) (callerArgs: CallerArgs<_>) paramName = + // 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 //------------------------------------------------------------------------- @@ -10270,12 +10310,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 = @@ -10533,6 +10587,28 @@ 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) + let argNullness = + if TypeNullIsTrueValue g callerArgTy || TypeNullIsExtraValueNew g mMethExpr callerArgTy then + g.knownWithNull + else + nullnessOfTy g callerArgTy + MethInfoWithModifiedReturnType(baseMinfo, replaceNullnessOfTy argNullness 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 bfe806990db..235178c5c9c 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/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index d1097a7c416..ca5e0de3282 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/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/FSComp.txt b/src/Compiler/FSComp.txt index d29b5d3dde7..3de67465b9f 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1822,3 +1822,4 @@ featurePreprocessorElif,"#elif preprocessor directive" 3889,tastNamespaceAndTypeWithSameNameInAssembly,"The namespace '%s' clashes with the type '%s'." 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 dd2b7cebe14..8eb82ec2639 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/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 diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 3ea52c5b169..f5e640e333a 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 f9fc62909ef..bc9c845156c 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 eca7c688eed..f05e6331b5f 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 0922cee5acd..4b788c13f9d 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 d39f0a33a85..4311c49506a 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 6f4969e8148..789d2988c11 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 fb488a6c240..798392551c2 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 2720c59fc22..1fd7a4031d6 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 8fc5566545f..a6be61c9baa 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 16eb9766b81..b5462f0fb5c 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 da5fd475a81..5f305968333 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 c43aa519603..8d88f3d149f 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 d287b294184..e4c15e85736 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 2f96624917e..075a5b58eb0 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -374,6 +374,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..553673a90fd --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NotNullIfNotNullTests.fs @@ -0,0 +1,503 @@ +module Language.NotNullIfNotNull + +open FSharp.Test +open FSharp.Test.Compiler + +let withStrictNullness cu = + cu + |> withLangVersionPreview + |> withCheckNulls + |> withWarnOn 3261 + |> withOptions ["--warnaserror+"] + +let typeCheckWithStrictNullness cu = + cu + |> withStrictNullness + |> typecheck + +let csNotNullLib = + CSharp """ +#nullable enable +using System.Diagnostics.CodeAnalysis; +namespace NotNullLib { + public class C { + [return: NotNullIfNotNull("input")] + public static string? Echo(string? input) => input; + + // The result is non-null when the SECOND parameter is non-null. + [return: NotNullIfNotNull("second")] + public static string? DependsOnSecond(string? first, string? second) => second; + + // Generic echo: 'T' is inferred to the F# argument type with no coercion, so the + // argument's own nullness (including runtime representations like option/unit) is preserved. + [return: NotNullIfNotNull("input")] + public static T EchoGeneric(T input) => input; + + // Object echo: the argument is coerced to 'object', but a 'with null' nullness rides along. + [return: NotNullIfNotNull("input")] + public static object? EchoObj(object? input) => input; + } + + public static class Extensions { + // Degenerate case: the return depends on the 'this' parameter of a C#-style extension method. + // When called instance-style the receiver is an object argument, not an unnamed caller argument. + [return: NotNullIfNotNull("self")] + public static string? PreferSelf(this string? self, string? other) => self ?? other; + } + + public static class Variadic { + // The result depends on an optional parameter ('b') that is not in the first position. + [return: NotNullIfNotNull("b")] + public static string? PickB(string? a = null, string? b = null) => b ?? a; + + // The result depends on the first parameter, which precedes a params array. + [return: NotNullIfNotNull("first")] + public static string? JoinRest(string? first, params string?[] rest) => first; + } +}""" |> 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. We cannot currently represent nullness linking + // to multiple types (logical OR), 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] + |> withStrictNullness + |> compile + |> 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] + |> withStrictNullness + |> compile + |> 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] + |> withStrictNullness + |> compile + |> 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] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Csharp NotNullIfNotNull - extension this-parameter must be identified, not the explicit argument`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let maybeNull : string | null = "y" + +// Result depends on 'self' (the receiver), which is nullable -> result must stay nullable and warn. +let r : string = maybeNull.PreferSelf(notNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Csharp NotNullIfNotNull - optional parameter referenced positionally`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let maybeNull : string | null = "y" + +// 'b' is the referenced (second, optional) parameter, passed positionally and non-null -> result non-null. +let r : string = Variadic.PickB(maybeNull, notNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldSucceed + +[] +let ``Csharp NotNullIfNotNull - optional parameter referenced by name`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" + +// Only the referenced optional parameter is supplied, by name and non-null -> result non-null. +let r : string = Variadic.PickB(b = notNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldSucceed + +[] +let ``Csharp NotNullIfNotNull - optional parameter omitted stays nullable`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" + +// The referenced optional parameter 'b' is omitted (defaults to null) -> result stays nullable. +let r : string = Variadic.PickB(notNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Csharp NotNullIfNotNull - parameter before params array, non-null propagation`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let maybeNull : string | null = "y" + +// Referenced parameter 'first' precedes the params array; non-null first -> result non-null. +let r : string = Variadic.JoinRest(notNull, maybeNull, maybeNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldSucceed + +[] +let ``Csharp NotNullIfNotNull - parameter before params array, stays nullable`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let maybeNull : string | null = "y" + +// Referenced parameter 'first' is nullable -> result stays nullable regardless of params args. +let r : string = Variadic.JoinRest(maybeNull, notNull, notNull) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +// F# <-> runtime interop: a value can be 'null' at runtime even when its F# type is statically non-null. +// 'option' (None) is represented as null via UseNullAsTrueValue, so EchoGeneric of a None must keep the +// result nullable. This case is the one that exercises the TypeNullIsTrueValue branch of the derivation. +[] +let ``Csharp NotNullIfNotNull - generic echo of None stays nullable`` () = + FSharp """module MyLibrary +open NotNullLib + +let none : int option = None +let r : int option = C.EchoGeneric none +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +// unit is also represented as null at runtime, so EchoGeneric of '()' must keep the result nullable. +// Like None, this travels the same-tycon nullness subsumption path (unit-with-null vs unit-without-null). +[] +let ``Csharp NotNullIfNotNull - generic echo of unit stays nullable`` () = + FSharp """module MyLibrary +open NotNullLib + +let r : unit = C.EchoGeneric (()) +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +// Control: a genuinely non-null reference value yields a non-null result through the generic echo. +[] +let ``Csharp NotNullIfNotNull - generic echo of non-null reference is non-null`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "x" +let r : string = C.EchoGeneric notNull +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldSucceed + +// A 'T | null typar argument coming from a generic function keeps the result nullable. +[] +let ``Csharp NotNullIfNotNull - generic echo of nullable typar stays nullable`` () = + FSharp """module MyLibrary +open NotNullLib + +let wrap (x: 'T | null) : 'T = C.EchoGeneric x +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +// The object-accepting echo coerces the argument to 'object', but a 'with null' nullness rides along. +[] +let ``Csharp NotNullIfNotNull - object echo of nullable reference stays nullable`` () = + FSharp """module MyLibrary +open NotNullLib + +let maybeNull : string | null = "y" +let r : obj = C.EchoObj maybeNull +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldFail + |> withDiagnosticMessageMatches nullableExpected + +[] +let ``Csharp NotNullIfNotNull - object echo of non-null reference is non-null`` () = + FSharp """module MyLibrary +open NotNullLib + +let notNull : string = "y" +let r : obj = C.EchoObj notNull +""" + |> asLibrary + |> withReferences [csNotNullLib] + |> withStrictNullness + |> compile + |> shouldSucceed + +[] +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 +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] + |> withStrictNullness + |> compile + |> 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] + |> withStrictNullness + |> 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 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