找回密码
 立即注册
首页 业界区 业界 .NET源码生成器基于partial范式开发和nuget打包 ...

.NET源码生成器基于partial范式开发和nuget打包

少屠 4 天前
一、partial范式深度探讨



  • 前文介绍了partial范式简化SourceGenerator开发和测试
  • 查阅SourceGenerator之partial范式及测试
  • 本文讲partial范式开发和nuget打包,与前文有部分重叠,侧重点不同
二、本文以自动生成属性为例

1. 功能简介



  • 场景是通过一个属性获取对象,但不需要这个对象重复创建
  • 单例模式就是其中的场景之一
  • 这样的代码是千篇一律的,非常适合自动生成代码
2. 生成器代码



  • 直接套用ValuesGenerator基类
  • 查找标记了GenerateLazy的属性和方法
  • 预处理为GenerateLazySource对象
  • 执行GenerateLazySource
  • 问题是查找属性和方法不能使用官方的SyntaxValueProvider.ForAttributeWithMetadataName
  • ForAttributeWithMetadataName只能用来找partial的类
  • 这个场景类是partial但需要从方法(或属性)入手
  • 一个类可以有多个方法(或属性)被标记,同个方法(或属性)也可以标记生成多个属性
  • 为此笔者重写了这部分代替ForAttributeWithMetadataName
  1. [Generator(LanguageNames.CSharp)]
  2. public class GenerateLazyGenerator()
  3.     : ValuesGenerator<GenerateLazySource>(
  4.         Attribute,
  5.         new SyntaxFilter(false, SyntaxKind.PropertyDeclaration, SyntaxKind.MethodDeclaration),
  6.         GenerateLazyTransform.Instance,
  7.         new GeneratorExecutor<GenerateLazySource>())
  8. {
  9.     /// <summary>
  10.     /// Attribute标记
  11.     /// </summary>
  12.     public const string Attribute = "Hand.Cache.GenerateLazyAttribute";
  13. }
复制代码
3. GenerateProvider.CreateByAttribute



  • CreateByAttribute用于代替ForAttributeWithMetadataName
  • 先遍历SyntaxTree
  • 再查找节点并处理为需要的对象
  • 这样可以覆盖ForAttributeWithMetadataName的场景并扩展支持更多的需求
  • 限于篇幅不展开所有代码,大家可以到源码库查看
  1. public static IncrementalValuesProvider<TSource> CreateByAttribute<TSource>(IncrementalGeneratorInitializationContext context, string attributeName, ISyntaxFilter filter, IGeneratorTransform<TSource> transform)
  2. {
  3.     return context.CompilationProvider
  4.         .SelectMany(GetSyntaxTree)
  5.         .SelectMany((syntaxTree, cancellationToken) => GetAttribute(syntaxTree, attributeName, filter, transform, cancellationToken))
  6.         .WithTrackingName("Provider_ByAttribute");
  7. }
复制代码
4. GenerateLazyTransform处理



  • 由于我们定位的是方法或者属性,类型(TypeDeclarationSyntax)和类型符号(typeSymbol)需要自行获取
  • 另外对类型进行了校验,必须含partial修饰符
  • GetPropertyNameByAttribute获取Attribute配置
  • 如果当前是属性就处理为LazyPropertySource
  • 如果当前是方法就处理为LazyMethodSource
  • GenerateLazySource是抽象类,LazyPropertySource和LazyMethodSource是GenerateLazySource的子类
  • CheckSource判断是否有重名对象,如果有就不生成(强行会生成编译不过的文件)
  1. public class GenerateLazyTransform : IGeneratorTransform<GenerateLazySource>
  2. {
  3.     public GenerateLazySource? Transform(AttributeContext context, CancellationToken cancellation)
  4.     {
  5.         if (cancellation.IsCancellationRequested)
  6.             return null;
  7.         var targetNode = context.TargetNode;
  8.         if (targetNode.Parent is not TypeDeclarationSyntax type || !type.Modifiers.IsPartial())
  9.             return null;
  10.         var semanticModel = context.SemanticModel;
  11.         var typeSymbol = semanticModel.GetDeclaredSymbol(type, cancellation);
  12.         if (typeSymbol is null)
  13.             return null;
  14.         var compilation = semanticModel.Compilation;
  15.         var attributeType = compilation.GetTypeByMetadataName(GenerateLazyGenerator.Attribute);
  16.         if (attributeType is null)
  17.             return null;
  18.         var propertyName = GetPropertyNameByAttribute(context.Attributes, attributeType);
  19.         GenerateLazySource? source = null;
  20.         if (targetNode is PropertyDeclarationSyntax property)
  21.         {
  22.             var propertySymbol = semanticModel.GetDeclaredSymbol(property, cancellation);
  23.             if (propertySymbol is not null && propertySymbol.Type is INamedTypeSymbol symbol)
  24.                 source = new LazyPropertySource(property, type, typeSymbol, propertyName, symbol, property.Modifiers.IsStatic());
  25.         }
  26.         else if (targetNode is MethodDeclarationSyntax method)
  27.         {
  28.             var methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellation);
  29.             if (methodSymbol is not null && methodSymbol.ReturnType is INamedTypeSymbol symbol)
  30.                 source = new LazyMethodSource(method, type, typeSymbol, propertyName, symbol, method.Modifiers.IsStatic());
  31.         }
  32.         // 判断是否已经存在同名属性
  33.         // 不存在才返回
  34.         if (source is not null && CheckSource(source, compilation))
  35.             return source;
  36.         return null;
  37.     }
  38.     /// <summary>
  39.     /// 判断延迟缓存源对象是否合法
  40.     /// </summary>
  41.     /// <param name="source"></param>
  42.     /// <param name="compilation"></param>
  43.     /// <returns></returns>
  44.     public static bool CheckSource(GenerateLazySource source, Compilation compilation)
  45.     {
  46.         var descriptor = new SymbolTypeBuilder()
  47.             .WithProperty()
  48.             .WithField()
  49.             .Build(compilation, source.Symbol);
  50.         // 存在同名属性不生成
  51.         if(descriptor.GetProperty(source.PropertyName) is not null)
  52.             return false;
  53.         // 存在同名字段不生成
  54.         if (descriptor.GetField(source.ValueName) is not null)
  55.             return false;
  56.         if (descriptor.GetField(source.StateName) is not null)
  57.             return false;
  58.         if (descriptor.GetField(source.LockName) is not null)
  59.             return false;
  60.         return true;
  61.     }
  62.     // ...
  63. }
复制代码
5. GenerateLazySource



  • 首先复制原类型,不用管是类,是结构体还是record,是否有命名空间,这些与原类型保持一致即可
  • Clone会清理一些成员(方法、字段、属性等),避免编译出错
  • 定义了3个字段1个属性
  • 属性的get处理器是用开源项目EasySyntax定义的,非常简洁
  • 使用锁和双重判断实现的线程安全懒汉单例模式
  • 如果原方法(或属性)是静态的,生成对象也增加静态修饰符
  • GetValueExpression是从原代码中提取代码,这在LazyPropertySource和LazyMethodSources实现稍有不同
  1. public abstract class GenerateLazySource(TypeDeclarationSyntax type, INamedTypeSymbol typeSymbol, string propertyName, INamedTypeSymbol propertySymbol, bool isStatic, string valueName, string stateName, string lockName)
  2.     : IGeneratorSource
  3. {
  4.     public SyntaxGenerator Generate()
  5.     {
  6.         var builder = SyntaxGenerator.Clone(_type);
  7.         var _valueField = _propertyType.Field(_value.Identifier, SyntaxGenerator.DefaultLiteral)
  8.             .Private();
  9.         var _stateField = SyntaxGenerator.BoolType.Field(_state.Identifier, SyntaxGenerator.FalseLiteral)
  10.             .Private();
  11.         var _lockField = SyntaxGenerator.LockType.Field(_lock.Identifier, SyntaxFactory.ImplicitObjectCreationExpression())
  12.             .Private();
  13.         var property = _propertyType.Property(_propertyName, CreateAccessor())
  14.             .Public();
  15.         if (_isStatic)
  16.         {
  17.             builder.AddMember(_valueField.Static());
  18.             builder.AddMember(_stateField.Static());
  19.             builder.AddMember(_lockField.Static());
  20.             builder.AddMember(property.Static());
  21.         }
  22.         else
  23.         {
  24.             builder.AddMember(_valueField);
  25.             builder.AddMember(_stateField);
  26.             builder.AddMember(_lockField);
  27.             builder.AddMember(property);
  28.         }
  29.         
  30.         return builder;
  31.     }
  32.     /// <summary>
  33.     /// 构造属性处理器
  34.     /// </summary>
  35.     /// <returns></returns>
  36.     public AccessorDeclarationSyntax CreateAccessor()
  37.     {
  38.         return SyntaxGenerator.PropertyGetDeclaration()
  39.             .ToBuilder()
  40.             // if(_state)
  41.             .If(_state)
  42.                 // return _value
  43.                 .Return(_value)
  44.             // lock(_lock){
  45.             .Lock(_lock)
  46.                 //if(_state)
  47.                 .If(_state)
  48.                     // return _value
  49.                     .Return(_value)
  50.                 // _value = GetValue()
  51.                 .Add(_value.Assign(GetValueExpression()))
  52.                 // _state = true
  53.                 .Add(_state.Assign(SyntaxGenerator.TrueLiteral))
  54.                 // }
  55.                 .End()
  56.             // reurn _value
  57.             .Return(_value);
  58.     }
  59.     protected abstract ExpressionSyntax GetValueExpression();
  60.     // ...
  61.   }
复制代码
6. 按方法生成属性的Case

6.1 原始代码
  1. using Hand;
  2. using Hand.Cache;
  3. namespace GenerateCachedPropertyTests;
  4. public partial class MethodTests
  5. {
  6.     [GenerateLazy(""LazyTime"")]
  7.     public DateTime CreateTime()
  8.     {
  9.         return DateTime.Now;
  10.     }
  11. }
复制代码
6.2 生成代码
  1. //
  2. namespace GenerateCachedPropertyTests;
  3. partial class MethodTests
  4. {
  5.     private System.DateTime _valueLazyTime = default;
  6.     private bool _stateLazyTime = false;
  7.     private object _lockLazyTime = new();
  8.     public System.DateTime LazyTime
  9.     {
  10.         get
  11.         {
  12.             if (_stateLazyTime)
  13.                 return _valueLazyTime;
  14.             lock (_lockLazyTime)
  15.             {
  16.                 if (_stateLazyTime)
  17.                     return _valueLazyTime;
  18.                 _valueLazyTime = DateTime.Now;
  19.                 _stateLazyTime = true;
  20.             }
  21.             return _valueLazyTime;
  22.         }
  23.     }
  24. }
复制代码
7. 按属性生成属性的Case

7.1 原始代码
  1. using Hand;
  2. using Hand.Cache;
  3. namespace GenerateCachedPropertyTests;
  4. public partial class PropertyTests
  5. {
  6.     [GenerateLazy(""LazyTime"")]
  7.     public static DateTime Now { get; } = DateTime.Now;
  8. }
复制代码
7.2 生成代码



  • 原属性Now是静态的,生成的LazyTime也是静态的
  1. //
  2. namespace GenerateCachedPropertyTests;
  3. partial class PropertyTests
  4. {
  5.     private static System.DateTime _valueLazyTime = default;
  6.     private static bool _stateLazyTime = false;
  7.     private static object _lockLazyTime = new();
  8.     public static System.DateTime LazyTime
  9.     {
  10.         get
  11.         {
  12.             if (_stateLazyTime)
  13.                 return _valueLazyTime;
  14.             lock (_lockLazyTime)
  15.             {
  16.                 if (_stateLazyTime)
  17.                     return _valueLazyTime;
  18.                 _valueLazyTime = Now;
  19.                 _stateLazyTime = true;
  20.             }
  21.             return _valueLazyTime;
  22.         }
  23.     }
  24. }
复制代码
三、生成器nuget打包技巧

1. 开发容易打包难



  • 特别是包含依赖项的生成器打包更难
  • 首先分享一篇博客园扑克子博主的经验
  • 非常感谢这个博主,再此基础上笔者摸索出更好的方法
  • partial范式依赖EasySyntax和GenerateCore还有Microsoft.CodeAnalysis.CSharp的5.0版本
  • 如果不打包这些依赖会导致生成器无法正常工作
  • 打包方式不对又会导致生成器及依赖项目的dll会出现被调用项目的生成目录
  • 正常情况生成器用于编译时代码生成,自己本身不输出到生成目录
2. 还是自动生成属性项目为例



  • TargetFramework最好设置为netstandard2.0
  • EnforceExtendedAnalyzerRules最好设置为true
  • IncludeBuildOutput设置为fase,这是避免生成器本身输出到生成目录
  • 引用的IncludeAssets设置为compile和analyzers,compile是为了生成器本身编译,analyzers是为了能用于执行生成器时调用
  • PrivateAssets为compile是为了排除生成器的依赖项目参与调用生成器项目的编译,因为它是本项目私有不继续传递,会被排除
  • 最后None配置到analyzers/dotnet/cs就是生成器目录专用
  • 另外NoWarn配置为NU5128,是排除警告信息,由于生成器值只需要analyzers文件夹,没有lib文件夹导致警告是误报
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.         <PropertyGroup>
  3.                 <TargetFramework>netstandard2.0</TargetFramework>
  4.                 <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  5.                 <IncludeBuildOutput>false</IncludeBuildOutput>
  6.                 <NoWarn>$(NoWarn);NU5128</NoWarn>
  7.         </PropertyGroup>
  8.         <ItemGroup>
  9.                 <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
  10.                         <IncludeAssets>compile;analyzers</IncludeAssets>
  11.                         <PrivateAssets>compile</PrivateAssets>
  12.                 </PackageReference>
  13.                 <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0">
  14.                         <IncludeAssets>compile;analyzers</IncludeAssets>
  15.                         <PrivateAssets>compile</PrivateAssets>
  16.                 </PackageReference>
  17.         </ItemGroup>
  18.         <ItemGroup>
  19.                 <ProjectReference Include="..\Hand.GenerateCore\Hand.GenerateCore.csproj">
  20.                         <IncludeAssets>compile;analyzers</IncludeAssets>
  21.                         <PrivateAssets>compile</PrivateAssets>
  22.                 </ProjectReference>
  23.                 <ProjectReference Include="..\Hand.Generators.EasySyntax\Hand.Generators.EasySyntax.csproj">
  24.                         <IncludeAssets>compile;analyzers</IncludeAssets>
  25.                         <PrivateAssets>compile</PrivateAssets>
  26.                 </ProjectReference>
  27.         </ItemGroup>
  28.         <ItemGroup>
  29.                 <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  30.         </ItemGroup>
  31. </Project>
复制代码
3. 生成器依赖项目需要特殊配置



  • EasySyntax和GenerateCore就是生成器依赖项目
  • TargetFramework最好设置为netstandard2.0
  • EnforceExtendedAnalyzerRules最好设置为true
  • None也配置到analyzers/dotnet/cs是为了生成器调用
  • 这样nuget里面有两份dll,lib的可以直接引用,analyzers里面的生成器
  1. <Project Sdk="Microsoft.NET.Sdk">
  2.         <PropertyGroup>
  3.                 <TargetFramework>netstandard2.0</TargetFramework>
  4.                 <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  5.         </PropertyGroup>
  6.         <ItemGroup>
  7.                 <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
  8.                 <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
  9.         </ItemGroup>
  10.         <ItemGroup>
  11.                 <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  12.         </ItemGroup>
  13. </Project>
复制代码
4. 脚本的方法



  • 安装时执行install.ps1
  • 卸载时执行uninstall.ps1
  • 参考开源项目OneOf.SourceGenerator
  1. param($installPath, $toolsPath, $package, $project)
  2. $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve
  3. foreach($analyzersPath in $analyzersPaths)
  4. {
  5.     # Install the language agnostic analyzers.
  6.     if (Test-Path $analyzersPath)
  7.     {
  8.         foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll)
  9.         {
  10.             if($project.Object.AnalyzerReferences)
  11.             {
  12.                 $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
  13.             }
  14.         }
  15.     }
  16. }
  17. # $project.Type gives the language name like (C# or VB.NET)
  18. $languageFolder = ""
  19. if($project.Type -eq "C#")
  20. {
  21.     $languageFolder = "cs"
  22. }
  23. if($project.Type -eq "VB.NET")
  24. {
  25.     $languageFolder = "vb"
  26. }
  27. if($languageFolder -eq "")
  28. {
  29.     return
  30. }
  31. foreach($analyzersPath in $analyzersPaths)
  32. {
  33.     # Install language specific analyzers.
  34.     $languageAnalyzersPath = join-path $analyzersPath $languageFolder
  35.     if (Test-Path $languageAnalyzersPath)
  36.     {
  37.         foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll)
  38.         {
  39.             if($project.Object.AnalyzerReferences)
  40.             {
  41.                 $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
  42.             }
  43.         }
  44.     }
  45. }
复制代码
四、总结

1. 打包方法1



  • 需要的文件文件都通过PackagePath输出到analyzers
  • 问题是nuget包会增大,丢失了项目的依赖关系
2. 打包方法2



  • 通过IncludeAssets和PrivateAssets准确配置包作用域
  • PackagePath只打包当前文件
  • 问题是依赖自己不能控制的包(没有analyzers文件夹)不好处理
3. 打包方法3



  • 通过脚本处理nuget安装和卸载,无需analyzers输出
  • 缺点是要写额外脚本
4.笔者推荐方法2



  • 大家喜欢哪种方法呢
  • 有的时候可能需要不同方法配置使用
源码托管地址: https://github.com/donetsoftwork/Hand.Generators ,欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/hand.-generators
如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册