Skip to content

Commit

Permalink
[Rgen] Complete the BindingTypeAttribute analyzer. (#21946)
Browse files Browse the repository at this point in the history
The analyzer now checks the following:

* All usages of `bindingTypeAttribute<T>` have to happen on partial
classes.
* `BindingType<Class>` can only be used on classes.
* `BindingType<Category>` can only be used on static classes.
* `BindingType<Protocol>` can only be used on interfaces.
* `BindingType<StrongDictionary>` can only be used on classes.
* `BindingType` can only be used on enums.

Because we have not yet released any of the errors, we have sorted them
to be the first ones from 0001-0006.

---------

Co-authored-by: GitHub Actions Autoformatter <github-actions-autoformatter@xamarin.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
  • Loading branch information
4 people authored Jan 13, 2025
1 parent d0c520e commit 2c7759a
Show file tree
Hide file tree
Showing 13 changed files with 743 additions and 109 deletions.
12 changes: 12 additions & 0 deletions src/ObjCBindings/BindingTypeTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,16 @@ public enum Category : Int64 {
/// </summary>
Default = 0,
}

/// <summary>
/// Flags to be used on strong dictionary bindings.
/// </summary>
[Flags]
[Experimental ("APL0003")]
public enum StrongDictionary : Int64 {
/// <summary>
/// Use the default values.
/// </summary>
Default = 0,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
| Rule ID | Category | Severity | Notes |
|---------|----------|----------|---------------------------------------------------------------------------|
| RBI0001 | Usage | Error | Binding types should be declared as partial classes. |
| RBI0002 | Usage | Error | Smart enum values must be tagged with an Field<EnumValue> attribute. |
| RBI0003 | Usage | Error | Smart enum backing field cannot appear more than once. |
| RBI0004 | Usage | Error | Smart enum backing field must represent a valid C# identifier to be used. |
| RBI0005 | Usage | Error | Non Apple framework bindings must provide a library name. |
| RBI0006 | Usage | Warning | Do not provide the LibraryName for known Apple frameworks. |
| RBI0007 | Usage | Error | Enum values must be tagged with Field<EnumValue>. |
| RBI0002 | Usage | Error | BindingType<Class> must be on a class. |
| RBI0003 | Usage | Error | BindingType<Category> must be on a class. |
| RBI0004 | Usage | Error | BindingType<Category> must be on a static class. |
| RBI0005 | Usage | Error | BindingType<Protocol> must be on an interface. |
| RBI0006 | Usage | Error | BindingType must be on an enumerator. |
| RBI0007 | Usage | Error | BindingType<StrongDictionary> must be on a class. |
| RBI0008 | Usage | Error | Smart enum values must be tagged with an Field<EnumValue> attribute. |
| RBI0009 | Usage | Error | Smart enum backing field cannot appear more than once. |
| RBI0010 | Usage | Error | Smart enum backing field must represent a valid C# identifier to be used. |
| RBI0011 | Usage | Error | Non Apple framework bindings must provide a library name. |
| RBI0012 | Usage | Warning | Do not provide the LibraryName for known Apple frameworks. |
| RBI0013 | Usage | Error | Enum values must be tagged with Field<EnumValue>. |
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Macios.Bindings.Analyzer.Extensions;
using Microsoft.Macios.Generator;

namespace Microsoft.Macios.Bindings.Analyzer;

Expand All @@ -15,39 +19,255 @@ namespace Microsoft.Macios.Bindings.Analyzer;
/// pattern.
/// </summary>
[DiagnosticAnalyzer (LanguageNames.CSharp)]
public class BindingTypeSemanticAnalyzer : DiagnosticAnalyzer, IBindingTypeAnalyzer<ClassDeclarationSyntax> {

public class BindingTypeSemanticAnalyzer : DiagnosticAnalyzer, IBindingTypeAnalyzer<BaseTypeDeclarationSyntax> {
/// <summary>
/// All binding types should be partial.
/// </summary>
internal static readonly DiagnosticDescriptor RBI0001 = new (
"RBI0001",
new LocalizableResourceString (nameof (Resources.RBI0001Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0001MessageFormat), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0001MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0001Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// BindingType&lt;Class&gt; can only decorate partial classes.
/// </summary>
internal static readonly DiagnosticDescriptor RBI0002 = new (
"RBI0002",
new LocalizableResourceString (nameof (Resources.RBI0002Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0002MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0002Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// BindingType&lt;Category&gt; can only decorate partial classes.
/// </summary>
internal static readonly DiagnosticDescriptor RBI0003 = new (
"RBI0003",
new LocalizableResourceString (nameof (Resources.RBI0003Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0003MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0003Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// BindingType&lt;Category&gt; can only decorate static classes.
/// </summary>
internal static readonly DiagnosticDescriptor RBI0004 = new (
"RBI0004",
new LocalizableResourceString (nameof (Resources.RBI0004Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0004MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0004Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// BindingType&lt;Protocol&gt; can only decorate interfaces.
/// </summary>
internal static readonly DiagnosticDescriptor RBI0005 = new (
"RBI0005",
new LocalizableResourceString (nameof (Resources.RBI0005Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0005MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0005Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// BindingType can only decorate enumerators.
/// </summary>
internal static readonly DiagnosticDescriptor RBI0006 = new (
"RBI0006",
new LocalizableResourceString (nameof (Resources.RBI0006Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0006MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0006Description), Resources.ResourceManager,
typeof (Resources))
);

internal static readonly DiagnosticDescriptor RBI0007 = new (
"RBI0007",
new LocalizableResourceString (nameof (Resources.RBI0007Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0007MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0001Description), Resources.ResourceManager, typeof (Resources))
description: new LocalizableResourceString (nameof (Resources.RBI0007Description), Resources.ResourceManager,
typeof (Resources))
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [RBI0001];
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [
RBI0001,
RBI0002,
RBI0003,
RBI0004,
RBI0005,
RBI0006,
RBI0007,
];

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

void AnalysisContext (SyntaxNodeAnalysisContext context)
=> this.AnalyzeBindingType (context);

public ImmutableArray<Diagnostic> Analyze (PlatformName _, ClassDeclarationSyntax declarationNode, INamedTypeSymbol symbol)
static readonly HashSet<string> attributes = new HashSet<string> (AttributesNames.BindingTypes);
public IReadOnlySet<string> AttributeNames => attributes;

ImmutableArray<Diagnostic> ValidateClass (BaseTypeDeclarationSyntax declarationNode, INamedTypeSymbol symbol)
{
var bucket = ImmutableArray.CreateBuilder<Diagnostic> ();
if (declarationNode is ClassDeclarationSyntax classDeclarationSyntax) {
if (!classDeclarationSyntax.IsPartial ()) {
var partialDiagnostic = Diagnostic.Create (
descriptor: RBI0001, // Binding types should be declared as partial classes.
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (partialDiagnostic);
}
} else {
var notAClassDiagnostic = Diagnostic.Create (
descriptor: RBI0002, // BindingType<Class> must be on a class
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (notAClassDiagnostic);
}

return bucket.ToImmutable ();
}

ImmutableArray<Diagnostic> ValidateCategory (BaseTypeDeclarationSyntax declarationNode, INamedTypeSymbol symbol)
{
if (declarationNode.Modifiers.Any (x => x.IsKind (SyntaxKind.PartialKeyword)))
return [];
var bucket = ImmutableArray.CreateBuilder<Diagnostic> ();
if (declarationNode is ClassDeclarationSyntax classDeclarationSyntax) {
if (!classDeclarationSyntax.IsPartial ()) {
var partialDiagnostic = Diagnostic.Create (
descriptor: RBI0001, // Binding types should be declared as partial classes.
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (partialDiagnostic);
}

var diagnostic = Diagnostic.Create (RBI0001, // Binding types should be declared as partial classes.
declarationNode.Identifier.GetLocation (), // point to where the 'class' keyword is used
symbol.ToDisplayString ());
return [diagnostic];
if (!classDeclarationSyntax.IsStatic ()) {
var partialDiagnostic = Diagnostic.Create (
descriptor: RBI0004, // BindingType<Category> must be on a static class
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (partialDiagnostic);
}
} else {
var notAClassDiagnostic = Diagnostic.Create (
descriptor: RBI0003, // BindingType<Category> must be on a class
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (notAClassDiagnostic);
}

return bucket.ToImmutable ();
}

ImmutableArray<Diagnostic> ValidateProtocol (BaseTypeDeclarationSyntax declarationNode, INamedTypeSymbol symbol)
{
var bucket = ImmutableArray.CreateBuilder<Diagnostic> ();
if (declarationNode is InterfaceDeclarationSyntax interfaceDeclarationSyntax) {
if (!interfaceDeclarationSyntax.IsPartial ()) {
var partialDiagnostic = Diagnostic.Create (
descriptor: RBI0001, // Binding types should be declared as partial classes.
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (partialDiagnostic);
}
} else {
var notAInterfaceDiagnostic = Diagnostic.Create (
descriptor: RBI0005, // BindingType<Protocol> must be on an interface
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (notAInterfaceDiagnostic);
}

return bucket.ToImmutable ();
}

ImmutableArray<Diagnostic> ValidateStrongDictionary (BaseTypeDeclarationSyntax declarationNode,
INamedTypeSymbol symbol)
{
var bucket = ImmutableArray.CreateBuilder<Diagnostic> ();
if (declarationNode is ClassDeclarationSyntax classDeclarationSyntax) {
if (!classDeclarationSyntax.IsPartial ()) {
var partialDiagnostic = Diagnostic.Create (
descriptor: RBI0001, // Binding types should be declared as partial classes.
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (partialDiagnostic);
}
} else {
var notAInterfaceDiagnostic = Diagnostic.Create (
descriptor: RBI0007, // BindingType<StrongDictionary> must be on a class
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (notAInterfaceDiagnostic);
}

return bucket.ToImmutable ();
}

public ImmutableArray<Diagnostic> ValidateSmartEnum (BaseTypeDeclarationSyntax declarationNode,
INamedTypeSymbol symbol)
{
var bucket = ImmutableArray.CreateBuilder<Diagnostic> ();
if (declarationNode is not EnumDeclarationSyntax) {
var notAInterfaceDiagnostic = Diagnostic.Create (
descriptor: RBI0006, // BindingType must be on an enumerator
location: declarationNode.Identifier.GetLocation (),
messageArgs: symbol.ToDisplayString ());
bucket.Add (notAInterfaceDiagnostic);
}

return bucket.ToImmutable ();
}

public ImmutableArray<Diagnostic> Analyze (string matchedAttribute, PlatformName _,
BaseTypeDeclarationSyntax declarationNode, INamedTypeSymbol symbol)
=> matchedAttribute switch {
AttributesNames.BindingClassAttribute => ValidateClass (declarationNode, symbol),
AttributesNames.BindingCategoryAttribute => ValidateCategory (declarationNode, symbol),
AttributesNames.BindingProtocolAttribute => ValidateProtocol (declarationNode, symbol),
AttributesNames.BindingAttribute => ValidateSmartEnum (declarationNode, symbol),
AttributesNames.BindingStrongDictionaryAttribute => ValidateStrongDictionary (declarationNode, symbol),
_ => throw new InvalidOperationException ($"Not recognized attribute {matchedAttribute}.")
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Microsoft.Macios.Bindings.Analyzer.Extensions;

public static class BaseTypeDeclarationSyntaxExtensions {

/// <summary>
/// Returns if the base type declaration was declared as a partial one.
/// </summary>
/// <param name="baseTypeDeclarationSyntax">The declaration under test.</param>
/// <returns>True if the declaration is partial.</returns>
public static bool IsPartial (this BaseTypeDeclarationSyntax baseTypeDeclarationSyntax)
=> baseTypeDeclarationSyntax.Modifiers.Any (x => x.IsKind (SyntaxKind.PartialKeyword));

/// <summary>
/// Returns if the based type declaration was declared as a static one.
/// </summary>
/// <param name="baseTypeDeclarationSyntax">The declaration under test.</param>
/// <returns>True if the declaration is static.</returns>
public static bool IsStatic (this BaseTypeDeclarationSyntax baseTypeDeclarationSyntax)
=> baseTypeDeclarationSyntax.Modifiers.Any (x => x.IsKind (SyntaxKind.StaticKeyword));

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,18 @@ public static void AnalyzeBindingType<T> (this IBindingTypeAnalyzer<T> self, Syn
return;
}

// the c# syntax is a a list of lists of attributes. That is why we need to iterate through the list of lists
// 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 AttributesNames.BindingAttribute:
// validate that the class is partial, else we need to report an error
var diagnostics = self.Analyze (context.Compilation.GetCurrentPlatform (),
declarationNode, declaredSymbol);
foreach (var diagnostic in diagnostics)
context.ReportDiagnostic (diagnostic);
break;
}
// ignore attrs whose name we cannot get, or we do not care about
if (attributeType is null || !self.AttributeNames.Contains (attributeType))
continue;

var diagnostics = self.Analyze (attributeType, context.Compilation.GetCurrentPlatform (),
declarationNode, declaredSymbol);
foreach (var diagnostic in diagnostics)
context.ReportDiagnostic (diagnostic);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -10,5 +12,7 @@ namespace Microsoft.Macios.Bindings.Analyzer;
/// Interface to be implemented by those analyzer that will be looking at BindingTypes.
/// </summary>
public interface IBindingTypeAnalyzer<T> where T : BaseTypeDeclarationSyntax {
ImmutableArray<Diagnostic> Analyze (PlatformName platformName, T declarationNode, INamedTypeSymbol symbol);
IReadOnlySet<string> AttributeNames { get; }

ImmutableArray<Diagnostic> Analyze (string matchedAttribute, PlatformName platformName, T declarationNode, INamedTypeSymbol symbol);
}
Loading

0 comments on commit 2c7759a

Please sign in to comment.