-
Notifications
You must be signed in to change notification settings - Fork 256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Source generator samples #511
Merged
Merged
Changes from 4 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f3a533f
Add initial source generator samples
chsienki e7b4ef5
Add hello world generator, and clean up samples
chsienki 0e8d3eb
Add WIP Readme
chsienki 09053bb
Clean up project + remove warning
chsienki 6ebd7b7
Expand samples:
chsienki 61d81ab
Whitespace
chsienki 8c4859a
Add SourceGenerators.sln
chsienki c41a496
Add generated project to main samples SLN
chsienki c034ef8
Downgrade generated demo to netcoreapp3.0
chsienki af2f5c2
Update global.json
chsienki b15fee6
Bump arcade version too
chsienki 5985a80
Restore netcoreapp3.1
chsienki File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
🚧 Work In Progress | ||
======== | ||
|
||
These samples are for an in-progress feature of Roslyn. As such they may change or break as the feature is developed, and no level of support is implied. | ||
|
||
For more infomation on the Source Generators feature, see the [design document](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md). |
185 changes: 185 additions & 0 deletions
185
samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
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); | ||
} | ||
} | ||
} | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
||
namespace SourceGeneratorSamples | ||
{ | ||
[Generator] | ||
public class HelloWorldGenerator : ISourceGenerator | ||
{ | ||
public void Execute(SourceGeneratorContext context) | ||
{ | ||
// begin creating the source we'll inject into the users compilation | ||
StringBuilder sourceBuilder = new StringBuilder(@" | ||
using System; | ||
namespace HelloWorldGenerator | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be better to have this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. |
||
{ | ||
public static class HelloWorld | ||
{ | ||
public static void SayHello() | ||
{ | ||
Console.WriteLine(""Hello from generated code!""); | ||
Console.WriteLine(""The following syntax trees existed in the compilation that created this program:""); | ||
"); | ||
|
||
// using the context, get a list of syntax trees in the users compilation | ||
IEnumerable<SyntaxTree> syntaxTrees = context.Compilation.SyntaxTrees; | ||
|
||
// add the filepath of each tree to the class we're building | ||
foreach (SyntaxTree tree in syntaxTrees) | ||
{ | ||
sourceBuilder.AppendLine($@"Console.WriteLine(@"" - {tree.FilePath}"");"); | ||
} | ||
|
||
// finish creating the source to inject | ||
sourceBuilder.Append(@" | ||
} | ||
} | ||
}"); | ||
|
||
// inject the created source into the users compilation | ||
context.AddSource("helloWorldGenerator", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); | ||
} | ||
|
||
public void Initialize(InitializationContext context) | ||
{ | ||
// No initialization required | ||
} | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Text; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Xml; | ||
|
||
namespace Analyzer1 | ||
{ | ||
[Generator] | ||
public class SettingsXmlGenerator : ISourceGenerator | ||
{ | ||
private const string SettingsFileString = @" | ||
namespace XmlSettings | ||
{ | ||
public partial class XmlSettings | ||
{ | ||
|
||
} | ||
} | ||
"; | ||
public void Execute(SourceGeneratorContext context) | ||
{ | ||
// Using the context, get any additional files that end in .xmlsettings | ||
IEnumerable<AdditionalText> settingsFiles = context.AdditionalFiles.Where(at => at.Path.EndsWith(".xmlsettings")); | ||
foreach (AdditionalText settingsFile in settingsFiles) | ||
{ | ||
ProcessSettingsFile(settingsFile, context); | ||
} | ||
} | ||
|
||
private void ProcessSettingsFile(AdditionalText xmlFile, SourceGeneratorContext context) | ||
{ | ||
// try and load the settings file | ||
XmlDocument xmlDoc = new XmlDocument(); | ||
string text = xmlFile.GetText(context.CancellationToken).ToString(); | ||
try | ||
{ | ||
xmlDoc.LoadXml(text); | ||
} | ||
catch | ||
{ | ||
//TODO: issue a diagnostic that says we couldn't parse it | ||
return; | ||
} | ||
|
||
|
||
// create a class in the XmlSetting class that represnts this entry, and a static field that contains a singleton instance. | ||
string fileName = Path.GetFileName(xmlFile.Path); | ||
string name = xmlDoc.DocumentElement.GetAttribute("name"); | ||
|
||
StringBuilder sb = new StringBuilder($@" | ||
namespace AutoSettings | ||
{{ | ||
using System; | ||
using System.Xml; | ||
|
||
public partial class XmlSettings | ||
{{ | ||
|
||
public static {name}Settings {name} {{ get; }} = new {name}Settings(""{fileName}""); | ||
|
||
public class {name}Settings | ||
{{ | ||
|
||
XmlDocument xmlDoc = new XmlDocument(); | ||
|
||
internal {name}Settings(string fileName) | ||
{{ | ||
xmlDoc.Load(fileName); | ||
}} | ||
"); | ||
|
||
for(int i = 0; i < xmlDoc.DocumentElement.ChildNodes.Count; i++) | ||
{ | ||
XmlElement setting = (XmlElement)xmlDoc.DocumentElement.ChildNodes[i]; | ||
string settingName = setting.GetAttribute("name"); | ||
string settingType = setting.GetAttribute("type"); | ||
string defaultValue = setting.GetAttribute("defaultValue"); | ||
|
||
sb.Append($@" | ||
|
||
public {settingType} {settingName} | ||
{{ | ||
get | ||
{{ | ||
return ({settingType}) Convert.ChangeType(((XmlElement)xmlDoc.DocumentElement.ChildNodes[{i}]).InnerText, typeof({settingType})); | ||
}} | ||
}} | ||
"); | ||
} | ||
|
||
sb.Append("} } }"); | ||
|
||
context.AddSource($"Settings_{name}", SourceText.From(sb.ToString(), Encoding.UTF8)); | ||
} | ||
|
||
public void Initialize(InitializationContext context) | ||
{ | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to be missing the consumption project.