用Roslyn玩转代码之一: 解析与执行字符串表达式
??最近框架中的可視化界面設(shè)計(jì)需要使用到表達(dá)式引擎(解析代碼字符串并動(dòng)態(tài)執(zhí)行),之前舊框架的實(shí)現(xiàn)是將表達(dá)式字符串解析為語(yǔ)法樹(shù)后解釋執(zhí)行該表達(dá)式,本文介紹如何使用Roslyn解析表達(dá)式字符串,并直接轉(zhuǎn)換為L(zhǎng)inq的表達(dá)式后編譯執(zhí)行。
一、語(yǔ)法(Syntax)與語(yǔ)義(Semantic)
??C#的代碼通過(guò)Roslyn解析為相應(yīng)的語(yǔ)法樹(shù),并且利用語(yǔ)義分析可以獲取語(yǔ)法節(jié)點(diǎn)所對(duì)應(yīng)的符號(hào)及類(lèi)型信息,這樣利用這些信息可以正確的轉(zhuǎn)換為L(zhǎng)inq的表達(dá)式。這里作者就不展開(kāi)了,可以參考Roslyn文檔。
- 語(yǔ)法分析
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/syntax-analysis - 語(yǔ)義分析
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/semantic-analysis - 語(yǔ)法轉(zhuǎn)換
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/syntax-transformation
二、實(shí)現(xiàn)表達(dá)式解析器(ExpressionParser)
1. 解析字符串方法
??下面開(kāi)始創(chuàng)建一個(gè)類(lèi)庫(kù)工程,引用包Microsoft.CodeAnalysis.CSharp.Features,然后參照以下代碼創(chuàng)建ExpressionParser類(lèi), 靜態(tài)ParseCode()方法是解析字符串表達(dá)式的入口:
using System.Linq.Expressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace ExpEngine;
public sealed class ExpressionParser : CSharpSyntaxVisitor<Expression>
{
private ExpressionParser(SemanticModel semanticModel)
{
_semanticModel = semanticModel;
}
private readonly SemanticModel _semanticModel;
/// <summary>
/// 解析表達(dá)式字符串轉(zhuǎn)換為L(zhǎng)inq的表達(dá)式
/// </summary>
public static Expression ParseCode(string code)
{
var parseOptions = new CSharpParseOptions().WithLanguageVersion(LanguageVersion.CSharp11);
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithNullableContextOptions(NullableContextOptions.Enable);
var tree = CSharpSyntaxTree.ParseText(code, parseOptions);
var root = tree.GetCompilationUnitRoot();
var compilation = CSharpCompilation.Create("Expression", options: compilationOptions)
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location))
.AddSyntaxTrees(tree);
var semanticModel = compilation.GetSemanticModel(tree);
//檢查是否存在語(yǔ)義錯(cuò)誤
var diagnostics = semanticModel.GetDiagnostics();
var errors = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error);
if (errors > 0)
throw new Exception("表達(dá)式存在語(yǔ)義錯(cuò)誤");
var methodDecl = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First();
if (methodDecl.Body != null && methodDecl.Body.Statements.Count > 1)
throw new NotImplementedException("Parse block body");
if (methodDecl.ExpressionBody != null)
throw new NotImplementedException("Parse expression body");
var firstStatement = methodDecl.Body!.Statements.FirstOrDefault();
if (firstStatement is not ReturnStatementSyntax returnNode)
throw new Exception("表達(dá)式方法不是單行返回語(yǔ)句");
var parser = new ExpressionParser(semanticModel);
return parser.Visit(returnNode.Expression)!;
}
}
2. 解析運(yùn)行時(shí)類(lèi)型的方法
??因?yàn)檗D(zhuǎn)換過(guò)程中需要將Roslyn解析出來(lái)的類(lèi)型信息轉(zhuǎn)換為對(duì)應(yīng)的C#運(yùn)行時(shí)的類(lèi)型,所以需要實(shí)現(xiàn)類(lèi)型轉(zhuǎn)換的方法:
private readonly Dictionary<string, Type> _knownTypes = new()
{
{ "bool", typeof(bool) },
{ "byte", typeof(byte) },
{ "sbyte", typeof(sbyte) },
{ "short", typeof(short) },
{ "ushort", typeof(ushort) },
{ "int", typeof(int) },
{ "uint", typeof(uint) },
{ "long", typeof(long) },
{ "ulong", typeof(ulong) },
{ "float", typeof(float) },
{ "double", typeof(double) },
{ "char", typeof(char) },
{ "string", typeof(string) },
{ "object", typeof(object) },
};
/// <summary>
/// 根據(jù)類(lèi)型字符串獲取運(yùn)行時(shí)類(lèi)型
/// </summary>
private Type ResolveType(string typeName)
{
if (_knownTypes.TryGetValue(typeName, out var sysType))
return sysType;
//通過(guò)反射獲取類(lèi)型
var type = Type.GetType(typeName);
if (type == null)
throw new Exception($"Can't find type: {typeName} ");
return type;
}
3. 解析各類(lèi)語(yǔ)法節(jié)點(diǎn)轉(zhuǎn)換為對(duì)應(yīng)的Linq表達(dá)式
??這里舉一個(gè)簡(jiǎn)單的LiteralExpression轉(zhuǎn)換的例子,其他請(qǐng)參考源碼。需要注意的是Linq的表達(dá)式嚴(yán)格匹配類(lèi)型簽名,比如方法調(diào)用object.Equals(object a, object b), 如果參數(shù)a是int類(lèi)型,需要使用Expression.Convert(int, typeof(object))轉(zhuǎn)換為相應(yīng)的類(lèi)型。
private Type? GetConvertedType(SyntaxNode node)
{
var typeInfo = _semanticModel.GetTypeInfo(node);
Type? convertedType = null;
if (!SymbolEqualityComparer.Default.Equals(typeInfo.Type, typeInfo.ConvertedType))
convertedType = ResolveType((INamedTypeSymbol)typeInfo.ConvertedType!);
return convertedType;
}
public override Expression? VisitLiteralExpression(LiteralExpressionSyntax node)
{
var convertedType = GetConvertedType(node);
var res = Expression.Constant(node.Token.Value);
return convertedType == null ? res : Expression.Convert(res, convertedType);
}
三、測(cè)試解析與執(zhí)行表達(dá)式
??現(xiàn)在可以創(chuàng)建一個(gè)單元測(cè)試項(xiàng)目驗(yàn)證一下解析字符串表達(dá)式并執(zhí)行了,當(dāng)然實(shí)際應(yīng)用過(guò)程中應(yīng)緩存解析并編譯的表達(dá)式委托:
namespace UnitTests;
using static ExpEngine.ExpressionParser;
public class Tests
{
[Test]
public void StaticPropertyTest() => Assert.True(Run<object>("DateTime.Today") is DateTime);
[Test]
public void InstancePropertyTest() => Run<int>("DateTime.Today.Year");
[Test]
public void MethodCallTest1() => Run<DateTime>("DateTime.Today.AddDays(1 + 1)");
[Test]
public void MethodCallTest2() => Run<DateTime>("DateTime.Today.AddDays(DateTime.Today.Year)");
[Test]
public void MethodCallTest3() => Run<DateTime>("DateTime.Today.AddDays(int.Parse(\"1\"))");
[Test]
public void MethodCallTest4() => Assert.True(Run<bool>("Equals(new DateTime(1977,3,1), new DateTime(1977,3,1))"));
[Test]
public void PrefixUnaryTest() => Run<DateTime>("DateTime.Today.AddDays(-1)");
[Test]
public void NewTest() => Assert.True(Run<DateTime>("new DateTime(1977,3,16)") == new DateTime(1977, 3, 16));
[Test]
public void BinaryTest1() => Assert.True(Run<float>("3 + 2.6f") == 3 + 2.6f);
[Test]
public void BinaryTest2() => Assert.True(Run<bool>("3 >= 2.6f"));
}
四、 一些限制與TODO
??Linq的表達(dá)式本身存在一些限制,請(qǐng)參考文檔:
https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/
??另上述代碼僅示例,比如表達(dá)式輸入?yún)?shù)等未實(shí)現(xiàn),小伙伴們可以繼續(xù)自行完善。
總結(jié)
以上是生活随笔為你收集整理的用Roslyn玩转代码之一: 解析与执行字符串表达式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: uni-app+vue3+ts项目搭建完
- 下一篇: 开心自走棋:使用 Laf 云开发支撑数百