コードから{クラス名.メソッド名}を抽出する

## はじめに

- 新規にメソッドを作る際、既存のメソッド名（英単語）を参考にしたい
- コードの中からUTを抽出したい
- このコードにはどのくらいのメソッドが実装されているんだろう

と思うことが6カ月に1回くらいあると思います。
そんなときのために、メソッド一覧を抽出するプログラム(WPF)を作成しました。

## 実装
```xml:View/MainWindow.xaml
<Window x:Class="ClassMethodAnalyzer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModel="clr-namespace:ClassMethodAnalyzer.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
	<Window.DataContext>
		<viewModel:MainViewModel/>
	</Window.DataContext>
	<Grid>
		<Grid>
			<Button Content="Select Folder" HorizontalAlignment="Left" Margin="10" VerticalAlignment="Top" Width="100" Command="{Binding SelectFolderCommand}"/>
			<Button Content="Clear" HorizontalAlignment="Left" Margin="120,10,0,0" VerticalAlignment="Top" Width="100" Command="{Binding ClearCommand}"/>
			<TextBox Name="OutputTextBox" Margin="10,50,10,10" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" TextWrapping="Wrap" 
			         Text="{Binding AnalysisResult}" IsReadOnly="True" AcceptsReturn="True"/>
		</Grid>
	</Grid>
</Window>
```

```CSharp:ViewModel/MainViewModel.cs
using System.ComponentModel;
using System.Windows.Input;
using ClassMethodAnalyzer.Commands;
using ClassMethodAnalyzer.Model;
using Microsoft.WindowsAPICodePack.Dialogs;

namespace ClassMethodAnalyzer.ViewModel
{
    public class MainViewModel : INotifyPropertyChanged
    {
        private string _analysisResult;

        public string AnalysisResult
        {
            get => _analysisResult;
            set
            {
                _analysisResult = value;
                OnPropertyChanged(nameof(AnalysisResult));
            }
        }

        public ICommand SelectFolderCommand { get; }
        public ICommand ClearCommand { get; }

        public MainViewModel()
        {
            SelectFolderCommand = new RelayCommand(SelectFolder);
            ClearCommand = new RelayCommand(Clear);
        }

        private void SelectFolder()
        {
            var dialog = new CommonOpenFileDialog
            {
                IsFolderPicker = true
            };

            if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
            {
                string folderPath = dialog.FileName;
                AnalysisResult = CodeAnalyzer.Analyze(folderPath);
            }
        }

        private void Clear()
        {
            AnalysisResult = string.Empty;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
```

```CSharp:Commands/RelayCommand.cs
using System.Windows.Input;

namespace ClassMethodAnalyzer.Commands
{
    public class RelayCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        public RelayCommand(Action execute, Func<bool> canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
        public void Execute(object parameter) => _execute();
        public event EventHandler? CanExecuteChanged;
    }
}
```

```CSharp:Model/ClassMethods.cs
namespace ClassMethodAnalyzer.Model
{
    public class ClassMethods
    {
        public List<string> Methods { get; }
        public string Constructor { get; }

        public ClassMethods(List<string> methods, string constructor)
        {
            Methods = methods;
            Constructor = constructor;
        }
    }
}

```

```CSharp:Model/CodeAnalyzer.cs
using System.IO;
using System.Text.RegularExpressions;

namespace ClassMethodAnalyzer.Model
{
    public static class CodeAnalyzer
    {
        public static string Analyze(string folderPath)
        {
            var results = new List<string>();

            // フォルダ内の全ファイルを再帰的に取得し、bin/objフォルダを除外
            foreach (var filePath in GetCSharpFiles(folderPath))
            {
                var code = File.ReadAllText(filePath);
                var classNames = ExtractClassNames(code);
                var methodsByClass = ExtractMethodsByClass(code, classNames);

                foreach (var className in methodsByClass.Keys)
                {
                    foreach (var methodName in methodsByClass[className].Methods)
                    {
                        results.Add($"{className}.{methodName}");
                    }
                    if (methodsByClass[className].Constructor != null)
                    {
                        results.Add($"{className}.{methodsByClass[className].Constructor}");
                    }
                }
            }

            return string.Join("\n", results);
        }

        private static IEnumerable<string> GetCSharpFiles(string folderPath)
        {
            foreach (var file in Directory.GetFiles(folderPath, "*.cs", SearchOption.TopDirectoryOnly))
            {
                yield return file;
            }

            foreach (var directory in Directory.GetDirectories(folderPath))
            {
                // binまたはobjフォルダをスキップ
                if (directory.EndsWith("bin", StringComparison.OrdinalIgnoreCase) ||
                    directory.EndsWith("obj", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                // サブディレクトリを再帰的に取得
                foreach (var file in GetCSharpFiles(directory))
                {
                    yield return file;
                }
            }
        }

        private static List<string> ExtractClassNames(string code)
        {
            var classNames = new List<string>();
            var classPattern = @"\bclass\s+(\w+)";
            var matches = Regex.Matches(code, classPattern);

            foreach (Match match in matches)
            {
                classNames.Add(match.Groups[1].Value);
            }

            return classNames;
        }

        private static Dictionary<string, ClassMethods> ExtractMethodsByClass(string code, List<string> classNames)
        {
            var methodsByClass = new Dictionary<string, ClassMethods>();
            foreach (var className in classNames)
            {
                // クラス全体を取得
                var methodPattern = $@"{className}.*?\{{((?:.*?\n)*?)\}}";
                var classMatch = Regex.Match(code, methodPattern, RegexOptions.Singleline);

                if (classMatch.Success)
                {
                    var methods = new List<string>();
                    string constructor = null;
                    var methodPatternInClass = @"\b(?:public|private|protected|internal)?\s*(?:void|int|string|bool|float|double|var)\s+(\w+)\s*\(";
                    var constructorPattern = $@"\b{className}\s*\(";

                    var classBody = classMatch.Groups[1].Value;

                    // メソッドを抽出
                    var methodMatches = Regex.Matches(classBody, methodPatternInClass);
                    foreach (Match methodMatch in methodMatches)
                    {
                        methods.Add(methodMatch.Groups[1].Value);
                    }

                    // コンストラクタを抽出
                    var constructorMatch = Regex.Match(classBody, constructorPattern);
                    if (constructorMatch.Success)
                    {
                        constructor = className; // コンストラクタ名はクラス名と同じ
                    }

                    methodsByClass[className] = new ClassMethods(methods, constructor);
                }
            }

            return methodsByClass;
        }
    }
}

```


## デモ
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247638/86bd5d49-687e-4e27-78f4-e858881f9c9b.gif)


## Git Hub
https://github.com/i-tanaka730/ClassMethodAnalyzer