From 697eee977c287b2802275fef43c78e35f50e568a Mon Sep 17 00:00:00 2001 From: Manuel de la Pena Date: Thu, 17 Oct 2024 08:54:19 -0400 Subject: [PATCH] [Rgen] Implement SmartEnums code emittion. This change brings the following: 1. We now generate the Extension classes for the SmartEnums. 2. Bumped the roslyn version, the one we were using had a bug when using dir separators in the name hint for a generated file. 3. Generated code is added under the expected directory for bgen to allow the API diff. --- src/Makefile | 2 + src/Makefile.rgenerator | 3 + src/ObjCBindings/FieldAttribute.cs | 23 ++ src/ObjCBindings/FieldTag.cs | 10 + src/frameworks.sources | 2 + .../Microsoft.Macios.Binding.Common.csproj | 27 ++ .../Examples.cs | 34 +- ...oft.Macios.Bindings.Analyzer.Sample.csproj | 10 +- .../AnalyzerReleases.Shipped.md | 3 +- .../AnalyzerReleases.Unshipped.md | 10 +- .../BindingTypeCodeFixProvider.cs | 2 +- .../BindingTypeSemanticAnalyzer.cs | 64 ++-- .../IBindingTypeAnalyzerExtensions.cs | 42 +++ .../Extensions/PlatformNameExtensions.cs | 16 + .../IBindingTypeAnalyzer.cs | 12 + .../Microsoft.Macios.Bindings.Analyzer.csproj | 27 +- .../Resources.Designer.cs | 98 +++++- .../Resources.resx | 65 ++++ .../SmartEnumSemanticAnalyzer.cs | 165 +++++++++ .../Microsoft.Macios.Generator.Sample.csproj | 9 +- .../Attributes/FieldData.cs | 51 +++ .../AttributesNames.cs | 3 +- .../BindingSourceGeneratorGenerator.cs | 144 +++++--- .../Microsoft.Macios.Generator/CodeChanges.cs | 70 ++++ .../CodeChangesComparer.cs | 34 ++ .../Context/ContextFactory.cs | 1 - .../Context/ISymbolBindingContext.cs | 6 +- .../Context/RootBindingContext.cs | 82 +++-- .../Context/SymbolBindingContext.cs | 1 - .../Emitters/ClassEmitter.cs | 3 + .../Emitters/EnumEmitter.cs | 139 +++++++- .../Emitters/ICodeEmitter.cs | 2 + .../Emitters/InterfaceEmitter.cs | 2 + .../Emitters/LibraryEmitter.cs | 112 ++++++ .../Emitters/TrampolineEmitter.cs | 17 + .../BaseTypeDeclarationSyntaxExtensions.cs | 41 +++ .../Extensions/CompilationExtensions.cs | 25 ++ .../MemberDeclarationSyntaxExtensions.cs | 24 ++ .../Extensions/NamedTypeSymbolExtensions.cs | 47 +++ .../Extensions/TypeSymbolExtensions.cs | 28 ++ .../Microsoft.Macios.Generator.csproj | 21 +- .../TabbedStringBuilder.cs | 18 +- src/rgen/rgen.sln | 6 + .../BindingTypeSemanticAnalyzerTests.cs | 4 +- .../SmartEnumSemanticAnalyzerTests.cs | 330 ++++++++++++++++++ .../BaseGeneratorTestClass.cs | 34 +- ...aseTypeDeclarationSyntaxExtensionsTests.cs | 184 ++++++++++ .../BindingSourceGeneratorGeneratorTests.cs | 4 +- .../CodeChangesComparerTests.cs | 129 +++++++ .../CompilationExtensionsTest.cs | 22 ++ .../Microsoft.Macios.Generator.Tests.csproj | 17 +- .../SmartEnum/Data/AVCaptureDeviceTypeEnum.cs | 42 +++ .../Data/AVCaptureSystemPressureLevel.cs | 23 ++ .../SmartEnum/Data/AVMediaCharacteristics.cs | 74 ++++ .../SmartEnum/Data/CustomLibraryEnum.cs | 17 + .../Data/ExpectedAVCaptureDeviceTypeEnum.cs | 222 ++++++++++++ .../ExpectedAVCaptureSystemPressureLevel.cs | 132 +++++++ .../Data/ExpectedCustomLibraryEnum.cs | 103 ++++++ ...ExpectedCustomLibraryEnumLibrariesClass.cs | 19 + .../ExpectedMacOSAVMediaCharacteristics.cs | 313 +++++++++++++++++ .../Data/ExpectediOSAVMediaCharacteristics.cs | 328 +++++++++++++++++ .../SmartEnum/SmartEnumTests.cs | 94 +++++ .../TabbedStringBuilderTests.cs | 19 + 63 files changed, 3425 insertions(+), 186 deletions(-) create mode 100644 src/ObjCBindings/FieldAttribute.cs create mode 100644 src/ObjCBindings/FieldTag.cs create mode 100644 src/rgen/Microsoft.Macios.Binding.Common/Microsoft.Macios.Binding.Common.csproj create mode 100644 src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/IBindingTypeAnalyzerExtensions.cs create mode 100644 src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/PlatformNameExtensions.cs create mode 100644 src/rgen/Microsoft.Macios.Bindings.Analyzer/IBindingTypeAnalyzer.cs create mode 100644 src/rgen/Microsoft.Macios.Bindings.Analyzer/SmartEnumSemanticAnalyzer.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Attributes/FieldData.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/CodeChanges.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/CodeChangesComparer.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Emitters/LibraryEmitter.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Emitters/TrampolineEmitter.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Extensions/BaseTypeDeclarationSyntaxExtensions.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Extensions/CompilationExtensions.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Extensions/MemberDeclarationSyntaxExtensions.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Extensions/NamedTypeSymbolExtensions.cs create mode 100644 src/rgen/Microsoft.Macios.Generator/Extensions/TypeSymbolExtensions.cs create mode 100644 tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/SmartEnumSemanticAnalyzerTests.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/BaseTypeDeclarationSyntaxExtensionsTests.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/CodeChangesComparerTests.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/CompilationExtensionsTest.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureDeviceTypeEnum.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureSystemPressureLevel.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVMediaCharacteristics.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/CustomLibraryEnum.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureDeviceTypeEnum.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureSystemPressureLevel.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnum.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnumLibrariesClass.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedMacOSAVMediaCharacteristics.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectediOSAVMediaCharacteristics.cs create mode 100644 tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/SmartEnumTests.cs diff --git a/src/Makefile b/src/Makefile index bb088101958..6ae73e027d2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -455,7 +455,9 @@ $($(2)_DOTNET_BUILD_DIR)/$(4)/Microsoft.$(1)%dll $($(2)_DOTNET_BUILD_DIR)/$(4)/M $$(call Q_PROF_CSC,dotnet/$(4)-bit) \ $(DOTNET_CSC) \ $(DOTNET_FLAGS) \ + /analyzer:$(ROSLYN_GENERATOR_COMMON) \ /analyzer:$(ROSLYN_GENERATOR) \ + /generatedfilesout:$($(2)_DOTNET_BUILD_DIR)/generated-sources \ -unsafe \ -optimize \ $$(ARGS_$(1)) \ diff --git a/src/Makefile.rgenerator b/src/Makefile.rgenerator index 61bbed1d206..1c29cd07ba8 100644 --- a/src/Makefile.rgenerator +++ b/src/Makefile.rgenerator @@ -1,7 +1,10 @@ # Roslyn code generator ROSLYN_GENERATOR=$(DOTNET_BUILD_DIR)/common/rgen/Microsoft.Macios.Generator.dll +ROSLYN_GENERATOR_COMMON=$(DOTNET_BUILD_DIR)/common/rgen/Microsoft.Macios.Binding.Common.dll ROSLYN_GENERATOR_FILES := $(wildcard rgen/Microsoft.Macios.Generator/*.cs) ROSLYN_GENERATOR_FILES += $(wildcard rgen/Microsoft.Macios.Generator/*/*.cs) +ROSLYN_GENERATOR_FILES += $(wildcard rgen/Microsoft.Macios.Binding.Common/*.cs) +ROSLYN_GENERATOR_FILES += $(wildcard rgen/Microsoft.Macios.Binding.Common/*/*.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/)/ diff --git a/src/ObjCBindings/FieldAttribute.cs b/src/ObjCBindings/FieldAttribute.cs new file mode 100644 index 00000000000..2e8b4e7bea1 --- /dev/null +++ b/src/ObjCBindings/FieldAttribute.cs @@ -0,0 +1,23 @@ +using System; + +#nullable enable + +namespace ObjCBindings { + + [AttributeUsage (AttributeTargets.Property | AttributeTargets.Field)] + public sealed class FieldAttribute : Attribute where T : FieldTag { + public FieldAttribute (string symbolName) + { + SymbolName = symbolName; + } + + public FieldAttribute (string symbolName, string libraryName) + { + SymbolName = symbolName; + LibraryName = libraryName; + } + + public string SymbolName { get; set; } + public string? LibraryName { get; set; } + } +} diff --git a/src/ObjCBindings/FieldTag.cs b/src/ObjCBindings/FieldTag.cs new file mode 100644 index 00000000000..cf3b8f8a6a7 --- /dev/null +++ b/src/ObjCBindings/FieldTag.cs @@ -0,0 +1,10 @@ +using System; + +#nullable enable + +namespace ObjCBindings { + + public abstract class FieldTag {} + + public sealed class EnumValue : FieldTag {} +} diff --git a/src/frameworks.sources b/src/frameworks.sources index ec3f51ec8d6..fac676d5f8c 100644 --- a/src/frameworks.sources +++ b/src/frameworks.sources @@ -2015,6 +2015,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 \ diff --git a/src/rgen/Microsoft.Macios.Binding.Common/Microsoft.Macios.Binding.Common.csproj b/src/rgen/Microsoft.Macios.Binding.Common/Microsoft.Macios.Binding.Common.csproj new file mode 100644 index 00000000000..782195534fb --- /dev/null +++ b/src/rgen/Microsoft.Macios.Binding.Common/Microsoft.Macios.Binding.Common.csproj @@ -0,0 +1,27 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion) + enable + enable + + + + + external\ApplePlatform.cs + + + external\SdkVersions.cs + + + external\TargetFramework.cs + + + external\Frameworks.cs + + + external\PlatformName.cs + + + + diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs index 16717f55431..93f3ce92a24 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs @@ -1,14 +1,32 @@ +using Foundation; using ObjCBindings; -namespace Microsoft.Macios.Bindings.Analyzer.Sample; +namespace Microsoft.Macios.Bindings.Analyzer.Sample { + // If you don't see warnings, build the Analyzers Project. -// If you don't see warnings, build the Analyzers Project. + [BindingType] + public enum Test { + [Field ("TheSymbolNone", LibraryName = "/my/path/to/my/Manuel.framework")] + None, + [Field ("TheSymbolMedium", LibraryName = "/my/path/to/my/Manuel.framework")] + Medium, + [Field ("TheSymbolHigh", LibraryName = "/my/path/to/my/Manuel.framework")] + High, + } -[BindingType] -public class Examples { + // This does not regenerate a thing + namespace NestedExample { + [BindingType] + public enum Test { + [Field ("TheSymbolNone", LibraryName = "/my/path/to/my/Manuel.framework")] + None, + [Field ("TheSymbolMedium", LibraryName = "/my/path/to/my/Manuel.framework")] + Medium, + [Field ("TheSymbolHigh", LibraryName = "/my/path/to/my/Manuel.framework")] + High, + [Field ("TheSymbolFoo", LibraryName = "/my/path/to/my/Manuel.framework")] + Test, + } + } } -[BindingType] -public class Foo { - -} diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Microsoft.Macios.Bindings.Analyzer.Sample.csproj b/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Microsoft.Macios.Bindings.Analyzer.Sample.csproj index a02eefcf4af..c3668fe2f62 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Microsoft.Macios.Bindings.Analyzer.Sample.csproj +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Microsoft.Macios.Bindings.Analyzer.Sample.csproj @@ -1,12 +1,15 @@ - + - net$(BundledNETCoreAppTargetFrameworkVersion) + net$(BundledNETCoreAppTargetFrameworkVersion)-ios enable APL0003 + true + true + @@ -18,9 +21,6 @@ external\Attributes.cs - - external\PlatformName.cs - diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Shipped.md b/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Shipped.md index 5a165075fbc..0b2cd20c485 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Shipped.md +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Shipped.md @@ -3,5 +3,4 @@ ### New Rules | Rule ID | Category | Severity | Notes | -|---------|----------|----------|------------------------------------------------------| -| RBI0001 | Usage | Error | Binding types should be declared as partial classes. | \ No newline at end of file +|---------|----------|----------|------------------------------------------------------| \ No newline at end of file diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md b/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md index 44f7c8f4ef7..6ec519c3ae5 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/AnalyzerReleases.Unshipped.md @@ -1,4 +1,10 @@ ### New Rules -| Rule ID | Category | Severity | Notes | -|---------|----------|----------|-------| \ No newline at end of file +| 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 FieldAttribute. | +| RBI0003 | Usage | Error | Smart enum backing field cannot be an empty string. | +| RBI0004 | Usage | Error | Smart enum backing field cannot appear more than once | +| RBI0005 | Usage | Error | Non Apple framework bindings must provide a library name. | +| RBI0006 | Usage | Warning | Do not provide the LibraryName for known Apple frameworks. | diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeCodeFixProvider.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeCodeFixProvider.cs index 17b8a298360..036b08041c1 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeCodeFixProvider.cs +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeCodeFixProvider.cs @@ -20,7 +20,7 @@ namespace Microsoft.Macios.Bindings.Analyzer; public class BindingTypeCodeFixProvider : CodeFixProvider { // Specify the diagnostic IDs of analyzers that are expected to be linked. public sealed override ImmutableArray FixableDiagnosticIds { get; } = - ImmutableArray.Create (BindingTypeSemanticAnalyzer.DiagnosticId); + ImmutableArray.Create (BindingTypeSemanticAnalyzer.RBI0001.Id); // If you don't need the 'fix all' behaviour, return null. public override FixAllProvider? GetFixAllProvider () => null; diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeSemanticAnalyzer.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeSemanticAnalyzer.cs index fd5df71a0e8..2e535572d9f 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeSemanticAnalyzer.cs +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/BindingTypeSemanticAnalyzer.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Macios.Bindings.Analyzer.Extensions; namespace Microsoft.Macios.Bindings.Analyzer; @@ -12,23 +13,20 @@ namespace Microsoft.Macios.Bindings.Analyzer; /// pattern. /// [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"; +public class BindingTypeSemanticAnalyzer : DiagnosticAnalyzer, IBindingTypeAnalyzer { - static readonly DiagnosticDescriptor RBI0001 = new (DiagnosticId, Title, MessageFormat, Category, - DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); - public override ImmutableArray SupportedDiagnostics { get; } = - ImmutableArray.Create (RBI0001); + 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)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: new LocalizableResourceString (nameof (Resources.RBI0001Description), Resources.ResourceManager, typeof (Resources)) + ); + + public override ImmutableArray SupportedDiagnostics { get; } = [RBI0001]; public override void Initialize (AnalysisContext context) { @@ -38,35 +36,17 @@ public override void Initialize (AnalysisContext context) } void AnalysisContext (SyntaxNodeAnalysisContext context) - { - // only care about classes - if (context.Node is not ClassDeclarationSyntax classDeclarationNode) - return; + => this.AnalyzeBindingType (context); - var classSymbol = context.SemanticModel.GetDeclaredSymbol (classDeclarationNode); - if (classSymbol is null) - return; + public ImmutableArray Analyze (PlatformName _, ClassDeclarationSyntax declarationNode, INamedTypeSymbol symbol) + { + if (declarationNode.Modifiers.Any (x => x.IsKind (SyntaxKind.PartialKeyword))) + return []; - var boundAttributes = classSymbol.GetAttributes (); - if (boundAttributes.Length == 0) { - return; - } + var diagnostic = Diagnostic.Create (RBI0001, + declarationNode.Identifier.GetLocation (), // point to where the 'class' keyword is used + symbol.ToDisplayString ()); + return [diagnostic]; - // 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; - } - } } } diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/IBindingTypeAnalyzerExtensions.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/IBindingTypeAnalyzerExtensions.cs new file mode 100644 index 00000000000..a08eb31c692 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/IBindingTypeAnalyzerExtensions.cs @@ -0,0 +1,42 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Macios.Generator; +using Microsoft.Macios.Generator.Extensions; +using Diagnostic = Microsoft.CodeAnalysis.Diagnostic; + +namespace Microsoft.Macios.Bindings.Analyzer.Extensions; + +public static class BindingTypeAnalyzerExtensions { + public static void AnalyzeBindingType (this IBindingTypeAnalyzer self, SyntaxNodeAnalysisContext context) where T : BaseTypeDeclarationSyntax + { + // calculate the current compilation platform name + if (context.Node is not T declarationNode) + return; + + var declaredSymbol = context.SemanticModel.GetDeclaredSymbol (declarationNode); + if (declaredSymbol is null) + return; + + var boundAttributes = declaredSymbol.GetAttributes (); + if (boundAttributes.Length == 0) { + // do nothing since our generator only cares about declared types with the BindingType attribute + 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 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; + } + } + } +} diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/PlatformNameExtensions.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/PlatformNameExtensions.cs new file mode 100644 index 00000000000..2564975dfbe --- /dev/null +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Extensions/PlatformNameExtensions.cs @@ -0,0 +1,16 @@ +using Xamarin.Utils; + +namespace Microsoft.Macios.Bindings.Analyzer.Extensions; + +public static class PlatformNameExtensions { + + public static ApplePlatform ToApplePlatform (this PlatformName platformName) + => platformName switch { + PlatformName.iOS => ApplePlatform.iOS, + PlatformName.MacCatalyst => ApplePlatform.MacCatalyst, + PlatformName.MacOSX => ApplePlatform.MacOSX, + PlatformName.TvOS => ApplePlatform.TVOS, + _ => ApplePlatform.None, + }; + +} diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/IBindingTypeAnalyzer.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/IBindingTypeAnalyzer.cs new file mode 100644 index 00000000000..71855d16413 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/IBindingTypeAnalyzer.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Macios.Bindings.Analyzer; + +/// +/// Interface to be implemented by those analyzer that will be looking at BindingTypes. +/// +public interface IBindingTypeAnalyzer where T : BaseTypeDeclarationSyntax { + ImmutableArray Analyze (PlatformName platformName, T declarationNode, INamedTypeSymbol symbol); +} diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Microsoft.Macios.Bindings.Analyzer.csproj b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Microsoft.Macios.Bindings.Analyzer.csproj index 6f9e0902e43..0b07a1bc381 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Microsoft.Macios.Bindings.Analyzer.csproj +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Microsoft.Macios.Bindings.Analyzer.csproj @@ -14,12 +14,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + @@ -43,4 +44,24 @@ + + + external\AttributesNames.cs + + + external\FieldData.cs + + + Extensions\CompilationExtensions.cs + + + Extensions\TypeSymbolExtensions.cs + + + + + + + + diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs index bfe0d3152b0..12df21e8237 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.Designer.cs @@ -45,51 +45,117 @@ internal static System.Globalization.CultureInfo Culture { } } - internal static string AB0001Description { + internal static string RBI0001CodeFixTitle { get { - return ResourceManager.GetString("AB0001Description", resourceCulture); + return ResourceManager.GetString("RBI0001CodeFixTitle", resourceCulture); } } - internal static string AB0001MessageFormat { + internal static string RBI0001Description { get { - return ResourceManager.GetString("AB0001MessageFormat", resourceCulture); + return ResourceManager.GetString("RBI0001Description", resourceCulture); } } - internal static string AB0001Title { + internal static string RBI0001MessageFormat { get { - return ResourceManager.GetString("AB0001Title", resourceCulture); + return ResourceManager.GetString("RBI0001MessageFormat", resourceCulture); } } - internal static string RBI0001CodeFixTitle { + internal static string RBI0001Title { get { - return ResourceManager.GetString("RBI0001CodeFixTitle", resourceCulture); + return ResourceManager.GetString("RBI0001Title", resourceCulture); } } - internal static string RBI0001Description { + internal static string RBI0002Description { get { - return ResourceManager.GetString("RBI0001Description", resourceCulture); + return ResourceManager.GetString("RBI0002Description", resourceCulture); } } - internal static string RBI0001MessageFormat { + internal static string RBI0002MessageFormat { get { - return ResourceManager.GetString("RBI0001MessageFormat", resourceCulture); + return ResourceManager.GetString("RBI0002MessageFormat", resourceCulture); } } - internal static string RBI0001Title { + internal static string RBI0002Title { get { - return ResourceManager.GetString("RBI0001Title", resourceCulture); + return ResourceManager.GetString("RBI0002Title", resourceCulture); + } + } + + internal static string RBI0003Description { + get { + return ResourceManager.GetString("RBI0003Description", resourceCulture); + } + } + + internal static string RBI0003MessageFormat { + get { + return ResourceManager.GetString("RBI0003MessageFormat", resourceCulture); + } + } + + internal static string RBI0003Title { + get { + return ResourceManager.GetString("RBI0003Title", resourceCulture); + } + } + + internal static string RBI0004Description { + get { + return ResourceManager.GetString("RBI0004Description", resourceCulture); + } + } + + internal static string RBI0004MessageFormat { + get { + return ResourceManager.GetString("RBI0004MessageFormat", resourceCulture); + } + } + + internal static string RBI0004Title { + get { + return ResourceManager.GetString("RBI0004Title", resourceCulture); + } + } + + internal static string RBI0005Description { + get { + return ResourceManager.GetString("RBI0005Description", resourceCulture); + } + } + + internal static string RBI0005MessageFormat { + get { + return ResourceManager.GetString("RBI0005MessageFormat", resourceCulture); + } + } + + internal static string RBI0005Title { + get { + return ResourceManager.GetString("RBI0005Title", resourceCulture); + } + } + + internal static string RBI0006Description { + get { + return ResourceManager.GetString("RBI0006Description", resourceCulture); + } + } + + internal static string RBI0006MessageFormat { + get { + return ResourceManager.GetString("RBI0006MessageFormat", resourceCulture); } } - internal static string AB0002Description { + internal static string RBI0006Title { get { - return ResourceManager.GetString("AB0002Description", resourceCulture); + return ResourceManager.GetString("RBI0006Title", resourceCulture); } } } diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx index 2f228a310c3..d3438e4f3b5 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/Resources.resx @@ -34,4 +34,69 @@ Binding type declaration must be partial The title of the diagnostic. + + + In order for the code to be generated a smart enum value has to have a backing field. + An optional longer localizable description of the diagnostic. + + + The enum value '{0}' must be tagged with a Field<EnumValue> attribute + The format-able message the diagnostic displays. + + + Smart enum values must be tagged with an Field<EnumValue> attribute + The title of the diagnostic. + + + + In order for the code to be generated the backing filed of a smart enum value cannot be an empty string. + An optional longer localizable description of the diagnostic. + + + The enum value '{0}' backing field is an empty string + The format-able message the diagnostic displays. + + + Smart enum backing field cannot be an empty string + The title of the diagnostic. + + + + Smart enum backing field cannot appear more than once. + An optional longer localizable description of the diagnostic. + + + The enum value '{0}' backing field '{1}' is already in use + The format-able message the diagnostic displays. + + + Smart enum backing field cannot appear more than once. + The title of the diagnostic. + + + + Smart enum backing field for a non Apple framework must provide a library name. + An optional longer localizable description of the diagnostic. + + + The enum value '{0}' backing field must provide a library name + The format-able message the diagnostic displays. + + + Non Apple framework bindings must provide a library name + The title of the diagnostic. + + + + Fields of known Apple frameworks should not provide a LibraryName. + An optional longer localizable description of the diagnostic. + + + The backing Field of '{0}' should not provide a LibraryName + The format-able message the diagnostic displays. + + + Do not provide the LibraryName for known Apple frameworks + The title of the diagnostic. + diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer/SmartEnumSemanticAnalyzer.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer/SmartEnumSemanticAnalyzer.cs new file mode 100644 index 00000000000..049bba01acd --- /dev/null +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer/SmartEnumSemanticAnalyzer.cs @@ -0,0 +1,165 @@ +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; +using Microsoft.Macios.Generator.Attributes; +using Microsoft.Macios.Generator.Extensions; + +namespace Microsoft.Macios.Bindings.Analyzer; + + +/// +/// Analyzer to ensure that all enum values in an SmartEnum contains a Field attribute. +/// +[DiagnosticAnalyzer (LanguageNames.CSharp)] +public class SmartEnumSemanticAnalyzer : DiagnosticAnalyzer, IBindingTypeAnalyzer { + // All enum values must have a Field attribute + 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)) + ); + + // All Field symbols cannot be empty or white space + 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)) + ); + + // Do not allow duplicated backing fields + 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)) + ); + + // If not an apple framework, we should provide the library path + 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)) + ); + + // if apple framework, the library path should be empty + 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.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString (nameof (Resources.RBI0006Description), Resources.ResourceManager, typeof (Resources)) + ); + + public override ImmutableArray SupportedDiagnostics { get; } = [RBI0002, RBI0003, RBI0004, RBI0005, RBI0006]; + + public override void Initialize (AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution (); + context.RegisterSyntaxNodeAction (AnalysisContext, SyntaxKind.EnumDeclaration); + } + + void AnalysisContext (SyntaxNodeAnalysisContext context) + => this.AnalyzeBindingType (context); + + public ImmutableArray Analyze (PlatformName platformName, EnumDeclarationSyntax declarationNode, INamedTypeSymbol symbol) + { + // we want to ensure several things: + // 1. All enum values are marked with a Field attribute + // 2. All Field attributes have a symbol name + // 3. All symbol names have to be unique + // 4. If the Field attribute is not from a known apple library, the library name is set + // 5. If the Field attribute is from a known apple library the lib should be null + + // based on the platform decide if we are dealing with a known apple framework, we want all, not just the + // ones that are part of the simulator + var appleFrameworks = Frameworks.GetFrameworks (platformName.ToApplePlatform (), false); + var isAppleFramework = appleFrameworks.Find (symbol.ContainingNamespace.Name) is not null; + + // bucket with all the diagnostics we have found + var bucket = ImmutableArray.CreateBuilder (); + + var members = symbol.GetMembers ().OfType ().ToArray (); + // we do not allow backing fields to be duplicated, keep track of those added so far to raise + // a diagnostic if we find a duplicate + var backingFields = new HashSet (); + foreach (var fieldSymbol in members) { + var attributes = fieldSymbol.GetAttributeData (); + if (attributes.Count == 0) { + // 1. All enum values are marked with a Field attribute, therefore add a diagnostic + bucket.Add (Diagnostic.Create (RBI0002, + fieldSymbol.Locations.First (), + fieldSymbol.ToDisplayString ())); + continue; + } + + // Get all the FieldAttribute, parse it and add the data to the result + if (attributes.TryGetValue (AttributesNames.FieldAttribute, out var fieldAttrData)) { + var fieldSyntax = fieldAttrData.ApplicationSyntaxReference?.GetSyntax (); + if (fieldSyntax is null) { + // if we cant get the syntax reference, we have a bug + continue; + } + + if (FieldData.TryParse (fieldSyntax, fieldAttrData, out var fieldData)) { + // only provide diagnostics if we managed to parse the FieldData, else we have a bug in the + // analyzer + if (string.IsNullOrWhiteSpace (fieldData.SymbolName)) { + // 2. All Field attributes have a symbol name, therefore add a diagnostic + bucket.Add (Diagnostic.Create (RBI0003, + fieldSyntax.GetLocation (), + fieldSymbol.ToDisplayString ())); + } else if (!backingFields.Add (fieldData.SymbolName)) { + // 3. All symbol names have to be unique + bucket.Add (Diagnostic.Create (RBI0004, + fieldSyntax.GetLocation (), + fieldSymbol.ToDisplayString (), fieldData.SymbolName)); + } + + if (!isAppleFramework) { + // 4. If the Field attribute is not from a known apple library, the library name is set + if (string.IsNullOrWhiteSpace (fieldData.LibraryName)) { + bucket.Add (Diagnostic.Create (RBI0005, + fieldSyntax.GetLocation (), + fieldSymbol.ToDisplayString ())); + } + } else { + // 5. If the Field attribute is from a known apple library the lib should be null + if (fieldData.LibraryName is not null) { + bucket.Add (Diagnostic.Create (RBI0006, + fieldSyntax.GetLocation (), + fieldSymbol.ToDisplayString ())); + } + } + } else { + // report but msg + } + } + } + + return bucket.ToImmutable (); + } +} diff --git a/src/rgen/Microsoft.Macios.Generator.Sample/Microsoft.Macios.Generator.Sample.csproj b/src/rgen/Microsoft.Macios.Generator.Sample/Microsoft.Macios.Generator.Sample.csproj index e10b90a1b8f..88612805927 100644 --- a/src/rgen/Microsoft.Macios.Generator.Sample/Microsoft.Macios.Generator.Sample.csproj +++ b/src/rgen/Microsoft.Macios.Generator.Sample/Microsoft.Macios.Generator.Sample.csproj @@ -1,13 +1,15 @@ - + - net$(BundledNETCoreAppTargetFrameworkVersion) + net$(BundledNETCoreAppTargetFrameworkVersion)-ios enable Microsoft.Macios.Generator.Sample APL0003 + true + @@ -18,9 +20,6 @@ external\Attributes.cs - - external\PlatformName.cs - diff --git a/src/rgen/Microsoft.Macios.Generator/Attributes/FieldData.cs b/src/rgen/Microsoft.Macios.Generator/Attributes/FieldData.cs new file mode 100644 index 00000000000..35abd82623d --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Attributes/FieldData.cs @@ -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; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/AttributesNames.cs b/src/rgen/Microsoft.Macios.Generator/AttributesNames.cs index 62ac3b632b0..f4f3bd62034 100644 --- a/src/rgen/Microsoft.Macios.Generator/AttributesNames.cs +++ b/src/rgen/Microsoft.Macios.Generator/AttributesNames.cs @@ -5,5 +5,6 @@ namespace Microsoft.Macios.Generator; /// public static class AttributesNames { - public static readonly string BindingAttribute = "ObjCBindings.BindingTypeAttribute"; + public const string BindingAttribute = "ObjCBindings.BindingTypeAttribute"; + public const string FieldAttribute = "ObjCBindings.FieldAttribute"; } diff --git a/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs b/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs index 8b7d54e8533..d3da3bf1cbd 100644 --- a/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs +++ b/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Macios.Generator.Context; using Microsoft.Macios.Generator.Emitters; +using Microsoft.Macios.Generator.Extensions; namespace Microsoft.Macios.Generator; @@ -19,7 +20,7 @@ namespace Microsoft.Macios.Generator; /// [Generator] public class BindingSourceGeneratorGenerator : IIncrementalGenerator { - + static readonly CodeChangesComparer comparer = new(); /// public void Initialize (IncrementalGeneratorInitializationContext context) { @@ -36,6 +37,59 @@ public void Initialize (IncrementalGeneratorInitializationContext context) AddPipeline (context); AddPipeline (context); AddPipeline (context); + + // the library and trampoline pipelines depend on the changes of ALL the types that have the binding attr + // we will map all of them and calculate all changes + // This means, that while the enums/class/interfaces are not regenerated every single type, the library and + // trampolines are as long as there was a change we are interested in. + var provider = context.SyntaxProvider + .CreateSyntaxProvider ( + static (s, _) => { + switch (s) { + case EnumDeclarationSyntax: + case ClassDeclarationSyntax: + case InterfaceDeclarationSyntax: + return true; + default: + return false; + } + }, + (ctx, _) => GetDeclarationForSourceGen (ctx)) + .Where (t => t.BindingAttributeFound) // get the types with the binding attr + .Select ((t, _) => t.Changes) + .WithComparer (comparer); // get the lib data for the type declaration + + context.RegisterSourceOutput (context.CompilationProvider.Combine (provider.Collect ()), + ((ctx, t) => GenerateLibraryCode (ctx, t.Left, t.Right))); + } + + /// + /// Code generator that emmits the static classes that contain the pointers to the library used + /// by the binding. This is a single generated file. + /// + /// Source production context. + /// Current compilation. + /// Code changes from the last compilation. + static void GenerateLibraryCode (SourceProductionContext context, Compilation compilation, + ImmutableArray codeChangesList) + { + var rootContext = new RootBindingContext (compilation); + var sb = new TabbedStringBuilder (new()); + sb.WriteHeader (); + // not need to collect the using statements, this file is completely generated + var emitter = new LibraryEmitter (rootContext, sb, codeChangesList); + + if (emitter.TryEmit (out var diagnostics)) { + // only add file when we do generate code + var code = sb.ToString (); + context.AddSource ($"ObjCRuntime/{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); + } + } } /// @@ -49,11 +103,12 @@ static void AddPipeline (IncrementalGeneratorInitializationContext context) w .CreateSyntaxProvider ( static (s, _) => s is T, (ctx, _) => GetDeclarationForSourceGen (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 (ctx, t.Left, t.Right))); } /// @@ -64,25 +119,24 @@ static void AddPipeline (IncrementalGeneratorInitializationContext context) w /// Context used by the generator. /// The BaseTypeDeclarationSyntax we are interested in. /// A tuple that contains the BaseTypeDeclaration that was processed and a boolean that states if it should be processed or not. - static (T Declaration, bool BindingAttributeFound) GetDeclarationForSourceGen (GeneratorSyntaxContext context) + static (CodeChanges Changes, bool BindingAttributeFound) GetDeclarationForSourceGen (GeneratorSyntaxContext context) where T : BaseTypeDeclarationSyntax { - var classDeclarationSyntax = Unsafe.As (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 (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, 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, 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); } /// @@ -92,18 +146,26 @@ static void AddPipeline (IncrementalGeneratorInitializationContext context) w /// /// Root syntax tree of the base type declaration. /// String builder that will be used for the generated code. - static void CollectUsingStatements (SyntaxTree tree, TabbedStringBuilder sb) + /// The emitter that will generate the code. Provides any extra needed namespace. + 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 () - .Select (d => d.Name.ToString ()).ToArray (); + .Select (d => d.Name!.ToString ()).ToArray (); var usingDirectivesToKeep = new HashSet (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};"); @@ -117,52 +179,48 @@ static void CollectUsingStatements (SyntaxTree tree, TabbedStringBuilder sb) /// /// The generator context. /// The compilation unit. - /// The base type declarations marked by the BindingTypeAttribute. + /// The base type declarations marked by the BindingTypeAttribute. /// The type of type declaration. static void GenerateCode (SourceProductionContext context, Compilation compilation, - ImmutableArray baseTypeDeclarations) where T : BaseTypeDeclarationSyntax + ImmutableArray 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 (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 ("// "); - - // enable nullable! - sb.AppendLine (); - sb.AppendLine ("#nullable enable"); - sb.AppendLine (); - - CollectUsingStatements (baseTypeDeclarationSyntax.SyntaxTree, sb); - + var sb = new TabbedStringBuilder (new()); + 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) - && EmitterFactory.TryCreate (symbolBindingContext, sb, out var emitter)) { + 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 continue; } - } } - } diff --git a/src/rgen/Microsoft.Macios.Generator/CodeChanges.cs b/src/rgen/Microsoft.Macios.Generator/CodeChanges.cs new file mode 100644 index 00000000000..6495785ce54 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/CodeChanges.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Macios.Generator.Extensions; + +namespace Microsoft.Macios.Generator; + +/// +/// Structure that represents a set of changes that were made by the user that need to be appliend to the +/// generated code. +/// +struct CodeChanges { + public string FullyQualifiedSymbol { get; } + public BaseTypeDeclarationSyntax SymbolDeclaration { get; } + public ImmutableArray Members { get; } + + /// + /// Internal constructor added for testing purposes. + /// + /// The fully qualified name of the symbol. + /// The symbol declaration syntax. + /// The members that had changed. + internal CodeChanges (string fullyQualifiedSymbol, BaseTypeDeclarationSyntax declaration, ImmutableArray members) + { + FullyQualifiedSymbol = fullyQualifiedSymbol; + SymbolDeclaration = declaration; + Members = members; + } + + CodeChanges (GeneratorSyntaxContext context, EnumDeclarationSyntax enumDeclaration) + { + FullyQualifiedSymbol = enumDeclaration.GetFullyQualifiedIdentifier (); + SymbolDeclaration = enumDeclaration; + var bucket = ImmutableArray.CreateBuilder (); + // loop over the fields and add those that contain a FieldAttribute + var enumValueDeclaration = enumDeclaration.Members.OfType (); + foreach (var val in enumValueDeclaration) { + if (val.HasAttribute (context, AttributesNames.FieldAttribute)) { + bucket.Add (val.Identifier.ToFullString ()); + } + } + Members = bucket.ToImmutable (); + } + + CodeChanges (GeneratorSyntaxContext context, ClassDeclarationSyntax classDeclaration) + { + FullyQualifiedSymbol = classDeclaration.GetFullyQualifiedIdentifier (); + SymbolDeclaration = classDeclaration; + // TODO: to be implemented once we add class support + Members = []; + } + + CodeChanges (GeneratorSyntaxContext context, InterfaceDeclarationSyntax interfaceDeclaration) + { + FullyQualifiedSymbol = interfaceDeclaration.GetFullyQualifiedIdentifier (); + SymbolDeclaration = interfaceDeclaration; + // TODO: to be implemented once we add protocol support + Members = []; + } + + public static CodeChanges? FromDeclaration (GeneratorSyntaxContext context, + BaseTypeDeclarationSyntax baseTypeDeclarationSyntax) + => baseTypeDeclarationSyntax switch { + EnumDeclarationSyntax enumDeclarationSyntax => new CodeChanges (context, enumDeclarationSyntax), + InterfaceDeclarationSyntax interfaceDeclarationSyntax => new CodeChanges(context, interfaceDeclarationSyntax), + ClassDeclarationSyntax classDeclarationSyntax => new CodeChanges(context, classDeclarationSyntax), + _ => null + }; +} diff --git a/src/rgen/Microsoft.Macios.Generator/CodeChangesComparer.cs b/src/rgen/Microsoft.Macios.Generator/CodeChangesComparer.cs new file mode 100644 index 00000000000..50825cf4784 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/CodeChangesComparer.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Macios.Generator; + +/// +/// Custom code changes comparer used for the Roslyn code generation to invalidate caching. +/// +class CodeChangesComparer : IEqualityComparer { + + public bool Equals(CodeChanges x, CodeChanges y) + { + // we want to ignore the syntax declaration, is not something we want to compare since it can change, + // for example, with new attributes or comments. + if (x.FullyQualifiedSymbol != y.FullyQualifiedSymbol || x.Members.Length != y.Members.Length) + return false; + + // compare arrays of members + var xMembers = x.Members.Sort(); + var yMembers = y.Members.Sort(); + for (int i = 0; i < xMembers.Length; i++) { + if (xMembers[i] != yMembers[i]) { + return false; + } + } + + return true; + } + + public int GetHashCode(CodeChanges obj) + { + return HashCode.Combine(obj.FullyQualifiedSymbol, obj.Members); + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Context/ContextFactory.cs b/src/rgen/Microsoft.Macios.Generator/Context/ContextFactory.cs index 0805f661df8..b45e8e4d3d7 100644 --- a/src/rgen/Microsoft.Macios.Generator/Context/ContextFactory.cs +++ b/src/rgen/Microsoft.Macios.Generator/Context/ContextFactory.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; diff --git a/src/rgen/Microsoft.Macios.Generator/Context/ISymbolBindingContext.cs b/src/rgen/Microsoft.Macios.Generator/Context/ISymbolBindingContext.cs index a9bc1f3c242..bcfb1b0bafa 100644 --- a/src/rgen/Microsoft.Macios.Generator/Context/ISymbolBindingContext.cs +++ b/src/rgen/Microsoft.Macios.Generator/Context/ISymbolBindingContext.cs @@ -12,8 +12,8 @@ interface ISymbolBindingContext where T : BaseTypeDeclarationSyntax { T DeclarationSyntax { get; } string Namespace { get; } string SymbolName { get; } - RootBindingContext RootBindingContext { get; init; } - SemanticModel SemanticModel { get; init; } - INamedTypeSymbol Symbol { get; init; } + RootBindingContext RootBindingContext { get; } + SemanticModel SemanticModel { get; } + INamedTypeSymbol Symbol { get; } bool IsStatic { get; } } diff --git a/src/rgen/Microsoft.Macios.Generator/Context/RootBindingContext.cs b/src/rgen/Microsoft.Macios.Generator/Context/RootBindingContext.cs index 64159cb0657..6feeec908bb 100644 --- a/src/rgen/Microsoft.Macios.Generator/Context/RootBindingContext.cs +++ b/src/rgen/Microsoft.Macios.Generator/Context/RootBindingContext.cs @@ -1,7 +1,10 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.Macios.Generator.Extensions; namespace Microsoft.Macios.Generator.Context; @@ -13,35 +16,70 @@ namespace Microsoft.Macios.Generator.Context; /// to the current compilation. /// class RootBindingContext { - readonly Dictionary _libraries = new (); - public PlatformName CurrentPlatform { get; set; } public Compilation Compilation { get; set; } - public bool BindThirdPartyLibrary { get; set; } + + public Dictionary Libraries { get; } = new(); public RootBindingContext (Compilation compilation) { Compilation = compilation; - CurrentPlatform = PlatformName.None; - // use the reference assembly to determine what platform we are binding - foreach (var referencedAssemblyName in compilation.ReferencedAssemblyNames) { - switch (referencedAssemblyName.Name) { - case "Microsoft.iOS": - CurrentPlatform = PlatformName.iOS; - break; - case "Microsoft.MacCatalyst": - CurrentPlatform = PlatformName.MacCatalyst; - break; - case "Microsoft.macOS": - CurrentPlatform = PlatformName.MacOSX; - break; - case "Microsoft.tvOS": - CurrentPlatform = PlatformName.TvOS; - break; - default: - CurrentPlatform = PlatformName.None; - break; + CurrentPlatform = compilation.GetCurrentPlatform (); + } + + // TODO: clean code coming from the old generator + public bool TryComputeLibraryName (string? attributeLibraryName, string typeNamespace, + [NotNullWhen (true)] out string? libraryName, + out string? libraryPath) + { + libraryPath = null; + + if (!string.IsNullOrEmpty (attributeLibraryName)) { + // Remapped + libraryName = attributeLibraryName; + if (libraryName [0] == '+') { + switch (libraryName) { + case "+CoreImage": + CurrentPlatform.TryGetCoreImageMap (out libraryName); + break; + case "+CoreServices": + CurrentPlatform.TryGetCoreServicesMap (out libraryName); + break; + case "+PDFKit": + libraryName = "PdfKit"; + CurrentPlatform.TryGetPDFKitMap (out libraryPath); + break; + } + } else { + // we get something in LibraryName from FieldAttribute so we assume + // it is a path to a library, so we save the path and change library name + // to a valid identifier if needed + libraryPath = libraryName; + // without extension makes more sense, but we can't change it since it breaks compat + libraryName = Path.GetFileNameWithoutExtension (libraryName); + + if (libraryName.Contains ('.')) + libraryName = libraryName.Replace (".", string.Empty); } + } else { + libraryName = typeNamespace; } + + if (libraryName is not null && !Libraries.ContainsKey (libraryName)) + Libraries.Add (libraryName, libraryPath); + + return true; + } + + public bool IsSystemLibrary (string name) + { + // use the semantic model to get the ObjcRuntime.Constants type and see if we do have a value for the library + var symbol = Compilation.GetTypeByMetadataName ("ObjCRuntime.Constants"); + // this should not happen, we should have the Constants type + if (symbol is null) + return false; + return symbol.GetMembers().OfType () + .Select (f => f.Name) + .Any(s => s.Equals ($"{name}Library", StringComparison.Ordinal)); } } diff --git a/src/rgen/Microsoft.Macios.Generator/Context/SymbolBindingContext.cs b/src/rgen/Microsoft.Macios.Generator/Context/SymbolBindingContext.cs index b3d0c24cfbb..5fd8fc6fd90 100644 --- a/src/rgen/Microsoft.Macios.Generator/Context/SymbolBindingContext.cs +++ b/src/rgen/Microsoft.Macios.Generator/Context/SymbolBindingContext.cs @@ -18,7 +18,6 @@ public SymbolBindingContext (RootBindingContext rootBindingContext, SemanticModel = semanticModel; Symbol = symbol; } - } class SymbolBindingContext : SymbolBindingContext, ISymbolBindingContext where T : BaseTypeDeclarationSyntax { diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/ClassEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/ClassEmitter.cs index 0d56a01a1c1..bef0cc6e4a5 100644 --- a/src/rgen/Microsoft.Macios.Generator/Emitters/ClassEmitter.cs +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/ClassEmitter.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; @@ -8,6 +9,8 @@ namespace Microsoft.Macios.Generator.Emitters; class ClassEmitter (ClassBindingContext context, TabbedStringBuilder builder) : ICodeEmitter { public string SymbolName => context.SymbolName; + public IEnumerable UsingStatements => []; + public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) { diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/EnumEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/EnumEmitter.cs index b946986eac9..237c22eaf28 100644 --- a/src/rgen/Microsoft.Macios.Generator/Emitters/EnumEmitter.cs +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/EnumEmitter.cs @@ -1,8 +1,11 @@ +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Macios.Generator.Attributes; using Microsoft.Macios.Generator.Context; +using Microsoft.Macios.Generator.Extensions; namespace Microsoft.Macios.Generator.Emitters; @@ -10,10 +13,142 @@ class EnumEmitter (ISymbolBindingContext context, TabbedS : ICodeEmitter { public string SymbolName => $"{context.SymbolName}Extensions"; + public IEnumerable UsingStatements => ["System", "ObjCRuntime"]; - public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) + void Emit (TabbedStringBuilder classBlock, (IFieldSymbol Symbol, FieldData FieldData) enumField, int index) { - diagnostics = null; + var typeNamespace = enumField.Symbol.ContainingType.ContainingNamespace.Name; + if (!context.RootBindingContext.TryComputeLibraryName (enumField.FieldData.LibraryName, typeNamespace, + out string? libraryName, out string? libraryPath)) { + return; + } + + classBlock.AppendLine ($"[Field (\"{enumField.FieldData.SymbolName}\", \"{libraryPath ?? libraryName}\")]"); + + using (var propertyBlock = classBlock.CreateBlock ($"internal unsafe static IntPtr {enumField.FieldData.SymbolName}", true)) + using (var getterBlock = propertyBlock.CreateBlock ("get", true)) { + getterBlock.AppendLine ($"fixed (IntPtr *storage = &values [{index}])"); + getterBlock.AppendLine ( + $"\treturn Dlfcn.CachePointer (Libraries.{libraryName}.Handle, \"{enumField.FieldData.SymbolName}\", storage);"); + } + } + + bool TryEmit (TabbedStringBuilder classBlock, ImmutableArray<(IFieldSymbol Symbol, FieldData FieldData)> fields) + { + // keep track of the field symbols, they have to be unique, if we find a duplicate we return false and + // abort the code generation + var backingFields = new HashSet (); + for (var index = 0; index < fields.Length; index++) { + if (!backingFields.Add (fields [index].FieldData.SymbolName)) { + return false; + } + var field = fields [index]; + classBlock.AppendLine (); + Emit (classBlock, field, index); + } return true; } + + void Emit (TabbedStringBuilder classBlock, INamedTypeSymbol enumSymbol, + ImmutableArray<(IFieldSymbol Symbol, FieldData FieldData)>? members) + { + if (members is null) + return; + + // smart enum require 4 diff methods to be able to retrieve the values + + // Get constant + using (var getConstantBlock = classBlock.CreateBlock ($"public static NSString? GetConstant (this {enumSymbol.Name} self)", true)) { + getConstantBlock.AppendLine ("IntPtr ptr = IntPtr.Zero;"); + using (var switchBlock = getConstantBlock.CreateBlock ("switch ((int) self)", true)) { + for (var index = 0; index < members.Value.Length; index++) { + var (_, fieldData) = members.Value [index]; + switchBlock.AppendLine ($"case {index}: // {fieldData.SymbolName}"); + switchBlock.AppendLine ($"\tptr = {fieldData.SymbolName};"); + switchBlock.AppendLine ("\tbreak;"); + } + } + + getConstantBlock.AppendLine ("return (NSString?) Runtime.GetNSObject (ptr);"); + } + + classBlock.AppendLine (); + // Get value + using (var getValueBlock = classBlock.CreateBlock ($"public static {enumSymbol.Name} GetValue (NSString constant)", true)) { + getValueBlock.AppendLine ("if (constant is null)"); + getValueBlock.AppendLine ("\tthrow new ArgumentNullException (nameof (constant));"); + foreach ((IFieldSymbol? fieldSymbol, FieldData? fieldData) in members) { + getValueBlock.AppendLine ($"if (constant.IsEqualTo ({fieldData.SymbolName}))"); + getValueBlock.AppendLine ($"\treturn {enumSymbol.Name}.{fieldSymbol.Name};"); + } + + getValueBlock.AppendLine ( + "throw new NotSupportedException ($\"{constant} has no associated enum value on this platform.\");"); + } + + classBlock.AppendLine (); + // To ConstantArray + classBlock.AppendRaw ( +@$"internal static NSString?[]? ToConstantArray (this {enumSymbol.Name}[]? values) +{{ + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) {{ + var value = values [i]; + rv.Add (value.GetConstant ()); + }} + return rv.ToArray (); +}}"); + classBlock.AppendLine (); + classBlock.AppendLine (); + // ToEnumArray + classBlock.AppendRaw ( +@$"internal static {enumSymbol.Name}[]? ToEnumArray (this NSString[]? values) +{{ + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List<{enumSymbol.Name}> (); + for (var i = 0; i < values.Length; i++) {{ + var value = values [i]; + rv.Add (GetValue (value)); + }} + return rv.ToArray (); +}}"); + } + + public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) + { + diagnostics = null; + if (!context.Symbol.TryGetEnumFields (out var members, + out diagnostics) || members.Value.Length == 0) { + // return true to indicate that we did generate code, even if it's empty, the analyzer will take care + // of the rest + return true; + } + // in the old generator we had to copy over the enum, in this new approach the only code + // we need to create is the extension class for the enum that is backed by fields + builder.AppendLine (); + builder.AppendLine ($"namespace {context.Namespace};"); + builder.AppendLine (); + + builder.AppendGeneratedCodeAttribute (); + using (var classBlock = builder.CreateBlock ($"static public partial class {SymbolName}", true)) { + classBlock.AppendLine (); + classBlock.AppendLine ($"static IntPtr[] values = new IntPtr [{members.Value.Length}];"); + // foreach member in the enum we need to create a field that holds the value, the property emitter + // will take care of generating the property. Do not order by name to keep the order of the enum + if (!TryEmit (classBlock, members.Value)) { + diagnostics = []; // empty diagnostics since it was a user error + return false; + } + classBlock.AppendLine (); + + // emit the extension methods that will be used to get the values from the enum + Emit (classBlock, context.Symbol, members); + classBlock.AppendLine (); + } + + return true; + } } diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/ICodeEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/ICodeEmitter.cs index d78584e82bb..6ae7b9b10f0 100644 --- a/src/rgen/Microsoft.Macios.Generator/Emitters/ICodeEmitter.cs +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/ICodeEmitter.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; @@ -10,4 +11,5 @@ namespace Microsoft.Macios.Generator.Emitters; interface ICodeEmitter { public string SymbolName { get; } bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics); + IEnumerable UsingStatements { get; } } diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs index 23f77d0c4d7..65329736ad2 100644 --- a/src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; @@ -8,6 +9,7 @@ namespace Microsoft.Macios.Generator.Emitters; class InterfaceEmitter (ISymbolBindingContext context, TabbedStringBuilder builder) : ICodeEmitter { public string SymbolName { get; } = string.Empty; + public IEnumerable UsingStatements => []; public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) { diagnostics = null; diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/LibraryEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/LibraryEmitter.cs new file mode 100644 index 00000000000..df3c5e57c29 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/LibraryEmitter.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Macios.Generator.Context; +using Microsoft.Macios.Generator.Extensions; + +namespace Microsoft.Macios.Generator.Emitters; + +class LibraryEmitter ( + RootBindingContext context, + TabbedStringBuilder builder, + ImmutableArray codeChangesList) : ICodeEmitter { + public string SymbolName => "Libraries"; + INamedTypeSymbol? LibrarySymbol { get; } = context.Compilation.GetTypeByMetadataName ("ObjCRuntime.Libraries"); + + bool IsSymbolPreset (string className) + => LibrarySymbol is not null && + LibrarySymbol.GetMembers ().OfType () + .Any (v => v.Name == className); + + public IEnumerable UsingStatements { get; } = []; + + void GetEnumLibraries (INamedTypeSymbol symbol) + { + var typeNamespace = symbol.ContainingNamespace.Name; + if (!symbol.TryGetEnumFields (out var members, out var diagnostics)) + return; + // Libs in this case are coming from the FieldAttribute of ech of the members + foreach (var enumField in members) { + if (!context.TryComputeLibraryName (enumField.FieldData.LibraryName, typeNamespace, + out _, out _)) { + return; + } + } + } + + void GetClassLibraries (INamedTypeSymbol symbol) + { + } + + void GetInterfaceLibraries (INamedTypeSymbol symbol) + { + } + + public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) + { + diagnostics = []; + + builder.AppendLine ("using Foundation;"); + builder.AppendLine ("using ObjCBindings;"); + builder.AppendLine ("using ObjCRuntime;"); + builder.AppendLine ("using System;"); + + // keep track if we added a lib, if not we don't need to generate the class + bool added = false; + + // go over the code changes and retrieve any library data information with the help of the root context + foreach (var codeChange in codeChangesList) { + var semanticModel = context.Compilation.GetSemanticModel (codeChange.SymbolDeclaration.SyntaxTree); +#pragma warning disable RS1039 + if (semanticModel.GetDeclaredSymbol (codeChange.SymbolDeclaration) is not INamedTypeSymbol symbol) +#pragma warning restore RS1039 + continue; + switch (symbol.TypeKind) { + case TypeKind.Enum: + GetEnumLibraries (symbol); + break; + case TypeKind.Class: + GetClassLibraries (symbol); + break; + case TypeKind.Interface: + GetInterfaceLibraries (symbol); + break; + } + } + + builder.AppendLine (); + builder.AppendLine ($"namespace ObjCRuntime;"); + builder.AppendLine (); + + builder.AppendGeneratedCodeAttribute (); + using (var classBlock = builder.CreateBlock ($"static partial class {SymbolName}", true)) { + foreach (var (name, path) in context.Libraries.OrderBy (v => v.Key, + StringComparer.Ordinal)) { + // verify if the symbol is already defined by the runtime + var className = name.Replace (".", string.Empty); + if (IsSymbolPreset (className)) + continue; + + using (var nestedClass = + classBlock.CreateBlock ($"static public class {className}", true)) { + if (name == "__Internal") { + nestedClass.AppendLine ("static public readonly IntPtr Handle = Dlfcn.dlopen (null, 0);"); + } else if (context.IsSystemLibrary (name)) { + nestedClass.AppendLine ( + $"static public readonly IntPtr Handle = Dlfcn._dlopen (Constants.{name}Library, 0);"); + } else { + nestedClass.AppendLine ( + $"static public readonly IntPtr Handle = Dlfcn.dlopen (\"{path}\", 0);"); + } + } + + added = true; + } + } + + return added; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/TrampolineEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/TrampolineEmitter.cs new file mode 100644 index 00000000000..68593ca5fd9 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/TrampolineEmitter.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Macios.Generator.Emitters; + +public class TrampolineEmitter : ICodeEmitter { + public string SymbolName { get; } = string.Empty; + public IEnumerable UsingStatements { get; } = []; + public bool TryEmit ([NotNullWhen(false)] out ImmutableArray? diagnostics) + { + diagnostics = null; + return true; + } + +} diff --git a/src/rgen/Microsoft.Macios.Generator/Extensions/BaseTypeDeclarationSyntaxExtensions.cs b/src/rgen/Microsoft.Macios.Generator/Extensions/BaseTypeDeclarationSyntaxExtensions.cs new file mode 100644 index 00000000000..19c1a4d1951 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Extensions/BaseTypeDeclarationSyntaxExtensions.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Macios.Generator.Extensions; + +public static class BaseTypeDeclarationSyntaxExtensions { + public static string GetFullyQualifiedIdentifier (this BaseTypeDeclarationSyntax self) + { + var root = self.SyntaxTree.GetRoot (); + // check if the namespace is a file scoped one + var fileScoped = root.DescendantNodes () + .OfType () + .FirstOrDefault (); + + var namespaces = self.Ancestors () + .OfType () + .Reverse () + .ToArray (); + + // get all the classes + var parents = self.Ancestors () + .OfType () + .Reverse () + .ToArray (); + + var sb = new StringBuilder (); + if (fileScoped is not null) + sb.Append ($"{fileScoped.Name}."); + foreach (var ns in namespaces) { + sb.Append ($"{ns.Name}."); + } + + foreach (var c in parents) { + sb.Append ($"{c.Identifier.ToFullString ().Trim ()}."); + } + + sb.Append (self.Identifier.ToFullString ()); + return sb.ToString ().Trim (); + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Extensions/CompilationExtensions.cs b/src/rgen/Microsoft.Macios.Generator/Extensions/CompilationExtensions.cs new file mode 100644 index 00000000000..ae48a8753c3 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Extensions/CompilationExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace Microsoft.Macios.Generator.Extensions; + +static class CompilationExtensions { + + public static PlatformName GetCurrentPlatform (this Compilation self) + { + // use the reference assembly to determine what platform we are binding + foreach (var referenceAssembly in self.ReferencedAssemblyNames) { + switch (referenceAssembly.Name) { + case "Microsoft.iOS": + return PlatformName.iOS; + case "Microsoft.MacCatalyst": + return PlatformName.MacCatalyst; + case "Microsoft.macOS": + return PlatformName.MacOSX; + case "Microsoft.tvOS": + return PlatformName.TvOS; + } + } + + return PlatformName.None; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Extensions/MemberDeclarationSyntaxExtensions.cs b/src/rgen/Microsoft.Macios.Generator/Extensions/MemberDeclarationSyntaxExtensions.cs new file mode 100644 index 00000000000..29eb938e030 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Extensions/MemberDeclarationSyntaxExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Macios.Generator.Extensions; + +static class MemberDeclarationSyntaxExtensions { + public static bool HasAttribute (this MemberDeclarationSyntax self, GeneratorSyntaxContext context, + string attribute) + { + foreach (AttributeListSyntax attributeListSyntax in self.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 + + var currentName = attributeSymbol.ContainingType.ToDisplayString (); + + // Check the full name of the [Binding] attribute. + if (currentName == attribute) + return true; + } + + return false; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Extensions/NamedTypeSymbolExtensions.cs b/src/rgen/Microsoft.Macios.Generator/Extensions/NamedTypeSymbolExtensions.cs new file mode 100644 index 00000000000..e24f022b59f --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Extensions/NamedTypeSymbolExtensions.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Macios.Generator.Attributes; + +namespace Microsoft.Macios.Generator.Extensions; + +static class NamedTypeSymbolExtensions { + public static bool TryGetEnumFields (this INamedTypeSymbol enumSymbol, + [NotNullWhen (true)] + out ImmutableArray<(IFieldSymbol Symbol, FieldData FieldData)>? fields, + [NotNullWhen (false)] out ImmutableArray? diagnostics) + { + fields = null; + diagnostics = null; + + // because we are dealing with an enum, we need to get all the fields from the symbol but we need to + // keep the order in which they are defined in the source code. + + var fieldBucket = + ImmutableArray.CreateBuilder<(IFieldSymbol Symbol, FieldData FieldData)> (); + + var members = enumSymbol.GetMembers ().OfType ().ToArray (); + foreach (var fieldSymbol in members) { + var attributes = fieldSymbol.GetAttributeData (); + if (attributes.Count == 0) + continue; + + // Get all the FieldAttribute, parse it and add the data to the result + if (attributes.TryGetValue (AttributesNames.FieldAttribute, out var fieldAttrData)) { + var fieldSyntax = fieldAttrData.ApplicationSyntaxReference?.GetSyntax (); + if (fieldSyntax is null) + continue; + + if (FieldData.TryParse (fieldSyntax, fieldAttrData, out var fieldData)) { + fieldBucket.Add ((Symbol: fieldSymbol, FieldData: fieldData)); + } else { + // TODO: diagnostics + } + } + } + + fields = fieldBucket.ToImmutable (); + return true; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Extensions/TypeSymbolExtensions.cs b/src/rgen/Microsoft.Macios.Generator/Extensions/TypeSymbolExtensions.cs new file mode 100644 index 00000000000..52c14b43530 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Extensions/TypeSymbolExtensions.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Macios.Generator.Extensions; + +static class TypeSymbolExtensions { + + public static Dictionary GetAttributeData (this ISymbol symbol) + { + var boundAttributes = symbol.GetAttributes (); + if (boundAttributes.Length == 0) { + // return an empty dictionary if there are no attributes + return new (); + } + + var attributes = new Dictionary (); + foreach (var attributeData in boundAttributes) { + var attrName = attributeData.AttributeClass?.ToDisplayString (); + if (string.IsNullOrEmpty (attrName)) + continue; + if (!attributes.TryAdd (attrName, attributeData)) { + // TODO: diagnostics + } + } + + return attributes; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj b/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj index 7da28f54684..65474ceae72 100644 --- a/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj +++ b/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj @@ -13,13 +13,20 @@ Microsoft.Macios.Generator + + TRACE;CLASSIC_ENABLED + + + + TRACE;CLASSIC_ENABLED + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + @@ -27,15 +34,15 @@ <_Parameter1>Microsoft.Macios.Generator.Tests - - - external\PlatformName.cs - external\PlatformNameExtensions.cs + + + + diff --git a/src/rgen/Microsoft.Macios.Generator/TabbedStringBuilder.cs b/src/rgen/Microsoft.Macios.Generator/TabbedStringBuilder.cs index 530279b8644..1494228cff5 100644 --- a/src/rgen/Microsoft.Macios.Generator/TabbedStringBuilder.cs +++ b/src/rgen/Microsoft.Macios.Generator/TabbedStringBuilder.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using System.Text; @@ -140,7 +139,6 @@ public TabbedStringBuilder AppendRaw (string rawString) { // we will split the raw string in lines and then append them so that the // tabbing is correct - var span = rawString.AsSpan (); var lines = rawString.AsSpan ().Split ('\n'); var count = 0; foreach (var range in lines) { @@ -182,6 +180,22 @@ public TabbedStringBuilder AppendEditorBrowsableAttribute () return this; } + /// + /// Writes the autogenerated header and other pragmas. + /// + /// The current builder. + public TabbedStringBuilder WriteHeader () + { + // let people know this is generated code + AppendLine ("// "); + + // enable nullable! + AppendLine (); + AppendLine ("#nullable enable"); + AppendLine (); + return this; + } + /// /// Create a bew empty block. /// diff --git a/src/rgen/rgen.sln b/src/rgen/rgen.sln index 68499203954..701dc87108a 100644 --- a/src/rgen/rgen.sln +++ b/src/rgen/rgen.sln @@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Macios.Generator. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Macios.Bindings.Analyzer.Tests", "..\..\tests\rgen\Microsoft.Macios.Bindings.Analyzer.Tests\Microsoft.Macios.Bindings.Analyzer.Tests.csproj", "{1AC4A248-CC98-4392-8690-4E2CAF6E194B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Macios.Binding.Common", "Microsoft.Macios.Binding.Common\Microsoft.Macios.Binding.Common.csproj", "{536758BC-2A88-4B79-ABB1-6B39494A5FE6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,5 +44,9 @@ Global {1AC4A248-CC98-4392-8690-4E2CAF6E194B}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AC4A248-CC98-4392-8690-4E2CAF6E194B}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AC4A248-CC98-4392-8690-4E2CAF6E194B}.Release|Any CPU.Build.0 = Release|Any CPU + {536758BC-2A88-4B79-ABB1-6B39494A5FE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {536758BC-2A88-4B79-ABB1-6B39494A5FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {536758BC-2A88-4B79-ABB1-6B39494A5FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {536758BC-2A88-4B79-ABB1-6B39494A5FE6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs index 67c71504329..2800e2632da 100644 --- a/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs +++ b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs @@ -29,10 +29,10 @@ public class Examples { var compilation = CreateCompilation (nameof (CompareGeneratedCode), platform, inputText); var diagnostics = await RunAnalyzer (new BindingTypeSemanticAnalyzer (), compilation); var analyzerDiagnotics = diagnostics - .Where (d => d.Id == BindingTypeSemanticAnalyzer.DiagnosticId).ToArray (); + .Where (d => d.Id == BindingTypeSemanticAnalyzer.RBI0001.Id).ToArray (); Assert.Single (analyzerDiagnotics); // verify the diagnostic message - VerifyDiagnosticMessage (analyzerDiagnotics [0], BindingTypeSemanticAnalyzer.DiagnosticId, + VerifyDiagnosticMessage (analyzerDiagnotics [0], BindingTypeSemanticAnalyzer.RBI0001.Id, DiagnosticSeverity.Error, "The binding type 'Test.Examples' must declared as a partial class"); } } diff --git a/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/SmartEnumSemanticAnalyzerTests.cs b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/SmartEnumSemanticAnalyzerTests.cs new file mode 100644 index 00000000000..ecc4b772466 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/SmartEnumSemanticAnalyzerTests.cs @@ -0,0 +1,330 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Xamarin.Tests; +using Xamarin.Utils; +using Xunit; + +namespace Microsoft.Macios.Bindings.Analyzer.Tests; + +public class SmartEnumSemanticAnalyzerTests : BaseGeneratorWithAnalyzerTestClass { + + [Theory] + [PlatformInlineData (ApplePlatform.iOS)] + [PlatformInlineData (ApplePlatform.TVOS)] + [PlatformInlineData (ApplePlatform.MacOSX)] + [PlatformInlineData (ApplePlatform.MacCatalyst)] + public async Task SmartEnumMustHaveFieldAttribute (ApplePlatform platform) + { + const string inputText = @" +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureSystemPressureExampleLevel { + [Field (""AVCaptureSystemPressureLevelNominal"")] + Nominal, + + [Field (""AVCaptureSystemPressureLevelFair"")] + Fair, + + [Field (""AVCaptureSystemPressureLevelSerious"")] + Serious, + + [Field (""AVCaptureSystemPressureLevelCritical"")] + Critical, + + // missing field attribute, should be an error + Shutdown, +} +"; + + var compilation = CreateCompilation (nameof (SmartEnumSemanticAnalyzerTests), platform, inputText); + var diagnostics = await RunAnalyzer (new SmartEnumSemanticAnalyzer (), compilation); + var analyzerDiagnotics = diagnostics + .Where (d => d.Id == SmartEnumSemanticAnalyzer.RBI0002.Id).ToArray (); + Assert.Single (analyzerDiagnotics); + // verify the diagnostic message + VerifyDiagnosticMessage (analyzerDiagnotics [0], SmartEnumSemanticAnalyzer.RBI0002.Id, + DiagnosticSeverity.Error, "The enum value 'AVFoundation.AVCaptureSystemPressureExampleLevel.Shutdown' must be tagged with a Field attribute"); + } + + [Theory] + [PlatformInlineData (ApplePlatform.iOS)] + [PlatformInlineData (ApplePlatform.TVOS)] + [PlatformInlineData (ApplePlatform.MacOSX)] + [PlatformInlineData (ApplePlatform.MacCatalyst)] + public async Task SmartEnumSymbolMustBeCorrect (ApplePlatform platform) + { + const string inputText = @" +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureSystemPressureExampleLevel { + [Field (""AVCaptureSystemPressureLevelNominal"")] + Nominal, + + [Field (""AVCaptureSystemPressureLevelFair"")] + Fair, + + [Field (""AVCaptureSystemPressureLevelSerious"")] + Serious, + + [Field (""AVCaptureSystemPressureLevelCritical"")] + Critical, + + // empty field, this should be an error + [Field ("" "")] + Shutdown, +}"; + + var compilation = CreateCompilation (nameof (SmartEnumSemanticAnalyzerTests), platform, inputText); + var diagnostics = await RunAnalyzer (new SmartEnumSemanticAnalyzer (), compilation); + var analyzerDiagnotics = diagnostics + .Where (d => d.Id == SmartEnumSemanticAnalyzer.RBI0003.Id).ToArray (); + Assert.Single (analyzerDiagnotics); + VerifyDiagnosticMessage (analyzerDiagnotics [0], SmartEnumSemanticAnalyzer.RBI0003.Id, + DiagnosticSeverity.Error, "The enum value 'AVFoundation.AVCaptureSystemPressureExampleLevel.Shutdown' backing field is an empty string"); + } + + const string AppleFrameworkLib = @" +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureSystemPressureExampleLevel { + [Field (""AVCaptureSystemPressureLevelNominal"")] + Nominal, + + [Field (""AVCaptureSystemPressureLevelFair"")] + Fair, + + [Field (""AVCaptureSystemPressureLevelSerious"")] + Serious, + + [Field (""AVCaptureSystemPressureLevelCritical"")] + Critical, + + // do not do this with apple frameworks + [Field (""AVCaptureSystemPressureLevelShutdown"", ""/path/to/not/needed/lib"")] + Shutdown, +}"; + + const string AppleFrameworkLibNamedParameter = @" +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureSystemPressureExampleLevel { + [Field (""AVCaptureSystemPressureLevelNominal"")] + Nominal, + + [Field (""AVCaptureSystemPressureLevelFair"")] + Fair, + + [Field (""AVCaptureSystemPressureLevelSerious"")] + Serious, + + [Field (""AVCaptureSystemPressureLevelCritical"")] + Critical, + + // do not do this with apple frameworks + [Field (""AVCaptureSystemPressureLevelShutdown"", LibraryName = ""/path/to/not/needed/lib"")] + Shutdown, +}"; + + [Theory] + [PlatformInlineData (ApplePlatform.iOS, AppleFrameworkLib)] + [PlatformInlineData (ApplePlatform.iOS, AppleFrameworkLibNamedParameter)] + [PlatformInlineData (ApplePlatform.TVOS, AppleFrameworkLib)] + [PlatformInlineData (ApplePlatform.TVOS, AppleFrameworkLibNamedParameter)] + [PlatformInlineData (ApplePlatform.MacOSX, AppleFrameworkLib)] + [PlatformInlineData (ApplePlatform.MacOSX, AppleFrameworkLibNamedParameter)] + [PlatformInlineData (ApplePlatform.MacCatalyst, AppleFrameworkLib)] + [PlatformInlineData (ApplePlatform.MacCatalyst, AppleFrameworkLibNamedParameter)] + public async Task SmartEnumAppleFrameworkNotLibrary (ApplePlatform platform, string inputText) + { + var compilation = CreateCompilation (nameof (SmartEnumSemanticAnalyzerTests), platform, inputText); + var diagnostics = await RunAnalyzer (new SmartEnumSemanticAnalyzer(), compilation); + + var analyzerDiagnotics = diagnostics + .Where (d => d.Id == SmartEnumSemanticAnalyzer.RBI0006.Id).ToArray (); + Assert.Single (analyzerDiagnotics); + VerifyDiagnosticMessage (analyzerDiagnotics [0], SmartEnumSemanticAnalyzer.RBI0006.Id, + DiagnosticSeverity.Warning, "The backing Field of 'AVFoundation.AVCaptureSystemPressureExampleLevel.Shutdown' should not provide a LibraryName"); + } + + const string CustomLibraryMissingLibraryName = @" +using System; +using System.Runtime.Versioning; +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace CustomLibrary; + +[BindingType] +public enum CustomLibraryEnum { + [Field (""None"", ""/path/to/customlibrary.framework"")] + None, + [Field (""Medium"", ""/path/to/customlibrary.framework"")] + Medium, + // missing lib, this is an error + [Field (""High"")] + High, +} +"; + const string CustomLibraryEmptyLibraryName = @" +using System; +using System.Runtime.Versioning; +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace CustomLibrary; + +[BindingType] +public enum CustomLibraryEnum { + [Field (""None"", ""/path/to/customlibrary.framework"")] + None, + [Field (""Medium"", ""/path/to/customlibrary.framework"")] + Medium, + // empty lib, this is an error + [Field (""High"", "" "")] + High, +} +"; + + const string CustomLibraryEmptyLibraryNameParameter = @" +using System; +using System.Runtime.Versioning; +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace CustomLibrary; + +[BindingType] +public enum CustomLibraryEnum { + [Field (""None"", ""/path/to/customlibrary.framework"")] + None, + [Field (""Medium"", ""/path/to/customlibrary.framework"")] + Medium, + // empty lib, this is an error + [Field (""High"", LibraryName = "" "")] + High, +} +"; + + [Theory] + [PlatformInlineData (ApplePlatform.iOS, CustomLibraryMissingLibraryName)] + [PlatformInlineData (ApplePlatform.iOS, CustomLibraryEmptyLibraryName)] + [PlatformInlineData (ApplePlatform.iOS, CustomLibraryEmptyLibraryNameParameter)] + [PlatformInlineData (ApplePlatform.TVOS, CustomLibraryMissingLibraryName)] + [PlatformInlineData (ApplePlatform.TVOS, CustomLibraryEmptyLibraryName)] + [PlatformInlineData (ApplePlatform.TVOS, CustomLibraryEmptyLibraryNameParameter)] + [PlatformInlineData (ApplePlatform.MacOSX, CustomLibraryMissingLibraryName)] + [PlatformInlineData (ApplePlatform.MacOSX, CustomLibraryEmptyLibraryName)] + [PlatformInlineData (ApplePlatform.MacOSX, CustomLibraryEmptyLibraryNameParameter)] + [PlatformInlineData (ApplePlatform.MacCatalyst, CustomLibraryMissingLibraryName)] + [PlatformInlineData (ApplePlatform.MacCatalyst, CustomLibraryEmptyLibraryName)] + [PlatformInlineData (ApplePlatform.MacCatalyst, CustomLibraryEmptyLibraryNameParameter)] + public async Task SmartEnumThirdPartyLibrary (ApplePlatform platform, string inputText) + { + var compilation = CreateCompilation (nameof (SmartEnumSemanticAnalyzerTests), platform, inputText); + var diagnostics = await RunAnalyzer (new SmartEnumSemanticAnalyzer(), compilation); + var analyzerDiagnotics = diagnostics + .Where (d => d.Id == SmartEnumSemanticAnalyzer.RBI0005.Id).ToArray (); + Assert.Single (analyzerDiagnotics); + VerifyDiagnosticMessage (analyzerDiagnotics [0], SmartEnumSemanticAnalyzer.RBI0005.Id, + DiagnosticSeverity.Error, "The enum value 'CustomLibrary.CustomLibraryEnum.High' backing field must provide a library name"); + } + + [Theory] + [PlatformInlineData (ApplePlatform.iOS)] + [PlatformInlineData (ApplePlatform.TVOS)] + [PlatformInlineData (ApplePlatform.MacOSX)] + [PlatformInlineData (ApplePlatform.MacCatalyst)] + public async Task SmartEnumNoFieldAttributes (ApplePlatform platform) + { + const string inputText = @" +using System; +using System.Runtime.Versioning; +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace CustomLibrary; + +[BindingType] +public enum CustomLibraryEnum { + None, + Medium, + High, +} +"; + var compilation = CreateCompilation (nameof (SmartEnumSemanticAnalyzerTests), platform, inputText); + var diagnostics = await RunAnalyzer (new SmartEnumSemanticAnalyzer(), compilation); + var analyzerDiagnotics = diagnostics + .Where (d => d.Id == SmartEnumSemanticAnalyzer.RBI0002.Id).ToArray (); + // we should have a diagnostic for each enum value + Assert.Equal (3, analyzerDiagnotics.Length); + } + + [Theory] + [PlatformInlineData (ApplePlatform.iOS)] + [PlatformInlineData (ApplePlatform.TVOS)] + [PlatformInlineData (ApplePlatform.MacOSX)] + [PlatformInlineData (ApplePlatform.MacCatalyst)] + public async Task SmarEnumFieldNotDuplicated (ApplePlatform platform) + { + + const string inputText = @" +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureSystemPressureExampleLevel { + [Field (""AVCaptureSystemPressureLevelNominal"")] + Nominal, + + // duplicated, this should be an error + [Field (""AVCaptureSystemPressureLevelNominal"")] + Fair, + + [Field (""AVCaptureSystemPressureLevelSerious"")] + Serious, + + [Field (""AVCaptureSystemPressureLevelCritical"")] + Critical, + + [Field (""AVCaptureSystemPressureLevelShutdown"")] + Shutdown, +}"; + + var compilation = CreateCompilation (nameof (SmartEnumSemanticAnalyzerTests), platform, inputText); + var diagnostics = await RunAnalyzer (new SmartEnumSemanticAnalyzer(), compilation); + var analyzerDiagnotics = diagnostics + .Where (d => d.Id == SmartEnumSemanticAnalyzer.RBI0004.Id).ToArray (); + Assert.Single (analyzerDiagnotics); + var ms = analyzerDiagnotics [0].GetMessage (); + VerifyDiagnosticMessage (analyzerDiagnotics [0], SmartEnumSemanticAnalyzer.RBI0004.Id, + DiagnosticSeverity.Error, "The enum value 'AVFoundation.AVCaptureSystemPressureExampleLevel.Fair' backing field 'AVCaptureSystemPressureLevelNominal' is already in use"); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/BaseGeneratorTestClass.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/BaseGeneratorTestClass.cs index 1ed459e25cc..1d7b85464b2 100644 --- a/tests/rgen/Microsoft.Macios.Generator.Tests/BaseGeneratorTestClass.cs +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/BaseGeneratorTestClass.cs @@ -16,22 +16,31 @@ namespace Microsoft.Macios.Generator.Tests; /// public class BaseGeneratorTestClass { protected BindingSourceGeneratorGenerator GeneratorGenerator; - protected CSharpGeneratorDriver _driver; + protected CSharpGeneratorDriver Driver; + + // list of the defines for each platform, this is passed to the parser to ensure that + // we are testing the platforms as if they were being compiled. + readonly Dictionary platformDefines = new () { + { ApplePlatform.iOS, new [] { "__IOS__" } }, + { ApplePlatform.TVOS, new [] { "__TVOS__" } }, + { ApplePlatform.MacOSX, new [] { "__MACOS__" } }, + { ApplePlatform.MacCatalyst, new [] { "__MACCATALYST__" } }, + }; public BaseGeneratorTestClass () { GeneratorGenerator = new BindingSourceGeneratorGenerator (); - _driver = CSharpGeneratorDriver.Create (GeneratorGenerator); + Driver = CSharpGeneratorDriver.Create (GeneratorGenerator); } protected Compilation RunGeneratorsAndUpdateCompilation (Compilation compilation, out ImmutableArray diagnostics) { - _driver.RunGeneratorsAndUpdateCompilation (compilation, out var updatedCompilation, out diagnostics); + Driver.RunGeneratorsAndUpdateCompilation (compilation, out var updatedCompilation, out diagnostics); return updatedCompilation; } protected GeneratorDriverRunResult RunGenerators (Compilation compilation) - => _driver.RunGenerators (compilation).GetRunResult (); + => Driver.RunGenerators (compilation).GetRunResult (); protected Compilation CreateCompilation (string name, ApplePlatform platform, params string [] sources) { @@ -46,12 +55,17 @@ protected Compilation CreateCompilation (string name, ApplePlatform platform, pa } else { throw new InvalidOperationException ($"Could not find platform dll for {platform}"); } - var trees = sources.Select (s => CSharpSyntaxTree.ParseText (s)); - var options = new CSharpCompilationOptions (OutputKind.NetModule); + + var parseOptions = new CSharpParseOptions (LanguageVersion.Latest, DocumentationMode.None, preprocessorSymbols: platformDefines [platform]); ; + var trees = sources.Select (s => CSharpSyntaxTree.ParseText (s, parseOptions)); + + var options = new CSharpCompilationOptions (OutputKind.NetModule) + .WithAllowUnsafe (true); + return CSharpCompilation.Create (name, trees, references, options); } - protected void CompareGeneratedCode (ApplePlatform platform, string className, string inputFileName, string inputText, string outputFileName, string expectedOutputText) + protected void CompareGeneratedCode (ApplePlatform platform, string className, string inputFileName, string inputText, string outputFileName, string expectedOutputText, string? expectedLibraryText) { // We need to create a compilation with the required source code. var compilation = CreateCompilation (nameof (CompareGeneratedCode), platform, inputText); @@ -66,5 +80,11 @@ protected void CompareGeneratedCode (ApplePlatform platform, string className, s Assert.Equal (expectedOutputText, generatedFileSyntax.GetText ().ToString (), ignoreLineEndingDifferences: true); + if (expectedLibraryText is not null) { + // validate that Library.g.cs was created by the LibraryEmitter and matches the expectation + var generatedLibSyntax = runResult.GeneratedTrees.Single (t => t.FilePath.EndsWith ("Libraries.g.cs")); + Assert.Equal (expectedLibraryText, generatedLibSyntax.GetText ().ToString()); + } + } } diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/BaseTypeDeclarationSyntaxExtensionsTests.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/BaseTypeDeclarationSyntaxExtensionsTests.cs new file mode 100644 index 00000000000..9a25b924868 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/BaseTypeDeclarationSyntaxExtensionsTests.cs @@ -0,0 +1,184 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Macios.Generator.Extensions; +using Xunit; + +namespace Microsoft.Macios.Generator.Tests; + +public class BaseTypeDeclarationSyntaxExtensionsTests { + [Fact] + public void GetFullyQualifiedIdentifierFileScopedNamespace () + { + var compilationUnit = SyntaxFactory.CompilationUnit () + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.FileScopedNamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Test")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Foo") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))))))) + .NormalizeWhitespace (); + var declaration = compilationUnit.SyntaxTree.GetRoot () + .DescendantNodes () + .OfType () + .FirstOrDefault (); + Assert.NotNull (declaration); + Assert.Equal ("Test.Foo", declaration.GetFullyQualifiedIdentifier ()); + } + + [Fact] + public void GetFullyQualifiedIdentifierFileScopedNamespaceNestedClass () + { + var compilationUnit = SyntaxFactory.CompilationUnit () + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.FileScopedNamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Test")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Foo") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Bar") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))))))))) + .NormalizeWhitespace (); + var declaration = compilationUnit.SyntaxTree.GetRoot () + .DescendantNodes () + .OfType () + .LastOrDefault (); + Assert.NotNull (declaration); + Assert.Equal ("Test.Foo.Bar", declaration.GetFullyQualifiedIdentifier ()); + } + + [Fact] + public void GetFullyQualifiedIdentifierNamespaceDeclaration () + { + var compilationUnit = SyntaxFactory.CompilationUnit () + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.NamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Test")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Foo") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))))))) + .NormalizeWhitespace (); + var declaration = compilationUnit.SyntaxTree.GetRoot () + .DescendantNodes () + .OfType () + .FirstOrDefault (); + Assert.NotNull (declaration); + Assert.Equal ("Test.Foo", declaration.GetFullyQualifiedIdentifier ()); + } + + [Fact] + public void GetFullyQualifiedIdentifierMultipleNamespaceDeclaration () + { + var compilationUnit = SyntaxFactory.CompilationUnit () + .WithMembers ( + SyntaxFactory.List ( + new MemberDeclarationSyntax [] { + SyntaxFactory.NamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Test")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Foo") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))))), + SyntaxFactory.NamespaceDeclaration ( + SyntaxFactory.IdentifierName ("TestBar")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Bar") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))))) + })) + .NormalizeWhitespace (); + // get the first namespace, get the class and assert that only the first namespace is used + var nsDeclaration = compilationUnit.SyntaxTree.GetRoot () + .DescendantNodes () + .OfType () + .FirstOrDefault (); + Assert.NotNull (nsDeclaration); + var declaration = nsDeclaration.DescendantNodes () + .OfType () + .FirstOrDefault (); + Assert.NotNull (declaration); + Assert.Equal ("Test.Foo", declaration.GetFullyQualifiedIdentifier ()); + } + + [Fact] + public void GetFullyQualifiedIdentifierNestedNamespaceDeclaration () + { + var compilationUnit = SyntaxFactory.CompilationUnit () + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.NamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Foo")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.NamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Bar")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Test") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))))))))) + .NormalizeWhitespace (); + var declaration = compilationUnit.SyntaxTree.GetRoot () + .DescendantNodes () + .OfType () + .FirstOrDefault (); + Assert.NotNull (declaration); + Assert.Equal ("Foo.Bar.Test", declaration.GetFullyQualifiedIdentifier ()); + } + + [Fact] + public void GetFullyQualifiedIdentifierNamespaceDeclarationNestedClass () + { + var compilationUnit = SyntaxFactory.CompilationUnit () + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.NamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Foo")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.NamespaceDeclaration ( + SyntaxFactory.IdentifierName ("Bar")) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Test") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.ClassDeclaration ("Final") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind + .PublicKeyword))))))))))) + .NormalizeWhitespace (); + var declaration = compilationUnit.SyntaxTree.GetRoot () + .DescendantNodes () + .OfType () + .LastOrDefault (); + Assert.NotNull (declaration); + Assert.Equal ("Foo.Bar.Test.Final", declaration.GetFullyQualifiedIdentifier ()); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs index fd8738e7d3e..99279577131 100644 --- a/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs @@ -23,9 +23,9 @@ public partial class AVAudioPcmBuffer : AVAudioBuffer { #nullable enable -using System; using Foundation; using ObjCBindings; +using System; namespace TestNamespace { @@ -48,7 +48,7 @@ public void CorrectUsingImports (ApplePlatform platform, string input, string ex platform, usingImportInput); // Run generators and retrieve all results. - var runResult = _driver.RunGenerators (compilation).GetRunResult (); + var runResult = Driver.RunGenerators (compilation).GetRunResult (); Assert.Empty (runResult.Diagnostics); // ensure that we do have all the needed attributes present diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/CodeChangesComparerTests.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/CodeChangesComparerTests.cs new file mode 100644 index 00000000000..e4cd1cf3323 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/CodeChangesComparerTests.cs @@ -0,0 +1,129 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Xunit; + +namespace Microsoft.Macios.Generator.Tests; + +public class CodeChangesComparerTests { + readonly BaseTypeDeclarationSyntax syntax; + readonly CompilationUnitSyntax compilationUnitSyntax; + readonly CodeChangesComparer comparer; + + public CodeChangesComparerTests () + { + comparer = new(); + // no, you do not need to this by hand, visit https://roslynquoter.azurewebsites.net to generate the syntax + compilationUnitSyntax = SyntaxFactory.CompilationUnit () + .WithMembers ( + SyntaxFactory.List ( + new MemberDeclarationSyntax [] { + SyntaxFactory.PropertyDeclaration ( + SyntaxFactory.IdentifierName ("namespace"), + SyntaxFactory.Identifier ("Test")) + .WithAccessorList ( + SyntaxFactory.AccessorList () + .WithCloseBraceToken ( + SyntaxFactory.MissingToken (SyntaxKind.CloseBraceToken))), + SyntaxFactory.EnumDeclaration ("Test") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))) + .WithMembers ( + SyntaxFactory.SeparatedList ( + new SyntaxNodeOrToken [] { + SyntaxFactory.EnumMemberDeclaration ( + SyntaxFactory.Identifier ("First")), + SyntaxFactory.Token (SyntaxKind.CommaToken), + SyntaxFactory.EnumMemberDeclaration ( + SyntaxFactory.Identifier ("Second")), + SyntaxFactory.Token (SyntaxKind.CommaToken), + SyntaxFactory.EnumMemberDeclaration ( + SyntaxFactory.Identifier ("Last")), + SyntaxFactory.Token (SyntaxKind.CommaToken) + })), + SyntaxFactory.ClassDeclaration ("MyTestClass") + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))) + .WithMembers ( + SyntaxFactory.SingletonList ( + SyntaxFactory.PropertyDeclaration ( + SyntaxFactory.PredefinedType ( + SyntaxFactory.Token (SyntaxKind.IntKeyword)), + SyntaxFactory.Identifier ("Count")) + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword))) + .WithAccessorList ( + SyntaxFactory.AccessorList ( + SyntaxFactory.List ( + new AccessorDeclarationSyntax [] { + SyntaxFactory.AccessorDeclaration ( + SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken ( + SyntaxFactory.Token (SyntaxKind.SemicolonToken)), + SyntaxFactory.AccessorDeclaration ( + SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken ( + SyntaxFactory.Token (SyntaxKind.SemicolonToken)) + }))) + .WithInitializer ( + SyntaxFactory.EqualsValueClause ( + SyntaxFactory.LiteralExpression ( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal (0)))) + .WithSemicolonToken ( + SyntaxFactory.Token (SyntaxKind.SemicolonToken)))) + .WithCloseBraceToken ( + SyntaxFactory.Token ( + SyntaxFactory.TriviaList (), + SyntaxKind.CloseBraceToken, + SyntaxFactory.TriviaList ( + SyntaxFactory.Trivia ( + SyntaxFactory.SkippedTokensTrivia () + .WithTokens ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.CloseBraceToken))))))) + })) + .NormalizeWhitespace (); + syntax = compilationUnitSyntax.SyntaxTree.GetRoot () + .DescendantNodes ().OfType ().FirstOrDefault ()!; + } + + [Fact] + public void CompareDiffQualifiedName () + { + var codeChange1 = new CodeChanges ("codeChange1", syntax, ["First", "Second", "Last"]); + var codeChange2 = new CodeChanges ("codeChange2", syntax, ["First", "Second", "Last"]); + Assert.False (comparer.Equals (codeChange1, codeChange2)); + } + + [Fact] + public void CompareDiffMembers () + { + var codeChange1 = new CodeChanges ("codeChange1", syntax, ["First", "Second", "Last"]); + var codeChange2 = new CodeChanges ("codeChange1", syntax, ["First", "Second"]); + Assert.False (comparer.Equals (codeChange1, codeChange2)); + } + + [Fact] + public void CompareSameMembers () + { + // add a diff declaration syntax, but keep the name and the members + var secondSyntax = compilationUnitSyntax.SyntaxTree.GetRoot ().DescendantNodes () + .OfType ().FirstOrDefault ()!; + var codeChange1 = new CodeChanges ("codeChange1", syntax, ["First", "Second", "Last"]); + var codeChange2 = new CodeChanges ("codeChange1", secondSyntax, ["First", "Second", "Last"]); + Assert.True (comparer.Equals (codeChange1, codeChange2)); + } + + [Fact] + public void CompareSameMembersUnsorted () + { + var codeChange1 = new CodeChanges ("codeChange1", syntax, ["First", "Second", "Last"]); + var codeChange2 = new CodeChanges ("codeChange1", syntax, ["Last", "First", "Second"]); + Assert.True (comparer.Equals (codeChange1, codeChange2)); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/CompilationExtensionsTest.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/CompilationExtensionsTest.cs new file mode 100644 index 00000000000..793cc13aa1a --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/CompilationExtensionsTest.cs @@ -0,0 +1,22 @@ +using Microsoft.Macios.Generator.Extensions; +using Xamarin.Tests; +using Xamarin.Utils; +using Xunit; + +namespace Microsoft.Macios.Generator.Tests; + +public class CompilationExtensionsTest : BaseGeneratorTestClass { + + [Theory] + [PlatformInlineData (ApplePlatform.iOS, PlatformName.iOS)] + [PlatformInlineData (ApplePlatform.TVOS, PlatformName.TvOS)] + [PlatformInlineData (ApplePlatform.MacOSX, PlatformName.MacOSX)] + [PlatformInlineData (ApplePlatform.MacCatalyst, PlatformName.MacCatalyst)] + public void GetCurrentPlatformTests (ApplePlatform platform, PlatformName expectedPlatform) + { + // get the current compilation for the platform and assert we return the correct one from + // the compilation + var compilation = CreateCompilation (nameof (GetCurrentPlatformTests), platform); + Assert.Equal (expectedPlatform, compilation.GetCurrentPlatform ()); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/Microsoft.Macios.Generator.Tests.csproj b/tests/rgen/Microsoft.Macios.Generator.Tests/Microsoft.Macios.Generator.Tests.csproj index 417b2c63d19..da015307ef7 100644 --- a/tests/rgen/Microsoft.Macios.Generator.Tests/Microsoft.Macios.Generator.Tests.csproj +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/Microsoft.Macios.Generator.Tests.csproj @@ -10,6 +10,8 @@ + + @@ -20,6 +22,7 @@ + @@ -36,24 +39,20 @@ external\ExecutionHelper.cs - - external\ApplePlatform.cs - - - external\TargetFramework.cs - external\StringUtils.cs external\Execution.cs - - external\SdkVersions.cs - external\Cache.cs + + + + + diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureDeviceTypeEnum.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureDeviceTypeEnum.cs new file mode 100644 index 00000000000..742ae874e89 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureDeviceTypeEnum.cs @@ -0,0 +1,42 @@ +using System; +using Foundation; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureDeviceType { + + [Field ("AVCaptureDeviceTypeBuiltInMicrophone")] + BuiltInMicrophone, + + [Field ("AVCaptureDeviceTypeBuiltInWideAngleCamera")] + BuiltInWideAngleCamera, + + [Field ("AVCaptureDeviceTypeBuiltInTelephotoCamera")] + BuiltInTelephotoCamera, + + [Field ("AVCaptureDeviceTypeBuiltInDuoCamera")] + BuiltInDuoCamera, + + [Field ("AVCaptureDeviceTypeBuiltInDualCamera")] + BuiltInDualCamera, + + [Field ("AVCaptureDeviceTypeBuiltInTrueDepthCamera")] + BuiltInTrueDepthCamera, + + [Field ("AVCaptureDeviceTypeBuiltInUltraWideCamera")] + BuiltInUltraWideCamera, + + [Field ("AVCaptureDeviceTypeBuiltInTripleCamera")] + BuiltInTripleCamera, + + [Field ("AVCaptureDeviceTypeBuiltInDualWideCamera")] + BuiltInDualWideCamera, + + [Field ("AVCaptureDeviceTypeExternalUnknown")] + ExternalUnknown, + + [Field ("AVCaptureDeviceTypeBuiltInLiDARDepthCamera")] + BuiltInLiDarDepthCamera, +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureSystemPressureLevel.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureSystemPressureLevel.cs new file mode 100644 index 00000000000..ce12690fd11 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVCaptureSystemPressureLevel.cs @@ -0,0 +1,23 @@ +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureSystemPressureLevel { + [Field ("AVCaptureSystemPressureLevelNominal")] + Nominal, + + [Field ("AVCaptureSystemPressureLevelFair")] + Fair, + + [Field ("AVCaptureSystemPressureLevelSerious")] + Serious, + + [Field ("AVCaptureSystemPressureLevelCritical")] + Critical, + + [Field ("AVCaptureSystemPressureLevelShutdown")] + Shutdown, +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVMediaCharacteristics.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVMediaCharacteristics.cs new file mode 100644 index 00000000000..2b986f95a5a --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/AVMediaCharacteristics.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.Versioning; +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +[SupportedOSPlatform ("maccatalyst13.1")] +enum AVMediaCharacteristics { + [Field ("AVMediaCharacteristicVisual")] + Visual = 0, + + [Field ("AVMediaCharacteristicAudible")] + Audible = 1, + + [Field ("AVMediaCharacteristicLegible")] + Legible = 2, + + [Field ("AVMediaCharacteristicFrameBased")] + FrameBased = 3, + + [Field ("AVMediaCharacteristicUsesWideGamutColorSpace")] + UsesWideGamutColorSpace = 4, + + [Field ("AVMediaCharacteristicIsMainProgramContent")] + IsMainProgramContent = 5, + + [Field ("AVMediaCharacteristicIsAuxiliaryContent")] + IsAuxiliaryContent = 6, + + [Field ("AVMediaCharacteristicContainsOnlyForcedSubtitles")] + ContainsOnlyForcedSubtitles = 7, + + [Field ("AVMediaCharacteristicTranscribesSpokenDialogForAccessibility")] + TranscribesSpokenDialogForAccessibility = 8, + + [Field ("AVMediaCharacteristicDescribesMusicAndSoundForAccessibility")] + DescribesMusicAndSoundForAccessibility = 9, + + [Field ("AVMediaCharacteristicDescribesVideoForAccessibility")] + DescribesVideoForAccessibility = 10, + +#if !__MACOS__ + [Field ("AVMediaCharacteristicEasyToRead")] + EasyToRead = 11, +#endif + + [Field ("AVMediaCharacteristicLanguageTranslation")] + LanguageTranslation = 12, + + [Field ("AVMediaCharacteristicDubbedTranslation")] + DubbedTranslation = 13, + + [Field ("AVMediaCharacteristicVoiceOverTranslation")] + VoiceOverTranslation = 14, + + [SupportedOSPlatform ("ios13.0")] + [SupportedOSPlatform ("tvos13.0")] + [Field ("AVMediaCharacteristicIsOriginalContent")] + IsOriginalContent = 15, + + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("tvos14.0")] + [SupportedOSPlatform ("maccatalyst14.0")] + [Field ("AVMediaCharacteristicContainsHDRVideo")] + ContainsHdrVideo = 16, + + [SupportedOSPlatform ("ios13.0")] + [SupportedOSPlatform ("tvos13.0")] + [Field ("AVMediaCharacteristicContainsAlphaChannel")] + ContainsAlphaChannel = 17, +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/CustomLibraryEnum.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/CustomLibraryEnum.cs new file mode 100644 index 00000000000..30c6ea67e62 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/CustomLibraryEnum.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.Versioning; +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace CustomLibrary; + +[BindingType] +public enum CustomLibraryEnum { + [Field ("None", "/path/to/customlibrary.framework")] + None, + [Field ("Medium", "/path/to/customlibrary.framework")] + Medium, + [Field ("High", "/path/to/customlibrary.framework")] + High, +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureDeviceTypeEnum.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureDeviceTypeEnum.cs new file mode 100644 index 00000000000..c748e4e3671 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureDeviceTypeEnum.cs @@ -0,0 +1,222 @@ +// + +#nullable enable + +using Foundation; +using ObjCBindings; +using ObjCRuntime; +using System; + +namespace AVFoundation; + +[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)] +static public partial class AVCaptureDeviceTypeExtensions +{ + + static IntPtr[] values = new IntPtr [11]; + + [Field ("AVCaptureDeviceTypeBuiltInMicrophone", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInMicrophone + { + get + { + fixed (IntPtr *storage = &values [0]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInMicrophone", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInWideAngleCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInWideAngleCamera + { + get + { + fixed (IntPtr *storage = &values [1]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInWideAngleCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInTelephotoCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInTelephotoCamera + { + get + { + fixed (IntPtr *storage = &values [2]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInTelephotoCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInDuoCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInDuoCamera + { + get + { + fixed (IntPtr *storage = &values [3]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInDuoCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInDualCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInDualCamera + { + get + { + fixed (IntPtr *storage = &values [4]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInDualCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInTrueDepthCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInTrueDepthCamera + { + get + { + fixed (IntPtr *storage = &values [5]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInTrueDepthCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInUltraWideCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInUltraWideCamera + { + get + { + fixed (IntPtr *storage = &values [6]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInUltraWideCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInTripleCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInTripleCamera + { + get + { + fixed (IntPtr *storage = &values [7]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInTripleCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInDualWideCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInDualWideCamera + { + get + { + fixed (IntPtr *storage = &values [8]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInDualWideCamera", storage); + } + } + + [Field ("AVCaptureDeviceTypeExternalUnknown", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeExternalUnknown + { + get + { + fixed (IntPtr *storage = &values [9]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeExternalUnknown", storage); + } + } + + [Field ("AVCaptureDeviceTypeBuiltInLiDARDepthCamera", "AVFoundation")] + internal unsafe static IntPtr AVCaptureDeviceTypeBuiltInLiDARDepthCamera + { + get + { + fixed (IntPtr *storage = &values [10]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureDeviceTypeBuiltInLiDARDepthCamera", storage); + } + } + + public static NSString? GetConstant (this AVCaptureDeviceType self) + { + IntPtr ptr = IntPtr.Zero; + switch ((int) self) + { + case 0: // AVCaptureDeviceTypeBuiltInMicrophone + ptr = AVCaptureDeviceTypeBuiltInMicrophone; + break; + case 1: // AVCaptureDeviceTypeBuiltInWideAngleCamera + ptr = AVCaptureDeviceTypeBuiltInWideAngleCamera; + break; + case 2: // AVCaptureDeviceTypeBuiltInTelephotoCamera + ptr = AVCaptureDeviceTypeBuiltInTelephotoCamera; + break; + case 3: // AVCaptureDeviceTypeBuiltInDuoCamera + ptr = AVCaptureDeviceTypeBuiltInDuoCamera; + break; + case 4: // AVCaptureDeviceTypeBuiltInDualCamera + ptr = AVCaptureDeviceTypeBuiltInDualCamera; + break; + case 5: // AVCaptureDeviceTypeBuiltInTrueDepthCamera + ptr = AVCaptureDeviceTypeBuiltInTrueDepthCamera; + break; + case 6: // AVCaptureDeviceTypeBuiltInUltraWideCamera + ptr = AVCaptureDeviceTypeBuiltInUltraWideCamera; + break; + case 7: // AVCaptureDeviceTypeBuiltInTripleCamera + ptr = AVCaptureDeviceTypeBuiltInTripleCamera; + break; + case 8: // AVCaptureDeviceTypeBuiltInDualWideCamera + ptr = AVCaptureDeviceTypeBuiltInDualWideCamera; + break; + case 9: // AVCaptureDeviceTypeExternalUnknown + ptr = AVCaptureDeviceTypeExternalUnknown; + break; + case 10: // AVCaptureDeviceTypeBuiltInLiDARDepthCamera + ptr = AVCaptureDeviceTypeBuiltInLiDARDepthCamera; + break; + } + return (NSString?) Runtime.GetNSObject (ptr); + } + + public static AVCaptureDeviceType GetValue (NSString constant) + { + if (constant is null) + throw new ArgumentNullException (nameof (constant)); + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInMicrophone)) + return AVCaptureDeviceType.BuiltInMicrophone; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInWideAngleCamera)) + return AVCaptureDeviceType.BuiltInWideAngleCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInTelephotoCamera)) + return AVCaptureDeviceType.BuiltInTelephotoCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInDuoCamera)) + return AVCaptureDeviceType.BuiltInDuoCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInDualCamera)) + return AVCaptureDeviceType.BuiltInDualCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInTrueDepthCamera)) + return AVCaptureDeviceType.BuiltInTrueDepthCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInUltraWideCamera)) + return AVCaptureDeviceType.BuiltInUltraWideCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInTripleCamera)) + return AVCaptureDeviceType.BuiltInTripleCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInDualWideCamera)) + return AVCaptureDeviceType.BuiltInDualWideCamera; + if (constant.IsEqualTo (AVCaptureDeviceTypeExternalUnknown)) + return AVCaptureDeviceType.ExternalUnknown; + if (constant.IsEqualTo (AVCaptureDeviceTypeBuiltInLiDARDepthCamera)) + return AVCaptureDeviceType.BuiltInLiDarDepthCamera; + throw new NotSupportedException ($"{constant} has no associated enum value on this platform."); + } + + internal static NSString?[]? ToConstantArray (this AVCaptureDeviceType[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (value.GetConstant ()); + } + return rv.ToArray (); + } + + internal static AVCaptureDeviceType[]? ToEnumArray (this NSString[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (GetValue (value)); + } + return rv.ToArray (); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureSystemPressureLevel.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureSystemPressureLevel.cs new file mode 100644 index 00000000000..83444b481be --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedAVCaptureSystemPressureLevel.cs @@ -0,0 +1,132 @@ +// + +#nullable enable + +using Foundation; +using ObjCBindings; +using ObjCRuntime; +using System; + +namespace AVFoundation; + +[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)] +static public partial class AVCaptureSystemPressureLevelExtensions +{ + + static IntPtr[] values = new IntPtr [5]; + + [Field ("AVCaptureSystemPressureLevelNominal", "AVFoundation")] + internal unsafe static IntPtr AVCaptureSystemPressureLevelNominal + { + get + { + fixed (IntPtr *storage = &values [0]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureSystemPressureLevelNominal", storage); + } + } + + [Field ("AVCaptureSystemPressureLevelFair", "AVFoundation")] + internal unsafe static IntPtr AVCaptureSystemPressureLevelFair + { + get + { + fixed (IntPtr *storage = &values [1]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureSystemPressureLevelFair", storage); + } + } + + [Field ("AVCaptureSystemPressureLevelSerious", "AVFoundation")] + internal unsafe static IntPtr AVCaptureSystemPressureLevelSerious + { + get + { + fixed (IntPtr *storage = &values [2]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureSystemPressureLevelSerious", storage); + } + } + + [Field ("AVCaptureSystemPressureLevelCritical", "AVFoundation")] + internal unsafe static IntPtr AVCaptureSystemPressureLevelCritical + { + get + { + fixed (IntPtr *storage = &values [3]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureSystemPressureLevelCritical", storage); + } + } + + [Field ("AVCaptureSystemPressureLevelShutdown", "AVFoundation")] + internal unsafe static IntPtr AVCaptureSystemPressureLevelShutdown + { + get + { + fixed (IntPtr *storage = &values [4]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVCaptureSystemPressureLevelShutdown", storage); + } + } + + public static NSString? GetConstant (this AVCaptureSystemPressureLevel self) + { + IntPtr ptr = IntPtr.Zero; + switch ((int) self) + { + case 0: // AVCaptureSystemPressureLevelNominal + ptr = AVCaptureSystemPressureLevelNominal; + break; + case 1: // AVCaptureSystemPressureLevelFair + ptr = AVCaptureSystemPressureLevelFair; + break; + case 2: // AVCaptureSystemPressureLevelSerious + ptr = AVCaptureSystemPressureLevelSerious; + break; + case 3: // AVCaptureSystemPressureLevelCritical + ptr = AVCaptureSystemPressureLevelCritical; + break; + case 4: // AVCaptureSystemPressureLevelShutdown + ptr = AVCaptureSystemPressureLevelShutdown; + break; + } + return (NSString?) Runtime.GetNSObject (ptr); + } + + public static AVCaptureSystemPressureLevel GetValue (NSString constant) + { + if (constant is null) + throw new ArgumentNullException (nameof (constant)); + if (constant.IsEqualTo (AVCaptureSystemPressureLevelNominal)) + return AVCaptureSystemPressureLevel.Nominal; + if (constant.IsEqualTo (AVCaptureSystemPressureLevelFair)) + return AVCaptureSystemPressureLevel.Fair; + if (constant.IsEqualTo (AVCaptureSystemPressureLevelSerious)) + return AVCaptureSystemPressureLevel.Serious; + if (constant.IsEqualTo (AVCaptureSystemPressureLevelCritical)) + return AVCaptureSystemPressureLevel.Critical; + if (constant.IsEqualTo (AVCaptureSystemPressureLevelShutdown)) + return AVCaptureSystemPressureLevel.Shutdown; + throw new NotSupportedException ($"{constant} has no associated enum value on this platform."); + } + + internal static NSString?[]? ToConstantArray (this AVCaptureSystemPressureLevel[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (value.GetConstant ()); + } + return rv.ToArray (); + } + + internal static AVCaptureSystemPressureLevel[]? ToEnumArray (this NSString[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (GetValue (value)); + } + return rv.ToArray (); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnum.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnum.cs new file mode 100644 index 00000000000..cdfddf06403 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnum.cs @@ -0,0 +1,103 @@ +// + +#nullable enable + +using Foundation; +using ObjCBindings; +using ObjCRuntime; +using System; +using System.Runtime.Versioning; + +namespace CustomLibrary; + +[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)] +static public partial class CustomLibraryEnumExtensions +{ + + static IntPtr[] values = new IntPtr [3]; + + [Field ("None", "/path/to/customlibrary.framework")] + internal unsafe static IntPtr None + { + get + { + fixed (IntPtr *storage = &values [0]) + return Dlfcn.CachePointer (Libraries.customlibrary.Handle, "None", storage); + } + } + + [Field ("Medium", "/path/to/customlibrary.framework")] + internal unsafe static IntPtr Medium + { + get + { + fixed (IntPtr *storage = &values [1]) + return Dlfcn.CachePointer (Libraries.customlibrary.Handle, "Medium", storage); + } + } + + [Field ("High", "/path/to/customlibrary.framework")] + internal unsafe static IntPtr High + { + get + { + fixed (IntPtr *storage = &values [2]) + return Dlfcn.CachePointer (Libraries.customlibrary.Handle, "High", storage); + } + } + + public static NSString? GetConstant (this CustomLibraryEnum self) + { + IntPtr ptr = IntPtr.Zero; + switch ((int) self) + { + case 0: // None + ptr = None; + break; + case 1: // Medium + ptr = Medium; + break; + case 2: // High + ptr = High; + break; + } + return (NSString?) Runtime.GetNSObject (ptr); + } + + public static CustomLibraryEnum GetValue (NSString constant) + { + if (constant is null) + throw new ArgumentNullException (nameof (constant)); + if (constant.IsEqualTo (None)) + return CustomLibraryEnum.None; + if (constant.IsEqualTo (Medium)) + return CustomLibraryEnum.Medium; + if (constant.IsEqualTo (High)) + return CustomLibraryEnum.High; + throw new NotSupportedException ($"{constant} has no associated enum value on this platform."); + } + + internal static NSString?[]? ToConstantArray (this CustomLibraryEnum[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (value.GetConstant ()); + } + return rv.ToArray (); + } + + internal static CustomLibraryEnum[]? ToEnumArray (this NSString[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (GetValue (value)); + } + return rv.ToArray (); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnumLibrariesClass.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnumLibrariesClass.cs new file mode 100644 index 00000000000..afa20d70dd9 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedCustomLibraryEnumLibrariesClass.cs @@ -0,0 +1,19 @@ +// + +#nullable enable + +using Foundation; +using ObjCBindings; +using ObjCRuntime; +using System; + +namespace ObjCRuntime; + +[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)] +static partial class Libraries +{ + static public class customlibrary + { + static public readonly IntPtr Handle = Dlfcn.dlopen ("/path/to/customlibrary.framework", 0); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedMacOSAVMediaCharacteristics.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedMacOSAVMediaCharacteristics.cs new file mode 100644 index 00000000000..3ea93bd507d --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectedMacOSAVMediaCharacteristics.cs @@ -0,0 +1,313 @@ +// + +#nullable enable + +using Foundation; +using ObjCBindings; +using ObjCRuntime; +using System; +using System.Runtime.Versioning; + +namespace AVFoundation; + +[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)] +static public partial class AVMediaCharacteristicsExtensions +{ + + static IntPtr[] values = new IntPtr [17]; + + [Field ("AVMediaCharacteristicVisual", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicVisual + { + get + { + fixed (IntPtr *storage = &values [0]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicVisual", storage); + } + } + + [Field ("AVMediaCharacteristicAudible", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicAudible + { + get + { + fixed (IntPtr *storage = &values [1]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicAudible", storage); + } + } + + [Field ("AVMediaCharacteristicLegible", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicLegible + { + get + { + fixed (IntPtr *storage = &values [2]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicLegible", storage); + } + } + + [Field ("AVMediaCharacteristicFrameBased", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicFrameBased + { + get + { + fixed (IntPtr *storage = &values [3]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicFrameBased", storage); + } + } + + [Field ("AVMediaCharacteristicUsesWideGamutColorSpace", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicUsesWideGamutColorSpace + { + get + { + fixed (IntPtr *storage = &values [4]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicUsesWideGamutColorSpace", storage); + } + } + + [Field ("AVMediaCharacteristicIsMainProgramContent", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicIsMainProgramContent + { + get + { + fixed (IntPtr *storage = &values [5]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicIsMainProgramContent", storage); + } + } + + [Field ("AVMediaCharacteristicIsAuxiliaryContent", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicIsAuxiliaryContent + { + get + { + fixed (IntPtr *storage = &values [6]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicIsAuxiliaryContent", storage); + } + } + + [Field ("AVMediaCharacteristicContainsOnlyForcedSubtitles", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicContainsOnlyForcedSubtitles + { + get + { + fixed (IntPtr *storage = &values [7]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicContainsOnlyForcedSubtitles", storage); + } + } + + [Field ("AVMediaCharacteristicTranscribesSpokenDialogForAccessibility", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicTranscribesSpokenDialogForAccessibility + { + get + { + fixed (IntPtr *storage = &values [8]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicTranscribesSpokenDialogForAccessibility", storage); + } + } + + [Field ("AVMediaCharacteristicDescribesMusicAndSoundForAccessibility", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicDescribesMusicAndSoundForAccessibility + { + get + { + fixed (IntPtr *storage = &values [9]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicDescribesMusicAndSoundForAccessibility", storage); + } + } + + [Field ("AVMediaCharacteristicDescribesVideoForAccessibility", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicDescribesVideoForAccessibility + { + get + { + fixed (IntPtr *storage = &values [10]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicDescribesVideoForAccessibility", storage); + } + } + + [Field ("AVMediaCharacteristicLanguageTranslation", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicLanguageTranslation + { + get + { + fixed (IntPtr *storage = &values [11]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicLanguageTranslation", storage); + } + } + + [Field ("AVMediaCharacteristicDubbedTranslation", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicDubbedTranslation + { + get + { + fixed (IntPtr *storage = &values [12]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicDubbedTranslation", storage); + } + } + + [Field ("AVMediaCharacteristicVoiceOverTranslation", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicVoiceOverTranslation + { + get + { + fixed (IntPtr *storage = &values [13]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicVoiceOverTranslation", storage); + } + } + + [Field ("AVMediaCharacteristicIsOriginalContent", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicIsOriginalContent + { + get + { + fixed (IntPtr *storage = &values [14]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicIsOriginalContent", storage); + } + } + + [Field ("AVMediaCharacteristicContainsHDRVideo", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicContainsHDRVideo + { + get + { + fixed (IntPtr *storage = &values [15]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicContainsHDRVideo", storage); + } + } + + [Field ("AVMediaCharacteristicContainsAlphaChannel", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicContainsAlphaChannel + { + get + { + fixed (IntPtr *storage = &values [16]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicContainsAlphaChannel", storage); + } + } + + public static NSString? GetConstant (this AVMediaCharacteristics self) + { + IntPtr ptr = IntPtr.Zero; + switch ((int) self) + { + case 0: // AVMediaCharacteristicVisual + ptr = AVMediaCharacteristicVisual; + break; + case 1: // AVMediaCharacteristicAudible + ptr = AVMediaCharacteristicAudible; + break; + case 2: // AVMediaCharacteristicLegible + ptr = AVMediaCharacteristicLegible; + break; + case 3: // AVMediaCharacteristicFrameBased + ptr = AVMediaCharacteristicFrameBased; + break; + case 4: // AVMediaCharacteristicUsesWideGamutColorSpace + ptr = AVMediaCharacteristicUsesWideGamutColorSpace; + break; + case 5: // AVMediaCharacteristicIsMainProgramContent + ptr = AVMediaCharacteristicIsMainProgramContent; + break; + case 6: // AVMediaCharacteristicIsAuxiliaryContent + ptr = AVMediaCharacteristicIsAuxiliaryContent; + break; + case 7: // AVMediaCharacteristicContainsOnlyForcedSubtitles + ptr = AVMediaCharacteristicContainsOnlyForcedSubtitles; + break; + case 8: // AVMediaCharacteristicTranscribesSpokenDialogForAccessibility + ptr = AVMediaCharacteristicTranscribesSpokenDialogForAccessibility; + break; + case 9: // AVMediaCharacteristicDescribesMusicAndSoundForAccessibility + ptr = AVMediaCharacteristicDescribesMusicAndSoundForAccessibility; + break; + case 10: // AVMediaCharacteristicDescribesVideoForAccessibility + ptr = AVMediaCharacteristicDescribesVideoForAccessibility; + break; + case 11: // AVMediaCharacteristicLanguageTranslation + ptr = AVMediaCharacteristicLanguageTranslation; + break; + case 12: // AVMediaCharacteristicDubbedTranslation + ptr = AVMediaCharacteristicDubbedTranslation; + break; + case 13: // AVMediaCharacteristicVoiceOverTranslation + ptr = AVMediaCharacteristicVoiceOverTranslation; + break; + case 14: // AVMediaCharacteristicIsOriginalContent + ptr = AVMediaCharacteristicIsOriginalContent; + break; + case 15: // AVMediaCharacteristicContainsHDRVideo + ptr = AVMediaCharacteristicContainsHDRVideo; + break; + case 16: // AVMediaCharacteristicContainsAlphaChannel + ptr = AVMediaCharacteristicContainsAlphaChannel; + break; + } + return (NSString?) Runtime.GetNSObject (ptr); + } + + public static AVMediaCharacteristics GetValue (NSString constant) + { + if (constant is null) + throw new ArgumentNullException (nameof (constant)); + if (constant.IsEqualTo (AVMediaCharacteristicVisual)) + return AVMediaCharacteristics.Visual; + if (constant.IsEqualTo (AVMediaCharacteristicAudible)) + return AVMediaCharacteristics.Audible; + if (constant.IsEqualTo (AVMediaCharacteristicLegible)) + return AVMediaCharacteristics.Legible; + if (constant.IsEqualTo (AVMediaCharacteristicFrameBased)) + return AVMediaCharacteristics.FrameBased; + if (constant.IsEqualTo (AVMediaCharacteristicUsesWideGamutColorSpace)) + return AVMediaCharacteristics.UsesWideGamutColorSpace; + if (constant.IsEqualTo (AVMediaCharacteristicIsMainProgramContent)) + return AVMediaCharacteristics.IsMainProgramContent; + if (constant.IsEqualTo (AVMediaCharacteristicIsAuxiliaryContent)) + return AVMediaCharacteristics.IsAuxiliaryContent; + if (constant.IsEqualTo (AVMediaCharacteristicContainsOnlyForcedSubtitles)) + return AVMediaCharacteristics.ContainsOnlyForcedSubtitles; + if (constant.IsEqualTo (AVMediaCharacteristicTranscribesSpokenDialogForAccessibility)) + return AVMediaCharacteristics.TranscribesSpokenDialogForAccessibility; + if (constant.IsEqualTo (AVMediaCharacteristicDescribesMusicAndSoundForAccessibility)) + return AVMediaCharacteristics.DescribesMusicAndSoundForAccessibility; + if (constant.IsEqualTo (AVMediaCharacteristicDescribesVideoForAccessibility)) + return AVMediaCharacteristics.DescribesVideoForAccessibility; + if (constant.IsEqualTo (AVMediaCharacteristicLanguageTranslation)) + return AVMediaCharacteristics.LanguageTranslation; + if (constant.IsEqualTo (AVMediaCharacteristicDubbedTranslation)) + return AVMediaCharacteristics.DubbedTranslation; + if (constant.IsEqualTo (AVMediaCharacteristicVoiceOverTranslation)) + return AVMediaCharacteristics.VoiceOverTranslation; + if (constant.IsEqualTo (AVMediaCharacteristicIsOriginalContent)) + return AVMediaCharacteristics.IsOriginalContent; + if (constant.IsEqualTo (AVMediaCharacteristicContainsHDRVideo)) + return AVMediaCharacteristics.ContainsHdrVideo; + if (constant.IsEqualTo (AVMediaCharacteristicContainsAlphaChannel)) + return AVMediaCharacteristics.ContainsAlphaChannel; + throw new NotSupportedException ($"{constant} has no associated enum value on this platform."); + } + + internal static NSString?[]? ToConstantArray (this AVMediaCharacteristics[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (value.GetConstant ()); + } + return rv.ToArray (); + } + + internal static AVMediaCharacteristics[]? ToEnumArray (this NSString[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (GetValue (value)); + } + return rv.ToArray (); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectediOSAVMediaCharacteristics.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectediOSAVMediaCharacteristics.cs new file mode 100644 index 00000000000..0d121407e33 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/Data/ExpectediOSAVMediaCharacteristics.cs @@ -0,0 +1,328 @@ +// + +#nullable enable + +using Foundation; +using ObjCBindings; +using ObjCRuntime; +using System; +using System.Runtime.Versioning; + +namespace AVFoundation; + +[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)] +static public partial class AVMediaCharacteristicsExtensions +{ + + static IntPtr[] values = new IntPtr [18]; + + [Field ("AVMediaCharacteristicVisual", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicVisual + { + get + { + fixed (IntPtr *storage = &values [0]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicVisual", storage); + } + } + + [Field ("AVMediaCharacteristicAudible", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicAudible + { + get + { + fixed (IntPtr *storage = &values [1]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicAudible", storage); + } + } + + [Field ("AVMediaCharacteristicLegible", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicLegible + { + get + { + fixed (IntPtr *storage = &values [2]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicLegible", storage); + } + } + + [Field ("AVMediaCharacteristicFrameBased", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicFrameBased + { + get + { + fixed (IntPtr *storage = &values [3]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicFrameBased", storage); + } + } + + [Field ("AVMediaCharacteristicUsesWideGamutColorSpace", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicUsesWideGamutColorSpace + { + get + { + fixed (IntPtr *storage = &values [4]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicUsesWideGamutColorSpace", storage); + } + } + + [Field ("AVMediaCharacteristicIsMainProgramContent", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicIsMainProgramContent + { + get + { + fixed (IntPtr *storage = &values [5]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicIsMainProgramContent", storage); + } + } + + [Field ("AVMediaCharacteristicIsAuxiliaryContent", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicIsAuxiliaryContent + { + get + { + fixed (IntPtr *storage = &values [6]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicIsAuxiliaryContent", storage); + } + } + + [Field ("AVMediaCharacteristicContainsOnlyForcedSubtitles", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicContainsOnlyForcedSubtitles + { + get + { + fixed (IntPtr *storage = &values [7]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicContainsOnlyForcedSubtitles", storage); + } + } + + [Field ("AVMediaCharacteristicTranscribesSpokenDialogForAccessibility", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicTranscribesSpokenDialogForAccessibility + { + get + { + fixed (IntPtr *storage = &values [8]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicTranscribesSpokenDialogForAccessibility", storage); + } + } + + [Field ("AVMediaCharacteristicDescribesMusicAndSoundForAccessibility", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicDescribesMusicAndSoundForAccessibility + { + get + { + fixed (IntPtr *storage = &values [9]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicDescribesMusicAndSoundForAccessibility", storage); + } + } + + [Field ("AVMediaCharacteristicDescribesVideoForAccessibility", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicDescribesVideoForAccessibility + { + get + { + fixed (IntPtr *storage = &values [10]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicDescribesVideoForAccessibility", storage); + } + } + + [Field ("AVMediaCharacteristicEasyToRead", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicEasyToRead + { + get + { + fixed (IntPtr *storage = &values [11]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicEasyToRead", storage); + } + } + + [Field ("AVMediaCharacteristicLanguageTranslation", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicLanguageTranslation + { + get + { + fixed (IntPtr *storage = &values [12]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicLanguageTranslation", storage); + } + } + + [Field ("AVMediaCharacteristicDubbedTranslation", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicDubbedTranslation + { + get + { + fixed (IntPtr *storage = &values [13]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicDubbedTranslation", storage); + } + } + + [Field ("AVMediaCharacteristicVoiceOverTranslation", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicVoiceOverTranslation + { + get + { + fixed (IntPtr *storage = &values [14]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicVoiceOverTranslation", storage); + } + } + + [Field ("AVMediaCharacteristicIsOriginalContent", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicIsOriginalContent + { + get + { + fixed (IntPtr *storage = &values [15]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicIsOriginalContent", storage); + } + } + + [Field ("AVMediaCharacteristicContainsHDRVideo", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicContainsHDRVideo + { + get + { + fixed (IntPtr *storage = &values [16]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicContainsHDRVideo", storage); + } + } + + [Field ("AVMediaCharacteristicContainsAlphaChannel", "AVFoundation")] + internal unsafe static IntPtr AVMediaCharacteristicContainsAlphaChannel + { + get + { + fixed (IntPtr *storage = &values [17]) + return Dlfcn.CachePointer (Libraries.AVFoundation.Handle, "AVMediaCharacteristicContainsAlphaChannel", storage); + } + } + + public static NSString? GetConstant (this AVMediaCharacteristics self) + { + IntPtr ptr = IntPtr.Zero; + switch ((int) self) + { + case 0: // AVMediaCharacteristicVisual + ptr = AVMediaCharacteristicVisual; + break; + case 1: // AVMediaCharacteristicAudible + ptr = AVMediaCharacteristicAudible; + break; + case 2: // AVMediaCharacteristicLegible + ptr = AVMediaCharacteristicLegible; + break; + case 3: // AVMediaCharacteristicFrameBased + ptr = AVMediaCharacteristicFrameBased; + break; + case 4: // AVMediaCharacteristicUsesWideGamutColorSpace + ptr = AVMediaCharacteristicUsesWideGamutColorSpace; + break; + case 5: // AVMediaCharacteristicIsMainProgramContent + ptr = AVMediaCharacteristicIsMainProgramContent; + break; + case 6: // AVMediaCharacteristicIsAuxiliaryContent + ptr = AVMediaCharacteristicIsAuxiliaryContent; + break; + case 7: // AVMediaCharacteristicContainsOnlyForcedSubtitles + ptr = AVMediaCharacteristicContainsOnlyForcedSubtitles; + break; + case 8: // AVMediaCharacteristicTranscribesSpokenDialogForAccessibility + ptr = AVMediaCharacteristicTranscribesSpokenDialogForAccessibility; + break; + case 9: // AVMediaCharacteristicDescribesMusicAndSoundForAccessibility + ptr = AVMediaCharacteristicDescribesMusicAndSoundForAccessibility; + break; + case 10: // AVMediaCharacteristicDescribesVideoForAccessibility + ptr = AVMediaCharacteristicDescribesVideoForAccessibility; + break; + case 11: // AVMediaCharacteristicEasyToRead + ptr = AVMediaCharacteristicEasyToRead; + break; + case 12: // AVMediaCharacteristicLanguageTranslation + ptr = AVMediaCharacteristicLanguageTranslation; + break; + case 13: // AVMediaCharacteristicDubbedTranslation + ptr = AVMediaCharacteristicDubbedTranslation; + break; + case 14: // AVMediaCharacteristicVoiceOverTranslation + ptr = AVMediaCharacteristicVoiceOverTranslation; + break; + case 15: // AVMediaCharacteristicIsOriginalContent + ptr = AVMediaCharacteristicIsOriginalContent; + break; + case 16: // AVMediaCharacteristicContainsHDRVideo + ptr = AVMediaCharacteristicContainsHDRVideo; + break; + case 17: // AVMediaCharacteristicContainsAlphaChannel + ptr = AVMediaCharacteristicContainsAlphaChannel; + break; + } + return (NSString?) Runtime.GetNSObject (ptr); + } + + public static AVMediaCharacteristics GetValue (NSString constant) + { + if (constant is null) + throw new ArgumentNullException (nameof (constant)); + if (constant.IsEqualTo (AVMediaCharacteristicVisual)) + return AVMediaCharacteristics.Visual; + if (constant.IsEqualTo (AVMediaCharacteristicAudible)) + return AVMediaCharacteristics.Audible; + if (constant.IsEqualTo (AVMediaCharacteristicLegible)) + return AVMediaCharacteristics.Legible; + if (constant.IsEqualTo (AVMediaCharacteristicFrameBased)) + return AVMediaCharacteristics.FrameBased; + if (constant.IsEqualTo (AVMediaCharacteristicUsesWideGamutColorSpace)) + return AVMediaCharacteristics.UsesWideGamutColorSpace; + if (constant.IsEqualTo (AVMediaCharacteristicIsMainProgramContent)) + return AVMediaCharacteristics.IsMainProgramContent; + if (constant.IsEqualTo (AVMediaCharacteristicIsAuxiliaryContent)) + return AVMediaCharacteristics.IsAuxiliaryContent; + if (constant.IsEqualTo (AVMediaCharacteristicContainsOnlyForcedSubtitles)) + return AVMediaCharacteristics.ContainsOnlyForcedSubtitles; + if (constant.IsEqualTo (AVMediaCharacteristicTranscribesSpokenDialogForAccessibility)) + return AVMediaCharacteristics.TranscribesSpokenDialogForAccessibility; + if (constant.IsEqualTo (AVMediaCharacteristicDescribesMusicAndSoundForAccessibility)) + return AVMediaCharacteristics.DescribesMusicAndSoundForAccessibility; + if (constant.IsEqualTo (AVMediaCharacteristicDescribesVideoForAccessibility)) + return AVMediaCharacteristics.DescribesVideoForAccessibility; + if (constant.IsEqualTo (AVMediaCharacteristicEasyToRead)) + return AVMediaCharacteristics.EasyToRead; + if (constant.IsEqualTo (AVMediaCharacteristicLanguageTranslation)) + return AVMediaCharacteristics.LanguageTranslation; + if (constant.IsEqualTo (AVMediaCharacteristicDubbedTranslation)) + return AVMediaCharacteristics.DubbedTranslation; + if (constant.IsEqualTo (AVMediaCharacteristicVoiceOverTranslation)) + return AVMediaCharacteristics.VoiceOverTranslation; + if (constant.IsEqualTo (AVMediaCharacteristicIsOriginalContent)) + return AVMediaCharacteristics.IsOriginalContent; + if (constant.IsEqualTo (AVMediaCharacteristicContainsHDRVideo)) + return AVMediaCharacteristics.ContainsHdrVideo; + if (constant.IsEqualTo (AVMediaCharacteristicContainsAlphaChannel)) + return AVMediaCharacteristics.ContainsAlphaChannel; + throw new NotSupportedException ($"{constant} has no associated enum value on this platform."); + } + + internal static NSString?[]? ToConstantArray (this AVMediaCharacteristics[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (value.GetConstant ()); + } + return rv.ToArray (); + } + + internal static AVMediaCharacteristics[]? ToEnumArray (this NSString[]? values) + { + if (values is null) + return null; + var rv = new global::System.Collections.Generic.List (); + for (var i = 0; i < values.Length; i++) { + var value = values [i]; + rv.Add (GetValue (value)); + } + return rv.ToArray (); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/SmartEnumTests.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/SmartEnumTests.cs new file mode 100644 index 00000000000..0bf42801392 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/SmartEnum/SmartEnumTests.cs @@ -0,0 +1,94 @@ +using System.Collections; +using System.Collections.Generic; +using Xamarin.Tests; +using Xunit; +using Xamarin.Utils; + +namespace Microsoft.Macios.Generator.Tests.SmartEnum; + + +/// +/// Test all the field generation code. +/// +public class SmartEnumTests : BaseGeneratorTestClass +{ + public class TestDataGenerator : BaseTestDataGenerator, IEnumerable + { + readonly List<(ApplePlatform Platform, string ClassName, string BindingFile, string OutputFile, string? LibraryText)> _data = new() + { + (ApplePlatform.iOS, "AVCaptureDeviceTypeExtensions", "AVCaptureDeviceTypeEnum.cs", "ExpectedAVCaptureDeviceTypeEnum.cs", null), + (ApplePlatform.iOS, "AVCaptureSystemPressureLevelExtensions", "AVCaptureSystemPressureLevel.cs", "ExpectedAVCaptureSystemPressureLevel.cs", null), + (ApplePlatform.iOS, "AVMediaCharacteristicsExtensions", "AVMediaCharacteristics.cs", "ExpectediOSAVMediaCharacteristics.cs", null), + (ApplePlatform.MacOSX, "AVMediaCharacteristicsExtensions", "AVMediaCharacteristics.cs", "ExpectedMacOSAVMediaCharacteristics.cs", null), + (ApplePlatform.MacOSX, "CustomLibraryEnumExtensions", "CustomLibraryEnum.cs", "ExpectedCustomLibraryEnum.cs", "ExpectedCustomLibraryEnumLibrariesClass.cs"), + }; + + public IEnumerator GetEnumerator() + { + foreach (var testData in _data) + { + var libraryText = string.IsNullOrEmpty(testData.LibraryText) ? + null : ReadFileAsString(testData.LibraryText); + yield return [ + testData.Platform, + testData.ClassName, + testData.BindingFile, + ReadFileAsString(testData.BindingFile), + testData.OutputFile, + ReadFileAsString(testData.OutputFile), + libraryText!, + ]; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [Theory] + [ClassData(typeof(TestDataGenerator))] + public void ExtensionGenerationTests (ApplePlatform platform, string className, string inputFileName, string inputText, string outputFileName, string expectedOutputText, string? expectedLibraryText) + => CompareGeneratedCode(platform, className, inputFileName, inputText, outputFileName, expectedOutputText, expectedLibraryText); + + [Theory] + [PlatformInlineData(ApplePlatform.iOS)] + [PlatformInlineData(ApplePlatform.TVOS)] + [PlatformInlineData(ApplePlatform.MacOSX)] + [PlatformInlineData(ApplePlatform.MacCatalyst)] + public void DuplicatedSymbolsDoNotGenerateCode (ApplePlatform platform) + { + const string inputText = @" +using Foundation; +using ObjCRuntime; +using ObjCBindings; + +namespace AVFoundation; + +[BindingType] +public enum AVCaptureSystemPressureExampleLevel { + [Field (""AVCaptureSystemPressureLevelNominal"")] + Nominal, + + // duplicated, this should be an error + [Field (""AVCaptureSystemPressureLevelNominal"")] + Fair, + + [Field (""AVCaptureSystemPressureLevelSerious"")] + Serious, + + [Field (""AVCaptureSystemPressureLevelCritical"")] + Critical, + + [Field (""AVCaptureSystemPressureLevelShutdown"")] + Shutdown, +}"; + + // We need to create a compilation with the required source code. + var compilation = CreateCompilation (nameof (CompareGeneratedCode), platform, inputText); + + // Run generators and retrieve all results. + var runResult = RunGenerators (compilation); + + // bad formed bindings should not generate code + Assert.Empty (runResult.GeneratedTrees); + } +} diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/TabbedStringBuilderTests.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/TabbedStringBuilderTests.cs index 0333e5b138d..489dfb5917b 100644 --- a/tests/rgen/Microsoft.Macios.Generator.Tests/TabbedStringBuilderTests.cs +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/TabbedStringBuilderTests.cs @@ -206,4 +206,23 @@ public void CreateBlockTest (uint tabCount, string expectedTabs, string blockTyp Assert.Equal (expected, result); } + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void WriteHeaderTest (uint tabCount, string expectedTabs) + { + var expected = $@"{expectedTabs}// + +{expectedTabs}#nullable enable + +"; + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + block.WriteHeader (); + result = block.ToString (); + } + Assert.Equal (expected, result); + } + }