Skip to content

[Version 10.0] Feature support for CallerArgumentExpression attribute#1535

Draft
RexJaeschke wants to merge 8 commits intodraft-10from
v10-CallerArgumentExpression-attribute
Draft

[Version 10.0] Feature support for CallerArgumentExpression attribute#1535
RexJaeschke wants to merge 8 commits intodraft-10from
v10-CallerArgumentExpression-attribute

Conversation

@RexJaeschke
Copy link
Contributor

@RexJaeschke RexJaeschke commented Jan 19, 2026

This is Rex's adaptation of the corresponding MS proposal.

FYI, during testing, I discovered a feature that is not documented in the MS proposal: Not only is leading and trailing whitespace removed from an argument, but so too are any outermost grouping parens, with these two removal processes done repeatedly, as necessary, so an argument token set of ( ( (123) + 0) ) is reduced to (123) + 0.

@RexJaeschke RexJaeschke added this to the C# 10 milestone Jan 19, 2026
@RexJaeschke RexJaeschke added type: feature This issue describes a new feature Review: pending Proposal is available for review labels Jan 19, 2026
@RexJaeschke RexJaeschke marked this pull request as draft January 19, 2026 18:38
@BillWagner BillWagner force-pushed the v10-CallerArgumentExpression-attribute branch from dbed563 to f3c17e4 Compare March 19, 2026 01:15

The type of the target parameter shall have a standard conversion from `string`.

> *Note:* This means no user-defined conversions from `string` are allowed, and in practice means the type of such a parameter must be `string`, `object`, or an interface implemented by `string`. *end note*
Copy link
Contributor

Choose a reason for hiding this comment

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

Or dynamic.

Copy link
Contributor

Choose a reason for hiding this comment

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

The "First-class Span Types" proposal will also add a standard implicit conversion from string to ReadOnlySpan<char>.


- Leading and trailing white space is removed both before and after any outermost grouping parentheses are removed.
- All outermost grouping parentheses are removed both before and after any leading and trailing white space is removed.
- All other *input_element*s are retained verbatim (including white space, comments, *Unicode_Escape_Sequence*s, and `@` prefixes on identifiers).
Copy link
Contributor

Choose a reason for hiding this comment

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

Roslyn deletes some comments here:

using System;
using System.Runtime.CompilerServices;
#nullable enable
class Test
{
    public static void M(int val = 0, [CallerArgumentExpression("val")] string? text = null)
    {
        Console.WriteLine($"val = {val}, text = <{text}>");
        M(/*a*/ ( /*b*/ 1 /*c*/ + /*d*/ 2 /*e*/ ) /*f*/);
    }
}

lowered to:

      M(3, "1 /*c*/ + /*d*/ 2");

Copy link
Contributor Author

@RexJaeschke RexJaeschke Mar 19, 2026

Choose a reason for hiding this comment

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

@KalleOlaviNiemitalo At a glance, this suggests that comments before the first token (redundant parens excluded) and after the last token are treated as whitespace---so are removed---while those between tokens are preserved. This follows the rule I wrote at Line 887 above.

Copy link
Contributor

Choose a reason for hiding this comment

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

White space is defined in 6.3.4 and does not include comments.

@KalleOlaviNiemitalo
Copy link
Contributor

KalleOlaviNiemitalo commented Mar 19, 2026

I'm seeing odd interactions with params, may be a compiler bug:

using System.Runtime.CompilerServices;
#nullable enable
class Test
{
    string M(
        int first,
        [CallerArgumentExpression("vals")] string text = "N/A",
        params int[] vals)
    {
        return text;
    }

    static void N()
    {
        string a = new Test().M(1);
        string b = new Test().M(2, vals: new[] { 3 });
        string c = new Test().M(4, vals: 5);
    }
}

Lowered to:

    static void N()
    {
        // OK: the default value of text is used, because there is no argument for vals.
        string a = new Test().M(
            first: 1,
            text: "N/A",
            vals: System.Array.Empty<int>());

        // OK: the argument expression for vals is used.
        string b = new Test().M(
            first: 2,
            text: "new[] { 3 }",
            vals: new int[1] { 3 });

        // What on earth?
        // I expected text: "5".
        string c = new Test().M(
            first: 4,
            text: "new Test().M(4, vals: 5)",
            vals: new int[1] { 5 });
    }

Likewise in an attribute:

using System;
using System.Runtime.CompilerServices;

class TestAttribute: Attribute
{
    public TestAttribute(
        [CallerArgumentExpression("val")] string text = "N/A",
        params int[] val)
    {
    }
    
    public char C { get; set; }
}

// [Test("Test(val: 1, C = 'x')", new int[] { 1 })]
[Test(val: 1, C = 'x')]
class Test
{
}


- Leading and trailing white space is removed both before and after any outermost grouping parentheses are removed.
- All outermost grouping parentheses are removed both before and after any leading and trailing white space is removed.
- All other *input_element*s are retained verbatim (including white space, comments, *Unicode_Escape_Sequence*s, and `@` prefixes on identifiers).
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Mar 19, 2026

Choose a reason for hiding this comment

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

input_element does not include PP_Directive but AFAICT those are also retained, if they occur within the argument expression.

Suggested change
- All other *input_element*s are retained verbatim (including white space, comments, *Unicode_Escape_Sequence*s, and `@` prefixes on identifiers).
- All other *input_element*s and *PP_Directive*s are retained verbatim (including white space, comments, *Unicode_Escape_Sequence*s, and `@` prefixes on identifiers).

If an explicit argument is passed for the target parameter, no string is captured, and that parameter takes on that argument’s value. Otherwise, the text for the argument corresponding to the sibling parameter is converted to a captured string, according to the following rules:

- Leading and trailing white space is removed both before and after any outermost grouping parentheses are removed.
- All outermost grouping parentheses are removed both before and after any leading and trailing white space is removed.
Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose "grouping parentheses" here means that the parentheses are part of the same parenthesized_expression, rather than a tuple_expression:

using System.Runtime.CompilerServices;
#nullable enable
class Test
{
    public static void M(
        (int, int) val,
        [CallerArgumentExpression("val")] string? text = null)
    {
    }
    
    static void N()
    {
        // text: "(1, 2)" rather than "1, 2"
        M(((1, 2)));
    }
}

(In the grammar, a deconstruction_tuple likewise has parentheses, but that cannot be an argument_value.)


> *Note:* This means no user-defined conversions from `string` are allowed, and in practice means the type of such a parameter must be `string`, `object`, or an interface implemented by `string`. *end note*

If an explicit argument is passed for the target parameter, no string is captured, and that parameter takes on that argument’s value. Otherwise, the text for the argument corresponding to the sibling parameter is converted to a captured string, according to the following rules:
Copy link
Contributor

Choose a reason for hiding this comment

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

Apparently, "the text for the argument" means the expression or variable_reference part of argument_value, or the attribute_argument_expression. It would be good to have an example that shows the in/out/ref part of argument_value is not included in the text.

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

Labels

Review: pending Proposal is available for review type: feature This issue describes a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants