📄 cli/src/ScanCommand.cs
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Cfa.Cli;

internal sealed class ScanCommand : Command
{
    private static Argument<FileInfo> PathArgument { get; } =
        new("path")
        {
            Description =
                "Path to the solution file, or the directory containing it. (Defaults to the current directory.)",
            DefaultValueFactory = result =>
            {
                var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
                switch (
                    directory
                        .GetFiles("*.sln?", SearchOption.TopDirectoryOnly)
                        .Where(f => f.Extension is ".sln" or ".slnx")
                        .ToArray()
                )
                {
                    case []:
                        result.AddError("No solution files found in the current directory.");
                        return null!;
                    case [var file]:
                        return file;
                    default:
                        result.AddError("Multiple solution files found in the current directory.");
                        return null!;
                }
            },
            CustomParser = result =>
            {
                if (result.Tokens is not [var token] || string.IsNullOrWhiteSpace(token.Value))
                {
                    result.AddError("No path specified.");
                    return null;
                }
                if (Directory.Exists(token.Value))
                {
                    var directory = new DirectoryInfo(token.Value);
                    switch (
                        directory
                            .GetFiles("*.sln?", SearchOption.TopDirectoryOnly)
                            .Where(f => f.Extension is ".sln" or ".slnx")
                            .ToArray()
                    )
                    {
                        case []:
                            result.AddError("No solution files found in the specified directory.");
                            return null;
                        case [var file]:
                            return file;
                        default:
                            result.AddError("Multiple solution files found in the specified directory.");
                            return null;
                    }
                }
                if (File.Exists(token.Value))
                {
                    return new FileInfo(token.Value);
                }
                result.AddError("The specified path does not exist.");
                return null;
            },
        };

    private static Option<(FileInfo, int)> StartFileOption { get; } =
        new("--start-file", "-f")
        {
            Description = "The start file and line number for the analysis.",
            Required = true,
            CustomParser = result =>
            {
                if (result.Tokens is not [var fileAndLine])
                {
                    result.AddError("Start file must be specified.");
                    return default;
                }
                if (fileAndLine.Value.Split(":", 2) is not [var file, var lineString])
                {
                    result.AddError("Start file must include a line number separated by ':'.");
                    return default;
                }
                if (!int.TryParse(lineString, out var line) || line <= 0)
                {
                    result.AddError("Invalid line number specified.");
                    return default;
                }
                return (new FileInfo(file), line);
            },
        };

    public ScanCommand()
        : base("scan", "Scan the code for analysis")
    {
        Action = new ScanAction();
        Arguments.Add(PathArgument);
        Options.Add(StartFileOption);
    }

    internal sealed class ScanAction : AsynchronousCommandLineAction
    {
        public override async Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken)
        {
            var solutionFile = parseResult.GetRequiredValue(PathArgument);
            using var scanner = await Scanner.Create(solutionFile, cancellationToken);

            var (startFile, startLine) = parseResult.GetRequiredValue(StartFileOption);
            var symbol = await scanner.BuildCallGraph(startFile, startLine, cancellationToken);
            Console.WriteLine($"Found symbol: {symbol}");
            Console.WriteLine($"Symbol type: {symbol.GetType().FullName}");

            return 0;
        }
    }
}