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

Feature: CallerArgumentExpressionAttribute #51952

Conversation

Youssef1313
Copy link
Member

@Youssef1313 Youssef1313 commented Mar 18, 2021

Proposal: dotnet/csharplang#287
Test plan: #52745

TODO:

  • Add a warning if the parameter name specified in the attribute is incorrect. Done.

Open questions:

  • Do we need to support VB?

  • Do we need to add a warning for pre-C# 10 using this feature?

  • What should happen for the following case:

    public static void M(
        [CallerMemberName] string callerName = "",
        [CallerArgumentExpression("callerName")] string argumentExp = "")
    {
        Console.WriteLine(callerName);
        Console.WriteLine(argumentExp);
    }
  • What should happen for the following case, should this have a warning?

    M(); M("value");
    
    public static void M(
        [CallerArgumentExpression("p")] string p = "")
    {
        Console.WriteLine(p);
    }

@Youssef1313 Youssef1313 force-pushed the features/caller-expression-info branch from 50a83c5 to cb0ab7d Compare March 18, 2021 10:29
@Youssef1313 Youssef1313 force-pushed the features/caller-expression-info branch from 0cd335f to 28f460a Compare March 18, 2021 21:02
@Youssef1313 Youssef1313 marked this pull request as ready for review March 18, 2021 21:58
@Youssef1313 Youssef1313 requested a review from a team as a code owner March 18, 2021 21:58
@Youssef1313
Copy link
Member Author

Ready for initial review.

@333fred 333fred changed the base branch from main to features/caller-argument-expression March 19, 2021 17:34
@333fred
Copy link
Member

333fred commented Mar 19, 2021

@Youssef1313 I've created a feature branch for this work. Feel free to put your open questions as prototype comments in the source: the syntax is // PROTOTYPE(caller-expr): comment, so something similar. Just use the same name everywhere for the feature name. It may be a while before LDM can address the questions, so lets not block work on it.

@@ -603,6 +619,30 @@ internal override CSharpAttributeData EarlyDecodeWellKnownAttribute(ref EarlyDec
{
arguments.GetOrCreateData<ParameterEarlyWellKnownAttributeData>().HasCallerMemberNameAttribute = true;
}
else if (CSharpAttributeData.IsTargetEarlyAttribute(arguments.AttributeType, arguments.AttributeSyntax, AttributeDescription.CallerArgumentExpressionAttribute))
Copy link
Member

Choose a reason for hiding this comment

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

As the language exists today we can do this as an early attribute, but if we do implement the nameof proposal (allowing nameof(parameter) in parameters/attributes on methods) I don't believe it will end well (this will cause a loop or fail to bind entirely). Please add a prototype comment to consider moving binding of the actual attribute data into regular attribute binding.

Copy link
Member Author

Choose a reason for hiding this comment

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

@333fred To make sure I understand this correctly, this is about nameof binding being done after we are trying to get the parameter name?

This also affects existing code, for example:

if (CSharpAttributeData.IsTargetEarlyAttribute(arguments.AttributeType, arguments.AttributeSyntax, AttributeDescription.ConditionalAttribute))
{
boundAttribute = arguments.Binder.GetAttribute(arguments.AttributeSyntax, arguments.AttributeType, out hasAnyDiagnostics);
if (!boundAttribute.HasErrors)
{
string name = boundAttribute.GetConstructorArgument<string>(0, SpecialType.System_String);
arguments.GetOrCreateData<CommonTypeEarlyWellKnownAttributeData>().AddConditionalSymbol(name);
if (!hasAnyDiagnostics)
{
return boundAttribute;
}
}
return null;
}

Copy link
Member

Choose a reason for hiding this comment

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

To make sure I understand this correctly, this is about nameof binding being done after we are trying to get the parameter name?

I'm not sure I understand the question. My concern here is:

  1. We're attempting to bind the parameter
  2. The parameter has this attribute, and uses nameof on a parameter in this method (even itself!)
  3. Attempting to bind the nameof argument causes us to bind the method, which causes us to bind the nameof, which causes us to bind the method, which... etc.

Today, the parameter is not in scope for that nameof, so our standard early attribute handling will work correctly without a chance for a loop. I'm not certain it will work correctly in the face of an updated nameof, however, so to do the right thing we'd want to move binding of the attribute argument to the standard binding location, which isn't susceptible to this problem.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I got the general idea here. I think (most likely) this will require fixes in other existing areas first (e.g, the ConditionalAttribute above), so I'll probably follow the same work and add tests.

@@ -419,6 +424,11 @@ public void NullableWarnings()
ErrorCode.WRN_AnalyzerReferencesFramework,
ErrorCode.WRN_UnreadRecordParameter,
ErrorCode.WRN_DoNotCompareFunctionPointers,
ErrorCode.WRN_CallerArgumentExpressionParamForUnconsumedLocation,
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

ErrorCode.WRN_CallerArgumentExpressionParamForUnconsumedLocation, [](start = 20, length = 65)

It looks like this test requires unnecessary maintenance. Perhap the check for the range of error codes at the beginning of the loop should be adjusted instead? Based on the comment only "Nullable-unrelated warnings in the C# 8 range should be added to this array." #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

@AlekseyTs I guess you meant this to be a "general" feedback on this test, rather than this PR specifically. Is that right?
Should I target main for that change in a separate PR, or file an issue for discussion?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you meant this to be a "general" feedback on this test, rather than this PR specifically. Is that right?

It is specific to this PR because there are changes here.

Should I target main for that change in a separate PR, or file an issue for discussion?

I guess either way is fine as long as we either fix it in this PR, or make sure it will be fixed before merging the feature into main.


In reply to: 607929347 [](ancestors = 607929347)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry I initially misunderstood your comment. Fixed in 418ec88.

@Youssef1313
Copy link
Member Author

can you take another look?

It looks like the issue around an unnecessary bit flag in PEParameterSymbol hasn't been addressed.

Will give that a look 👀

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 6, 2021

Done with review pass (commit 39) #Closed

@Youssef1313
Copy link
Member Author

Youssef1313 commented Apr 6, 2021

public bool TryGetWellKnownAttribute(WellKnownAttributeFlags flag, out bool value)
{
int theBits = _bits; // Read this.bits once to ensure the consistency of the value and completion flags.
value = (theBits & ((int)flag << WellKnownAttributeDataOffset)) != 0;
return (theBits & ((int)flag << WellKnownAttributeCompletionFlagOffset)) != 0;
}

If I understand this method correctly, it looks like both flags are needed. One flag specifies whether we can get the attribute data or not, and the other specifies the actual data. Both SetWellKnownAttribute and TryGetWellKnownAttribute were built on the assumption that we have two bits for each attribute. Let me know if there is something I'm missing @AlekseyTs

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 6, 2021

If I understand this method correctly, it looks like both flags are needed. One flag specifies whether we can get the attribute data or not, and the other specifies the actual data. Both SetWellKnownAttribute and TryGetWellKnownAttribute were built on the assumption that we have two bits for each attribute. Let me know if there is something I'm missing

I believe that is not the issue I am referring to. See https://github.com/dotnet/roslyn/pull/51952/files#r600068668 #Closed

@Youssef1313
Copy link
Member Author

Youssef1313 commented Apr 6, 2021

If I understand this method correctly, it looks like both flags are needed. One flag specifies whether we can get the attribute data or not, and the other specifies the actual data. Both SetWellKnownAttribute and TryGetWellKnownAttribute were built on the assumption that we have two bits for each attribute. Let me know if there is something I'm missing

I believe that is not the issue I am referring to. See https://github.com/dotnet/roslyn/pull/51952/files#r600068668

Thanks. Does b99bb7a address that?

@@ -596,6 +599,14 @@ private bool HasCallerMemberNameAttribute
}
}

private bool HasCallerArgumentExpressionAttribute
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

HasCallerArgumentExpressionAttribute [](start = 21, length = 36)

This property feels unnecessary. Also, it feels like we should be able to avoid looking for the attribute twice. First in this property and then to extract parameter name from it. #Closed

Copy link
Member Author

@Youssef1313 Youssef1313 Apr 6, 2021

Choose a reason for hiding this comment

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

@AlekseyTs It feels much more simple if I didn't follow the existing pattern for other caller info, i.e, not even touch PackedFlags. I attempted to do that in e8023b1. Let me know if there are any concerns with the approach I took.

@@ -656,6 +658,44 @@ internal override bool IsCallerMemberName
}
}

private int? _cachedCallerArgumentExpressionParameterIndex;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2021

Choose a reason for hiding this comment

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

private int? _cachedCallerArgumentExpressionParameterIndex; [](start = 8, length = 59)

Please place the field declaration at the beginning of the class declaration, next to other fields. Also, we use "lazy" prefix for fields with delayed initialization. #Closed

bool isCallerArgumentExpression = !HasCallerLineNumberAttribute
&& !HasCallerFilePathAttribute
&& !HasCallerMemberNameAttribute
&& info.HasValue
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2021

Choose a reason for hiding this comment

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

&& info.HasValue [](start = 20, length = 16)

Is this check cheap? Consider doing it first. #Closed

{
if (_cachedCallerArgumentExpressionParameterIndex.HasValue)
{
return _cachedCallerArgumentExpressionParameterIndex.Value;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2021

Choose a reason for hiding this comment

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

Value [](start = 73, length = 5)

GetValueOrDefault? It avoids another HasValue check. #Closed

{
if (parameters[i].Name == parameterName)
{
_cachedCallerArgumentExpressionParameterIndex = i;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2021

Choose a reason for hiding this comment

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

_cachedCallerArgumentExpressionParameterIndex = i; [](start = 28, length = 50)

This assignment is not guaranteed to be atomic, therefore, the implementation of this property is not thread safe. That is why we don't use Nullable<T> for "lazy" members. For example, we use ThreeState instead of bool?. It looks like we can use a simple int field with initial value (-2), for example, to indicate uninitialized state. #Closed

}
}

_cachedCallerArgumentExpressionParameterIndex = -1;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2021

Choose a reason for hiding this comment

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

_cachedCallerArgumentExpressionParameterIndex = -1; [](start = 16, length = 51)

Same comment #Closed


private const int WellKnownAttributeDataOffset = 0;
private const int WellKnownAttributeCompletionFlagOffset = 8;
private const int RefKindOffset = 16;
private const int FlowAnalysisAnnotationsOffset = 20;

private const int RefKindMask = 0x3;
private const int WellKnownAttributeDataMask = 0xFF;

private const int WellKnownAttributeDataMask = (0x1 << 8) - 1;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2021

Choose a reason for hiding this comment

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

(0x1 << 8) - 1 [](start = 59, length = 14)

Consider reverting back to 0xFF literal. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 7, 2021

Done with review pass (commit 43) #Closed

@Youssef1313 Youssef1313 requested a review from AlekseyTs April 8, 2021 11:32
Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 44)

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 44)

@AlekseyTs
Copy link
Contributor

@333fred Please review the latest revision. Please squash commits during merging.

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

LGTM (commit 44)

@333fred 333fred merged commit 52f6db1 into dotnet:features/caller-argument-expression Apr 10, 2021
@333fred
Copy link
Member

333fred commented Apr 10, 2021

Thanks @Youssef1313!

@Youssef1313 Youssef1313 deleted the features/caller-expression-info branch April 10, 2021 06:14
@Youssef1313
Copy link
Member Author

Thanks a lot for review @AlekseyTs and @333fred.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants