Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect [MemberNotNull] on local functions #75448

Merged
merged 14 commits into from
Oct 31, 2024
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Oct 9, 2024

Fixes #56256

The MemberNotNull attribute takes effect if either:

  • the context is non-static (ie. the containing method and the nested local functions are not static). The target member can be static or not
  • the context is static (ie. either the containing method or the local function is static) and the target member is static

When the MemberNotNull takes effect, three things happen:

  1. after the local function is called, the state of the target member is changed to not-null
  2. when entering the local function, the state of the target member is changed to maybe-null
  3. when existing the local function, we warn if the target member isn't not-null

Note: I didn't gate this change behind LangVer as it seems small and only affects nullability warnings, but I'm open to it.

@jcouv jcouv self-assigned this Oct 9, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged Issues and PRs which have not yet been triaged by a lead label Oct 9, 2024
@jcouv jcouv marked this pull request as ready for review October 14, 2024 15:25
@jcouv jcouv requested a review from a team as a code owner October 14, 2024 15:25
@RikkiGibson RikkiGibson self-assigned this Oct 14, 2024
@jcouv
Copy link
Member Author

jcouv commented Oct 15, 2024

@dotnet/roslyn-compiler for second review. Thanks

RikkiGibson
RikkiGibson previously approved these changes Oct 15, 2024
@RikkiGibson RikkiGibson requested a review from a team October 15, 2024 19:32
field4.ToString(); // 1

[MemberNotNull(nameof(field1), nameof(field2))]
bool init() => throw null!;
Copy link
Member

@cston cston Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool init() => throw null!;

Consider testing within nested local function.

void init()
{
    init2();
    [MemberNotNull(nameof(field1), nameof(field2))]
    void init2() { }
}
``` #Closed

field4.ToString(); // 1

[MemberNotNull(nameof(field1), nameof(field2))]
bool init() => throw null!;
Copy link
Member

@cston cston Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool init() => throw null!;

Consider testing the equivalent case with a static local function, with the same instance members.

[MemberNotNull(nameof(field1), nameof(field2))]
static bool init() => throw null!;
``` #Closed

""", MemberNotNullAttributeDefinition]);

c.VerifyDiagnostics(
// (6,23): error CS0103: The name 'field1' does not exist in the current context
Copy link
Member

@cston cston Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'field1'

Consider testing with a defined field.

...

partial class Program
{
    object? field1;
}
``` #Closed

@jcouv jcouv marked this pull request as draft October 17, 2024 00:12
}
}
}
else if (returnStatement.ExpressionOpt is { ConstantValueOpt: { IsBoolean: true, BooleanValue: bool value } })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 this if block was removed (it was not reachable)

@@ -791,113 +791,6 @@ void checkMemberStateOnConstructorExit(MethodSymbol constructor, Symbol member,
}
}

void enforceMemberNotNullOnMember(SyntaxNode? syntaxOpt, LocalState state, MethodSymbol method, string memberName)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 A number of local functions were switched to methods below


return GetOrCreateSlot(member, containingSlot);
if (member.IsStatic != (containingSlot == 0))
Copy link
Member

@cston cston Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

member.IsStatic != (containingSlot == 0)

Do we get here when member is static but the containing method is an instance method? #Resolved

Copy link
Member Author

@jcouv jcouv Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes
This is illustrated by MemberNotNull_StaticMemberOnInstanceMethod
The opposite scenario is illustrated by MemberNotNull_InstanceMemberOnStaticMethod

Update: there's something fishy with that scenario. Investigating...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making a fix to align the logic in ApplyMemberPostConditions and GetSlotForMemberPostCondition regarding this scenario

}

var container = current.ContainingSymbol;
if (container.Kind == SymbolKind.NamedType)
Copy link
Member

@cston cston Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (container.Kind == SymbolKind.NamedType)

Perhaps check for SymbolKind.Method instead:

if (container.Kind != SymbolKind.Method)
{
   ...
}
current = (MethodSymbol)container;

Then, the containing loop could be the following, and the final return -1; removed.

MethodSymbol current = method;
while (true)
``` #Closed

{
init();
field1.ToString();
field2.ToString(); // 1
Copy link
Member

@cston cston Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field2.ToString(); // 1

Is this warning expected? (It looks like we don't report a warning for the corresponding static method case - see sharplab.io.) #Closed

{
init();
field1.ToString();
field2.ToString(); // 1
Copy link
Member

@cston cston Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field2.ToString(); // 1

Reporting a warning seems incorrect here. #Closed

{
MakeMembersMaybeNull(lambdaOrFunctionSymbol, lambdaOrFunctionSymbol.NotNullMembers);
MakeMembersMaybeNull(lambdaOrFunctionSymbol, lambdaOrFunctionSymbol.NotNullWhenTrueMembers);
MakeMembersMaybeNull(lambdaOrFunctionSymbol, lambdaOrFunctionSymbol.NotNullWhenFalseMembers);
Copy link
Member

@cston cston Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why set these members to maybe null rather than relying on the state from the calling method? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in another comment

[MemberNotNull(nameof(field1), nameof(field2))]
void init()
{
field1.ToString(); // 1
Copy link
Member

@cston cston Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field1.ToString(); // 1

Are we reporting a warning for field1 because we're setting the members to maybe null when entering the local function? Is that necessary rather than relying on the state from the calling method as we would do if there were no [MemberNotNull] attributes? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's why.
For the enforcement of MemberNotNull attribute within a method body, we set the initial state of the field to maybe-null when entering the body and check that it's been changed to not-null by the time you exit the method. That way we know that the method did explicitly set a not-null state.
I used the same design for local functions. That analysis/enforcement can be performed by looking at the local function in isolation, without regard to its invocations.

// Note: we add a synthesized BoundReturnStatement in BindLocalFunctionStatement
// using the block body as syntax, but we report the diagnostic on the closing brace in that case
c.VerifyEmitDiagnostics(
// (16,9): warning CS8774: Member 'field1' must have a non-null value when exiting.
Copy link
Member

@cston cston Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'field1'

Why report a warning for field1? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the same behavior as with methods. If you put the MemberNotNull attribute, you must set the field to a not-null value by the time you exit the method. sharplab

@jcouv jcouv requested a review from cston October 30, 2024 16:56
@jcouv
Copy link
Member Author

jcouv commented Oct 30, 2024

@cston @RikkiGibson This is ready for another look. Thanks

[Fact]
public void MemberNotNull_LocalFunction()
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/56256")]
public void MemberNotNull_InstanceLocalFunction()
Copy link
Member

@cston cston Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InstanceLocalFunction

Consider naming this _NonStaticLocalFunction, for consistency with other tests, and because there's no requirement that the local function captures this (and doesn't in this case). There are a couple of other uses of _InstanceLocalFunction below as well. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. Sorry, I missed those

while (current.ContainingSymbol is MethodSymbol container)
{
current = container;
anyStatic |= container.IsStatic;
Copy link
Member

@cston cston Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyStatic |= container.IsStatic

Consider removing anyStatic and change this to if (container.IsStatic) return 0; #Resolved

Comment on lines 21184 to 21185
[MemberNotNull(nameof(field1), nameof(field2))]
bool init() => throw null!;
Copy link
Contributor

@RikkiGibson RikkiGibson Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be interesting to have versions of a few of these tests (e.g. referencing instance members in 'static context'), where the end of the local function is reachable. #Pending

@@ -447,7 +447,7 @@ lSelect:
Debug.Assert(node.MethodGroupOpt Is Nothing)

Dim delegateType As NamedTypeSymbol = DirectCast(node.Type, NamedTypeSymbol)
Debug.Assert(delegateType.TypeKind = TypeKind.Delegate)
Debug.Assert(delegateType.TypeKind = TYPEKIND.Delegate)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this change intended?

@@ -21002,7 +21002,7 @@ public void M()
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/56256")]
public void MemberNotNull_LocalFunction_BaseField()
public void MemberNotNull_NonStaticLocalFunction_BaseField()
Copy link
Contributor

@RikkiGibson RikkiGibson Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any test for 'MemberNotNull' on a local function within a constructor?

Copy link
Contributor

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM just minor questions/nits.

@jcouv jcouv enabled auto-merge (squash) October 31, 2024 22:41
@jcouv jcouv merged commit 6200694 into dotnet:main Oct 31, 2024
24 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Oct 31, 2024
@jjonescz jjonescz modified the milestones: Next, 17.13 P2 Nov 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[MemberNotNull] isn't respected on local functions
4 participants