Skip to content

Commit

Permalink
Add EmbeddedAttribute API for source generators (#76583)
Browse files Browse the repository at this point in the history
Adds a new API to ensure that Microsoft.CodeAnalysis.EmbeddedAttribute is available in the compilation, and updates the cookbook to include information about using the attribute.
  • Loading branch information
333fred authored Jan 18, 2025
1 parent 026982b commit b813d79
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 10 deletions.
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
{
internal sealed partial class EmbeddedAttribute : global::System.Attribute
{
}
}
""";

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()
{
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

0 comments on commit b813d79

Please sign in to comment.