Skip to content

Commit

Permalink
[RGen] First inclusion of a roslyn generator for the bindings. (#21389)
Browse files Browse the repository at this point in the history
We are doing the following:

1. Adding the rgen directory with the following solutions:
* Analyzer: Analyzer that will catch errors in the bindings. At the
moment it provides a single error when the BindingTypeAttribute is used
in a nont partial type.
   * Analyzer Tests: Allows tests for the analyzer.
   * Analyzer Sample: Sample project to test the analyzer.
* Code Generator: A code generator that adds the BindingTypeAttribute to
the compilcation.
   * Code Generator Tests: Allows tests for the generator.
   * Code Sample: Sample project for the code generator.
2. Make rule to build the roslyn code generator.
3. Makefile changes to add the code generator as part as the second
compilation of the bindings.

This changes add the starting gounds to move to roslyn.

---------

Co-authored-by: GitHub Actions Autoformatter <github-actions-autoformatter@xamarin.com>
Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
Co-authored-by: Alex Soto <alex@soto.dev>
  • Loading branch information
4 people authored Oct 15, 2024
1 parent 6abfff6 commit 0f35909
Show file tree
Hide file tree
Showing 49 changed files with 1,176 additions and 156 deletions.
7 changes: 7 additions & 0 deletions docs/preview-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,10 @@ We've tentatively set .NET 11 as the release when we'll stop marking FSKit as pr
The diagnostic id for FSKit is APL0002.

[1]: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute?view=net-8.0

## Rgen (APL0003)

Rgen is the new Roslyn codegenerator based binding tool. The tool is underdevelopment and its API is open to change until
a stable release is announced.

The diagnostic id for Rgen is APL0003.
6 changes: 4 additions & 2 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ DOTNET_WARNINGS_TO_FIX = -nowarn:$(CSC_WARNINGS_TO_FIX)
DOTNET_CORE_WARNINGS_TO_FIX = -nowarn:$(CSC_WARNINGS_TO_FIX),$(BGEN_WARNINGS_TO_FIX)

include ./Makefile.generator
include ./Makefile.rgenerator
include ./generator-diff.mk

SHARED_RESX = $(TOP)/tools/mtouch/Errors.resx
Expand Down Expand Up @@ -355,7 +356,7 @@ $($(2)_DOTNET_BUILD_DIR)/core-$(3).dll: $($(2)_DOTNET_CORE_SOURCES) frameworks.s
$($(2)_DOTNET_BUILD_DIR)/$(3)-generated-sources: $(DOTNET_GENERATOR) $($(2)_DOTNET_APIS) $($(2)_DOTNET_BUILD_DIR)/core-$(3).dll $(DOTNET_BINDING_ATTRIBUTES) $($(2)_DOTNET_BUILD_DIR)/$(3).rsp | $($(2)_DOTNET_BUILD_DIR)/generated-sources
$$(Q_DOTNET_GEN) $$< @$($(2)_DOTNET_BUILD_DIR)/$(3).rsp

$($(2)_DOTNET_BUILD_DIR)/$(3).rsp: Makefile Makefile.generator frameworks.sources $(DOTNET_COMPILER) | $($(2)_DOTNET_BUILD_DIR)
$($(2)_DOTNET_BUILD_DIR)/$(3).rsp: Makefile Makefile.generator Makefile.rgenerator frameworks.sources $(ROSLYN_GENERATOR) $(DOTNET_COMPILER) | $($(2)_DOTNET_BUILD_DIR)
$(Q) echo \
$($(2)_GENERATOR_FLAGS) \
$(DOTNET_GENERATOR_FLAGS) \
Expand Down Expand Up @@ -450,10 +451,11 @@ $(2)_DOTNET_PLATFORM_ASSEMBLY_DIR_DEPENDENCIES = \
$($(2)_DOTNET_BUILD_DIR)/$(4) \
$($(2)_DOTNET_BUILD_DIR)/ref \

$($(2)_DOTNET_BUILD_DIR)/$(4)/Microsoft.$(1)%dll $($(2)_DOTNET_BUILD_DIR)/$(4)/Microsoft.$(1)%pdb $$($(2)_$(4)_REF_TARGET) $$($(2)_$(4)_DOC_TARGET): $$($(2)_DOTNET_PLATFORM_ASSEMBLY_DEPENDENCIES) | $$($(2)_DOTNET_PLATFORM_ASSEMBLY_DIR_DEPENDENCIES)
$($(2)_DOTNET_BUILD_DIR)/$(4)/Microsoft.$(1)%dll $($(2)_DOTNET_BUILD_DIR)/$(4)/Microsoft.$(1)%pdb $$($(2)_$(4)_REF_TARGET) $$($(2)_$(4)_DOC_TARGET): $$($(2)_DOTNET_PLATFORM_ASSEMBLY_DEPENDENCIES) $$(ROSLYN_GENERATOR) | $$($(2)_DOTNET_PLATFORM_ASSEMBLY_DIR_DEPENDENCIES)
$$(call Q_PROF_CSC,dotnet/$(4)-bit) \
$(DOTNET_CSC) \
$(DOTNET_FLAGS) \
/analyzer:$(ROSLYN_GENERATOR) \
-unsafe \
-optimize \
$$(ARGS_$(1)) \
Expand Down
8 changes: 8 additions & 0 deletions src/Makefile.rgenerator
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Roslyn code generator
ROSLYN_GENERATOR=$(DOTNET_BUILD_DIR)/common/rgen/Microsoft.Macios.Generator.dll
ROSLYN_GENERATOR_FILES := $(wildcard rgen/Microsoft.Macios.Generator/*.cs)

$(ROSLYN_GENERATOR): Makefile.rgenerator $(ROSLYN_GENERATOR_FILES)
$(Q_DOTNET_BUILD) $(DOTNET) publish rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj $(DOTNET_BUILD_VERBOSITY) /p:Configuration=Debug /p:IntermediateOutputPath=$(abspath $(DOTNET_BUILD_DIR)/IDE/obj/common/rgen)/ /p:OutputPath=$(abspath $(DOTNET_BUILD_DIR)/IDE/bin/common/rgen/)/
@mkdir -p $(dir $@)
$(Q) $(CP) -r $(DOTNET_BUILD_DIR)/IDE/bin/common/rgen/publish/* $(dir $@)
25 changes: 25 additions & 0 deletions src/ObjCBindings/BindingTypeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;

namespace ObjCBindings {

/// <summary>
/// Attribute that indicates that a class or enum is a binding type. This attribute is used by the binding generator
/// to generate all the necessary code for the binding. The attribute can be used in a class or enum and it is required.
///
/// If the attribute is used in a class, the class must be partial otherwise the generator will fail.
/// </summary>
[Experimental ("APL0003")]
[AttributeUsage (AttributeTargets.Class | System.AttributeTargets.Enum, AllowMultiple = false)]
public class BindingTypeAttribute : Attribute {

/// <summary>
/// Indicates the name of the binding type. This is the name that will be used by the registrar to make the
/// class available in the ObjC runtime. The default value is string.Empty, in that case the generator
/// will use the name of the C# class.
/// </summary>
public string Name { get; set; } = string.Empty;
}

}
1 change: 1 addition & 0 deletions src/frameworks.sources
Original file line number Diff line number Diff line change
Expand Up @@ -2012,6 +2012,7 @@ SHARED_CORE_SOURCES = \
DotNetGlobals.cs \
MinimumVersions.cs \
MonoPInvokeCallbackAttribute.cs \
ObjCBindings/BindingTypeAttribute.cs \
ObjCRuntime/ArgumentSemantic.cs \
ObjCRuntime/BindAsAttribute.cs \
ObjCRuntime/Blocks.cs \
Expand Down
12 changes: 12 additions & 0 deletions src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Microsoft.Macios.Bindings.Analyzer.Sample;

// If you don't see warnings, build the Analyzers Project.

[BindingType]
public class Examples {
}

[BindingType]
public class Foo {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Macios.Generator\Microsoft.Macios.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\Microsoft.Macios.Bindings.Analyzer\Microsoft.Macios.Bindings.Analyzer.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\..\src\bgen\Attributes.cs" >
<Link>external\Attributes.cs</Link>
</Compile>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Release 1.0

### New Rules

| Rule ID | Category | Severity | Notes |
|---------|----------|----------|------------------------------------------------------|
| RBI0001 | Usage | Error | Binding types should be declared as partial classes. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### New Rules

| Rule ID | Category | Severity | Notes |
|---------|----------|----------|-------|
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using SyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind;

namespace Microsoft.Macios.Bindings.Analyzer;

/// <summary>
/// Code fix provider that adds the 'partial' modifier to the class decorated with BindingTypeAttribute.
/// </summary>
[ExportCodeFixProvider (LanguageNames.CSharp, Name = nameof (BindingTypeCodeFixProvider)), Shared]
public class BindingTypeCodeFixProvider : CodeFixProvider {
// Specify the diagnostic IDs of analyzers that are expected to be linked.
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create (BindingTypeSemanticAnalyzer.DiagnosticId);

// If you don't need the 'fix all' behaviour, return null.
public override FixAllProvider? GetFixAllProvider () => null;

public sealed override async Task RegisterCodeFixesAsync (CodeFixContext context)
{
var diagnostic = context.Diagnostics.Single ();
var diagnosticSpan = diagnostic.Location.SourceSpan;
var root = await context.Document.GetSyntaxRootAsync (context.CancellationToken).ConfigureAwait (false);
var diagnosticNode = root?.FindNode (diagnosticSpan);

// To get the required metadata, we should match the Node to the specific type: 'ClassDeclarationSyntax'.
if (diagnosticNode is not ClassDeclarationSyntax declaration)
return;

// Register a code action that will invoke the fix.
context.RegisterCodeFix (
CodeAction.Create (
title: Resources.RBI0001CodeFixTitle,
createChangedDocument: c => MakePartialClassAsync (context.Document, declaration, c),
equivalenceKey: nameof (Resources.RBI0001CodeFixTitle)),
diagnostic);
}

async Task<Document> MakePartialClassAsync (Document document,
ClassDeclarationSyntax classDeclarationSyntax, CancellationToken cancellationToken)
{
var partialClass = classDeclarationSyntax.AddModifiers (SyntaxFactory.Token (SyntaxKind.PartialKeyword));
var oldRoot = await document.GetSyntaxRootAsync (cancellationToken).ConfigureAwait (false);
if (oldRoot is null)
return document;

var newRoot = oldRoot.ReplaceNode (classDeclarationSyntax, partialClass);
return document.WithSyntaxRoot (newRoot);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.Macios.Bindings.Analyzer;

/// <summary>
/// Analyzer that ensures that the types that have been declared as binding types are partial and follow the correct
/// pattern.
/// </summary>
[DiagnosticAnalyzer (LanguageNames.CSharp)]
public class BindingTypeSemanticAnalyzer : DiagnosticAnalyzer {
internal const string DiagnosticId = "RBI0001";
static readonly LocalizableString Title = new LocalizableResourceString (nameof (Resources.RBI0001Title),
Resources.ResourceManager, typeof (Resources));
static readonly LocalizableString MessageFormat =
new LocalizableResourceString (nameof (Resources.RBI0001MessageFormat), Resources.ResourceManager,
typeof (Resources));
static readonly LocalizableString Description =
new LocalizableResourceString (nameof (Resources.RBI0001Description), Resources.ResourceManager,
typeof (Resources));
const string Category = "Usage";

static readonly DiagnosticDescriptor RBI0001 = new (DiagnosticId, Title, MessageFormat, Category,
DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create (RBI0001);

public override void Initialize (AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution ();
context.RegisterSyntaxNodeAction (AnalysisContext, SyntaxKind.ClassDeclaration);
}

void AnalysisContext (SyntaxNodeAnalysisContext context)
{
// only care about classes
if (context.Node is not ClassDeclarationSyntax classDeclarationNode)
return;

var classSymbol = context.SemanticModel.GetDeclaredSymbol (classDeclarationNode);
if (classSymbol is null)
return;

var boundAttributes = classSymbol.GetAttributes ();
if (boundAttributes.Length == 0) {
return;
}

// the c# syntax is a a list of lists of attributes. That is why we need to iterate through the list of lists
foreach (var attributeData in boundAttributes) {
// based on the type use the correct parser to retrieve the data
var attributeType = attributeData.AttributeClass?.ToDisplayString ();
switch (attributeType) {
case "ObjCBindings.BindingTypeAttribute":
// validate that the class is partial, else we need to report an error
if (!classDeclarationNode.Modifiers.Any (x => x.IsKind (SyntaxKind.PartialKeyword))) {
var diagnostic = Diagnostic.Create (RBI0001,
classDeclarationNode.Identifier.GetLocation (), // point to where the 'class' keyword is used
classSymbol.ToDisplayString ());
context.ReportDiagnostic (diagnostic);
}
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>

<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>

<RootNamespace>Microsoft.Macios.Bindings.Analyzer</RootNamespace>
<AssemblyName>Microsoft.Macios.Bindings.Analyzer</AssemblyName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<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"/>
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Microsoft.Macios.Bindings.Analyzer.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<EmbeddedResource Update="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<Compile Update="Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"DebugRoslynAnalyzers": {
"commandName": "DebugRoslynComponent",
"targetProject": "../Microsoft.Macios.Bindings.Analyzer.Sample/Microsoft.Macios.Bindings.Analyzer.Sample.csproj"
}
}
}
26 changes: 26 additions & 0 deletions src/rgen/Microsoft.Macios.Bindings.Analyzer/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Roslyn Analyzers Sample

Roslyn analyzer to be installed along side the Roslyn Conde generator that will help developers work on Microsoft.Macios bindings.

## Content
### Microsoft.Macios.Bindings.Analyzer

A .NET Standard project with implementations of sample analyzers and code fix providers.
**You must build this project to see the results (warnings) in the IDE.**

### Microsoft.Macios.Bindings.Analyzer.Sample
A project that references the sample analyzers. Note the parameters of `ProjectReference` in [Microsoft.Macios.Bindings.Analyzer.Sample.csproj](../Microsoft.Macios.Bindings.Analyzer.Sample/Microsoft.Macios.Bindings.Analyzer.Sample.csproj), they make sure that the project is referenced as a set of analyzers.

### Microsoft.Macios.Bindings.Analyzer.Tests
Unit tests for the sample analyzers and code fix provider. The easiest way to develop language-related features is to start with unit tests.

## How To?
### How to debug?
- Use the [launchSettings.json](Properties/launchSettings.json) profile.
- Debug tests (in VSCode).

### How can I determine which syntax nodes I should expect?
Consider installing the Roslyn syntax tree viewer plugin [Rossynt](https://plugins.jetbrains.com/plugin/16902-rossynt/).

### Learn more about wiring analyzers
The complete set of information is available at [roslyn github repo wiki](https://github.com/dotnet/roslyn/blob/main/docs/wiki/README.md).
Loading

5 comments on commit 0f35909

@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.