diff --git a/src/Makefile.generator b/src/Makefile.generator index a88c56ec1d50..50f3353b9d12 100644 --- a/src/Makefile.generator +++ b/src/Makefile.generator @@ -70,6 +70,6 @@ $(RSP_DIR)/dotnet/%-defines-dotnet.rsp: frameworks.sources Makefile.generator ge $(Q) ./generate-defines.csharp $@.tmp '$(filter-out $(DOTNET_REMOVED_$(shell echo $* | tr a-z A-Z)_FRAMEWORKS),$($(shell echo $* | tr a-z A-Z)_FRAMEWORKS))' $(Q) mv $@.tmp $@ -$(DOTNET_BUILD_DIR)/Xamarin.Apple.BindingAttributes.dll: bgen/Attributes.cs Makefile.generator | $(DOTNET_BUILD_DIR) - $(Q_DOTNET_BUILD) $(SYSTEM_CSC) $(DOTNET_FLAGS) -out:$@ $< +$(DOTNET_BUILD_DIR)/Xamarin.Apple.BindingAttributes.dll: bgen/Attributes.cs bgen/PlatformName.cs Makefile.generator | $(DOTNET_BUILD_DIR) + $(Q_DOTNET_BUILD) $(SYSTEM_CSC) $(DOTNET_FLAGS) -out:$@ bgen/Attributes.cs bgen/PlatformName.cs diff --git a/src/Makefile.rgenerator b/src/Makefile.rgenerator index e3bd9f2b997a..61bbed1d2067 100644 --- a/src/Makefile.rgenerator +++ b/src/Makefile.rgenerator @@ -1,6 +1,7 @@ # Roslyn code generator ROSLYN_GENERATOR=$(DOTNET_BUILD_DIR)/common/rgen/Microsoft.Macios.Generator.dll ROSLYN_GENERATOR_FILES := $(wildcard rgen/Microsoft.Macios.Generator/*.cs) +ROSLYN_GENERATOR_FILES += $(wildcard rgen/Microsoft.Macios.Generator/*/*.cs) $(ROSLYN_GENERATOR): Makefile.rgenerator $(ROSLYN_GENERATOR_FILES) $(Q_DOTNET_BUILD) $(DOTNET) publish rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj $(DOTNET_BUILD_VERBOSITY) /p:Configuration=Debug /p:IntermediateOutputPath=$(abspath $(DOTNET_BUILD_DIR)/IDE/obj/common/rgen)/ /p:OutputPath=$(abspath $(DOTNET_BUILD_DIR)/IDE/bin/common/rgen/)/ diff --git a/src/bgen/Attributes.cs b/src/bgen/Attributes.cs index 0096d35b1d5f..25ad9f46cd6b 100644 --- a/src/bgen/Attributes.cs +++ b/src/bgen/Attributes.cs @@ -883,15 +883,6 @@ public class NoMethodAttribute : Attribute { } #if NET -public enum PlatformName : byte { - None, - MacOSX, - iOS, - WatchOS, - TvOS, - MacCatalyst, -} - public enum AvailabilityKind { Introduced, Deprecated, diff --git a/src/bgen/Extensions/PlatformNameExtensions.cs b/src/bgen/Extensions/PlatformNameExtensions.cs index 982e5eb1bd8f..ea0b3914cac9 100644 --- a/src/bgen/Extensions/PlatformNameExtensions.cs +++ b/src/bgen/Extensions/PlatformNameExtensions.cs @@ -1,132 +1,74 @@ -using System; -using System.IO; -using ObjCRuntime; -using Xamarin.Utils; +using System.Diagnostics.CodeAnalysis; public static class PlatformNameExtensions { - public static string GetApplicationClassName (this PlatformName currentPlatform) + public static bool TryGetApplicationClassName (this PlatformName currentPlatform, [NotNullWhen (true)] out string? className) { switch (currentPlatform) { case PlatformName.iOS: case PlatformName.WatchOS: case PlatformName.TvOS: case PlatformName.MacCatalyst: - return "UIApplication"; + className = "UIApplication"; + return true; case PlatformName.MacOSX: - return "NSApplication"; + className = "NSApplication"; + return true; default: - throw new BindingException (1047, currentPlatform); + className = null; + return false; } } - public static int GetXamcoreVersion (this PlatformName currentPlatform) - { -#if NET - return 4; -#else - switch (currentPlatform) { - case PlatformName.MacOSX: - case PlatformName.iOS: - return 2; - case PlatformName.TvOS: - case PlatformName.WatchOS: - return 3; - default: - return 4; - } -#endif - } - - public static string GetCoreImageMap (this PlatformName currentPlatform) + public static bool TryGetCoreImageMap (this PlatformName currentPlatform, [NotNullWhen (true)] out string? coreImageMap) { switch (currentPlatform) { case PlatformName.iOS: case PlatformName.WatchOS: case PlatformName.TvOS: case PlatformName.MacCatalyst: - return "CoreImage"; + coreImageMap = "CoreImage"; + return true; case PlatformName.MacOSX: - return "Quartz"; + coreImageMap = "Quartz"; + return true; default: - throw new BindingException (1047, currentPlatform); + coreImageMap = null; + return false; } } - public static string GetCoreServicesMap (this PlatformName currentPlatform) + public static bool TryGetCoreServicesMap (this PlatformName currentPlatform, [NotNullWhen (true)] out string? coreServicesMap) { switch (currentPlatform) { case PlatformName.iOS: case PlatformName.WatchOS: case PlatformName.TvOS: case PlatformName.MacCatalyst: - return "MobileCoreServices"; - case PlatformName.MacOSX: - return "CoreServices"; - default: - throw new BindingException (1047, currentPlatform); - } - } - - public static string GetPDFKitMap (this PlatformName currentPlatform) - { - switch (currentPlatform) { - case PlatformName.iOS: - case PlatformName.MacCatalyst: - return "PDFKit"; - case PlatformName.MacOSX: - return "Quartz"; - default: - throw new BindingException (1047, currentPlatform); - } - } - - public static ApplePlatform AsApplePlatform (this PlatformName platform) - { - switch (platform) { - case PlatformName.iOS: - return ApplePlatform.iOS; - case PlatformName.TvOS: - return ApplePlatform.TVOS; - case PlatformName.MacCatalyst: - return ApplePlatform.MacCatalyst; + coreServicesMap = "MobileCoreServices"; + return true; case PlatformName.MacOSX: - return ApplePlatform.MacOSX; - case PlatformName.WatchOS: - return ApplePlatform.WatchOS; - case PlatformName.None: - return ApplePlatform.None; + coreServicesMap = "CoreServices"; + return true; default: - throw new ArgumentOutOfRangeException (nameof (platform), platform, $"Unknown platform: {platform}"); + coreServicesMap = null; + return false; } } - static string GetSdkRoot (this PlatformName currentPlatform) + public static bool TryGetPDFKitMap (this PlatformName currentPlatform, [NotNullWhen (true)] out string? pdfKitMap) { switch (currentPlatform) { case PlatformName.iOS: - case PlatformName.WatchOS: - case PlatformName.TvOS: case PlatformName.MacCatalyst: - var sdkRoot = Environment.GetEnvironmentVariable ("MD_MTOUCH_SDK_ROOT"); - if (string.IsNullOrEmpty (sdkRoot)) - sdkRoot = "/Library/Frameworks/Xamarin.iOS.framework/Versions/Current"; - return sdkRoot; + pdfKitMap = "PDFKit"; + return true; case PlatformName.MacOSX: - var macSdkRoot = Environment.GetEnvironmentVariable ("XamarinMacFrameworkRoot"); - if (string.IsNullOrEmpty (macSdkRoot)) - macSdkRoot = "/Library/Frameworks/Xamarin.Mac.framework/Versions/Current"; - return macSdkRoot; + pdfKitMap = "Quartz"; + return true; default: - throw new BindingException (1047, currentPlatform); + pdfKitMap = null; + return false; } } - - public static string GetPath (this PlatformName currentPlatform, params string [] paths) - { - var fullPaths = new string [paths.Length + 1]; - fullPaths [0] = currentPlatform.GetSdkRoot (); - Array.Copy (paths, 0, fullPaths, 1, paths.Length); - return Path.Combine (fullPaths); - } } diff --git a/src/bgen/Extensions/PlatformNameExtensionsBgen.cs b/src/bgen/Extensions/PlatformNameExtensionsBgen.cs new file mode 100644 index 000000000000..45e1a29ba443 --- /dev/null +++ b/src/bgen/Extensions/PlatformNameExtensionsBgen.cs @@ -0,0 +1,102 @@ +using System; +using System.IO; +using Xamarin.Utils; + +public static class PlatformNameExtensionsBgen { + + // wrapper that allows us to use the same code for rgen and bgen + public static string GetApplicationClassName (this PlatformName currentPlatform) + { + if (currentPlatform.TryGetApplicationClassName (out var applicationClassName)) + return applicationClassName; + throw new BindingException (1047, currentPlatform); + } + + public static string GetCoreImageMap (this PlatformName currentPlatform) + { + if (currentPlatform.TryGetCoreImageMap (out var coreImageMap)) + return coreImageMap; + throw new BindingException (1047, currentPlatform); + } + + public static string GetCoreServicesMap (this PlatformName currentPlatform) + { + if (currentPlatform.TryGetCoreServicesMap (out var coreServicesMap)) + return coreServicesMap; + throw new BindingException (1047, currentPlatform); + } + + public static string GetPDFKitMap (this PlatformName currentPlatform) + { + if (currentPlatform.TryGetPDFKitMap (out var pdfKitMap)) + return pdfKitMap; + throw new BindingException (1047, currentPlatform); + } + + public static int GetXamcoreVersion (this PlatformName currentPlatform) + { +#if NET + return 4; +#else + switch (currentPlatform) { + case PlatformName.MacOSX: + case PlatformName.iOS: + return 2; + case PlatformName.TvOS: + case PlatformName.WatchOS: + return 3; + default: + return 4; + } +#endif + } + + public static ApplePlatform AsApplePlatform (this PlatformName platform) + { + switch (platform) { + case PlatformName.iOS: + return ApplePlatform.iOS; + case PlatformName.TvOS: + return ApplePlatform.TVOS; + case PlatformName.MacCatalyst: + return ApplePlatform.MacCatalyst; + case PlatformName.MacOSX: + return ApplePlatform.MacOSX; + case PlatformName.WatchOS: + return ApplePlatform.WatchOS; + case PlatformName.None: + return ApplePlatform.None; + default: + throw new ArgumentOutOfRangeException (nameof (platform), platform, $"Unknown platform: {platform}"); + } + } + + static string GetSdkRoot (this PlatformName currentPlatform) + { + switch (currentPlatform) { + case PlatformName.iOS: + case PlatformName.WatchOS: + case PlatformName.TvOS: + case PlatformName.MacCatalyst: + var sdkRoot = Environment.GetEnvironmentVariable ("MD_MTOUCH_SDK_ROOT"); + if (string.IsNullOrEmpty (sdkRoot)) + sdkRoot = "/Library/Frameworks/Xamarin.iOS.framework/Versions/Current"; + return sdkRoot; + case PlatformName.MacOSX: + var macSdkRoot = Environment.GetEnvironmentVariable ("XamarinMacFrameworkRoot"); + if (string.IsNullOrEmpty (macSdkRoot)) + macSdkRoot = "/Library/Frameworks/Xamarin.Mac.framework/Versions/Current"; + return macSdkRoot; + default: + throw new BindingException (1047, currentPlatform); + } + } + + public static string GetPath (this PlatformName currentPlatform, params string [] paths) + { + var fullPaths = new string [paths.Length + 1]; + fullPaths [0] = currentPlatform.GetSdkRoot (); + Array.Copy (paths, 0, fullPaths, 1, paths.Length); + return Path.Combine (fullPaths); + } +} diff --git a/src/bgen/PlatformName.cs b/src/bgen/PlatformName.cs new file mode 100644 index 000000000000..c17caa5d6bf4 --- /dev/null +++ b/src/bgen/PlatformName.cs @@ -0,0 +1,8 @@ +public enum PlatformName : byte { + None, + MacOSX, + iOS, + WatchOS, + TvOS, + MacCatalyst, +} diff --git a/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs b/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs index fe4905a77980..16717f554311 100644 --- a/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs +++ b/src/rgen/Microsoft.Macios.Bindings.Analyzer.Sample/Examples.cs @@ -1,3 +1,5 @@ +using ObjCBindings; + namespace Microsoft.Macios.Bindings.Analyzer.Sample; // If you don't see warnings, build the Analyzers Project. 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 bc9520bfbacf..a02eefcf4af5 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 @@ -3,6 +3,7 @@ net$(BundledNETCoreAppTargetFrameworkVersion) enable + APL0003 @@ -11,9 +12,15 @@ + + external\BindginTypeAttribute.cs + external\Attributes.cs + + external\PlatformName.cs + 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 f21144e77c4c..e10b90a1b8fd 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 @@ -4,6 +4,7 @@ net$(BundledNETCoreAppTargetFrameworkVersion) enable Microsoft.Macios.Generator.Sample + APL0003 @@ -11,9 +12,15 @@ + + external\BindingTypeAttribute.cs + external\Attributes.cs + + external\PlatformName.cs + diff --git a/src/rgen/Microsoft.Macios.Generator.Sample/SampleBinding.cs b/src/rgen/Microsoft.Macios.Generator.Sample/SampleBinding.cs index 28d28329ae21..7c2427c1473e 100644 --- a/src/rgen/Microsoft.Macios.Generator.Sample/SampleBinding.cs +++ b/src/rgen/Microsoft.Macios.Generator.Sample/SampleBinding.cs @@ -1,3 +1,5 @@ +using ObjCBindings; + namespace Microsoft.Macios.Generator.Sample; // This code will not compile until you build the project with the Source Generators diff --git a/src/rgen/Microsoft.Macios.Generator/AttributesNames.cs b/src/rgen/Microsoft.Macios.Generator/AttributesNames.cs new file mode 100644 index 000000000000..62ac3b632b01 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/AttributesNames.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Macios.Generator; + +/// +/// Contains all the names of the attributes that are used by the binding generator. +/// +public static class AttributesNames { + + public static readonly string BindingAttribute = "ObjCBindings.BindingTypeAttribute"; +} diff --git a/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs b/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs index fcc630f160ce..8b7d54e8533f 100644 --- a/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs +++ b/src/rgen/Microsoft.Macios.Generator/BindingSourceGeneratorGenerator.cs @@ -1,16 +1,26 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using Microsoft.Macios.Generator.Context; +using Microsoft.Macios.Generator.Emitters; namespace Microsoft.Macios.Generator; /// -/// A sample source generator that creates a custom report based on class properties. The target class should be annotated with the 'Generators.ReportAttribute' attribute. -/// When using the source code as a baseline, an incremental source generator is preferable because it reduces the performance overhead. +/// A sample source generator that creates a custom report based on class properties. The target class should be +/// annotated with the 'Generators.ReportAttribute' attribute. +/// When using the source code as a baseline, an incremental source generator is preferable because it reduces +/// the performance overhead. /// [Generator] public class BindingSourceGeneratorGenerator : IIncrementalGenerator { + /// public void Initialize (IncrementalGeneratorInitializationContext context) { // Add the binding generator attributes to the compilation. This are only available when the @@ -19,6 +29,140 @@ public void Initialize (IncrementalGeneratorInitializationContext context) context.RegisterPostInitializationOutput (ctx => ctx.AddSource ( fileName, SourceText.From (content, Encoding.UTF8))); } + + // binding can use different 'types'. To be able to generate the code we are going to add a different + // function for each of the 'types' we are interested in generating that will be added to the compiler + // pipeline + AddPipeline (context); + AddPipeline (context); + AddPipeline (context); + } + + /// + /// Generic method that adds a provider and a code generator to the pipeline. + /// + /// The compilation context + /// The base type declaration that we are going to generate. + static void AddPipeline (IncrementalGeneratorInitializationContext context) where T : BaseTypeDeclarationSyntax + { + var provider = context.SyntaxProvider + .CreateSyntaxProvider ( + static (s, _) => s is T, + (ctx, _) => GetDeclarationForSourceGen (ctx)) + .Where (t => t.BindingAttributeFound) + .Select ((t, _) => t.Declaration); + + context.RegisterSourceOutput (context.CompilationProvider.Combine (provider.Collect ()), + ((ctx, t) => GenerateCode (ctx, t.Left, t.Right))); + } + + /// + /// Generic method that can be used to filter/match a BaseTypeDeclarationSyntax with the BindingTypeAttribute. + /// Because our generator is focused only on Enum, Interface and Class declarations we can use a generic method + /// that will match the type + the presence of the attribute. + /// + /// 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) + 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 + + string attributeName = attributeSymbol.ContainingType.ToDisplayString (); + + // Check the full name of the [Binding] attribute. + if (attributeName == AttributesNames.BindingAttribute) + return (classDeclarationSyntax, true); + } + + return (classDeclarationSyntax, false); + } + + /// + /// Collect the using statements from the class declaration root syntaxt tree and add them to the string builder + /// that will be used to generate the code. This way we ensure that we have all the namespaces needed by the + /// generated code. + /// + /// 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) + { + // 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 (); + 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) { + if (string.IsNullOrEmpty (ns)) + continue; + sb.AppendLine ($"using {ns};"); + } + } + + /// + /// Generic method that allows to call a emitter for ta type that will emit the binding code. All code generation + /// is very similar. Get create a tabbed string builder to write the code with the needed using statemens from + /// the original syntax tree and we pass it to the emitter that will generate the code. + /// + /// The generator context. + /// The compilation unit. + /// 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 + { + var rootContext = new RootBindingContext (compilation); + foreach (var baseTypeDeclarationSyntax in baseTypeDeclarations) { + var semanticModel = compilation.GetSemanticModel (baseTypeDeclarationSyntax.SyntaxTree); + if (semanticModel.GetDeclaredSymbol (baseTypeDeclarationSyntax) is not INamedTypeSymbol namedTypeSymbol) + 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); + + + // 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 (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)); + } 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/Context/ClassBindingContext.cs b/src/rgen/Microsoft.Macios.Generator/Context/ClassBindingContext.cs new file mode 100644 index 000000000000..8278c6437e6f --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Context/ClassBindingContext.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Macios.Generator.Context; + +class ClassBindingContext : SymbolBindingContext { + public string RegisterName { get; init; } + + public ClassBindingContext (RootBindingContext context, SemanticModel semanticModel, + INamedTypeSymbol symbol, ClassDeclarationSyntax declarationSyntax) + : base (context, semanticModel, symbol, declarationSyntax) + { + RegisterName = + symbol.Name; //TODO: placeholder -> should this be extracted from the BindingTypeAttribute + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Context/ContextFactory.cs b/src/rgen/Microsoft.Macios.Generator/Context/ContextFactory.cs new file mode 100644 index 000000000000..0805f661df89 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Context/ContextFactory.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Macios.Generator.Context; + +static class ContextFactory { + public static bool TryCreate (RootBindingContext context, SemanticModel semanticModel, + INamedTypeSymbol symbol, T declarationSyntax, [NotNullWhen (true)] out ISymbolBindingContext? bindingContext) where T : BaseTypeDeclarationSyntax + { + bindingContext = declarationSyntax switch { + ClassDeclarationSyntax c => Unsafe.As> ( + new ClassBindingContext (context, semanticModel, symbol, c)), + EnumDeclarationSyntax => new SymbolBindingContext (context, semanticModel, symbol, declarationSyntax), + _ => null + }; + return bindingContext is not null; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Context/ISymbolBindingContext.cs b/src/rgen/Microsoft.Macios.Generator/Context/ISymbolBindingContext.cs new file mode 100644 index 000000000000..a9bc1f3c242f --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Context/ISymbolBindingContext.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Macios.Generator.Context; + +/// +/// Interface that represents a symbol binding context. We use an interface to allow the usage of a coveriance type, +/// because it removes the need to a cast. +/// +/// The base type declaration whose context we have +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; } + bool IsStatic { get; } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Context/RootBindingContext.cs b/src/rgen/Microsoft.Macios.Generator/Context/RootBindingContext.cs new file mode 100644 index 000000000000..64159cb0657b --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Context/RootBindingContext.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Macios.Generator.Context; + +/// +/// Shared context through the entire code generation. This context allows to collect data that will be +/// later use to generate the Trampoline.g.cs file. once all classed are processed. +/// +/// The class also provides a number or properties that will allow to determine the platform we are binding and access +/// 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 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; + } + } + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Context/SymbolBindingContext.cs b/src/rgen/Microsoft.Macios.Generator/Context/SymbolBindingContext.cs new file mode 100644 index 000000000000..b3d0c24cfbb8 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Context/SymbolBindingContext.cs @@ -0,0 +1,36 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Macios.Generator.Context; + +class SymbolBindingContext { + + public RootBindingContext RootBindingContext { get; init; } + public SemanticModel SemanticModel { get; init; } + public INamedTypeSymbol Symbol { get; init; } + + public bool IsStatic => Symbol.IsStatic; + + public SymbolBindingContext (RootBindingContext rootBindingContext, + SemanticModel semanticModel, INamedTypeSymbol symbol) + { + RootBindingContext = rootBindingContext; + SemanticModel = semanticModel; + Symbol = symbol; + } + +} + +class SymbolBindingContext : SymbolBindingContext, ISymbolBindingContext where T : BaseTypeDeclarationSyntax { + + public T DeclarationSyntax { get; } + public string Namespace => Symbol.ContainingNamespace.ToDisplayString (); + public string SymbolName => Symbol.Name; + + public SymbolBindingContext (RootBindingContext rootBindingContext, + SemanticModel semanticModel, INamedTypeSymbol symbol, T declarationSyntax) + : base (rootBindingContext, semanticModel, symbol) + { + DeclarationSyntax = declarationSyntax; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/ClassEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/ClassEmitter.cs new file mode 100644 index 000000000000..0d56a01a1c10 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/ClassEmitter.cs @@ -0,0 +1,24 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.Macios.Generator.Context; + +namespace Microsoft.Macios.Generator.Emitters; + +class ClassEmitter (ClassBindingContext context, TabbedStringBuilder builder) : ICodeEmitter { + public string SymbolName => context.SymbolName; + + public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) + { + + builder.AppendLine (); + diagnostics = null; + // add the namespace and the class declaration + using (var namespaceBlock = builder.CreateBlock ($"namespace {context.Namespace}", true)) { + using (var classBlock = namespaceBlock.CreateBlock ($"public partial class {SymbolName}", true)) { + classBlock.AppendLine ("// TODO: add binding code here"); + } + } + return true; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/EmitterFactory.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/EmitterFactory.cs new file mode 100644 index 000000000000..7d9237ce35b5 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/EmitterFactory.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Macios.Generator.Context; + +namespace Microsoft.Macios.Generator.Emitters; + +/// +/// Returns the emitter that is related to the provided declaration type. +/// +static class EmitterFactory { + public static bool TryCreate (ISymbolBindingContext context, TabbedStringBuilder builder, + [NotNullWhen (true)] out ICodeEmitter? emitter) + where T : BaseTypeDeclarationSyntax + { + emitter = context switch { + ClassBindingContext classContext => new ClassEmitter (classContext, builder), + ISymbolBindingContext enumContext => new EnumEmitter (enumContext, builder), + ISymbolBindingContext interfaceContext => new InterfaceEmitter (interfaceContext, builder), + _ => null + }; + return emitter is not null; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/EnumEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/EnumEmitter.cs new file mode 100644 index 000000000000..b946986eac9f --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/EnumEmitter.cs @@ -0,0 +1,19 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Macios.Generator.Context; + +namespace Microsoft.Macios.Generator.Emitters; + +class EnumEmitter (ISymbolBindingContext context, TabbedStringBuilder builder) + : ICodeEmitter { + + public string SymbolName => $"{context.SymbolName}Extensions"; + + public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) + { + diagnostics = null; + return true; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/ICodeEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/ICodeEmitter.cs new file mode 100644 index 000000000000..d78584e82bb1 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/ICodeEmitter.cs @@ -0,0 +1,13 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Macios.Generator.Emitters; + +/// +/// Interface to be implemented by all those classes that know how to emit code for a binding. +/// +interface ICodeEmitter { + public string SymbolName { get; } + bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics); +} diff --git a/src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs b/src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs new file mode 100644 index 000000000000..23f77d0c4d76 --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/Emitters/InterfaceEmitter.cs @@ -0,0 +1,16 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Macios.Generator.Context; + +namespace Microsoft.Macios.Generator.Emitters; + +class InterfaceEmitter (ISymbolBindingContext context, TabbedStringBuilder builder) : ICodeEmitter { + public string SymbolName { get; } = string.Empty; + public bool TryEmit ([NotNullWhen (false)] out ImmutableArray? diagnostics) + { + diagnostics = null; + return true; + } +} diff --git a/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj b/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj index f9c73ed0396a..7da28f54684d 100644 --- a/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj +++ b/src/rgen/Microsoft.Macios.Generator/Microsoft.Macios.Generator.csproj @@ -22,5 +22,20 @@ + + + <_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 new file mode 100644 index 000000000000..530279b8644c --- /dev/null +++ b/src/rgen/Microsoft.Macios.Generator/TabbedStringBuilder.cs @@ -0,0 +1,228 @@ +using System; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Microsoft.Macios.Generator; + + +/// +/// String builder wrapper that keeps track of the current indentation level by abusing the IDisposable pattern. Rather +/// than dispose data, what the IDisposable pattern allows is to create a new block with an increased indentation, that +/// way we do not need to keep track of the current indentation level. +/// +/// var classBlock = new TabbedStringBuilder (sb); +/// classBlock.AppendLine ("public static NSString? GetConstant (this {enumSymbol.Name} self)"); +/// // open a new {} block, no need to keep track of the indentation, the new block has it +/// using (var getConstantBlock = classBlock.CreateBlock (isBlock: true)) { +/// // write the contents of the method here. +/// } +/// +/// +class TabbedStringBuilder : IDisposable { + readonly StringBuilder sb; + readonly uint tabCount; + readonly bool isBlock; + bool disposed; + + /// + /// Created a new tabbed string builder that will use the given sb to write code. + /// + /// The string builder to be used to write code. + /// The original tab size. + /// States if we are creating a {} block. + public TabbedStringBuilder (StringBuilder builder, uint currentCount = 0, bool block = false) + { + sb = builder; + isBlock = block; + if (isBlock) { + // increase by 1 because we are in a block + tabCount = currentCount; + WriteTabs ().Append ('{').AppendLine (); + tabCount++; + } else { + tabCount = currentCount; + } + } + + StringBuilder WriteTabs () => sb.Append ('\t', (int) tabCount); + + /// + /// Append a new empty line to the string builder using the correct tab size. + /// + /// The current tabbed string builder. + public TabbedStringBuilder AppendLine () + { + sb.AppendLine (); + return this; + } + + /// + /// Append conteng, but do not add a \n + /// + /// The content to append. + /// The current builder. + public TabbedStringBuilder Append (string line) + { + if (string.IsNullOrWhiteSpace (line)) { + sb.Append (line); + } else { + WriteTabs ().Append (line); + } + return this; + } + + /// + /// Append conteng, but do not add a \n + /// + /// The content to append. + /// The current builder. + public TabbedStringBuilder Append (ReadOnlySpan span) + { + if (span.IsWhiteSpace ()) { + sb.Append (span); + } else { + WriteTabs ().Append (span); + } + + return this; + } + + /// + /// Append a new tabbed line. + /// + /// The line to append. + /// The current builder. + public TabbedStringBuilder AppendLine (string line) + { + if (string.IsNullOrWhiteSpace (line)) { + sb.AppendLine (line); + } else { + WriteTabs ().AppendLine (line); + } + return this; + } + + /// + /// Append a new tabbed lien from the span. + /// + /// The line to append. + /// The current builder. + public TabbedStringBuilder AppendLine (ReadOnlySpan span) + { + if (span.IsWhiteSpace ()) { + sb.Append (span).AppendLine (); + } else { + WriteTabs ().Append (span).AppendLine (); + } + + return this; + } + + public TabbedStringBuilder Append (ref DefaultInterpolatedStringHandler handler) + { + WriteTabs ().Append (handler.ToStringAndClear ()); + return this; + } + + public TabbedStringBuilder AppendLine (ref DefaultInterpolatedStringHandler handler) + { + WriteTabs ().Append (handler.ToStringAndClear ()).AppendLine (); + return this; + } + + /// + /// Append a new raw literal by prepending the correct indentation. + /// + /// The raw string to append. + /// The current builder. + 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) { + if (count > 0) + AppendLine (); + var line = rawString.AsSpan (range); + Append (line); + count++; + } + return this; + } + + /// + /// Append the generated code attribute to the current string builder. Added for convenience. + /// + /// If the binding is Optimizable or not. + /// The current builder. + public TabbedStringBuilder AppendGeneratedCodeAttribute (bool optimizable = true) + { + if (optimizable) { + const string attr = "[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]"; + AppendLine (attr); + } else { + const string attr = "[BindingImpl (BindingImplOptions.GeneratedCode)]"; + AppendLine (attr); + } + + return this; + } + + /// + /// Append a EditorBrowsable attribute. Added for convenience. + /// + /// The current builder. + public TabbedStringBuilder AppendEditorBrowsableAttribute () + { + const string attr = "[EditorBrowsable (EditorBrowsableState.Never)]"; + AppendLine (attr); + return this; + } + + /// + /// Create a bew empty block. + /// + /// If it is a block that uses {} or not. + /// The new bloc. + public TabbedStringBuilder CreateBlock (bool block) => CreateBlock (string.Empty, block); + + /// + /// Create a new block with the given line. This method can be used to write if/else statements. + /// + /// The new line to append + /// If the new line should considered a block. + /// The current builder. + public TabbedStringBuilder CreateBlock (string line, bool block) + { + if (!string.IsNullOrEmpty (line)) { + WriteTabs ().AppendLine (line); + } + + return new TabbedStringBuilder (sb, tabCount, block); + } + + /// + /// Return the string builder as a string. + /// + /// + public override string ToString () + { + Dispose (); + return sb.ToString (); + } + + /// + /// Does not really dispose anything, it just closes the current block. + /// + public void Dispose () + { + if (disposed || !isBlock) return; + + disposed = true; + sb.Append ('\t', (int) tabCount - 1); + sb.Append ('}').AppendLine (); + } +} diff --git a/tests/generator/PlatformNameExtensionsTests.cs b/tests/generator/PlatformNameExtensionsTests.cs index e955c69ab010..6daef60a5042 100644 --- a/tests/generator/PlatformNameExtensionsTests.cs +++ b/tests/generator/PlatformNameExtensionsTests.cs @@ -17,7 +17,10 @@ public class PlatformNameExtensions { [TestCase (PlatformName.MacCatalyst, "UIApplication")] [TestCase (PlatformName.MacOSX, "NSApplication")] public void GetApplicationClassNameTest (PlatformName platformName, string expected) - => Assert.AreEqual (expected, platformName.GetApplicationClassName ()); + { + Assert.True (platformName.TryGetApplicationClassName (out var applicationClassName)); + Assert.AreEqual (expected, applicationClassName); + } [TestCase (PlatformName.MacOSX, 2)] [TestCase (PlatformName.iOS, 2)] diff --git a/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs index 48cba5be3fbd..67c715043299 100644 --- a/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs +++ b/tests/rgen/Microsoft.Macios.Bindings.Analyzer.Tests/BindingTypeSemanticAnalyzerTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Xamarin.Tests; @@ -27,10 +28,11 @@ public class Examples { var compilation = CreateCompilation (nameof (CompareGeneratedCode), platform, inputText); var diagnostics = await RunAnalyzer (new BindingTypeSemanticAnalyzer (), compilation); - Assert.Single (diagnostics); + var analyzerDiagnotics = diagnostics + .Where (d => d.Id == BindingTypeSemanticAnalyzer.DiagnosticId).ToArray (); + Assert.Single (analyzerDiagnotics); // verify the diagnostic message - var location = diagnostics [0].Location; - VerifyDiagnosticMessage (diagnostics [0], BindingTypeSemanticAnalyzer.DiagnosticId, + VerifyDiagnosticMessage (analyzerDiagnotics [0], BindingTypeSemanticAnalyzer.DiagnosticId, DiagnosticSeverity.Error, "The binding type 'Test.Examples' must declared as a partial class"); } } diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs index a101ccbbd214..fd8738e7d3e3 100644 --- a/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/BindingSourceGeneratorGeneratorTests.cs @@ -1,5 +1,4 @@ using System.Linq; -using Microsoft.CodeAnalysis.CSharp; using Xamarin.Tests; using Xamarin.Utils; using Xunit; @@ -9,36 +8,52 @@ namespace Microsoft.Macios.Generator.Tests; // Unit test that ensures that all the generator attributes are correctly added in the compilation initialization public class BindingSourceGeneratorGeneratorTests : BaseGeneratorTestClass { - const string SampleBindingType = @" + const string usingImportInput = @" +using System; +using Foundation; +using ObjCBindings; namespace TestNamespace; [BindingType (Name = ""AVAudioPCMBuffer"")] -interface AVAudioPcmBuffer : AVAudioBuffer { +public partial class AVAudioPcmBuffer : AVAudioBuffer { +} +"; + + const string usingImportOutput = @"// + +#nullable enable + +using System; +using Foundation; +using ObjCBindings; + +namespace TestNamespace +{ + public partial class AVAudioPcmBuffer + { + // TODO: add binding code here + } } "; [Theory] - [PlatformInlineData (ApplePlatform.iOS)] - [PlatformInlineData (ApplePlatform.TVOS)] - [PlatformInlineData (ApplePlatform.MacOSX)] - [PlatformInlineData (ApplePlatform.MacCatalyst)] - public void AttributesAreNotPresent (ApplePlatform platform) + [PlatformInlineData (ApplePlatform.iOS, usingImportInput, usingImportOutput)] + [PlatformInlineData (ApplePlatform.TVOS, usingImportInput, usingImportOutput)] + [PlatformInlineData (ApplePlatform.MacOSX, usingImportInput, usingImportOutput)] + [PlatformInlineData (ApplePlatform.MacCatalyst, usingImportInput, usingImportOutput)] + public void CorrectUsingImports (ApplePlatform platform, string input, string expectedOutput) { // We need to create a compilation with the required source code. - var compilation = CreateCompilation (nameof (AttributesAreNotPresent), - platform, SampleBindingType); + var compilation = CreateCompilation (nameof (CorrectUsingImports), + platform, usingImportInput); // Run generators and retrieve all results. var runResult = _driver.RunGenerators (compilation).GetRunResult (); + Assert.Empty (runResult.Diagnostics); // ensure that we do have all the needed attributes present - var expectedGeneratedAttributes = new [] { - "BindingTypeAttribute.g.cs", - }; - - foreach (string generatedAttribute in expectedGeneratedAttributes) { - var generatedFile = runResult.GeneratedTrees.SingleOrDefault (t => t.FilePath.EndsWith (generatedAttribute)); - Assert.Null (generatedFile); - } + var generatedFile = runResult.GeneratedTrees.SingleOrDefault (t => t.FilePath.EndsWith ("AVAudioPcmBuffer.g.cs")); + Assert.NotNull (generatedFile); + Assert.Equal (expectedOutput, generatedFile.GetText ().ToString ()); } } diff --git a/tests/rgen/Microsoft.Macios.Generator.Tests/TabbedStringBuilderTests.cs b/tests/rgen/Microsoft.Macios.Generator.Tests/TabbedStringBuilderTests.cs new file mode 100644 index 000000000000..0333e5b138d0 --- /dev/null +++ b/tests/rgen/Microsoft.Macios.Generator.Tests/TabbedStringBuilderTests.cs @@ -0,0 +1,209 @@ +using System.Text; +using Xunit; + +namespace Microsoft.Macios.Generator.Tests; + +public class TabbedStringBuilderTests { + StringBuilder sb; + + public TabbedStringBuilderTests () + { + sb = new (); + } + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void ConstructorNotBlockTest (uint tabCount, string expectedTabs) + { + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + block.AppendLine ("Test"); + result = block.ToString (); + } + Assert.Equal ($"{expectedTabs}Test\n", result); + } + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void ConstructorBlockTest (uint tabCount, string expectedTabs) + { + string result; + using (var block = new TabbedStringBuilder (sb, tabCount, true)) { + block.AppendLine ("Test"); + result = block.ToString (); + } + Assert.Equal ($"{expectedTabs}{{\n{expectedTabs}\tTest\n{expectedTabs}}}\n", result); + } + + [Theory] + [InlineData (0)] + [InlineData (1)] + [InlineData (5)] + public void AppendLineTest (uint tabCount) + { + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + block.AppendLine (); + result = block.ToString (); + } + // an empty line should have not tabs + Assert.Equal ("\n", result); + } + + [Theory] + [InlineData ("// test comment", 0, "")] + [InlineData ("var t = 1;", 1, "\t")] + [InlineData ("Console.WriteLine (\"1\");", 5, "\t\t\t\t\t")] + public void AppendLineStringTest (string line, uint tabCount, string expectedTabs) + { + string result; + using (var block = new TabbedStringBuilder (sb, tabCount, true)) { + block.AppendLine (line); + result = block.ToString (); + } + Assert.Equal ($"{expectedTabs}{{\n{expectedTabs}\t{line}\n{expectedTabs}}}\n", result); + } + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void AppendInterpolatedLineTest (uint tabCount, string expectedTabs) + { + string result; + var val1 = "Hello"; + var val2 = "World"; + var val3 = '!'; + var line = "Hello World!"; + var expected = $"{expectedTabs}{{\n{expectedTabs}\t{line}\n{expectedTabs}}}\n"; + using (var block = new TabbedStringBuilder (sb, tabCount, true)) { + block.AppendLine ($"{val1} {val2}{val3}"); + result = block.ToString (); + } + Assert.Equal (expected, result); + } + + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void AppendRawTest (uint tabCount, string expectedTabs) + { + var input = @" +## Raw string +Because we are using a raw string we expected: + 1. The string to be split in lines + 2. All lines should have the right indentation + - This means nested one + 3. And all lines should have the correct tabs +"; + var expected = $@" +{expectedTabs}## Raw string +{expectedTabs}Because we are using a raw string we expected: +{expectedTabs} 1. The string to be split in lines +{expectedTabs} 2. All lines should have the right indentation +{expectedTabs} - This means nested one +{expectedTabs} 3. And all lines should have the correct tabs +"; + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + block.AppendRaw (input); + result = block.ToString (); + } + Assert.Equal (expected, result); + } + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void AppendGeneratedCodeAttributeTest (uint tabCount, string expectedTabs) + { + var expected = $"{expectedTabs}[BindingImpl (BindingImplOptions.GeneratedCode)]\n"; + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + block.AppendGeneratedCodeAttribute (false); + result = block.ToString (); + } + Assert.Equal (expected, result); + } + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void AppendGeneratedCodeAttributeOptimizableTest (uint tabCount, string expectedTabs) + { + var expected = $"{expectedTabs}[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]\n"; + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + block.AppendGeneratedCodeAttribute (); + result = block.ToString (); + } + Assert.Equal (expected, result); + } + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void AppendEditorBrowsableAttributeTest (uint tabCount, string expectedTabs) + { + var expected = $"{expectedTabs}[EditorBrowsable (EditorBrowsableState.Never)]\n"; + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + block.AppendEditorBrowsableAttribute (); + result = block.ToString (); + } + Assert.Equal (expected, result); + } + + [Theory] + [InlineData (0, "")] + [InlineData (1, "\t")] + [InlineData (5, "\t\t\t\t\t")] + public void CreateEmptyBlockTest (uint tabCount, string expectedTabs) + { + var blockContent = "// the test"; + var expected = $@"{expectedTabs}{{ +{expectedTabs}{"\t"}{blockContent} +{expectedTabs}}} +"; + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + using (var nested = block.CreateBlock (true)) { + nested.AppendLine (blockContent); + } + result = block.ToString (); + } + Assert.Equal (expected, result); + } + + [Theory] + [InlineData (0, "", "if (true)")] + [InlineData (1, "\t", "using (var t = new StringBuilder)")] + [InlineData (5, "\t\t\t\t\t", "fixed (*foo)")] + public void CreateBlockTest (uint tabCount, string expectedTabs, string blockType) + { + var blockContent = "// the test"; + var expected = $@"{expectedTabs}{blockType} +{expectedTabs}{{ +{expectedTabs}{"\t"}{blockContent} +{expectedTabs}}} +"; + string result; + using (var block = new TabbedStringBuilder (sb, tabCount)) { + using (var nested = block.CreateBlock (blockType, true)) { + nested.AppendLine (blockContent); + } + result = block.ToString (); + } + Assert.Equal (expected, result); + } + +}