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

Add EmbeddedAttribute API for source generators #76583

Merged
merged 6 commits into from
Jan 18, 2025
Merged
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
24 changes: 21 additions & 3 deletions docs/features/incremental-generators.cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ wrapper around `StringBuilder` that will keep track of indent level and prepend
[this](https://github.com/dotnet/roslyn/issues/52914#issuecomment-1732680995) conversation on the performance of `NormalizeWhitespace` for more examples, performance
measurements, and discussion on why we don't believe that `SyntaxNode`s are a good abstraction for this use case.

### Put `Microsoft.CodeAnalysis.EmbeddedAttribute` on generated marker types

Users might depend on your generator in multiple projects in the same solution, and these projects will often have `InternalsVisibleTo` applied. This means that your
`internal` marker attributes may be defined in multiple projects, and the compiler will warn about this. While this doesn't block compilation, it can be irritating to
users. To avoid this, mark such attributes with `Microsoft.CodeAnalysis.EmbeddedAttribute`; when the compiler sees this attribute on a type from separate assembly or
project, it will not include that type in lookup results. To ensure that `Microsoft.CodeAnalysis.EmbeddedAttribute` is available in the compilation, call the
`AddEmbeddedAttributeDefinition` helper method in your `RegisterPostInitializationOutput` callback.

Another option is to provide an assembly in your nuget package that defines your marker attributes, but this can be more difficult to author. We recommend the
`EmbeddedAttribute` approach, unless you need to support versions of Roslyn lower than 4.14.

## Designs

This section is broken down by user scenarios, with general solutions listed first, and more specific examples later on.
Expand Down Expand Up @@ -157,11 +168,14 @@ public class CustomGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static postInitializationContext => {
postInitializationContext.AddEmbeddedAttributeDefinition();
postInitializationContext.AddSource("myGeneratedFile.cs", SourceText.From("""
using System;
using Microsoft.CodeAnalysis;

namespace GeneratedNamespace
{
[Embedded]
internal sealed class GeneratedAttribute : Attribute
{
}
Expand Down Expand Up @@ -244,11 +258,13 @@ public class AugmentingGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static postInitializationContext =>
postInitializationContext.AddEmbeddedAttributeDefinition();
postInitializationContext.AddSource("myGeneratedFile.cs", SourceText.From("""
using System;
using Microsoft.CodeAnalysis;
namespace GeneratedNamespace
{
[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Method), Embedded]
internal sealed class GeneratedAttribute : Attribute
{
}
Expand Down Expand Up @@ -686,14 +702,16 @@ public class AutoImplementGenerator : IIncrementalGenerator
{
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddEmbeddedAttributeDefinition();
//Generate the AutoImplementProperties Attribute
const string autoImplementAttributeDeclarationCode = $$"""
// <auto-generated/>
using System;
using Microsoft.CodeAnalysis;
namespace {{AttributeNameSpace}};

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class {{AttributeClassName}} : Attribute
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false), Embedded]
internal sealed class {{AttributeClassName}} : Attribute
{
public Type[] InterfacesTypes { get; }
public {{AttributeClassName}}(params Type[] interfacesTypes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ internal override SyntaxTree ParseGeneratedSourceText(GeneratedSourceText input,

internal override string SourceExtension => ".cs";

internal override string EmbeddedAttributeDefinition => """
namespace Microsoft.CodeAnalysis
333fred marked this conversation as resolved.
Show resolved Hide resolved
{
internal sealed partial class EmbeddedAttribute : global::System.Attribute
333fred marked this conversation as resolved.
Show resolved Hide resolved
{
}
}
""";

internal override ISyntaxHelper SyntaxHelper => CSharpSyntaxHelper.Instance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2446,6 +2446,88 @@ class C { }
});
}

[Fact]
public void IncrementalGenerator_PostInit_AddEmbeddedAttributeSource_Adds()
{
var source = @"
class C { }
";
var parseOptions = TestOptions.RegularPreview;
Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions);
compilation.VerifyDiagnostics();

Assert.Single(compilation.SyntaxTrees);

var callback = (IncrementalGeneratorInitializationContext ctx) => ctx.RegisterPostInitializationOutput(c => c.AddEmbeddedAttributeDefinition());
var generator1 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator(callback));
var generator2 = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator2(callback));

GeneratorDriver driver = CSharpGeneratorDriver.Create([generator1, generator2], parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _);

var results = driver.GetRunResult().Results;
Assert.Equal(2, results.Length);

foreach (var runResult in results)
{
Assert.Single(runResult.GeneratedSources);

var generatedSource = runResult.GeneratedSources[0];

Assert.Equal("""
namespace Microsoft.CodeAnalysis
{
internal sealed partial class EmbeddedAttribute : global::System.Attribute
{
}
}
""", generatedSource.SourceText.ToString());
Assert.Equal("Microsoft.CodeAnalysis.EmbeddedAttribute.cs", generatedSource.HintName);
}

outputCompilation.VerifyDiagnostics();
}

[Fact]
public void IncrementalGenerator_PostInit_AddEmbeddedAttributeSource_DoubleAdd_Throws()
333fred marked this conversation as resolved.
Show resolved Hide resolved
{
var source = @"
class C { }
";
var parseOptions = TestOptions.RegularPreview;
Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDllThrowing, parseOptions: parseOptions);
compilation.VerifyDiagnostics();

Assert.Single(compilation.SyntaxTrees);

var generator = new IncrementalGeneratorWrapper(new PipelineCallbackGenerator((ctx) =>
{
ctx.RegisterPostInitializationOutput(c =>
{
c.AddEmbeddedAttributeDefinition();
Assert.Throws<ArgumentException>("hintName", () => c.AddEmbeddedAttributeDefinition());
});
}));

GeneratorDriver driver = CSharpGeneratorDriver.Create([generator], parseOptions: parseOptions, driverOptions: TestOptions.GeneratorDriverOptions);
driver = driver.RunGenerators(compilation);
var runResult = driver.GetRunResult().Results[0];

Assert.Single(runResult.GeneratedSources);

var generatedSource = runResult.GeneratedSources[0];

Assert.Equal("""
namespace Microsoft.CodeAnalysis
{
internal sealed partial class EmbeddedAttribute : global::System.Attribute
{
}
}
""", generatedSource.SourceText.ToString());
Assert.Equal("Microsoft.CodeAnalysis.EmbeddedAttribute.cs", generatedSource.HintName);
}

[Fact]
public void Incremental_Generators_Can_Be_Cancelled()
{
Expand Down
3 changes: 2 additions & 1 deletion src/Compilers/Core/Portable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Microsoft.CodeAnalysis.GeneratorFilterContext.Generator.get -> Microsoft.CodeAna
Microsoft.CodeAnalysis.GeneratorFilterContext.GeneratorFilterContext() -> void
Microsoft.CodeAnalysis.GeneratorRunResult.HostOutputs.get -> System.Collections.Immutable.ImmutableDictionary<string!, object!>!
Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind.Host = 8 -> Microsoft.CodeAnalysis.IncrementalGeneratorOutputKind
Microsoft.CodeAnalysis.IncrementalGeneratorPostInitializationContext.AddEmbeddedAttributeDefinition() -> void
Microsoft.CodeAnalysis.IPropertySymbol.PartialDefinitionPart.get -> Microsoft.CodeAnalysis.IPropertySymbol?
Microsoft.CodeAnalysis.IPropertySymbol.PartialImplementationPart.get -> Microsoft.CodeAnalysis.IPropertySymbol?
Microsoft.CodeAnalysis.IPropertySymbol.IsPartialDefinition.get -> bool
Expand All @@ -25,4 +26,4 @@ Microsoft.CodeAnalysis.Compilation.CreatePreprocessingSymbol(string! name) -> Mi
[RSEXPERIMENTAL004]Microsoft.CodeAnalysis.HostOutputProductionContext.CancellationToken.get -> System.Threading.CancellationToken
[RSEXPERIMENTAL004]Microsoft.CodeAnalysis.HostOutputProductionContext.HostOutputProductionContext() -> void
[RSEXPERIMENTAL004]Microsoft.CodeAnalysis.IncrementalGeneratorInitializationContext.RegisterHostOutput<TSource>(Microsoft.CodeAnalysis.IncrementalValueProvider<TSource> source, System.Action<Microsoft.CodeAnalysis.HostOutputProductionContext, TSource>! action) -> void
[RSEXPERIMENTAL004]Microsoft.CodeAnalysis.IncrementalGeneratorInitializationContext.RegisterHostOutput<TSource>(Microsoft.CodeAnalysis.IncrementalValuesProvider<TSource> source, System.Action<Microsoft.CodeAnalysis.HostOutputProductionContext, TSource>! action) -> void
[RSEXPERIMENTAL004]Microsoft.CodeAnalysis.IncrementalGeneratorInitializationContext.RegisterHostOutput<TSource>(Microsoft.CodeAnalysis.IncrementalValuesProvider<TSource> source, System.Action<Microsoft.CodeAnalysis.HostOutputProductionContext, TSource>! action) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, Diagnos
var inputBuilder = ArrayBuilder<SyntaxInputNode>.GetInstance();
var postInitSources = ImmutableArray<GeneratedSyntaxTree>.Empty;
var pipelineContext = new IncrementalGeneratorInitializationContext(
inputBuilder, outputBuilder, this.SyntaxHelper, this.SourceExtension, compilation.CatchAnalyzerExceptions);
inputBuilder, outputBuilder, this.SyntaxHelper, this.SourceExtension, this.EmbeddedAttributeDefinition, compilation.CatchAnalyzerExceptions);

Exception? ex = null;
try
Expand Down Expand Up @@ -462,6 +462,8 @@ private static ImmutableArray<IIncrementalGenerator> GetIncrementalGenerators(Im

internal abstract string SourceExtension { get; }

internal abstract string EmbeddedAttributeDefinition { get; }

internal abstract ISyntaxHelper SyntaxHelper { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public readonly partial struct IncrementalGeneratorInitializationContext
private readonly ArrayBuilder<SyntaxInputNode> _syntaxInputBuilder;
private readonly ArrayBuilder<IIncrementalGeneratorOutputNode> _outputNodes;
private readonly string _sourceExtension;

private readonly string _embeddedAttributeDefinition;
internal readonly ISyntaxHelper SyntaxHelper;
internal readonly bool CatchAnalyzerExceptions;

Expand All @@ -36,12 +36,14 @@ internal IncrementalGeneratorInitializationContext(
ArrayBuilder<IIncrementalGeneratorOutputNode> outputNodes,
ISyntaxHelper syntaxHelper,
string sourceExtension,
string embeddedAttributeDefinition,
bool catchAnalyzerExceptions)
{
_syntaxInputBuilder = syntaxInputBuilder;
_outputNodes = outputNodes;
SyntaxHelper = syntaxHelper;
_sourceExtension = sourceExtension;
_embeddedAttributeDefinition = embeddedAttributeDefinition;
CatchAnalyzerExceptions = catchAnalyzerExceptions;
}

Expand Down Expand Up @@ -73,7 +75,7 @@ internal IncrementalValueProvider<CompilationOptions> CompilationOptionsProvider

public void RegisterImplementationSourceOutput<TSource>(IncrementalValuesProvider<TSource> source, Action<SourceProductionContext, TSource> action) => RegisterSourceOutput(source.Node, action, IncrementalGeneratorOutputKind.Implementation, _sourceExtension);

public void RegisterPostInitializationOutput(Action<IncrementalGeneratorPostInitializationContext> callback) => _outputNodes.Add(new PostInitOutputNode(callback.WrapUserAction(CatchAnalyzerExceptions)));
public void RegisterPostInitializationOutput(Action<IncrementalGeneratorPostInitializationContext> callback) => _outputNodes.Add(new PostInitOutputNode(callback.WrapUserAction(CatchAnalyzerExceptions), _embeddedAttributeDefinition));

[Experimental(RoslynExperiments.GeneratorHostOutputs, UrlFormat = RoslynExperiments.GeneratorHostOutputs_Url)]
public void RegisterHostOutput<TSource>(IncrementalValueProvider<TSource> source, Action<HostOutputProductionContext, TSource> action) => source.Node.RegisterOutput(new HostOutputNode<TSource>(source.Node, action.WrapUserAction(CatchAnalyzerExceptions)));
Expand Down Expand Up @@ -101,10 +103,12 @@ private void RegisterSourceOutput<TSource>(IIncrementalGeneratorNode<TSource> no
public readonly struct IncrementalGeneratorPostInitializationContext
{
internal readonly AdditionalSourcesCollection AdditionalSources;
private readonly string _embeddedAttributeDefinition;

internal IncrementalGeneratorPostInitializationContext(AdditionalSourcesCollection additionalSources, CancellationToken cancellationToken)
internal IncrementalGeneratorPostInitializationContext(AdditionalSourcesCollection additionalSources, string embeddedAttributeDefinition, CancellationToken cancellationToken)
{
AdditionalSources = additionalSources;
_embeddedAttributeDefinition = embeddedAttributeDefinition;
CancellationToken = cancellationToken;
}

Expand All @@ -129,6 +133,16 @@ internal IncrementalGeneratorPostInitializationContext(AdditionalSourcesCollecti
/// Directory separators "/" and "\" are allowed in <paramref name="hintName"/>, they are normalized to "/" regardless of host platform.
/// </remarks>
public void AddSource(string hintName, SourceText sourceText) => AdditionalSources.Add(hintName, sourceText);

/// <summary>
/// Adds a <see cref="SourceText" /> to the compilation containing the definition of <c>Microsoft.CodeAnalysis.EmbeddedAttribute</c>.
/// The source will have a <c>hintName</c> of Microsoft.CodeAnalysis.EmbeddedAttribute.
/// </summary>
/// <remarks>
/// This attribute can be used to mark a type as being only visible to the current assembly. Most commonly, any types provided during this <see cref="IncrementalGeneratorPostInitializationContext"/>
/// should be marked with this attribute to prevent them from being used by other assemblies. The attribute will prevent any downstream assemblies from consuming the type.
/// </remarks>
public void AddEmbeddedAttributeDefinition() => AddSource("Microsoft.CodeAnalysis.EmbeddedAttribute", SourceText.From(_embeddedAttributeDefinition, encoding: Encoding.UTF8));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ namespace Microsoft.CodeAnalysis
internal sealed class PostInitOutputNode : IIncrementalGeneratorOutputNode
{
private readonly Action<IncrementalGeneratorPostInitializationContext, CancellationToken> _callback;
private readonly string _embeddedAttributeDefinition;

public PostInitOutputNode(Action<IncrementalGeneratorPostInitializationContext, CancellationToken> callback)
public PostInitOutputNode(Action<IncrementalGeneratorPostInitializationContext, CancellationToken> callback, string embeddedAttributeDefinition)
{
_callback = callback;
_embeddedAttributeDefinition = embeddedAttributeDefinition;
}

public IncrementalGeneratorOutputKind Kind => IncrementalGeneratorOutputKind.PostInit;

public void AppendOutputs(IncrementalExecutionContext context, CancellationToken cancellationToken)
{
_callback(new IncrementalGeneratorPostInitializationContext(context.Sources, cancellationToken), cancellationToken);
_callback(new IncrementalGeneratorPostInitializationContext(context.Sources, _embeddedAttributeDefinition, cancellationToken), cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ Namespace Microsoft.CodeAnalysis.VisualBasic
End Get
End Property

Friend Overrides ReadOnly Property EmbeddedAttributeDefinition As String
Get
Return "Namespace Microsoft.CodeAnalysis
Friend NotInheritable Partial Class EmbeddedAttribute
Inherits Global.System.Attribute
End Class
End Namespace"
End Get
End Property

Friend Overrides ReadOnly Property SyntaxHelper As ISyntaxHelper = VisualBasicSyntaxHelper.Instance
End Class
End Namespace
Loading