用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代 ...
随机推荐
- oracle优化-分页查询的错误认识
对于分页查询,上一篇文章总结了实现分页查询的办法.同时给出等价写法,另外在执行计划角度验证SQL的等价性https://www.cnblogs.com/handhead/p/13856505.html ...
- linux常用命令(六)
用于查找系统文件的相关命令 grep find locate grep:查找文件中符号条件的字符串(关键词) 命令语法:grep [选项] 查找模式 [文件名] 选项 选项含义 -E 模式是一个可扩展 ...
- Jmeter内的参数有文件时,如何传参?
文件类型:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 尊重原创,转载请注明出处,谢谢!!
- 从0到1实现 OpenTiny 组件库跨框架技术
本文分享自华为云社区<从0到1实现 OpenTiny 组件库跨框架技术>,作者:华为云社区精选 . 在华为云<DTSE Tech Talk>技术直播第44期<0基础玩转 ...
- 一篇了解springboot3请求参数种类及接口测试
SpringBoot3数据请求: 原始数据请求: //原始方式 @RequestMapping("/simpleParam") public String simpleParam( ...
- keepalived部署+nginx高可用
nginx+keepalived搞性能web网络架构实战配置: 环境准备: keepalived+nginx-1: 192.168.1.23 keepalived+nginx-2: 192.168.1 ...
- http协议与apache
http协议与apache 1.httpd协议 两台主机通信需要socket文件 yum insatll -y nc [root@localhost ~]#nc -l 8000 #主机1 ...
- Langchain-Chatchat项目:5.1-ChatGLM3-6B工具调用
在语义.数学.推理.代码.知识等不同角度的数据集上测评显示,ChatGLM3-6B-Base 具有在10B以下的基础模型中最强的性能.ChatGLM3-6B采用了全新设计的Prompt格式,除正常 ...
- Spring Cloud Seata 系列:Seata-Server (1.7.1)安装与配置(集成 Nacos)
目录 一.简介 术语 事务模式 XA 模式 AT 模式 TCC 模式 Sage 模式 说明 二.Seata Server 存储模式 1.file 模式 ①修改application.xml ②启动Se ...
- KMeans算法全面解析与应用案例
本文深入探讨了KMeans聚类算法的核心原理.实际应用.优缺点以及在文本聚类中的特殊用途,为您在聚类分析和自然语言处理方面提供有价值的见解和指导. 关注TechLead,分享AI全维度知识.作者拥有1 ...