Description
CallerCharacterNumberAttribute
- Proposed
- Prototype: Not Started
- Implementation: Not Started
- Specification: Not Started
Summary
Add a CallerCharacterNumberAttribute
which when applied on an optional parameter, the compiler replaces with the caller's character (column) number, similar to https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callerlinenumberattribute?view=netcore-3.1.
The character number, is "how many characters along is this in the string representing this line".
Motivation
Consider a source generator that generates a method which acts differently depending on where it's called form. To do so it switches on CallerLineNumber, and CallerFilePath.
For example, a source generator might provide a method to print the expression passed into it as an argument (see it on sharplab):
using System;
using System.Runtime.CompilerServices;
Console.WriteLine(Helpers.PrintExpression(1 + 3 + 7));
Console.WriteLine(Helpers.PrintExpression(new object()));
Console.WriteLine(Helpers.PrintExpression(5 == 7));
// This code is all generated by a source generator
public static partial class Helpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string PrintExpression(object expression, [CallerFilePath] string filePath = default, [CallerLineNumber] int lineNumber = default)
{
return (lineNumber, filePath) switch {
(4, "Main.cs") => "1 + 3 + 7" ,
(5, "Main.cs") => "new object()",
(6, "Main.cs") => "5 == 7",
_ => ""
};
}
}
This approach is actually optimized quite nicely right now by the jit (assuming the method is less than 64kb of IL, but that limit may be lifted by .NET 6, and can be alleviated by creating a tree of methods if necessary).
However it won't work at the moment if PrintExpression
is called twice on the same line. To differentiate that we'll need access to the caller's character number.
Detailed design
Add an attribute System.Runtime.CompilerServices.CallerCharacterNumberAttribute
:
namespace System.Runtime.CompilerServices
{
[System.AttributeUsage(System.AttributeTargets.Parameter, Inherited=false)]
public sealed class CallerCharacterNumberAttribute : Attribute {}
}
Everything is as for the other Caller
attributes: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/caller-information, except it provides character number.
The above example could now be written as:
using System;
using System.Runtime.CompilerServices;
Console.WriteLine(Helpers.PrintExpression(1 + 3 + 7));
Console.WriteLine(Helpers.PrintExpression(new object()));
Console.WriteLine(Helpers.PrintExpression(5 == 7));
// This code is all generated by a source generator
public static partial class Helpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string PrintExpression(object expression, [CallerFilePath] string filePath = default, [CallerLineNumber] int lineNumber = default, [CallerCharacterNumber] int characterNumber = default)
{
return (lineNumber, characterNumber, filePath) switch {
(4, 42, "Main.cs") => "1 + 3 + 7" ,
(5, 42, "Main.cs") => "new object()",
(6, 42, "Main.cs") => "5 == 7",
_ => ""
};
}
}
Drawbacks
The main question is whether this is a pattern we want to encourage in the first place. Using the trio of CallerFilePath
, CallerLineNumber
, and CallerCharacterNumber
to distinguish the location something is called from and run something different in each case, is a hacky workaround to get around the no source code rewriting limitation of source generators. It's not clear whether it's any better than source rewriting, and relies heavily on the JIT to do a good job to be efficient.
Alternatives
Do nothing
Unresolved questions
Seeing is this is pretty much identical to CallerLineNumber, I can't imagine there are any unresolved design questions.
Relevant issues/discussions
See #3987 which could be solved using this technique.
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-09.md#callercharacternumberattribute
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-06.md#callercharacternumberattribute