Skip to content

Commit

Permalink
[Rgen] Provide a data model to make the code generator more performan…
Browse files Browse the repository at this point in the history
…t. (#21517)

In order to fully take advantage of the
[caching](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md#caching)
provided by the incremental code generator infrastructure we move to use
a data model that will represent the code changes that will trigger a
regeneration of certain classes.

The current implementation only takes care of the changes that will
trigger a regeneration on enums:

1. Enum fully qualified name changed.
2. Attributes added or removed in the enum.
2. Enum values added/removed.

We provide our own Comparer to have more control on how the structures
are compared.

---------

Co-authored-by: GitHub Actions Autoformatter <[email protected]>
Co-authored-by: Rolf Bjarne Kvinge <[email protected]>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent f11253b commit d51fa94
Show file tree
Hide file tree
Showing 43 changed files with 1,907 additions and 99 deletions.
17 changes: 8 additions & 9 deletions src/AVFoundation/AVCaptureReactionType.rgen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System;
using System.Runtime.Versioning;

using Foundation;
using ObjCRuntime;
using ObjCBindings;

Expand All @@ -16,28 +15,28 @@ namespace AVFoundation {
[SupportedOSPlatform ("maccatalyst17.0")]
[SupportedOSPlatform ("macos14.0")]
public enum AVCaptureReactionType {
[Field ("AVCaptureReactionTypeThumbsUp")]
[Field<EnumValue> ("AVCaptureReactionTypeThumbsUp")]
ThumbsUp,

[Field ("AVCaptureReactionTypeThumbsDown")]
[Field<EnumValue> ("AVCaptureReactionTypeThumbsDown")]
ThumbsDown,

[Field ("AVCaptureReactionTypeBalloons")]
[Field<EnumValue> ("AVCaptureReactionTypeBalloons")]
Balloons,

[Field ("AVCaptureReactionTypeHeart")]
[Field<EnumValue> ("AVCaptureReactionTypeHeart")]
Heart,

[Field ("AVCaptureReactionTypeFireworks")]
[Field<EnumValue> ("AVCaptureReactionTypeFireworks")]
Fireworks,

[Field ("AVCaptureReactionTypeRain")]
[Field<EnumValue> ("AVCaptureReactionTypeRain")]
Rain,

[Field ("AVCaptureReactionTypeConfetti")]
[Field<EnumValue> ("AVCaptureReactionTypeConfetti")]
Confetti,

[Field ("AVCaptureReactionTypeLasers")]
[Field<EnumValue> ("AVCaptureReactionTypeLasers")]
Lasers,
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/ObjCBindings/FieldAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Diagnostics.CodeAnalysis;

#nullable enable

namespace ObjCBindings {

/// <summary>
/// Generic attribute that is used to mark code to be backed by a field in ObjC.
/// </summary>
[Experimental ("APL0003")]
[AttributeUsage (AttributeTargets.Property | AttributeTargets.Field)]
public sealed class FieldAttribute<T> : Attribute where T : FieldTag {

/// <summary>
/// Create a new FieldAttribute for the given symbol and using the namespace as its containing library.
/// <param name="symbolName">The name of the symbol.</param>
/// </summary>
public FieldAttribute (string symbolName)
{
SymbolName = symbolName;
}

/// <summary>
/// Create a new FieldAttribute for the given symbol in the provided library.
/// <param name="symbolName">The name of the symbol.</param>
/// <param name="libraryName">The name of the library that contains the symbol.</param>
/// </summary>
public FieldAttribute (string symbolName, string libraryName)
{
SymbolName = symbolName;
LibraryName = libraryName;
}


/// <summary>
/// Get/Set the symbol represented by the attribute.
/// </summary>
public string SymbolName { get; set; }

/// <summary>
/// Get/Set the library that contains the symbol..
/// </summary>
public string? LibraryName { get; set; }
}
}
20 changes: 20 additions & 0 deletions src/ObjCBindings/FieldTag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Diagnostics.CodeAnalysis;

#nullable enable

namespace ObjCBindings {

/// <summary>
/// Base class use to flag a FieldAttribute usage. Each FieldAttribute must have a flag attached to it so that the
/// binding generator analyzer can verify the binding definition.
/// </summary>
[Experimental ("APL0003")]
public abstract class FieldTag { }

/// <summary>
/// Field flag that states that the field is used as a Enum value.
/// </summary>
[Experimental ("APL0003")]
public sealed class EnumValue : FieldTag { }
}
2 changes: 2 additions & 0 deletions src/frameworks.sources
Original file line number Diff line number Diff line change
Expand Up @@ -1986,6 +1986,8 @@ SHARED_CORE_SOURCES = \
MinimumVersions.cs \
MonoPInvokeCallbackAttribute.cs \
ObjCBindings/BindingTypeAttribute.cs \
ObjCBindings/FieldAttribute.cs \
ObjCBindings/FieldTag.cs \
ObjCRuntime/ArgumentSemantic.cs \
ObjCRuntime/BindAsAttribute.cs \
ObjCRuntime/Blocks.cs \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2"/>
</ItemGroup>

<ItemGroup>
Expand Down
51 changes: 51 additions & 0 deletions src/rgen/Microsoft.Macios.Generator/Attributes/FieldData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace Microsoft.Macios.Generator.Attributes;

record FieldData {
public string SymbolName { get; }
public string? LibraryName { get; private set; }

FieldData (string symbolName, string? libraryName = null)
{
SymbolName = symbolName;
LibraryName = libraryName;
}

public static bool TryParse (SyntaxNode attributeSyntax, AttributeData attributeData,
[NotNullWhen (true)] out FieldData? data)
{
data = default;

var count = attributeData.ConstructorArguments.Length;
switch (count) {
case 1:
data = new ((string) attributeData.ConstructorArguments [0].Value!);
break;
case 2:
data = new ((string) attributeData.ConstructorArguments [0].Value!,
(string) attributeData.ConstructorArguments [1].Value!);
break;
default:
// 0 should not be an option..
return false;
}

if (attributeData.NamedArguments.Length == 0)
return true;

// LibraryName can be a param value
foreach (var (name, value) in attributeData.NamedArguments) {
switch (name) {
case "LibraryName":
data.LibraryName = (string?) value.Value!;
break;
default:
data = null;
return false;
}
}
return true;
}
}
3 changes: 2 additions & 1 deletion src/rgen/Microsoft.Macios.Generator/AttributesNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ namespace Microsoft.Macios.Generator;
/// </summary>
public static class AttributesNames {

public static readonly string BindingAttribute = "ObjCBindings.BindingTypeAttribute";
public const string BindingAttribute = "ObjCBindings.BindingTypeAttribute";
public const string FieldAttribute = "ObjCBindings.FieldAttribute<ObjCBindings.EnumValue>";
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Macios.Generator.Context;
using Microsoft.Macios.Generator.DataModel;
using Microsoft.Macios.Generator.Emitters;
using Microsoft.Macios.Generator.Extensions;

namespace Microsoft.Macios.Generator;

Expand All @@ -19,7 +21,6 @@ namespace Microsoft.Macios.Generator;
/// </summary>
[Generator]
public class BindingSourceGeneratorGenerator : IIncrementalGenerator {

internal static readonly DiagnosticDescriptor RBI0000 = new (
"RBI0000",
new LocalizableResourceString (nameof (Resources.RBI0000Title), Resources.ResourceManager, typeof (Resources)),
Expand All @@ -30,6 +31,7 @@ public class BindingSourceGeneratorGenerator : IIncrementalGenerator {
description: new LocalizableResourceString (nameof (Resources.RBI0000Description), Resources.ResourceManager, typeof (Resources))
);

static readonly CodeChangesComparer comparer = new ();
/// <inheritdoc cref="IIncrementalGenerator"/>
public void Initialize (IncrementalGeneratorInitializationContext context)
{
Expand Down Expand Up @@ -59,11 +61,12 @@ static void AddPipeline<T> (IncrementalGeneratorInitializationContext context) w
.CreateSyntaxProvider (
static (s, _) => s is T,
(ctx, _) => GetDeclarationForSourceGen<T> (ctx))
.Where (t => t.BindingAttributeFound)
.Select ((t, _) => t.Declaration);
.Where (t => t.BindingAttributeFound) // get the types with the binding attr
.Select ((t, _) => t.Changes)
.WithComparer (comparer);

context.RegisterSourceOutput (context.CompilationProvider.Combine (provider.Collect ()),
((ctx, t) => GenerateCode (ctx, t.Left, t.Right)));
((ctx, t) => GenerateCode<T> (ctx, t.Left, t.Right)));
}

/// <summary>
Expand All @@ -74,25 +77,24 @@ static void AddPipeline<T> (IncrementalGeneratorInitializationContext context) w
/// <param name="context">Context used by the generator.</param>
/// <typeparam name="T">The BaseTypeDeclarationSyntax we are interested in.</typeparam>
/// <returns>A tuple that contains the BaseTypeDeclaration that was processed and a boolean that states if it should be processed or not.</returns>
static (T Declaration, bool BindingAttributeFound) GetDeclarationForSourceGen<T> (GeneratorSyntaxContext context)
static (CodeChanges Changes, bool BindingAttributeFound) GetDeclarationForSourceGen<T> (GeneratorSyntaxContext context)
where T : BaseTypeDeclarationSyntax
{
var classDeclarationSyntax = Unsafe.As<T> (context.Node);

// Go through all attributes of the class.
foreach (AttributeListSyntax attributeListSyntax in classDeclarationSyntax.AttributeLists)
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) {
if (context.SemanticModel.GetSymbolInfo (attributeSyntax).Symbol is not IMethodSymbol attributeSymbol)
continue; // if we can't get the symbol, ignore it
// we do know that the context node has to be one of the base type declarations
var declarationSyntax = Unsafe.As<T> (context.Node);

string attributeName = attributeSymbol.ContainingType.ToDisplayString ();
// check if we do have the binding attr, else there nothing to retrieve
bool isBindingType = declarationSyntax.HasAttribute (context.SemanticModel, AttributesNames.BindingAttribute);

// Check the full name of the [Binding] attribute.
if (attributeName == AttributesNames.BindingAttribute)
return (classDeclarationSyntax, true);
}
if (!isBindingType) {
// return an empty data + false
return (default, false);
}

return (classDeclarationSyntax, false);
var codeChanges = CodeChanges.FromDeclaration (context.SemanticModel, declarationSyntax);
// if code changes are null, return the default value and a false to later ignore the change
return codeChanges is not null ?
(codeChanges.Value, isBindingType) : (default, false);
}

/// <summary>
Expand All @@ -102,18 +104,26 @@ static void AddPipeline<T> (IncrementalGeneratorInitializationContext context) w
/// </summary>
/// <param name="tree">Root syntax tree of the base type declaration.</param>
/// <param name="sb">String builder that will be used for the generated code.</param>
static void CollectUsingStatements (SyntaxTree tree, TabbedStringBuilder sb)
/// <param name="emitter">The emitter that will generate the code. Provides any extra needed namespace.</param>
static void CollectUsingStatements (SyntaxTree tree, TabbedStringBuilder sb, ICodeEmitter emitter)
{
// collect all using from the syntax tree, add them to a hash to make sure that we don't have duplicates
// and add those usings that we do know we need for bindings.
var usingDirectives = tree.GetRoot ()
.DescendantNodes ()
.OfType<UsingDirectiveSyntax> ()
.Select (d => d.Name.ToString ()).ToArray ();
.Select (d => d.Name!.ToString ()).ToArray ();
var usingDirectivesToKeep = new HashSet<string> (usingDirectives) {
// add the using statements that we know we need and print them to the sb
};
foreach (var ns in usingDirectivesToKeep) {

// add those using statements needed by the emitter
foreach (var ns in emitter.UsingStatements) {
usingDirectivesToKeep.Add (ns);
}

// add them sorted so that we have testeable generated code
foreach (var ns in usingDirectivesToKeep.OrderBy (s => s)) {
if (string.IsNullOrEmpty (ns))
continue;
sb.AppendLine ($"using {ns};");
Expand All @@ -127,55 +137,50 @@ static void CollectUsingStatements (SyntaxTree tree, TabbedStringBuilder sb)
/// </summary>
/// <param name="context">The generator context.</param>
/// <param name="compilation">The compilation unit.</param>
/// <param name="baseTypeDeclarations">The base type declarations marked by the BindingTypeAttribute.</param>
/// <param name="changesList">The base type declarations marked by the BindingTypeAttribute.</param>
/// <typeparam name="T">The type of type declaration.</typeparam>
static void GenerateCode<T> (SourceProductionContext context, Compilation compilation,
ImmutableArray<T> baseTypeDeclarations) where T : BaseTypeDeclarationSyntax
ImmutableArray<CodeChanges> changesList) where T : BaseTypeDeclarationSyntax
{
var rootContext = new RootBindingContext (compilation);
foreach (var baseTypeDeclarationSyntax in baseTypeDeclarations) {
var semanticModel = compilation.GetSemanticModel (baseTypeDeclarationSyntax.SyntaxTree);
if (semanticModel.GetDeclaredSymbol (baseTypeDeclarationSyntax) is not INamedTypeSymbol namedTypeSymbol)
foreach (var change in changesList) {
var declaration = Unsafe.As<T> (change.SymbolDeclaration);
var semanticModel = compilation.GetSemanticModel (declaration.SyntaxTree);
// This is a bug in the roslyn analyzer for roslyn generator https://github.com/dotnet/roslyn-analyzers/issues/7436
#pragma warning disable RS1039
if (semanticModel.GetDeclaredSymbol (declaration) is not INamedTypeSymbol namedTypeSymbol)
#pragma warning restore RS1039
continue;

// init sb and add all the using statements from the base type declaration
var sb = new TabbedStringBuilder (new ());
// let people know this is generated code
sb.AppendLine ("// <auto-generated>");

// enable nullable!
sb.AppendLine ();
sb.AppendLine ("#nullable enable");
sb.AppendLine ();

CollectUsingStatements (baseTypeDeclarationSyntax.SyntaxTree, sb);

sb.WriteHeader ();

// delegate semantic model and syntax tree analysis to the emitter who will generate the code and knows
// best
if (ContextFactory.TryCreate (rootContext, semanticModel, namedTypeSymbol, baseTypeDeclarationSyntax, out var symbolBindingContext)
if (ContextFactory.TryCreate (rootContext, semanticModel, namedTypeSymbol, declaration,
out var symbolBindingContext)
&& EmitterFactory.TryCreate (symbolBindingContext, sb, out var emitter)) {
CollectUsingStatements (change.SymbolDeclaration.SyntaxTree, sb, emitter);

if (emitter.TryEmit (out var diagnostics)) {
// only add file when we do generate code
var code = sb.ToString ();
context.AddSource ($"{emitter.SymbolName}.g.cs", SourceText.From (code, Encoding.UTF8));
context.AddSource ($"{symbolBindingContext.Namespace}/{emitter.SymbolName}.g.cs",
SourceText.From (code, Encoding.UTF8));
} else {
// add to the diagnostics and continue to the next possible candidate
foreach (Diagnostic diagnostic in diagnostics) {
context.ReportDiagnostic (diagnostic);
}
}

} else {
// we don't have a emitter for this type, so we can't generate the code, add a diagnostic letting the
// user we do not support what he is trying to do, this is a bug in the code generator and we
// cannot recover from it. We do not want to crash but we generate no code.
// user we do not support what he is trying to do
context.ReportDiagnostic (Diagnostic.Create (RBI0000,
baseTypeDeclarationSyntax.GetLocation (),
declaration.GetLocation (),
namedTypeSymbol.ToDisplayString ().Trim ()));
}

}
}

}
Loading

5 comments on commit d51fa94

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.