Skip to content
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 12 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VisualBasicToCSharpConverte
EndProject
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VisualBasicToCSharpConverter.UnitTests", "samples\VisualBasic\VisualBasicToCSharpConverter\VisualBasicToCSharpConverter.Test\VisualBasicToCSharpConverter.UnitTests.vbproj", "{5B7D7569-B5EE-4C01-9AFA-BC1958588160}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{14D18F51-6B59-49D5-9AB7-08B38417A459}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorSamples", "samples\CSharp\SourceGenerators\SourceGeneratorSamples\SourceGeneratorSamples.csproj", "{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}"
EndProject
Copy link
Contributor

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.

Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -269,6 +273,10 @@ Global
{5B7D7569-B5EE-4C01-9AFA-BC1958588160}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B7D7569-B5EE-4C01-9AFA-BC1958588160}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B7D7569-B5EE-4C01-9AFA-BC1958588160}.Release|Any CPU.Build.0 = Release|Any CPU
{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -325,6 +333,8 @@ Global
{8E1C9AEC-6EF1-43A8-A378-52C5C0E40532} = {CDA94F62-E35A-4913-8045-D9D42416513C}
{ECB83742-8023-4609-B139-D7B78DD66ED9} = {8E1C9AEC-6EF1-43A8-A378-52C5C0E40532}
{5B7D7569-B5EE-4C01-9AFA-BC1958588160} = {8E1C9AEC-6EF1-43A8-A378-52C5C0E40532}
{14D18F51-6B59-49D5-9AB7-08B38417A459} = {C3FB27E9-C8EE-4F76-B0AA-7CD67A7E652B}
{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045} = {14D18F51-6B59-49D5-9AB7-08B38417A459}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B849838B-3D7A-4B6B-BE07-285DCB1588F4}
Expand Down
6 changes: 6 additions & 0 deletions samples/CSharp/SourceGenerators/README.md
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).
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);
}
}
}
}
}
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to have this be HelloWorldGenerated to distinguish from the name of the generator

Copy link
Member Author

Choose a reason for hiding this comment

The 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
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading