Nullness FS3261: pinpoint receiver range, name the member & binding in dot-access warnings#19814
Nullness FS3261: pinpoint receiver range, name the member & binding in dot-access warnings#19814T-Gro wants to merge 19 commits into
Conversation
…ning (#19658, sprint 1/3) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… sprint 2/3) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ness warning (#19658, sprint 3/3) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tests - Extract withObjArgContext helper in ResolveOverloading and UnifyUniqueOverloading, replacing 3 inline match/rewrap duplicates - Trim MemberAccessOnNullable doc comment to 1 line (named labels self-document) - Remove redundant replaceNullnessOfTy (showNullnessAnnotations=false handles display) - Tighten release notes: 'dotted method or property access' (not indexer/records) - Add 5 new edge-case tests: chained access, mutable receiver, overloaded method, extension method, static call negative test - Clean up existing 3 tests: remove EntryPoint boilerplate, minimal source code Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sumesType (#19658) The MemberAccessOnNullable context is meant only for the receiver's outer nullness check. It was leaking into recursive subsumption/equality checks on tuple components, fun-type domains, and especially generic type arguments, producing a dot-access warning with the wrong type (the inner type-arg) and pinning unrelated deep nullness warnings to the receiver range. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#19658) Defense-in-depth: avoid setting ContextInfo.MemberAccessOnNullable on csenv when nullness checking is off, so future code that branches on this context case without re-checking g.checkNullness can't accidentally fire. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…line (#19658) Matches the convention of all sibling FS3261 handlers. When the receiver and the member access are on different source lines (fluent chains), the warning now hyperlinks the member range so IDEs surface both locations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ames (#19658) - tryGetBindingName recurses through Expr.Op(TOp.Coerce, ...) so binding names surface for interface-upcast receivers. - vref.IsCompilerGenerated check prevents internal names (_arg1, matchValue, copyOfStruct, ...) from leaking into user-facing messages. - Document why ConstraintSolverNullnessWarningOnDotAccess uses showNullnessAnnotations = Some false (avoid redundant '| null' phrasing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…19658) - Property setter and piped lambda receiver: verify new message fires. - Indexer, F# record field, anonymous record, SRTP: pin current behavior of paths intentionally not routed through TcMethodApplication. Marked KNOWN GAP in test names. Widening coverage is tracked under #17409. - Anonymous record gap is documented via the FS3260 'does not support a nullness qualification' error since '{| ... |} | null' is rejected by the type system; this still pins that the dot-access path cannot fire. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the anonymous (range * string * string option) option tuple with a named ObjArgInfo record for readability, and extract the withObjArgContext closure (duplicated in ResolveOverloadingCore, ResolveOverloading, and UnifyUniqueOverloading) into a single top-level applyObjArgContext function. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Inside the else-branch recursion, shadow csenv with the stripped variant so MemberAccessOnNullable context is dropped by default for deep subsumption descents. Keep the original under csenvOuter for the two sites that genuinely describe the outer ty1/ty2 pair: the same-tycon outer-nullness check, and the FindUniqueFeasibleSupertype recursion which still subsumes the original ty1. This eliminates the fragility of having to remember to use csenvInner on every new recursive branch — the safe behavior is now the default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…el (#19658) Merge seven template-identical Issue 19658 dot-access nullness tests into a single [<Theory>][<InlineData>] driven by (source, range, member, binding, type) tuples. The mechanical repetition added noise without helping readability. Rename the four 'KNOWN GAP' tests to neutral 'falls back to generic nullness warning' / 'rejects nullable annotation' wording. These tests pin the intentional scope boundary (paths NOT routed through TcMethodApplication) and are not bugs. Update the comment block above them to reflect this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#19658) Internal API change: ResolveOverloading and ResolveOverloadingCore no longer take an objArgInfo parameter. The public entry points ResolveOverloadingForCall and UnifyUniqueOverloading still accept the ObjArgInfo and set ContextInfo.MemberAccessOnNullable on the constructed ConstraintSolverEnv. CanMemberSigsMatchUpToCheck call sites locally strip the context for the non-obj-arg callbacks so deep arg/return mismatches keep using the generic FS3261 message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…es (#19658) CODE-QUALITY fixup: the strip-MemberAccessOnNullable pattern was inlined at 5 sites (SolveTypeSubsumesType + 4 overload resolution sites in ResolveOverloading). Extract a single helper near MakeConstraintSolverEnv and call it at all five callsites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use minimalStringOfTypeWithNullness instead of suppressing nullness annotations. The type 'string' was misleading — the actual type is 'string | null'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Entity.Typars was a method taking a range parameter that was passed to LazyWithContext.Force as context for error recovery. For IL-imported entities shared via FrameworkImportsCache, the first caller's range was cached permanently, creating non-deterministic typar ranges in parallel compilations. Analysis of git history (back to the first open-source commit) shows the range parameter was never meaningful: - F# entities use NotLazy for entity_typars — Force ignores the context - IL entities receive the import-time range via NewILTycon, stored in entity_range — the Force-time range was redundant Change Entity.Typars from a method to a property that uses entity.Range as the Force context. Remove the now-redundant TyparsNoRange member. Clean up cascading unused range parameters in callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
❗ Release notes required
Warning No PR link found in some release notes, please consider adding it.
|
… IlxGen.fs - Replace List<int>.First() (LINQ extension, fails on net472) with List<int>.Contains(1) (instance method, works on all TFMs) - Run fantomas on IlxGen.fs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MemberAccessOnNullable now carries ObjArgInfo directly instead of re-allocating a 3-tuple. Dead _m: range parameters removed from TryFindMetadataInfoOfExternalEntityRef, ReparentSlotSigToUseMethodTypars, GetFormalTyparsOfDeclaringType, GetTyparInst, IsTyconRefUsedForCSharpStyleExtensionMembers, CanAutoOpenTyconRef, ResolveNestedTypeThroughAbbreviation, and cascading callers (GetXmlDocSigOfEntityRef, GetXmlDocSigOfEvent, GetXmlDocSigOfILFieldInfo, IsTypeUsedForCSharpStyleExtensionMembers). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
T-Gro
left a comment
There was a problem hiding this comment.
🤖 AI-generated review
Findings (4 inline comments posted separately):
-
[API Surface — High]
ResolveOverloadingForCallandUnifyUniqueOverloading(ConstraintSolver.fsi:268,282) gain a newobjArgInfoparameter. These are public FCS API — this is a breaking change for downstream consumers. Consider threading viaeContextInfoinstead of adding a parameter, or document in release notes. -
[Type System — Low]
SolveTypeSubsumesTypeline 1661:csenvOuterpassed toFindUniqueFeasibleSupertyperecursive call is intentionally correct but contradicts the comment at 1530-1535 (''all recursive callsites use stripped context''). Add inline comment. -
[Diagnostic Quality — Low]
tryGetBindingName(CheckExpressions.fs:10441) is intentionally conservative. Worth a code comment notingExpr.Letwrapping won't be drilled through (fallback is safe). -
[Diagnostic Quality — Low]
SeeAlsocondition (CompilerDiagnostics.fs:752) correctly checks both StartLine and EndLine. The adjacentConstraintSolverNullnessWarningWithTypes(line 738) only checks StartLine — consider unifying for consistency.
T-Gro
left a comment
There was a problem hiding this comment.
Review Summary
Verdict: ✅ LGTM — Well-structured PR with correct implementation and strong test coverage.
The two changes (targeted dot-access nullness diagnostic + Entity.Typars cleanup) are cleanly separated. The stripMemberAccessOnNullableCtx / csenvOuter pattern correctly prevents context leakage while preserving the targeted diagnostic for the outer receiver check.
Verified Safe
- Entity.Typars → property: The range parameter was always the entity's own range for F# types, and IL-imported types get
entity.Rangeset at import time. Fixes a real parallel compilation race. - TypedTreePickle.fs:
x.Typars≡x.entity_typars.Force(x.entity_range)— no binary compat risk. csenvOuterpattern inSolveTypeSubsumesType: Outer context correctly preserved only for same-tycon nullness check and supertype hierarchy recursion (FindUniqueFeasibleSupertype). Inner type-arg/tuple/fun recursion uses stripped context. No leak.TypesMustSubsumeinResolveOverloadingCore: Correctly usescsenv(with context) since obj-arg subsumption is the check that fires the dot-access warning. All other checks (instantiations, args, return) correctly usecsenvNoCtx.stripTyEqnsdiscipline: AllTType_*matches operate on already-stripped types.
Suggestions (non-blocking)
-
API surface:
ResolveOverloadingForCallandUnifyUniqueOverloadinginConstraintSolver.fsigain a newobjArgInfo: ObjArgInfo optionparameter. These are public FCS API functions — downstream consumers (Ionide, FsAutoComplete) will need to update call sites. The release notes should mention this, or alternatively thread the info purely througheContextInfoon the constructedcsenv(which the function already controls) to avoid changing these signatures. -
Comment clarity at line ~1530: The comment says "all recursive callsites below" use stripped context, but line 1661 intentionally uses
csenvOuterforFindUniqueFeasibleSupertype. A brief note explaining why that one is correct (outer-level subsumption traversal, not inner-type recursion) would prevent future confusion. -
SeeAlso condition consistency: The new dot-access warning checks
m.StartLine <> m2.StartLine || m.EndLine <> m2.EndLinewhile the adjacentConstraintSolverNullnessWarningWithTypesonly checksm.StartLine <> m2.StartLine. Consider unifying the condition.
Test Coverage
Comprehensive: generic receivers, extension methods, LINQ, mutables, compiler-generated vals, complex expressions, static calls, multi-line with SeeAlso, piped lambdas, out-of-scope paths (indexers, record fields, SRTP, anon records). The negative test (nullness off) and the boundary tests (e.g. nested nullable generic) are particularly valuable.
Nice work — the UX improvement for users is significant and the implementation is careful about not over-applying the new message.
Fixes #19658, Fixes #17409
Architecture
New
ContextInfo.MemberAccessOnNullablethreads receiver range + member name + optional binding name through constraint solving. When FS3261 fires during dot-access, a dedicatedConstraintSolverNullnessWarningOnDotAccessemits a targeted diagnostic instead of the generic "types do not have compatible nullability".Before → After
1. Simple method call on nullable receiver
x.PadLeft(10)—Nullness warning: The types 'string' and 'string | null' do not have compatible nullability.x—Nullness warning: Possible dereference of a null value when accessing member 'PadLeft' on the nullable value 'x' of type 'string | null'.2. Chained call — warning targets the actual nullable hop
x.Trim().Lengthandx.Trim()—Nullness warning: The types 'string' and 'string | null' do not have compatible nullability.x—Nullness warning: Possible dereference of a null value when accessing member 'Trim' on the nullable value 'x' of type 'string | null'.3. Multi-line receiver gets "See also" pointing at call site
.ToUpper()—Nullness warning: The types 'string' and 'string | null' do not have compatible nullability.data—Nullness warning: Possible dereference of a null value when accessing member 'ToUpper' on the nullable value 'data' of type 'string | null'. See also <line with .ToUpper()>4. Property chain — warning pinpoints where nullability starts
foo.Prop.ToString()—Nullness warning: The types 'Bar' and 'Bar | null' do not have compatible nullability.foo.Prop—Nullness warning: Possible dereference of a null value when accessing member 'ToString' on a nullable expression of type 'Bar | null'.Entity.Typars cleanup (included because it fixes a parallel test race exposed by the new tests)
Entity.Typarschanged from methodTypars(m: range)to property. Therangeparameter was unused for F# types (alwaysNotLazy— typars carry their own ranges from unpickle) and caused a parallel test race for IL-imported types (shared BCL entities viaFrameworkImportsCache+LazyWithContextcaching first caller's range). Now usesentity.Rangedeterministically. Removed redundantTyparsNoRange.