.NET C# mit Roslyn
Dabei kann man diverse Schritte zwischen C# und IL analysieren und auch manipulieren (in Form von Ergänzung). Die Manipulation macht unter anderem bei Aspekt orientierter Programmierung Sinn, wenn man Cross-Cutting-Concerns auslagern will. Das ging bis dato nur mit PostSharp gut. Vor- und Nachteile wurden hier im Detail abgehandelt. Ich habe Roslyn für die Analyse von Source-Code verwendet. Es funktioniert sehr gut. Die Challenge ist eher der Kompilierprozess per API – der ist ein wenig wackelig.
using System;
using System.Text.Json;
namespace HelloWorld
{
public interface IWeatherForecast
{
void makeASunnyDay();
}
public class WeatherForecast : IWeatherForecast
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
public void makeASunnyDay()
{
TemperatureCelsius = 39;
Summary = "Sunny";
}
}
class Program
{
static void Main(string[] args)
{
var weatherForecast = new WeatherForecast
{
Date = DateTime.Parse("2019-08-01"),
TemperatureCelsius = 25,
Summary = "Hot"
};
string jsonString = JsonSerializer.Serialize(weatherForecast);
Console.WriteLine(jsonString);
}
}
}
Das ist das zu analysierende Programm. Ansclhießend kann man mit der Roslyn API direkt Analysen drauf tätigen:
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RoslynHelloWorld
{
class Program
{
static async Task Main(string[] args)
{
MSBuildLocator.RegisterDefaults();
var slnFile = new FileInfo(@"C:\Users\michael.vodep\source\repos\HelloWorld\HelloWorld.sln");
RestoreNuget(slnFile);
using (var workspace = MSBuildWorkspace.Create())
{
workspace.WorkspaceFailed += WorkspaceFailed;
var solution = await workspace.OpenSolutionAsync(slnFile.FullName);
var helloWorldProject = solution.Projects.Single(p => p.Name == "HelloWorld");
var helloWorldCompilation = await helloWorldProject.GetCompilationAsync();
PrintErrors(helloWorldCompilation);
// Was a reference Foo added?
var referenceAdded = helloWorldCompilation.References.Any(r => r.Display.Contains("Foo"));
Console.WriteLine($"Reference Foo was added: {referenceAdded}");
// Was a method called?
var serializeMethodCalls = await FindSymbolInfowByMethodAsync(helloWorldProject, "System.Text.Json.JsonSerializer", "Serialize");
foreach(var methodCall in serializeMethodCalls)
{
Console.WriteLine($"{methodCall.SymbolInfo.Name} -> {string.Join(",", methodCall.Arguments)}");
}
// Does the class with the interface IWeatherForcast has a specific member?
var weatherForcastSymbolInfo = GetNamedTypeSymbols(helloWorldCompilation).FirstOrDefault(s => s.Interfaces.Any(i => i.Name == "IWeatherForecast"));
var hasMember = weatherForcastSymbolInfo.MemberNames.Any(m => m == "Date");
Console.WriteLine($"Class with interface IWeatherForecast has member Date: {hasMember}");
}
Console.ReadKey();
}
private static void PrintErrors(Compilation compilation)
{
Console.ForegroundColor = ConsoleColor.Red;
foreach (var error in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error))
{
Console.WriteLine($"ERROR {error.GetMessage()}");
}
Console.ResetColor();
}
private static void WorkspaceFailed(object sender, WorkspaceDiagnosticEventArgs e)
{
Console.WriteLine(e.Diagnostic.ToString());
}
private static void RestoreNuget(FileInfo slnFile)
{
using (Process compiler = new Process())
{
compiler.StartInfo.FileName = "dotnet";
compiler.StartInfo.Arguments = $"restore {slnFile}";
compiler.StartInfo.UseShellExecute = true;
compiler.Start();
compiler.WaitForExit();
}
}
private class MethodCallFindResult
{
public IEnumerable<string> Arguments { get; internal set; }
public ISymbol SymbolInfo { get; internal set; }
}
private static async Task<IList<MethodCallFindResult>> FindSymbolInfowByMethodAsync(Project project, string containingType, string methodName)
{
IList<MethodCallFindResult> result = new List<MethodCallFindResult>();
foreach (var document in project.Documents)
{
var syntaxRoot = await document.GetSyntaxRootAsync();
var semanticModel = await document.GetSemanticModelAsync();
foreach (var methodInvocation in syntaxRoot.DescendantNodes().OfType<InvocationExpressionSyntax>())
{
var findResult = new MethodCallFindResult
{
Arguments = methodInvocation.ArgumentList.Arguments.Select(a => a.ToString())
};
var symbolInfo = semanticModel.GetSymbolInfo(methodInvocation).Symbol;
if (symbolInfo.Name == methodName && symbolInfo.ContainingType.ToString() == containingType)
{
findResult.SymbolInfo = symbolInfo;
result.Add(findResult);
}
}
}
return result;
}
private static IEnumerable<INamedTypeSymbol> GetNamedTypeSymbols(Compilation compilation)
{
var stack = new Stack<INamespaceSymbol>();
stack.Push(compilation.GlobalNamespace);
while (stack.Count > 0)
{
var @namespace = stack.Pop();
foreach (var member in @namespace.GetMembers())
{
if (member is INamespaceSymbol memberAsNamespace)
{
stack.Push(memberAsNamespace);
}
else if (member is INamedTypeSymbol memberAsNamedTypeSymbol)
{
yield return memberAsNamedTypeSymbol;
}
}
}
}
}
}
Ein paar wichtige Findings:
- MSBuildLocator.RegisterDefaults(): Ohne dem konnte ich nicht kompilieren. In den Tiefen der GitHub Issues gefunden
- Das Suchen von Methodenaufrufen (FindSymbolInfowByMethodAsync) ist auch nicht einfach - aber logisch. Wenn man es in Visual Studio macht, dann klickt man zuerst auch auf ein Source-Code Fragment und sucht dann danach. Nichts anderes macht der Code.
Fazit: Roslyn öffnet die Tore für Code-Analyse. Tolle Sache!