用Roslyn玩转代码之一: 解析与执行字符串表达式

最近框架中的可视化界面设计需要使用到表达式引擎(解析代码字符串并动态执行),之前旧框架的实现是将表达式字符串解析为语法树后解释执行该表达式,本文介绍如何使用Roslyn解析表达式字符串,并直接转换为Linq的表达式后编译执行。
一、语法(Syntax)与语义(Semantic)
C#的代码通过Roslyn解析为相应的语法树,并且利用语义分析可以获取语法节点所对应的符号及类型信息,这样利用这些信息可以正确的转换为Linq的表达式。这里作者就不展开了,可以参考Roslyn文档。
- 语法分析
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/syntax-analysis - 语义分析
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/semantic-analysis - 语法转换
https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/syntax-transformation
二、实现表达式解析器(ExpressionParser)
1. 解析字符串方法
下面开始创建一个类库工程,引用包Microsoft.CodeAnalysis.CSharp.Features,然后参照以下代码创建ExpressionParser类, 静态ParseCode()方法是解析字符串表达式的入口:
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>
/// 解析表达式字符串转换为Linq的表达式
/// </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);
//检查是否存在语义错误
var diagnostics = semanticModel.GetDiagnostics();
var errors = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error);
if (errors > 0)
throw new Exception("表达式存在语义错误");
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("表达式方法不是单行返回语句");
var parser = new ExpressionParser(semanticModel);
return parser.Visit(returnNode.Expression)!;
}
}
2. 解析运行时类型的方法
因为转换过程中需要将Roslyn解析出来的类型信息转换为对应的C#运行时的类型,所以需要实现类型转换的方法:
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>
/// 根据类型字符串获取运行时类型
/// </summary>
private Type ResolveType(string typeName)
{
if (_knownTypes.TryGetValue(typeName, out var sysType))
return sysType;
//通过反射获取类型
var type = Type.GetType(typeName);
if (type == null)
throw new Exception($"Can't find type: {typeName} ");
return type;
}
3. 解析各类语法节点转换为对应的Linq表达式
这里举一个简单的LiteralExpression转换的例子,其他请参考源码。需要注意的是Linq的表达式严格匹配类型签名,比如方法调用object.Equals(object a, object b), 如果参数a是int类型,需要使用Expression.Convert(int, typeof(object))转换为相应的类型。
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);
}
三、测试解析与执行表达式
现在可以创建一个单元测试项目验证一下解析字符串表达式并执行了,当然实际应用过程中应缓存解析并编译的表达式委托:
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的表达式本身存在一些限制,请参考文档:
https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/
另上述代码仅示例,比如表达式输入参数等未实现,小伙伴们可以继续自行完善。
用Roslyn玩转代码之一: 解析与执行字符串表达式的更多相关文章
- 在C#开发中使用第三方组件LambdaParser、DynamicExpresso、Z.Expressions,实现动态解析/求值字符串表达式
在进行项目开发的时候,刚好需要用到对字符串表达式进行求值的处理场景,因此寻找了几个符合要求的第三方组件LambdaParser.DynamicExpresso.Z.Expressions,它们各自功能 ...
- 分享非常有用的Java程序 (关键代码)(六)---解析/读取XML 文件(重要)
原文:分享非常有用的Java程序 (关键代码)(六)---解析/读取XML 文件(重要) XML文件 <?xml version="1.0"?> <student ...
- Java打印整数的二进制表示(代码与解析)
Java打印整数的二进制表示(代码与解析) int a=-99; for(int i=0;i<32;i++){ int t=(a & 0x80000000>>>i)&g ...
- 《编译原理》画 DAG 图与求优化后的 4 元式代码- 例题解析
<编译原理>画 DAG 图与求优化后的 4 元式代码- 例题解析 DAG 图(Directed Acylic Graph)无环路有向图 (一)基本块 基本块是指程序中一顺序执行的语句序列, ...
- webpack优化之玩转代码分割和公共代码提取
前言 开发多页应用的时候,如果不对webpack打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码).当项目业务越来越复杂,打包出来 ...
- 人脸跟踪开源项目HyperFT代码算法解析及改进
一.简介 人脸识别已经成为计算机视觉领域中最热门的应用之一,其中,人脸信息处理的第一个环节便是人脸检测和人脸跟踪.人脸检测是指在输入的图像中确定所有人脸的位置.大小和姿势的过程.人脸跟踪是指在图像序列 ...
- Python使用pyexecjs代码案例解析
针对现在大部分的网站都是使用js加密,js加载的,并不能直接抓取出来,这时候就不得不适用一些三方类库来执行js语句 execjs,一个比较好用且容易上手的类库(支持py2,与py3),支持 JS ru ...
- 【技巧总结】Penetration Test Engineer[3]-Web-Security(SQL注入、XXS、代码注入、命令执行、变量覆盖、XSS)
3.Web安全基础 3.1.HTTP协议 1)TCP/IP协议-HTTP 应用层:HTTP.FTP.TELNET.DNS.POP3 传输层:TCP.UDP 网络层:IP.ICMP.ARP 2)常用方法 ...
- JS的解析与执行过程
JS的解析与执行过程 全局中的解析和执行过程 预处理:创建一个词法环境(LexicalEnvironment,在后面简写为LE),扫描JS中的用声明的方式声明的函数,用var定义的变量并将它们加到预处 ...
- 如何执行字符串的PHP代码
如何执行字符串的PHP代码 最近因项目需要,引出一个议题:如何执行字符串的php代码(php和html混写). 注:传统情况下,php代码存储在文件中,直接运行文件即可.以下讨论的情况是,如果php代 ...
随机推荐
- MongoDB慢日志
MongoDB慢日志 熟悉 Mysql 的人应该知道,Mysql 是有个慢查询日志的,它可以帮助我们进行优化我们的 sql,并提高我们系统的稳定性和流畅性.那么 MongoDB 中是否也有类似的功 ...
- 专为小白打造—Kafka一篇文章从入门到入土
一.什么是Kafka MQ消息队列作为最常用的中间件之一,其主要特性有:解耦.异步.限流/削峰. Kafka 和传统的消息系统(也称作消息中间件)都具备系统解耦.冗余存储.流量削峰.缓冲.异步通信.扩 ...
- Oracle字符串函数-Translate()总结
Oracle的Translate(expr,from_string,to_string)是字符串操作函数,实现from_string,to_string字符的一 一替换 1)典型示例: select ...
- Programming abstractions in C阅读笔记:p166-p175
<Programming Abstractions In C>学习第58天,p166-p175总结. 一.技术总结 1.斐波那契数列(Fibonacci Sequenc) (1)斐波那契数 ...
- 11G RAC 11.2.0.1.0实例evict故障处理
Aix 7.1 参考文档: https://blogs.oracle.com/database4cn/rac Resolving ORA-481 and "terminating the i ...
- jmeter不用工具获取随机值的几种方法
第一种:直接获取 "vcContent": "${__time(yyyyMMddHHmmss)}${__RandomString(8,QWERTYUIOPASDFGHJK ...
- umich cv-2-2
UMICH CV Linear Classifiers 在上一篇博文中,我们讨论了利用损失函数来判断一个权重矩阵的好坏,在这节中我们将讨论如何去找到最优的权重矩阵 想象我们要下到一个峡谷的底部,我们自 ...
- [最优化DP]决策单调性
决策单调性的概念&证明工具: 决策单调性,是在最优化dp中的可能出现的一种性质,利用它我们可以降低转移的复杂度. 首先dp中会有转移,每个状态都由若干个状态转移而来,最优化dp比较特殊,只能由 ...
- 【pwn】ciscn_2019_s_3 -- rop,gadget利用,泄露栈地址
这道题挺好的,可以帮助我更好的理解gadget的利用以及rop技术 首先,查一下程序保护情况 拖进ida分析 这里sys_read和sys_write是系统调用函数,看汇编可以分析出来 我们首先要了解 ...
- OPC 协议数据采集
kepserver OPC Connectivity Suite 让系统和应用程序工程师能够从单一应用程序中管理他们的 OPC 数据访问 (DA) 和 OPC 统一架构 (UA) 服务器.通过减少 ...