Skip to content

Commit 72b7494

Browse files
authored
Nullness feature :: New warning for functions insisting on a (WithNull) argument + typar equality fix (#16853)
Null-handling functions now have the option to raise a warning when they are called with a known-to-be-withoutNull argument. That way, API authors (incl. Fsharp.Core) can help with cleaning code from superflous null checks. While doing it, a strange error kept coming when using Option.ofObj and other functions with a (T | null) typar. It turned out that nullness info had been striped from nullable typars on stripTyparEqnsAux calls in some occasions, leading to treating all typars as 'KnownWithoutNull' even when that was not true.
1 parent 14efaa4 commit 72b7494

33 files changed

+657
-24
lines changed

src/Compiler/Checking/CheckExpressions.fs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5163,10 +5163,14 @@ and ConvSynPatToSynExpr synPat =
51635163
/// Check a long identifier 'Case' or 'Case argsR' that has been resolved to an active pattern case
51645164
and TcPatLongIdentActivePatternCase warnOnUpper (cenv: cenv) (env: TcEnv) vFlags patEnv ty (mLongId, item, apref, args, m) =
51655165
let g = cenv.g
5166-
51675166
let (TcPatLinearEnv(tpenv, names, takenNames)) = patEnv
51685167
let (APElemRef (apinfo, vref, idx, isStructRetTy)) = apref
51695168

5169+
let cenv =
5170+
match g.checkNullness,TryFindLocalizedFSharpStringAttribute g g.attrib_WarnOnWithoutNullArgumentAttribute vref.Attribs with
5171+
| true, (Some _ as warnMsg) -> {cenv with css.WarnWhenUsingWithoutNullOnAWithNullTarget = warnMsg}
5172+
| _ -> cenv
5173+
51705174
// Report information about the 'active recognizer' occurrence to IDE
51715175
CallNameResolutionSink cenv.tcSink (mLongId, env.NameEnv, item, emptyTyparInst, ItemOccurence.Pattern, env.eAccessRights)
51725176

@@ -8428,22 +8432,27 @@ and TcApplicationThen (cenv: cenv) (overallTy: OverallTy) env tpenv mExprAndArg
84288432
SynExpr.ComputationExpr (true, comp, m)
84298433
| _ -> synArg
84308434

8431-
let arg, tpenv =
8435+
let (arg, tpenv), cenv =
84328436
// treat left and right of '||' and '&&' as control flow, so for example
84338437
// f expr1 && g expr2
84348438
// will have debug points on "f expr1" and "g expr2"
8435-
let env =
8439+
let env,cenv =
84368440
match leftExpr with
84378441
| ApplicableExpr(expr=Expr.Val (vref, _, _))
84388442
| ApplicableExpr(expr=Expr.App (Expr.Val (vref, _, _), _, _, [_], _))
84398443
when valRefEq g vref g.and_vref
84408444
|| valRefEq g vref g.and2_vref
84418445
|| valRefEq g vref g.or_vref
84428446
|| valRefEq g vref g.or2_vref ->
8443-
{ env with eIsControlFlow = true }
8444-
| _ -> env
8445-
8446-
TcExprFlex2 cenv domainTy env false tpenv synArg
8447+
{ env with eIsControlFlow = true },cenv
8448+
| ApplicableExpr(expr=Expr.Val (valRef=vref))
8449+
| ApplicableExpr(expr=Expr.App (funcExpr=Expr.Val (valRef=vref))) ->
8450+
match TryFindLocalizedFSharpStringAttribute g g.attrib_WarnOnWithoutNullArgumentAttribute vref.Attribs with
8451+
| Some _ as msg -> env,{ cenv with css.WarnWhenUsingWithoutNullOnAWithNullTarget = msg}
8452+
| None -> env,cenv
8453+
| _ -> env,cenv
8454+
8455+
TcExprFlex2 cenv domainTy env false tpenv synArg, cenv
84478456

84488457
let exprAndArg, resultTy = buildApp cenv leftExpr resultTy arg mExprAndArg
84498458
TcDelayed cenv overallTy env tpenv mExprAndArg exprAndArg resultTy atomicFlag delayed

src/Compiler/Checking/ConstraintSolver.fs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ type ConstraintSolverState =
268268
/// Checks to run after all inference is complete.
269269
PostInferenceChecksFinal: ResizeArray<unit -> unit>
270270

271+
WarnWhenUsingWithoutNullOnAWithNullTarget: string option
271272
}
272273

273274
static member New(g, amap, infoReader, tcVal) =
@@ -277,7 +278,8 @@ type ConstraintSolverState =
277278
InfoReader = infoReader
278279
TcVal = tcVal
279280
PostInferenceChecksPreDefaults = ResizeArray()
280-
PostInferenceChecksFinal = ResizeArray() }
281+
PostInferenceChecksFinal = ResizeArray()
282+
WarnWhenUsingWithoutNullOnAWithNullTarget = None }
281283

282284
member this.PushPostInferenceCheck (preDefaults, check) =
283285
if preDefaults then
@@ -1041,6 +1043,9 @@ and SolveNullnessEquiv (csenv: ConstraintSolverEnv) m2 (trace: OptionalTrace) ty
10411043
| _, NullnessInfo.AmbivalentToNull -> CompleteD
10421044
| NullnessInfo.WithNull, NullnessInfo.WithNull -> CompleteD
10431045
| NullnessInfo.WithoutNull, NullnessInfo.WithoutNull -> CompleteD
1046+
// Warn for 'strict "must pass null"` APIs like Option.ofObj
1047+
| NullnessInfo.WithNull, NullnessInfo.WithoutNull when csenv.g.checkNullness && csenv.SolverState.WarnWhenUsingWithoutNullOnAWithNullTarget.IsSome ->
1048+
WarnD(Error(FSComp.SR.tcPassingWithoutNullToANullableExpectingFunc (csenv.SolverState.WarnWhenUsingWithoutNullOnAWithNullTarget.Value),m2))
10441049
// Allow expected of WithNull and actual of WithoutNull
10451050
// TODO NULLNESS: this is not sound in contravariant cases etc. It is assuming covariance.
10461051
| NullnessInfo.WithNull, NullnessInfo.WithoutNull -> CompleteD
@@ -1076,8 +1081,12 @@ and SolveNullnessSubsumesNullness (csenv: ConstraintSolverEnv) m2 (trace: Option
10761081
| _, NullnessInfo.AmbivalentToNull -> CompleteD
10771082
| NullnessInfo.WithNull, NullnessInfo.WithNull -> CompleteD
10781083
| NullnessInfo.WithoutNull, NullnessInfo.WithoutNull -> CompleteD
1084+
// Warn for 'strict "must pass null"` APIs like Option.ofObj
1085+
| NullnessInfo.WithNull, NullnessInfo.WithoutNull when csenv.g.checkNullness && csenv.SolverState.WarnWhenUsingWithoutNullOnAWithNullTarget.IsSome ->
1086+
WarnD(Error(FSComp.SR.tcPassingWithoutNullToANullableExpectingFunc (csenv.SolverState.WarnWhenUsingWithoutNullOnAWithNullTarget.Value),m2))
10791087
// Allow target of WithNull and actual of WithoutNull
1080-
| NullnessInfo.WithNull, NullnessInfo.WithoutNull -> CompleteD
1088+
| NullnessInfo.WithNull, NullnessInfo.WithoutNull ->
1089+
CompleteD
10811090
| NullnessInfo.WithoutNull, NullnessInfo.WithNull ->
10821091
if csenv.g.checkNullness then
10831092
if not (isObjTy csenv.g ty1) || not (isObjTy csenv.g ty2) then
@@ -3968,7 +3977,8 @@ let CreateCodegenState tcVal g amap =
39683977
ExtraCxs = HashMultiMap(10, HashIdentity.Structural)
39693978
InfoReader = InfoReader(g, amap)
39703979
PostInferenceChecksPreDefaults = ResizeArray()
3971-
PostInferenceChecksFinal = ResizeArray() }
3980+
PostInferenceChecksFinal = ResizeArray()
3981+
WarnWhenUsingWithoutNullOnAWithNullTarget = None}
39723982

39733983
/// Determine if a codegen witness for a trait will require witness args to be available, e.g. in generic code
39743984
let CodegenWitnessExprForTraitConstraintWillRequireWitnessArgs tcVal g amap m (traitInfo:TraitConstraintInfo) =
@@ -4063,7 +4073,8 @@ let IsApplicableMethApprox g amap m (minfo: MethInfo) availObjTy =
40634073
ExtraCxs = HashMultiMap(10, HashIdentity.Structural)
40644074
InfoReader = InfoReader(g, amap)
40654075
PostInferenceChecksPreDefaults = ResizeArray()
4066-
PostInferenceChecksFinal = ResizeArray() }
4076+
PostInferenceChecksFinal = ResizeArray()
4077+
WarnWhenUsingWithoutNullOnAWithNullTarget = None}
40674078
let csenv = MakeConstraintSolverEnv ContextInfo.NoContext css m (DisplayEnv.Empty g)
40684079
let minst = FreshenMethInfo m minfo
40694080
match minfo.GetObjArgTypes(amap, m, minst) with

src/Compiler/Checking/ConstraintSolver.fsi

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,32 @@ exception ArgDoesNotMatchError of
188188
/// A function that denotes captured tcVal, Used in constraint solver and elsewhere to get appropriate expressions for a ValRef.
189189
type TcValF = ValRef -> ValUseFlag -> TType list -> range -> Expr * TType
190190

191-
[<Sealed>]
192191
type ConstraintSolverState =
192+
{
193+
g: TcGlobals
194+
195+
amap: ImportMap
196+
197+
InfoReader: InfoReader
198+
199+
/// The function used to freshen values we encounter during trait constraint solving
200+
TcVal: TcValF
201+
202+
/// This table stores all unsolved, ungeneralized trait constraints, indexed by free type variable.
203+
/// That is, there will be one entry in this table for each free type variable in
204+
/// each outstanding, unsolved, ungeneralized trait constraint. Constraints are removed from the table and resolved
205+
/// each time a solution to an index variable is found.
206+
mutable ExtraCxs: Internal.Utilities.Collections.HashMultiMap<Stamp, TraitConstraintInfo * range>
207+
208+
/// Checks to run after all inference is complete, but before defaults are applied and internal unknowns solved
209+
PostInferenceChecksPreDefaults: ResizeArray<unit -> unit>
210+
211+
/// Checks to run after all inference is complete.
212+
PostInferenceChecksFinal: ResizeArray<unit -> unit>
213+
214+
WarnWhenUsingWithoutNullOnAWithNullTarget: string option
215+
}
216+
193217
static member New: TcGlobals * ImportMap * InfoReader * TcValF -> ConstraintSolverState
194218

195219
/// Add a post-inference check to run at the end of inference

src/Compiler/FSComp.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,12 @@ notAFunctionButMaybeDeclaration,"This value is not a function and cannot be appl
15261526
#3261 reserved for ConstraintSolverNullnessWarningWithTypes
15271527
#3261 reserved for ConstraintSolverNullnessWarningWithType
15281528
#3261 reserved for ConstraintSolverNullnessWarning
1529+
3262,tcPassingWithoutNullToANullableExpectingFunc,"Value known to be without null passed to a function meant for nullables: %s"
1530+
tcPassingWithoutNullToOptionOfObj,"You can create 'Some value' directly instead of 'ofObj', or consider not using an option for this value."
1531+
tcPassingWithoutNullToValueOptionOfObj,"You can create 'ValueSome value' directly instead of 'ofObj', or consider not using a voption for this value."
1532+
tcPassingWithoutNullToNonNullAP,"You can remove this |Null|NonNull| pattern usage."
1533+
tcPassingWithoutNullToNonNullQuickAP,"You can remove this |NonNullQuick| pattern usage."
1534+
tcPassingWithoutNullTononNullFunction,"You can remove this `nonNull` assertion."
15291535
3268,csNullNotNullConstraintInconsistent,"The constraints 'null' and 'not null' are inconsistent"
15301536
3271,tcNullnessCheckingNotEnabled,"The 'nullness checking' language feature is not enabled. This use of a nullness checking construct will be ignored."
15311537
csTypeHasNullAsTrueValue,"The type '%s' uses 'null' as a representation value but a non-null type is expected"

src/Compiler/TypedTree/TcGlobals.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,6 +1573,7 @@ type TcGlobals(
15731573
member val attrib_MeasureableAttribute = mk_MFCore_attrib "MeasureAnnotatedAbbreviationAttribute"
15741574
member val attrib_NoDynamicInvocationAttribute = mk_MFCore_attrib "NoDynamicInvocationAttribute"
15751575
member val attrib_NoCompilerInliningAttribute = mk_MFCore_attrib "NoCompilerInliningAttribute"
1576+
member val attrib_WarnOnWithoutNullArgumentAttribute = mk_MFCore_attrib "WarnOnWithoutNullArgumentAttribute"
15761577
member val attrib_SecurityAttribute = tryFindSysAttrib "System.Security.Permissions.SecurityAttribute"
15771578
member val attrib_SecurityCriticalAttribute = findSysAttrib "System.Security.SecurityCriticalAttribute"
15781579
member val attrib_SecuritySafeCriticalAttribute = findSysAttrib "System.Security.SecuritySafeCriticalAttribute"

src/Compiler/TypedTree/TypedTreeBasics.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ let rec stripTyparEqnsAux nullness0 canShortcut ty =
315315
addNullnessToTy nullness0 ty
316316
| TType_measure unt ->
317317
TType_measure (stripUnitEqnsAux canShortcut unt)
318-
| _ -> ty
318+
| _ -> addNullnessToTy nullness0 ty
319319

320320
let stripTyparEqns ty = stripTyparEqnsAux KnownWithoutNull false ty
321321

src/Compiler/TypedTree/TypedTreeOps.fs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3496,6 +3496,19 @@ let TryFindFSharpStringAttribute g nm attrs =
34963496
match TryFindFSharpAttribute g nm attrs with
34973497
| Some(Attrib(_, _, [ AttribStringArg b ], _, _, _, _)) -> Some b
34983498
| _ -> None
3499+
3500+
let TryFindLocalizedFSharpStringAttribute g nm attrs =
3501+
match TryFindFSharpAttribute g nm attrs with
3502+
| Some(Attrib(_, _, [ AttribStringArg b ], namedArgs, _, _, _)) ->
3503+
match namedArgs with
3504+
| ExtractAttribNamedArg "Localize" (AttribBoolArg true) ->
3505+
#if PROTO
3506+
Some b
3507+
#else
3508+
FSComp.SR.GetTextOpt(b)
3509+
#endif
3510+
| _ -> Some b
3511+
| _ -> None
34993512

35003513
let TryFindILAttribute (AttribInfo (atref, _)) attrs =
35013514
HasILAttribute atref attrs

src/Compiler/TypedTree/TypedTreeOps.fsi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,6 +2286,8 @@ val TryFindFSharpBoolAttributeAssumeFalse: TcGlobals -> BuiltinAttribInfo -> Att
22862286

22872287
val TryFindFSharpStringAttribute: TcGlobals -> BuiltinAttribInfo -> Attribs -> string option
22882288

2289+
val TryFindLocalizedFSharpStringAttribute: TcGlobals -> BuiltinAttribInfo -> Attribs -> string option
2290+
22892291
val TryFindFSharpInt32Attribute: TcGlobals -> BuiltinAttribInfo -> Attribs -> int32 option
22902292

22912293
/// Try to find a specific attribute on a type definition, where the attribute accepts a string argument.

src/Compiler/Utilities/Activity.fs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,16 @@ module internal Activity =
6060
member this.RootId =
6161
let rec rootID (act: Activity) =
6262
match act.Parent with
63-
| NonNull parent -> rootID parent
64-
| Null -> act.Id
63+
| null -> act.Id
64+
| parent -> rootID parent
6565

6666
rootID this
6767

6868
member this.Depth =
6969
let rec depth (act: Activity) acc =
7070
match act.Parent with
71-
| NonNull parent -> depth parent (acc + 1)
72-
| Null -> acc
71+
| null -> acc
72+
| parent -> depth parent (acc + 1)
7373

7474
depth this 0
7575

@@ -79,8 +79,8 @@ module internal Activity =
7979
let activity = activitySource.CreateActivity(name, ActivityKind.Internal)
8080

8181
match activity with
82-
| Null -> activity
83-
| NonNull activity ->
82+
| null -> activity
83+
| activity ->
8484
for key, value in tags do
8585
activity.AddTag(key, value) |> ignore
8686

@@ -90,7 +90,8 @@ module internal Activity =
9090

9191
let addEvent name =
9292
match Activity.Current with
93-
| NonNull activity when activity.Source = activitySource -> activity.AddEvent(ActivityEvent name) |> ignore
93+
| null -> ()
94+
| activity when activity.Source = activitySource -> activity.AddEvent(ActivityEvent name) |> ignore
9495
| _ -> ()
9596

9697
module Profiling =

src/Compiler/xlf/FSComp.txt.cs.xlf

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)