Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
13 changes: 10 additions & 3 deletions docs/csharp/roslyn-sdk/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,17 @@ practices.
## Source generators

Source generators aim to enable *compile time metaprogramming*, that is, code that can be created
at compile time and added to the compilation. Source generators are able to read the contents of
at compile time and added to the compilation. Source generators read the contents of
the compilation before running, as well as access any *additional files*. This ability enables them to
introspect both user C# code and generator-specific files. You can learn how to build incremental
source generators using the [source generator cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md).
introspect both user C# code and generator-specific files. Incremental source generators use a
pipeline model that filters and transforms data incrementally, keeping the IDE responsive.

To learn more, see the following resources:

- [Source generators overview](source-generators-overview.md)
- [Tutorial: Create an incremental source generator](tutorials/incremental-source-generator-tutorial.md)
- [Source generator samples](https://github.com/dotnet/samples/tree/main/csharp/roslyn-sdk/SourceGenerators) in the dotnet/samples repository
- [Roslyn incremental generators specification](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md)

## Next steps

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
obj/
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\GenerateMembersGenerator\GenerateMembersGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// <Program>
using GenerateMembersGenerator;

var person = new Person { FirstName = "Alice", LastName = "Smith", Age = 30 };

Console.WriteLine(person.Describe());
Console.WriteLine("Properties:");
foreach (string name in Person.PropertyNames)
{
Console.WriteLine($" {name}");
}

[GenerateMembers]
public partial class Person
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public int Age { get; set; }
}
// </Program>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
obj/
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#nullable enable

using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace GenerateMembersGenerator;

// <GenerateMembersGenerator>
/// <summary>
/// A source generator that adds a <c>Describe()</c> method and a
/// <c>PropertyNames</c> list to any <see langword="partial"/> class or struct
/// decorated with <c>[GenerateMembers]</c>.
/// </summary>
[Generator]
public class GenerateMembersIncrementalGenerator : IIncrementalGenerator
{
private const string AttributeFullName = "GenerateMembersGenerator.GenerateMembersAttribute";

// <Initialize>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. Emit the marker attribute so users don't need a separate reference.
context.RegisterPostInitializationOutput(static ctx =>
{
ctx.AddSource("GenerateMembersAttribute.g.cs", SourceText.From(AttributeSource, Encoding.UTF8));
});

// 2. Filter for type declarations annotated with [GenerateMembers].
IncrementalValuesProvider<TypeInfo> typeInfos = context.SyntaxProvider
.ForAttributeWithMetadataName(
AttributeFullName,
predicate: static (node, _) => node is TypeDeclarationSyntax,
transform: static (ctx, _) => GetTypeInfo(ctx))
.Where(static t => t is not null)!;

// 3. Generate source for each qualifying type.
context.RegisterSourceOutput(typeInfos, static (spc, typeInfo) =>
{
string source = GenerateSource(typeInfo);
string hintName = typeInfo.FullyQualifiedName
.Replace("global::", "")
.Replace("::", ".")
.Replace("<", "_")
.Replace(">", "_");
spc.AddSource($"{hintName}.GeneratedMembers.g.cs", SourceText.From(source, Encoding.UTF8));
});
}
// </Initialize>

// <GetTypeInfo>
private static TypeInfo? GetTypeInfo(GeneratorAttributeSyntaxContext context)
{
if (context.TargetSymbol is not INamedTypeSymbol typeSymbol)
return null;

string typeKeyword = context.TargetNode is StructDeclarationSyntax ? "struct" : "class";

ImmutableArray<IPropertySymbol> properties = typeSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.IsStatic && !p.IsIndexer)
.ToImmutableArray();

return new TypeInfo(
typeSymbol.ContainingNamespace.IsGlobalNamespace
? null
: typeSymbol.ContainingNamespace.ToDisplayString(),
typeSymbol.Name,
typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
typeKeyword,
properties.Select(p => (p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
.ToImmutableArray());
}
// </GetTypeInfo>

// <GenerateSource>
private static string GenerateSource(TypeInfo typeInfo)
{
var sb = new StringBuilder();

sb.AppendLine("// <auto-generated />");

if (typeInfo.Namespace is not null)
{
sb.AppendLine($"namespace {typeInfo.Namespace}");
sb.AppendLine("{");
}

sb.AppendLine($" partial {typeInfo.TypeKeyword} {typeInfo.Name}");
sb.AppendLine(" {");

// PropertyNames
sb.AppendLine(" /// <summary>Gets the names of all instance properties.</summary>");
sb.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList<string> PropertyNames { get; } =");
sb.Append(" new string[] { ");
sb.Append(string.Join(", ", typeInfo.Properties.Select(p => $"\"{p.Name}\"")));
sb.AppendLine(" };");
sb.AppendLine();

// Describe method
sb.AppendLine(" /// <summary>Returns a human-readable description of this instance.</summary>");
sb.AppendLine(" public string Describe()");
sb.AppendLine(" {");
sb.AppendLine($" var sb = new global::System.Text.StringBuilder();");
sb.AppendLine($" sb.AppendLine(\"{typeInfo.Name}\");");
foreach (var (name, _) in typeInfo.Properties)
{
sb.AppendLine($" sb.AppendLine($\" {name} = {{{name}}}\");");
}
sb.AppendLine(" return sb.ToString();");
sb.AppendLine(" }");

sb.AppendLine(" }");

if (typeInfo.Namespace is not null)
{
sb.AppendLine("}");
}

return sb.ToString();
}
// </GenerateSource>

// <AttributeSource>
private const string AttributeSource = @"// <auto-generated />
namespace GenerateMembersGenerator
{
/// <summary>
/// Add this attribute to a partial class or struct to automatically generate
/// a <c>Describe()</c> method and a <c>PropertyNames</c> list.
/// </summary>
[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct)]
internal sealed class GenerateMembersAttribute : global::System.Attribute
{
}
}
";
// </AttributeSource>

// <TypeInfoClass>
private sealed class TypeInfo
{
public string? Namespace { get; }
public string Name { get; }
public string FullyQualifiedName { get; }
public string TypeKeyword { get; }
public ImmutableArray<(string Name, string TypeFullName)> Properties { get; }

public TypeInfo(
string? ns,
string name,
string fullyQualifiedName,
string typeKeyword,
ImmutableArray<(string Name, string TypeFullName)> properties)
{
Namespace = ns;
Name = name;
FullyQualifiedName = fullyQualifiedName;
TypeKeyword = typeKeyword;
Properties = properties;
}
}
// </TypeInfoClass>
}
// </GenerateMembersGenerator>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bin/
obj/
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.CodeAnalysis;

// <MinimalGenerator>
[Generator]
public class MinimalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register a static source that always gets added to the compilation.
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("GeneratedHelper.g.cs", """
// <auto-generated/>
public static class GeneratedHelper
{
public static string Greet(string name) =>
$"Hello, {name}! (generated at compile time)";
}
""");
});
}
}
// </MinimalGenerator>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

</Project>
Loading