官方文档: https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md
public partial class ExampleViewModel { [AutoNotify] private string _text = "private field text"; [AutoNotify(PropertyName = "Count")] private int _amount = 5; }生成器实现
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace Analyzer1 { [Generator] public class AutoNotifyGenerator : ISourceGenerator { private const string attributeText = @" using System; namespace AutoNotify { [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] sealed class AutoNotifyAttribute : Attribute { public AutoNotifyAttribute() { } public string PropertyName { get; set; } } } "; public void Initialize(InitializationContext context) { // Register a syntax receiver that will be created for each generation pass context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); } public void Execute(SourceGeneratorContext context) { // add the attribute text context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8)); // retreive the populated receiver if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) return; // we\'re going to create a new compilation that contains the attribute. // TODO: we should allow source generators to provide source during initialize, so that this step isn\'t required. CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options)); // get the newly bound attribute, and INotifyPropertyChanged INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute"); INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"); // loop over the candidate fields, and keep the ones that are actually annotated List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>(); foreach (FieldDeclarationSyntax field in receiver.CandidateFields) { SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree); foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables) { // Get the symbol being decleared by the field, and keep it if its annotated IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol; if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default))) { fieldSymbols.Add(fieldSymbol); } } } // group the fields by class, and generate the source foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType)) { string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context); context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8)); } } private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context) { if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) { return null; //TODO: issue a diagnostic that it must be top level } string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); // begin building the generated source StringBuilder source = new StringBuilder($@" namespace {namespaceName} {{ public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} {{ "); // if the class doesn\'t implement INotifyPropertyChanged already, add it if (!classSymbol.Interfaces.Contains(notifySymbol)) { source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); } // create properties for each field foreach (IFieldSymbol fieldSymbol in fields) { ProcessField(source, fieldSymbol, attributeSymbol); } source.Append("} }"); return source.ToString(); } private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) { // get the name and type of the field string fieldName = fieldSymbol.Name; ITypeSymbol fieldType = fieldSymbol.Type; // get the AutoNotify attribute from the field, and any associated data AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; string propertyName = chooseName(fieldName, overridenNameOpt); if (propertyName.Length == 0 || propertyName == fieldName) { //TODO: issue a diagnostic that we can\'t process this field return; } source.Append($@" public {fieldType} {propertyName} {{ get {{ return this.{fieldName}; }} set {{ this.{fieldName} = value; this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName}))); }} }} "); string chooseName(string fieldName, TypedConstant overridenNameOpt) { if (!overridenNameOpt.IsNull) { return overridenNameOpt.Value.ToString(); } fieldName = fieldName.TrimStart(\'_\'); if (fieldName.Length == 0) return string.Empty; if (fieldName.Length == 1) return fieldName.ToUpper(); return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); } } /// <summary> /// Created on demand before each generation pass /// </summary> class SyntaxReceiver : ISyntaxReceiver { public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>(); /// <summary> /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation /// </summary> public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { // any field with at least one attribute is a candidate for property generation if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax && fieldDeclarationSyntax.AttributeLists.Count > 0) { CandidateFields.Add(fieldDeclarationSyntax); } } } } } 2.1.2 修改代码 代码文件修改一般来说,很少有这样实现的,代码文件都改了,我们码农还怎么写bug呀。
中间语言修改有很多语言编译的结果并不是直接的机器码,而是优化后的一个接近底层的中间层语言,方便扩展支持不同cpu,不同机器架构。