The design suggestion Support [<CallerArgumentExpression>] has been marked "approved in principle".
This RFC covers the detailed proposal for this suggestion.
- Suggestion
- Approved in principle
- Implementation
- Discussion
Support the [<CallerArgumentExpression>], including:
- When invoking existing methods in BCL or C#, pass the code text of the specified argument.
- Allow the use of this attribute when defining F# class methods.
Also, this is a part of the interoperability with C#.
The motivation, pros, and cons can be seen in the C# proposal. To summarize, this allows developers to capture the expressions passed to a method, to enable better error messages in diagnostic/testing APIs and reduce keystrokes.
- The compiler should be able to retrieve the original source code text within the specified text range during compilation, even if the range is affected by
#linedirectives. - When making method call, the compiler should do the following things to the optional parameters marked with the
[<CallerArgumentExpression>]attribute (and no other caller info attributes):-
Determine if the method call has has syntactic arguments since we can know the argument expression range only when the method call has syntactic arguments.
An informational warning will be emitted when the method call does not have syntactic arguments.
The following table show some examples:
Cases Allowed? What will be applied to the method call? System.ArgumentException.ThrowIfNullOrEmpty(null)Yes the argument expression (System.ArgumentException.ThrowIfNullOrEmpty) nullNo the parameter default value System.ArgumentException.ThrowIfNullOrEmpty <| nullNo the parameter default value null |> System.ArgumentException.ThrowIfNullOrEmptyNo the parameter default value let f = System.ArgumentException.ThrowIfNullOrEmpty in f(null)No the parameter default value -
Attempt to identify the argument which the attribute references.
-
Determine the textual range of the argument expression in the source code.
-
Use the retrieved text range to extract the source code snippet corresponding to the argument expression.
-
Bind the extracted code text to the parameter and propagate it as part of the method call.
-
If any step above fails, the optional parameter will use its declared default value.
-
The following examples show the expected behavior:
// The allowed cases
System.ArgumentException.ThrowIfNullOrEmpty null // paramName = "null"
System.ArgumentException.ThrowIfNullOrEmpty(argument = null) // paramName = "null"
System.ArgumentException.ThrowIfNullOrEmpty(null: string) // paramName = "null: string"
System.ArgumentException.ThrowIfNullOrEmpty(null
#line 1
: string) // paramName = "null\n#line 1\n : string"
// The not allowed cases
(System.ArgumentException.ThrowIfNullOrEmpty) null // paramName = ""
System.ArgumentException.ThrowIfNullOrEmpty <| null // paramName = ""
null |> System.ArgumentException.ThrowIfNullOrEmpty // paramName = ""
let f = System.ArgumentException.ThrowIfNullOrEmpty in f(null) // paramName = ""- The attribute can be applied to optional parameters in both F# and C#-style syntax.
- The attribute can reference the `` parameters, but need to check whether it can work with C#。
- When compile with the environment without [<CallerArgumentExpression>] like .NET Framework 4.7, the compiler should be able to discover the attribute that user defines.
- Test whether it can work with the situations that are mentioned in FS-1012 Support for caller info argument attributes like computation expressions.
The following examples show the expected behavior:
#if NETFRAMEWORK
namespace System.Runtime.CompilerServices
open System
[<AttributeUsage(AttributeTargets.Parameter, AllowMultiple=false, Inherited=false)>]
type CallerArgumentExpressionAttribute(parameterName: string) =
inherit Attribute()
member val ParameterName = parameterName
#endif
open System.Runtime.CompilerServices
open System.Runtime.InteropServices
namespace MyNamespace
type MyClass =
static member MyMethod(
argument,
``argument 2!``
[<CallerArgumentExpression "argument">] ?exp: string,
[<CallerArgumentExpression "argument 2!"; Optional; DefaultParameterValue "default">] exp2: string) =
exp, exp2
MyClass.MyMethod(1 + 1, 2.) // "1 + 1", "2."Under "10. Build the resulting elaborated expression by following these steps:" in 14.4 Method Application Resolution:
Passing a None value for each argument that corresponds to anImplicitlySuppliedFormalArgsPassing the default value for each argument that corresponds to an
ImplicitlySuppliedFormalArgs, that is:
The corresponding caller information if the parameter has caller-info attribute (
CallerLineNumber,CallerFileName,CallerCallerName,CallerArgumentExpression)Note: The
CallerArgumentExpressioninfomation applies only if the method call has syntactic argument, otherwise the default parameter value will be used.The default value for C# optional parameter
Nonefor F# optional parameter
In 17.1 Custom Attributes Recognized by F# add System.Runtime.CompilerServices.CallerArgumentExpressionAttribute
No.
As mentioned at the comment, we can use the F# specific [<ReflectedDefinition>] to do this.
Please address all necessary compatibility questions:
-
Is this a breaking change?
No
-
What happens when previous versions of the F# compiler encounter this design addition as source code?
It will works as before, ignore the attribute, and pass the default value as argument.
-
What happens when previous versions of the F# compiler encounter this design addition in compiled binaries?
It will works as before, as the attribute is very common in the BCL.
-
If this is a change or extension to FSharp.Core, what happens when previous versions of the F# compiler encounter this construct?
This change does not affect FSharp.Core.
- (FS1246) 'CallerArgumentExpression ".."' must be applied to an argument of type 'string', but has been applied to an argument of type '%s'
- (FS1247) 'CallerArgumentExpression ".."' can only be applied to optional arguments
- The [<CallerArgumentExpression>] on this parameter will have no effect because it's self-referential.
- The [<CallerArgumentExpression>] on this parameter will have no effect because it's applied with an invalid parameter name.
- The [<CallerArgumentExpression>] on this parameter will have no effect because it's overridden by the [<%s>].
- This usage blocks passing string representations of arguments to parameters annotated with [<CallerArgumentExpression>]. The default values of these parameters will be passed. Only the usages like
Method(arguments)can capture the string representation of arguments.
The feature requires the compiler to read and store all the code text in memory, which may have a negative impact on the performance of the compiler.
What parts of the design are still TBD?