Анализ кода на языке C# с использованием Roslyn

Одной из самых важных частей Roslyn является возможность анализа кода – Roslyn Compiler API. Для экспериментов с Roslyn Compiler API возьмем файл из библиотеки Monads.NET. Я разместил этот файл в ресурсах приложения, чтобы иметь быстрый доступ к нему из любого места в коде.

Complier API начинается с класса SyntaxTree – это класс являющийся точкой входа для работы с Roslyn Compiler API. Чтобы выполнить разбор исходного кода, необходимо вызвать статический метод ParseText (если исходный код содержится в строковой переменной) или ParseFile (если исходный код находится в файле, путь до конторого известен) у базового объекта SyntaxTree.

При анализе кода в Roslyn Compiler API очень удобно получить список всех дочерних элементов, а затем по типу выбрать то, что нас интересует. Каждый тип блока кода имеет собственный класс-наследник базового класса SyntaxNode. Например, элемент, описывающий метод класса представлен классом MethodDeclarationSyntax, элемент, описывающий цикл while – классом WhileStatementSyntax и т.д. Таким образом, чтобы получить список всех методов, которые находятся в исследуемом исходном файле можно использовать следующий код:

SyntaxTree tree = SyntaxTree.ParseText(source);
IEnumerable<MethodDeclarationSyntax> methods = tree
  .GetRoot()
  .DescendantNodes()
  .OfType<MethodDeclarationSyntax>();

При обращении к методу DescendantNodes() можно использовать лямбда-выражение, которое также отфильтрует набор исходных элементов.

После получения списка методов можно пройтись по нему и отобразить имя каждого метода:

foreach (MethodDeclarationSyntax method in methods)
{
	SyntaxToken methodName = method.Identifier;
	Console.WriteLine("{0}()", methodName);
}

Поскольку MethodDeclarationSyntax и другие синтаксические единицы наследники одного и того же базового класса, дальнейшие действия по анализу кода сводятся к похожим конструкциям. Давайте подсчитаем количество блоков разного типа в каждом из методов. Для этого сначала получим тело метода – это элемент типа BlockSyntax, внутри которого будем искать элементы нужного нам типа.

SyntaxTree tree = SyntaxTree.ParseText(source);
IEnumerable<MethodDeclarationSyntax> methods = tree
  .GetRoot()
  .DescendantNodes()
  .OfType<MethodDeclarationSyntax>();

foreach (MethodDeclarationSyntax method in methods)
{
  SyntaxToken methodName = method.Identifier;
  BlockSyntax methodBody = method.ChildNodes().OfType<BlockSyntax>().Single();

  int ifStatementCount = methodBody.DescendantNodes().OfType<IfStatementSyntax>().Count();
  int returnStatementCount = methodBody.DescendantNodes().OfType<ReturnStatementSyntax>().Count();
  int throwStatementCount = methodBody.DescendantNodes().OfType<ThrowStatementSyntax>().Count();
  int tryStatementCount = methodBody.DescendantNodes().OfType<TryStatementSyntax>().Count();

  Console.WriteLine("{0}()", methodName);
  Console.WriteLine("if: {0}\treturn: {1}\tthrow: {2}\ttry: {3}",
    ifStatementCount, returnStatementCount, throwStatementCount, tryStatementCount);
  Console.WriteLine();
}

Здесь для каждого метода подсчитывается число дочерних элементов каждого из типов.

Далее запросы к коду строятся аналогично. Когда это может быть полезно? Вариантов применения множество – подсчет метрик кода, любой анализ кода, автоматизированный рефакторинг кода и т.д.