0

Say I have a method PrintSourceCode(Action action) that accepts an Action delegate and is executed this way:

PrintSourceCode(() => 
{ // need this line number
    DoSomething();
})

Within PrintSourceCode I would like to print the source code of the action passed.

I was able to get the StackFrame, then read the source file and output it. However it produces the output of the entire PrintSourceCode() call rather that just the action.

        static void PrintSourceCode(Action action)
        {
            action();
            
            var mi = action.GetMethodInfo(); // not currently used

            var frame = new StackFrame(1, true);
            var lineNumber = frame.GetFileLineNumber();
            var fileName = frame.GetFileName();
            var lines = File.ReadAllLines(fileName);
            var result = new StringBuilder();

            for (var currLineIndex = lineNumber - 1; currLineIndex < lines.Length; currLineIndex++)
            {
                var currentLine = lines[currLineIndex];
                result.AppendLine(currentLine);
                if (Regex.IsMatch(currentLine, @";\s*$"))
                {
                    break;
                }
            }

            Console.WriteLine(result.ToString());
        }

Current output

PrintSourceCode(() =>
{
     DoSomething();

Desired output

{
     DoSomething();
}

I can call action.GetMethodInfo() however that doesn't seem to have the file/line information. And there is no way to get/construct the StackFrame because the action is not running.

Is there a way to get the file/line of an action from the outside of the action itself?

Alec Bryte
  • 580
  • 1
  • 6
  • 18

3 Answers3

1

I think this is what you're after. Hopefully helps you out.

Result: Console Output

Full Code:

public class TestPrintAction
{
    static readonly Regex regexActionBody = new Regex(@"(\{(\n.*)*\})\);");

    public static void Main()
    {
        PrintSourceCode(() => {
            Console.WriteLine("Hello World");

        });
    }


    static void PrintSourceCode(
        Action action,
        [System.Runtime.CompilerServices.CallerMemberName] string membername = "",
        [System.Runtime.CompilerServices.CallerFilePath] string filepath = "",
        [System.Runtime.CompilerServices.CallerLineNumber] int linenumber = 0)
    {
        Match _match = Match.Empty;
        string[] _fileLines;
        string[] _fileSubLines;
        string _fileSubText;
        string _out;

        //Uncomment To Run The Action
        //action();

        _fileLines = File.ReadLines(filepath).ToArray();

        _fileSubLines =
            _fileLines.Skip(linenumber-1)
            .ToArray();

        _fileSubText = string.Join(
            separator: "\n",
            value: _fileSubLines);

        try
        {
            _match =
                regexActionBody.Match(_fileSubText);

            _out =
                (_match.Success)
                ? _match.Groups[1].Value.ToString()
                : string.Empty;

        }
        catch (Exception ex)
        {
            Debugger.Break();
            _out = string.Empty;

        }

        Console.WriteLine(""
            + $"Caller Member Name: {membername}\n"
            + $"Caller File Path: {filepath}\n"
            + $"Caller Line Number: {linenumber}\n"
            + $"Action Body: {_out}"
            );
    }
}

Notes:

Utilize attributes from System.Runtime.CompilerServices in the PrintSourceCode method arguments to get file path and line number.

You can comment back in the Action if you need to run it in the PrintSourceCode method.

I added this to clear out the code before the call to PrintSourceCode. Allowed me to make the regex a little simpler. Suppose you could have a reader function to parse the Action Body, would not be as expensive as the Regex, whatever works best for your end result :-)

        _fileSubLines =
            _fileLines.Skip(linenumber-1)
            .ToArray();

Yes you can condense a lot of this. I just broke it out to show a little more depth to what is going on.

Alex8695
  • 479
  • 2
  • 5
0

Not sure if this code covers all your cases

static void PrintSourceCode(Action action)
{
    action();

    var frame = new StackFrame(1, true);
    var lineNumber = frame.GetFileLineNumber();
    var fileName = frame.GetFileName();
    var lines = File.ReadAllLines(fileName);
    var result = new StringBuilder();

    var bracket = 0;
    for (var currLineIndex = lineNumber; currLineIndex < lines.Length; currLineIndex++)
    {
        var currentLine = lines[currLineIndex];
          
        if (currentLine.Contains("{"))
        {
            bracket++;
        }
        else if (currentLine.Contains("}"))
        {
            bracket--;
        }
        if (bracket == 0)
        {
            currentLine = currentLine.Substring(0, currentLine.IndexOf("}") + 1);
            result.AppendLine(currentLine);
            break;
        }
        result.AppendLine(currentLine);
    }

    Console.WriteLine(result.ToString());
}

For

PrintSourceCode(() => 
{
    DoSomething();
})

It produces

{
     DoSomething();
}
user2250152
  • 14,658
  • 4
  • 33
  • 57
0

Directly, no.

Indirectly, yes (sort of). But you have to be prepared for a very brittle way of getting the answer.

You can modify the signature of PrintSourceCode to use CallerMemberArgumentExpression:

    private void PrintSourceCode(
        Action action,
        [CallerArgumentExpression(nameof(action))] string expression = null);

and you can modify the body of PrintSourceCode to strip out the "extraneous" information you mention (i.e. the () => part, the final ) part, and the whitespace).

There is no way to get what you want from a completely declarative point of view because it is not supported by any current version of C#/.NET. You have no choice but to resort to some imperative programming given the information that you can receive declaratively.

The imperative part means parsing and working with the declarative data using an algorithm (imperative). user2250152's answer provides a way to do that, though it would have to be modified to draw from the attribute (compile time) rather than from stack frame data, which is at runtime.

I thought to try and apply the Caller attributes to a delegate other than Action, but that just doesn't work. You lose the "action" parameter, and so have nothing to hang CallerMemberExpression onto.

Kit
  • 20,354
  • 4
  • 60
  • 103