Skip to content

Commit 0c94e4e

Browse files
authored
Support NotNullIfNotNullAttribute (#19977)
1 parent fd6ed49 commit 0c94e4e

31 files changed

Lines changed: 711 additions & 5 deletions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
* Debug: rework for expressions stepping ([PR #19894](https://github.com/dotnet/fsharp/pull/19894))
143143
* Debug: rework conditional erasure, fix stepping over literals ([PR #19897](https://github.com/dotnet/fsharp/pull/19897))
144144
* Debug: fix if and match condition sequence points ([PR #19932](https://github.com/dotnet/fsharp/pull/19932))
145+
* 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))
145146
* Checker: recover on checking language version ([PR ##19970](https://github.com/dotnet/fsharp/pull/19970))
146147
* Implied argument names for function-to-delegate coercions now fall back to the delegate's `Invoke` parameter names when the function has no recoverable names (e.g. a partial application like `System.Func<int, int>((+) 1)`), instead of synthetic `delegateArg0`, `delegateArg1`, … names. ([PR #20001](https://github.com/dotnet/fsharp/pull/20001))
147148
* Add internal `ResetCompilerGeneratedNameState` to `CompilerGlobalState` name generators so warm-checker re-compilation can produce fresh-process-identical generated names. ([PR #20017](https://github.com/dotnet/fsharp/pull/20017))

‎docs/release-notes/.Language/preview.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* 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))
44
* 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))
55
* 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))
6+
* 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))
67
* Added `AccessProtectedBaseFieldFromClosure` preview language feature: a derived member can now read a `protected` base-class field from an ordinary closure (lambda, delegate, `async`/`seq`/`lazy`, `function`, or list/array literal), which previously failed with FS1097 even though direct access compiles. Object expressions remain unsupported — bind the field to a local function or expose it through a member. ([Issue #5302](https://github.com/dotnet/fsharp/issues/5302))
78
* Added `ImprovedImpliedArgumentNamesPartTwo` language feature: when a function with no recoverable parameter names is coerced to a delegate (e.g. a partial application like `System.Func<int, int>((+) 1)`), the synthesized `Invoke` parameters take their names from the delegate's own `Invoke` signature instead of synthetic `delegateArg0`, `delegateArg1`, … names. ([PR #20001](https://github.com/dotnet/fsharp/pull/20001))
89

‎src/Compiler/AbstractIL/il.fs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,7 @@ type WellKnownILAttributes =
12581258
| RequiredMemberAttribute = (1u <<< 22)
12591259
| NullableContextAttribute = (1u <<< 23)
12601260
| AttributeUsageAttribute = (1u <<< 24)
1261+
| NotNullIfNotNullAttribute = (1u <<< 25)
12611262
| NotComputed = (1u <<< 31)
12621263

12631264
type internal ILAttributesStoredRepr =

‎src/Compiler/AbstractIL/il.fsi‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,7 @@ type WellKnownILAttributes =
912912
| RequiredMemberAttribute = (1u <<< 22)
913913
| NullableContextAttribute = (1u <<< 23)
914914
| AttributeUsageAttribute = (1u <<< 24)
915+
| NotNullIfNotNullAttribute = (1u <<< 25)
915916
| NotComputed = (1u <<< 31)
916917

917918
/// Represents the efficiency-oriented storage of ILAttributes in another item.

‎src/Compiler/Checking/Expressions/CheckExpressions.fs‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3359,6 +3359,46 @@ let GetMethodArgs arg =
33593359

33603360
unnamedCallerArgs, namedCallerArgs
33613361

3362+
let NotNullIfNotNullParamNames g (minfo: MethInfo) =
3363+
match minfo with
3364+
| ILMeth(ilMethInfo = ilminfo) when ilminfo.RawMetadata.Return.CustomAttrsStored.HasWellKnownAttribute (g, WellKnownILAttributes.NotNullIfNotNullAttribute) ->
3365+
ilminfo.RawMetadata.Return.CustomAttrs.AsArray()
3366+
|> Array.toList
3367+
|> List.choose (fun attr ->
3368+
if classifyILAttrib attr &&& WellKnownILAttributes.NotNullIfNotNullAttribute <> WellKnownILAttributes.None then
3369+
match decodeILAttribData attr with
3370+
| [ ILAttribElem.String (Some paramName) ], _ -> Some paramName
3371+
| _ -> None
3372+
else
3373+
None)
3374+
| FSMeth(valRef = vref) ->
3375+
match vref.ValReprInfo with
3376+
| Some (ValReprInfo(result = retInfo)) when ArgReprInfoHasWellKnownAttribute g WellKnownValAttributes.NotNullIfNotNullAttribute retInfo ->
3377+
retInfo.Attribs.AsList()
3378+
|> List.choose (fun attrib ->
3379+
if classifyValAttrib g attrib &&& WellKnownValAttributes.NotNullIfNotNullAttribute <> WellKnownValAttributes.None then
3380+
match attrib with
3381+
| Attrib(unnamedArgs = [ AttribStringArg paramName ]) -> Some paramName
3382+
| _ -> None
3383+
else
3384+
None)
3385+
| _ -> []
3386+
| _ -> []
3387+
3388+
// Resolve the caller argument bound to 'paramName' and return the type of its type-checked expression.
3389+
let TryGetCallerArgType g (minfo: MethInfo) (callerArgs: CallerArgs<_>) paramName =
3390+
// First try to find a named argument with the given name
3391+
callerArgs.Named
3392+
|> List.tryPick (List.tryPick (fun (CallerNamedArg(id, arg)) -> if id.idText = paramName then Some arg else None))
3393+
|> Option.orElseWith (fun () ->
3394+
// If there is no matching named argument, find the argument in the same position as the parameter with the given name
3395+
minfo.GetParamNames()
3396+
|> Seq.concat
3397+
|> Seq.tryFindIndex (fun nm -> match nm with Some nm -> nm = paramName | _ -> false)
3398+
|> Option.bind (fun idx -> Seq.concat callerArgs.Unnamed |> Seq.tryItem idx)
3399+
)
3400+
|> Option.map (fun arg -> tyOfExpr g arg.Expr)
3401+
33623402
//-------------------------------------------------------------------------
33633403
// Helpers dealing with sequence expressions
33643404
//-------------------------------------------------------------------------
@@ -10307,12 +10347,26 @@ and TcMethodApplication_UniqueOverloadInference
1030710347

1030810348
let arityFilteredCandidates = candidateMethsAndProps
1030910349

10310-
let makeOneCalledMeth (minfo, pinfoOpt, usesParamArrayConversion) =
10350+
let makeOneCalledMeth (minfo: MethInfo, pinfoOpt, usesParamArrayConversion) =
1031110351
let minst = FreshenMethInfo mItem minfo
1031210352
let callerTyArgs =
1031310353
match tyArgsOpt with
1031410354
| Some tyargs -> minfo.AdjustUserTypeInstForFSharpStyleIndexedExtensionMembers tyargs
1031510355
| None -> minst
10356+
10357+
// If the return value is [<NotNullIfNotNull>], give the return a fresh nullness inference variable here so that
10358+
// unique-overload inference does not prematurely commit the result to the declared (nullable) nullness. The real
10359+
// nullness is resolved post argument type-checking (see below), once the argument types are known.
10360+
let minfo =
10361+
if not minfo.IsConstructor && g.checkNullness && g.langVersion.SupportsFeature LanguageFeature.NotNullIfNotNull then
10362+
match NotNullIfNotNullParamNames g minfo with
10363+
| [ _ ] ->
10364+
let retTy = minfo.GetFSharpReturnType(cenv.amap, mMethExpr, callerTyArgs)
10365+
MethInfoWithModifiedReturnType(minfo, replaceNullnessOfTy (NewNullnessVar()) retTy)
10366+
| _ -> minfo
10367+
else
10368+
minfo
10369+
1031610370
CalledMeth<SynExpr>(cenv.infoReader, Some(env.NameEnv), isCheckingAttributeCall, FreshenMethInfo, mMethExpr, ad, minfo, minst, callerTyArgs, pinfoOpt, callerObjArgTys, callerArgs, usesParamArrayConversion, true, objTyOpt, staticTyOpt)
1031710371

1031810372
let preArgumentTypeCheckingCalledMethGroup =
@@ -10570,6 +10624,29 @@ and TcMethodApplication
1057010624
match tyArgsOpt with
1057110625
| Some tyargs -> minfo.AdjustUserTypeInstForFSharpStyleIndexedExtensionMembers tyargs
1057210626
| None -> minst
10627+
10628+
let minfo =
10629+
if not minfo.IsConstructor && g.checkNullness && g.langVersion.SupportsFeature LanguageFeature.NotNullIfNotNull then
10630+
// 'minfo' may already carry a placeholder return nullness from unique-overload inference (phase 1);
10631+
// strip it back to the base method before applying the real (argument-derived) nullness.
10632+
let baseMinfo = match minfo with MethInfoWithModifiedReturnType(inner, _) -> inner | _ -> minfo
10633+
match NotNullIfNotNullParamNames g baseMinfo with
10634+
| [ paramName ] ->
10635+
match TryGetCallerArgType g baseMinfo callerArgs paramName with
10636+
| Some callerArgTy ->
10637+
let callerArgTy = if isByrefTy g callerArgTy then destByrefTy g callerArgTy else callerArgTy
10638+
let retTy = baseMinfo.GetFSharpReturnType(cenv.amap, mMethExpr, callerTyArgs)
10639+
let argNullness =
10640+
if TypeNullIsTrueValue g callerArgTy || TypeNullIsExtraValueNew g mMethExpr callerArgTy then
10641+
g.knownWithNull
10642+
else
10643+
nullnessOfTy g callerArgTy
10644+
MethInfoWithModifiedReturnType(baseMinfo, replaceNullnessOfTy argNullness retTy)
10645+
| None -> baseMinfo
10646+
| _ -> baseMinfo
10647+
else
10648+
minfo
10649+
1057310650
CalledMeth<Expr>(cenv.infoReader, Some(env.NameEnv), isCheckingAttributeCall, FreshenMethInfo, mMethExpr, ad, minfo, minst, callerTyArgs, pinfoOpt, callerObjArgTys, callerArgs, usesParamArrayConversion, true, objTyOpt, staticTyOpt))
1057410651

1057510652
// Commit unassociated constraints prior to member overload resolution where there is ambiguity

‎src/Compiler/Checking/MethodCalls.fs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,14 @@ let rec BuildMethodCall tcVal g amap isMutable m isProp minfo valUseFlags minst
12501250
let expr = mkCoerceExpr (expr, retTy, m, exprTy)
12511251
expr, retTy
12521252

1253+
| MethInfoWithModifiedReturnType((FSMeth(_, _, vref, _) as innerMeth), retTy) ->
1254+
// Build the inner call directly, without re-invoking TakeObjAddrForMethodCall.
1255+
let vExpr, vExprTy = tcVal vref valUseFlags (innerMeth.DeclaringTypeInst @ minst) m
1256+
let expr, exprTy = BuildFSharpMethodApp g m vref vExpr vExprTy allArgs
1257+
1258+
let expr = mkCoerceExpr (expr, retTy, m, exprTy)
1259+
expr, retTy
1260+
12531261
| MethInfoWithModifiedReturnType _ ->
12541262
failwith "MethInfoWithModifiedReturnType: unexpected inner method kind"
12551263

‎src/Compiler/Checking/NicePrint.fs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1742,7 +1742,7 @@ module InfoMemberPrinting =
17421742

17431743
let layout,paramLayouts =
17441744
match denv.showCsharpCodeAnalysisAttributes, minfo with
1745-
| true, ILMeth(_g,mi,_e) ->
1745+
| true, (ILMeth(_, mi, _) | MethInfoWithModifiedReturnType(ILMeth(_, mi, _), _)) ->
17461746
let methodLayout =
17471747
// Render Method attributes and [return:..] attributes on separate lines above (@@) the method definition
17481748
PrintTypes.layoutCsharpCodeAnalysisIlAttributes denv (minfo.GetCustomAttrs()) (squareAngleL >> (@@)) layout

‎src/Compiler/DependencyManager/AssemblyResolveHandler.fs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type AssemblyResolveHandlerCoreclr(assemblyProbingPaths: AssemblyResolutionProbe
5454

5555
let assemblyPathOpt =
5656
assemblyPaths
57-
|> Seq.tryFind (fun path -> Path.GetFileNameWithoutExtension(path) = simpleName)
57+
|> Seq.tryFind (fun path -> String.Equals(Path.GetFileNameWithoutExtension(path), simpleName))
5858

5959
match assemblyPathOpt with
6060
| Some path -> loadAssembly path
@@ -84,7 +84,7 @@ type AssemblyResolveHandlerDeskTop(assemblyProbingPaths: AssemblyResolutionProbe
8484

8585
let assemblyPathOpt =
8686
assemblyPaths
87-
|> Seq.tryFind (fun path -> Path.GetFileNameWithoutExtension(path) = simpleName)
87+
|> Seq.tryFind (fun path -> String.Equals(Path.GetFileNameWithoutExtension(path), simpleName))
8888

8989
match assemblyPathOpt with
9090
| Some path -> Assembly.LoadFrom path

‎src/Compiler/FSComp.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,5 +1825,6 @@ featurePreprocessorElif,"#elif preprocessor directive"
18251825
3891,tcGenericAttributesNotSupported,"Generic attribute types are not supported in F#. The type '%s' has type parameters and cannot be used as an attribute."
18261826
featureExceptionFieldSerializationSupport,"emit GetObjectData and field-restoring deserialization constructor for exception types"
18271827
featureErrorOnMissingSignatureAttribute,"error (rather than warning) when an enforced compiler-semantic attribute is present in the .fs but missing from the .fsi"
1828+
featureNotNullIfNotNull,"honor the 'NotNullIfNotNull' attribute on a method's return value"
18281829
featureAccessProtectedBaseFieldFromClosure,"Access a protected base-class field from a closure inside a member"
18291830
featureImprovedImpliedArgumentNamesPartTwo,"Improved implied argument names with partial application"

‎src/Compiler/Facilities/LanguageFeatures.fs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type LanguageFeature =
110110
| PreprocessorElif
111111
| ExceptionFieldSerializationSupport
112112
| ErrorOnMissingSignatureAttribute
113+
| NotNullIfNotNull
113114
| AccessProtectedBaseFieldFromClosure
114115
| ImprovedImpliedArgumentNamesPartTwo
115116

@@ -256,6 +257,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)
256257
LanguageFeature.WarnWhenFunctionValueUsedAsInterpolatedStringArg, languageVersion110
257258
LanguageFeature.PreprocessorElif, languageVersion110
258259
LanguageFeature.ExceptionFieldSerializationSupport, languageVersion110
260+
LanguageFeature.NotNullIfNotNull, languageVersion110
259261
LanguageFeature.ImprovedImpliedArgumentNamesPartTwo, languageVersion110
260262

261263
// Difference between languageVersion110 and preview - 11.0 gets turned on automatically by picking a preview .NET 11 SDK
@@ -463,6 +465,7 @@ type LanguageVersion(versionText, ?disabledFeaturesArray: LanguageFeature array)
463465
| LanguageFeature.PreprocessorElif -> FSComp.SR.featurePreprocessorElif ()
464466
| LanguageFeature.ExceptionFieldSerializationSupport -> FSComp.SR.featureExceptionFieldSerializationSupport ()
465467
| LanguageFeature.ErrorOnMissingSignatureAttribute -> FSComp.SR.featureErrorOnMissingSignatureAttribute ()
468+
| LanguageFeature.NotNullIfNotNull -> FSComp.SR.featureNotNullIfNotNull ()
466469
| LanguageFeature.AccessProtectedBaseFieldFromClosure -> FSComp.SR.featureAccessProtectedBaseFieldFromClosure ()
467470
| LanguageFeature.ImprovedImpliedArgumentNamesPartTwo -> FSComp.SR.featureImprovedImpliedArgumentNamesPartTwo ()
468471

0 commit comments

Comments
 (0)