Skip to content
Open
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
108 changes: 108 additions & 0 deletions src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 53 additions & 1 deletion src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -540,11 +540,63 @@
<value>No result type or result type name was provided for an async method.</value>
</data>
<data name="RBI0040MessageFormat" xml:space="preserve">
<value>The method '{0}' was marked as async and has multiple parameters but does not provide a return type name, a nameless tuple will be generated for the async method</value>
<value>The method '{0}' was marked as async and has multiple parameters but does not provide a return type name, a nameless tuple will be generated for the async method</value>
<comment>{0} is the name of the async method.</comment>
</data>
<data name="RBI0040Title" xml:space="preserve">
<value>Not specified return type</value>
</data>

<!-- RBI0041 -->

<data name="RBI0041Description" xml:space="preserve">
<value>UIView subclasses must have a initWithFrame: constructor..</value>
</data>
<data name="RBI0041MessageFormat" xml:space="preserve">
<value>The class '{0}' inherits from UIView but does not expose a 'initWithFrame:' constructor</value>
<comment>{0} is the name of the class.</comment>
</data>
<data name="RBI0041Title" xml:space="preserve">
<value>Missing UIView initWithFrame:</value>
</data>

<!-- RBI0042 -->

<data name="RBI0042Description" xml:space="preserve">
<value>Category methods have to be partial.</value>
</data>
<data name="RBI0042MessageFormat" xml:space="preserve">
<value>The method '{0}' must me partial</value>
Copy link
Preview

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two typos in resource strings: 'must me partial' should be 'must be partial' and 'categoyr' should be 'category'.

Copilot uses AI. Check for mistakes.

<comment>{0} is the name of the method.</comment>
</data>
<data name="RBI0042Title" xml:space="preserve">
<value>Not partial category method</value>
</data>

<!-- RBI0043 -->

<data name="RBI0043Description" xml:space="preserve">
<value>All category methods have to be extensions.</value>
</data>
<data name="RBI0043MessageFormat" xml:space="preserve">
<value>The method '{0}' in category '{1}' has to be an extension method for '{2}'</value>
<comment>{0} is the name of the class.</comment>
</data>
<data name="RBI0043Title" xml:space="preserve">
<value>All methods in an category have to be extensions</value>
</data>

<!-- RBI0044 -->

<data name="RBI0044Description" xml:space="preserve">
<value>Extension methods have to be of the same type as of the category.</value>
</data>
<data name="RBI0044MessageFormat" xml:space="preserve">
<value>Extension method '{0}' in category '{1}' must have the first parameter type match the category's extended type '{2}' found '{3}'</value>
<comment>{0} is the name of the method, {1} the name of the categoyr, {2} is the category type and {3} is the type found</comment>
Copy link
Preview

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two typos in resource strings: 'must me partial' should be 'must be partial' and 'categoyr' should be 'category'.

Copilot uses AI. Check for mistakes.

</data>
<data name="RBI0044Title" xml:space="preserve">
<value>Wrong 'this' parameter in category method</value>
</data>

</root>
60 changes: 60 additions & 0 deletions src/rgen/Microsoft.Macios.Bindings.Analyzer/RgenDiagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,4 +616,64 @@ public static class RgenDiagnostics {
description: new LocalizableResourceString (nameof (Resources.RBI0040Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// Disgnostic descriptor for when a class inherits from UIView and is missing the initWithFrame: constructor.
/// </summary>
internal static readonly DiagnosticDescriptor RBI0041 = new (
"RBI0041",
new LocalizableResourceString (nameof (Resources.RBI0041Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0041MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0041Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// Disgnostic descriptor for when a category method is not partial
Copy link
Preview

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in XML documentation comments: 'Disgnostic' should be 'Diagnostic' in all three instances.

Copilot uses AI. Check for mistakes.

/// </summary>
internal static readonly DiagnosticDescriptor RBI0042 = new (
"RBI0042",
new LocalizableResourceString (nameof (Resources.RBI0042Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0042MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0042Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// Disgnostic descriptor for when a category method is not and extension
Copy link
Preview

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in XML documentation comments: 'Disgnostic' should be 'Diagnostic' in all three instances.

Copilot uses AI. Check for mistakes.

/// </summary>
internal static readonly DiagnosticDescriptor RBI0043 = new (
"RBI0043",
new LocalizableResourceString (nameof (Resources.RBI0043Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0043MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0043Description), Resources.ResourceManager,
typeof (Resources))
);

/// <summary>
/// Disgnostic descriptor for when a category method is not and extension of the category's target type
Copy link
Preview

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in XML documentation comments: 'Disgnostic' should be 'Diagnostic' in all three instances.

Copilot uses AI. Check for mistakes.

/// </summary>
internal static readonly DiagnosticDescriptor RBI0044 = new (
"RBI0044",
new LocalizableResourceString (nameof (Resources.RBI0044Title), Resources.ResourceManager, typeof (Resources)),
new LocalizableResourceString (nameof (Resources.RBI0044MessageFormat), Resources.ResourceManager,
typeof (Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: new LocalizableResourceString (nameof (Resources.RBI0044Description), Resources.ResourceManager,
typeof (Resources))
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.Macios.Generator;
using Microsoft.Macios.Generator.Attributes;
using Microsoft.Macios.Generator.Context;
using Microsoft.Macios.Generator.DataModel;
using ObjCBindings;
using static Microsoft.Macios.Generator.RgenDiagnostics;

namespace Microsoft.Macios.Bindings.Analyzer.Validators;
Expand All @@ -11,15 +17,69 @@ namespace Microsoft.Macios.Bindings.Analyzer.Validators;
/// </summary>
sealed class CategoryValidator : BindingValidator {

/// <summary>
/// Validates that all methods in a category binding are properly declared as partial extension methods
/// with the correct first parameter type matching the category's extended type.
/// </summary>
/// <param name="binding">The category binding to validate.</param>
/// <param name="context">The root context for validation.</param>
/// <param name="diagnostics">When this method returns, contains diagnostics for any invalid methods; otherwise, an empty array.</param>
/// <param name="location">The code location to be used for the diagnostics.</param>
/// <returns><c>true</c> if all methods are valid; otherwise, <c>false</c>.</returns>
bool ValidMethods (Binding binding, RootContext context,
out ImmutableArray<Diagnostic> diagnostics, Location? location = null)
{
// we need to make sure that all the methods of the category are:
// 1. partial
// 2. static
// 3. Have the correct first parameter type (the type that the category is extending)
var builder = ImmutableArray.CreateBuilder<Diagnostic> ();
foreach (var extensionMethod in binding.Methods) {
if (!ModifiersStrategies.IsPartial (extensionMethod.Modifiers, RBI0042,
out var partialDiagnostics, extensionMethod.Location, extensionMethod.Name)) {
builder.AddRange (partialDiagnostics);
}
// check that it is an extension method
var bindingData = (BindingTypeData<Category>) binding.BindingInfo;
if (extensionMethod.IsExtension) {
// ensure that the first parameter type matches the type that the category is extending
if (extensionMethod.Parameters [0].Type.FullyQualifiedName !=
bindingData.CategoryType.FullyQualifiedName) {
// we do not allow to mix types in the extension methods in a category
builder.Add (Diagnostic.Create (
RBI0044, // Extension methods in a category must have the first parameter type match the category's extended type.
extensionMethod.Location,
extensionMethod.Name,
binding.Name,
bindingData.CategoryType.FullyQualifiedName,
extensionMethod.Parameters [0].Type.FullyQualifiedName));
}
} else {
// method should be an extension
builder.Add (Diagnostic.Create (
RBI0043, // Extension methods in a category must be declared as extension methods.
extensionMethod.Location,
extensionMethod.Name,
binding.Name,
bindingData.CategoryType.FullyQualifiedName
));
}
}

diagnostics = builder.ToImmutable ();
return diagnostics.Length == 0;
}

/// <summary>
/// Initializes a new instance of the <see cref="CategoryValidator"/> class.
/// </summary>
public CategoryValidator ()
{

// all bindings must be partial
AddGlobalStrategy (RBI0001, IsPartial);
// categories must be static
AddGlobalStrategy (RBI0004, IsStatic);
// validate all methods in the category binding
AddGlobalStrategy ([RBI0042, RBI0043, RBI0044], ValidMethods);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,39 @@ bool ValidAsyncMethods (Binding binding, RootContext context,
return diagnostics.Length == 0;
}

/// <summary>
/// Validates that required constructors are present for specific base classes.
/// Currently checks that UIView bindings include the initWithFrame: constructor.
/// </summary>
/// <param name="binding">The binding to validate.</param>
/// <param name="context">The root context for validation.</param>
/// <param name="diagnostics">When this method returns, contains diagnostics for any missing required constructors; otherwise, an empty array.</param>
/// <param name="location">The code location to be used for the diagnostics.</param>
/// <returns><c>true</c> if all required constructors are present; otherwise, <c>false</c>.</returns>
bool ValidConstructors (Binding binding, RootContext context,
out ImmutableArray<Diagnostic> diagnostics, Location? location = null)
{
diagnostics = [];
// in this case we want to make sure that some base constructors are present. At the moment we only care about
// the initWithFrame: constructor from the UIView base class.
if (!binding.TypeInfo.IsView)
return true;
// get all the constructors and ensure that the initWithFrame: constructor is present
var hasInitWithFrame = binding.Constructors.Any (ctor => ctor.ExportMethodData.Selector == "initWithFrame:");
if (!hasInitWithFrame) {
// error, all UIView bindings must provide the initWithFrame: constructor to be valid
diagnostics = [
Diagnostic.Create (
descriptor: RBI0041,
location: location,
messageArgs: [
binding.Name,
])
];
}
return diagnostics.Length == 0;
}

/// <summary>
/// Initializes a new instance of the <see cref="ClassValidator"/> class.
/// </summary>
Expand All @@ -330,6 +363,9 @@ public ClassValidator ()
// validate that the selectors are not duplicated, this includes properties and methods
AddGlobalStrategy ([RBI0034], SelectorsAreUnique);

// validate that we have the required constructors for certain base classes like UIView
AddGlobalStrategy ([RBI0041], ValidConstructors);

// validate async methods. This is a global strategy because it needs to look at all the methods in the binding
// are validated together so that async methods do not have the same names
AddGlobalStrategy ([RBI0035, RBI0036, RBI0037, RBI0038, RBI0039, RBI0040], ValidAsyncMethods);
Expand Down
Loading