diff --git a/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs b/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs index 3ef19a810e1b..06946f8eee59 100644 --- a/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs +++ b/tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs @@ -456,4 +456,175 @@ public struct MyStruct { Assert.That (bindings.AdditionalFiles ["ApiDefinition.cs"].Trim (), Is.EqualTo (expectedApiDefinitionBindings.Trim ()), "Api definition"); Assert.That (bindings.AdditionalFiles ["StructsAndEnums.cs"].Trim (), Is.EqualTo (expectedStructAndEnumsBindings.Trim ()), "Struct and enums"); } + + [Test] + public void Scope_RelativePath () + { + // Verify that --scope with a relative path works correctly + // (the relative path should be resolved to absolute before matching). + var binder = new BindTool (); + var tmpdir = Cache.CreateTemporaryDirectory (); + var subdir = Path.Combine (tmpdir, "headers"); + Directory.CreateDirectory (subdir); + + // Create a header in the scoped directory + var scopedHeader = Path.Combine (subdir, "InScope.h"); + File.WriteAllText (scopedHeader, + """ + @interface InScopeClass { + } + @property int Value; + @end + """); + + // Create an umbrella header that includes both + var mainHeader = Path.Combine (tmpdir, "main.h"); + File.WriteAllText (mainHeader, $"#import \"{scopedHeader}\"\n"); + + binder.SplitDocuments = false; + binder.SourceFile = mainHeader; + binder.OutputDirectory = tmpdir; + + // Use a relative scope path (the bug was that this produced empty output) + var oldCwd = Environment.CurrentDirectory; + try { + Environment.CurrentDirectory = tmpdir; + binder.DirectoriesInScope.Add (Path.GetFullPath ("headers")); + } finally { + Environment.CurrentDirectory = oldCwd; + } + + Configuration.IgnoreIfIgnoredPlatform (binder.Platform); + binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform); + binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory (); + var bindings = binder.BindInOrOut (); + var expectedBindings = +""" +using Foundation; + +// @interface InScopeClass +interface InScopeClass { + // @property int Value; + [Export ("Value")] + int Value { get; set; } +} + +"""; + bindings.AssertSuccess (expectedBindings); + } + + [Test] + public void Scope_FiltersOutOfScopeDeclarations () + { + // Verify that declarations from headers outside the scope directory are not bound. + var binder = new BindTool (); + var tmpdir = Cache.CreateTemporaryDirectory (); + var scopedDir = Path.Combine (tmpdir, "scoped"); + var unscopedDir = Path.Combine (tmpdir, "unscoped"); + Directory.CreateDirectory (scopedDir); + Directory.CreateDirectory (unscopedDir); + + var scopedHeader = Path.Combine (scopedDir, "Scoped.h"); + File.WriteAllText (scopedHeader, + """ + @interface ScopedClass { + } + @property int A; + @end + """); + + var unscopedHeader = Path.Combine (unscopedDir, "Unscoped.h"); + File.WriteAllText (unscopedHeader, + """ + @interface UnscopedClass { + } + @property int B; + @end + """); + + var mainHeader = Path.Combine (tmpdir, "main.h"); + File.WriteAllText (mainHeader, $"#import \"{scopedHeader}\"\n#import \"{unscopedHeader}\"\n"); + + binder.SplitDocuments = false; + binder.SourceFile = mainHeader; + binder.OutputDirectory = tmpdir; + binder.DirectoriesInScope.Add (scopedDir); + Configuration.IgnoreIfIgnoredPlatform (binder.Platform); + binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform); + binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory (); + var bindings = binder.BindInOrOut (); + + // Only ScopedClass should be in the output, not UnscopedClass + var expectedBindings = +""" +using Foundation; + +// @interface ScopedClass +interface ScopedClass { + // @property int A; + [Export ("A")] + int A { get; set; } +} + +"""; + bindings.AssertSuccess (expectedBindings); + } + + [Test] + public void Scope_PrefixDoesNotFalseMatch () + { + // Verify that a scope of "/foo/bar" does not match "/foo/barbaz/header.h" + // (the scope must be a proper directory prefix with separator). + var binder = new BindTool (); + var tmpdir = Cache.CreateTemporaryDirectory (); + var scopedDir = Path.Combine (tmpdir, "scope"); + var falseMatchDir = Path.Combine (tmpdir, "scopeextra"); + Directory.CreateDirectory (scopedDir); + Directory.CreateDirectory (falseMatchDir); + + var scopedHeader = Path.Combine (scopedDir, "Good.h"); + File.WriteAllText (scopedHeader, + """ + @interface GoodClass { + } + @property int X; + @end + """); + + var falseMatchHeader = Path.Combine (falseMatchDir, "Bad.h"); + File.WriteAllText (falseMatchHeader, + """ + @interface BadClass { + } + @property int Y; + @end + """); + + var mainHeader = Path.Combine (tmpdir, "main.h"); + File.WriteAllText (mainHeader, $"#import \"{scopedHeader}\"\n#import \"{falseMatchHeader}\"\n"); + + binder.SplitDocuments = false; + binder.SourceFile = mainHeader; + binder.OutputDirectory = tmpdir; + binder.DirectoriesInScope.Add (scopedDir); + Configuration.IgnoreIfIgnoredPlatform (binder.Platform); + binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform); + binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory (); + var bindings = binder.BindInOrOut (); + + // Only GoodClass should appear (from "scope/"), not BadClass (from "scopeextra/") + var expectedBindings = +""" +using Foundation; + +// @interface GoodClass +interface GoodClass { + // @property int X; + [Export ("X")] + int X { get; set; } +} + +"""; + bindings.AssertSuccess (expectedBindings); + } } diff --git a/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs b/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs index 8406602d1bcf..7e76452e4acd 100644 --- a/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs +++ b/tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs @@ -154,7 +154,12 @@ bool IsInScope (Decl? decl) if (string.IsNullOrEmpty (fn)) return true; foreach (var dir in DirectoriesInScope) { - if (fn.StartsWith (dir, StringComparison.Ordinal)) + // Ensure the scope directory ends with a directory separator so that + // a scope of "/foo/bar" doesn't falsely match "/foo/barbaz/header.h". + var normalizedDir = dir.EndsWith (Path.DirectorySeparatorChar) || dir.EndsWith (Path.AltDirectorySeparatorChar) + ? dir + : dir + Path.DirectorySeparatorChar; + if (fn.StartsWith (normalizedDir, StringComparison.Ordinal)) return true; } diff --git a/tools/sharpie/Sharpie.Bind/Tools.cs b/tools/sharpie/Sharpie.Bind/Tools.cs index 1fd06094bfb4..3381d62a0962 100644 --- a/tools/sharpie/Sharpie.Bind/Tools.cs +++ b/tools/sharpie/Sharpie.Bind/Tools.cs @@ -33,7 +33,7 @@ public static int Bind (string [] arguments) { "s|sdk=", "Target SDK.", v => binder.Sdk = v }, { "f|framework=", "The input framework to bind. Implies setting the scope (--scope) to the framework, setting the namespace (--namespace) to the name of the framework, and no other sources/headers can be specified. If the framework provides an 'Info.plist' with SDK information (DTSDKName), the '-sdk' option will be implied as well (if not manually specified).", v => binder.SourceFramework = v }, { "header=", "The input header file to bind. This can also be a .framework directory.", v => binder.SourceFile = v }, - { "scope=", "Restrict following #include and #import directives declared in header files to within the specified DIR directory.", v => binder.DirectoriesInScope.Add (v) }, + { "scope=", "Restrict following #include and #import directives declared in header files to within the specified DIR directory.", v => binder.DirectoriesInScope.Add (Path.GetFullPath (v)) }, { "c|clang", "All arguments after this argument are not processed by Objective Sharpie and are proxied directly to Clang.", v => { } }, { "clang-resource-dir=", "Specify the Clang resource directory.", v => binder.ClangResourceDirectory = v }, { "platform-assembly=", "Specify the platform assembly to use for binding.", v => binder.PlatformAssembly = v },