diff --git a/src/Publicizer.Tests/PublicizerTests.cs b/src/Publicizer.Tests/PublicizerTests.cs index 6c47ff8..34b0754 100644 --- a/src/Publicizer.Tests/PublicizerTests.cs +++ b/src/Publicizer.Tests/PublicizerTests.cs @@ -916,4 +916,82 @@ class PrivateClass Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); Assert.That(File.Exists(logFilePath), Is.True, buildAppProcess.Output); } + + [Test] + public void PublicizeNestedTypeMember_AlsoPublicizesEnclosingType_CompilesAndRunsWithExitCode0() + { + using var libraryFolder = new TemporaryFolder(); + string libraryCodePath = Path.Combine(libraryFolder.Path, "Outer.cs"); + string libraryCode = """ + namespace PrivateNamespace; + class Outer + { + class Inner + { + private static string PrivateField = "foobar"; + } + } + """; + File.WriteAllText(libraryCodePath, libraryCode); + + string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj"); + string libraryCsproj = $""" + + + + {TestTargetFramework} + false + {libraryFolder.Path} + + + + + + + + """; + + File.WriteAllText(libraryCsprojPath, libraryCsproj); + ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); + Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); + + using var appFolder = new TemporaryFolder(); + string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); + // Reachable only if both Inner and its enclosing Outer are made accessible. + string appCode = "System.Console.Write(PrivateNamespace.Outer.Inner.PrivateField);"; + File.WriteAllText(appCodePath, appCode); + string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll"); + + string appCsproj = $""" + + + + {TestTargetFramework} + false + exe + {appFolder.Path} + + + + + + + + + + + """; + + string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); + File.WriteAllText(appCsprojPath, appCsproj); + string appPath = Path.Combine(appFolder.Path, "App.dll"); + NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); + + ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); + ProcessResult runAppProcess = Runner.Run("dotnet", appPath); + + Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); + Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); + Assert.That(runAppProcess.Output, Is.EqualTo("foobar"), runAppProcess.Output); + } } diff --git a/src/Publicizer/AssemblyEditor.cs b/src/Publicizer/AssemblyEditor.cs index 4bbc802..c1e38f8 100644 --- a/src/Publicizer/AssemblyEditor.cs +++ b/src/Publicizer/AssemblyEditor.cs @@ -9,18 +9,28 @@ internal static class AssemblyEditor { internal static bool PublicizeType(TypeDef type) { - TypeAttributes oldAttributes = type.Attributes; - type.Attributes &= ~TypeAttributes.VisibilityMask; + bool modified = false; - if (type.IsNested) + // A nested type is only reachable if every enclosing type is accessible too, + // so walk up the declaring-type chain and publicize each one. + for (TypeDef? current = type; current is not null; current = current.DeclaringType) { - type.Attributes |= TypeAttributes.NestedPublic; - } - else - { - type.Attributes |= TypeAttributes.Public; + TypeAttributes oldAttributes = current.Attributes; + current.Attributes &= ~TypeAttributes.VisibilityMask; + + if (current.IsNested) + { + current.Attributes |= TypeAttributes.NestedPublic; + } + else + { + current.Attributes |= TypeAttributes.Public; + } + + modified |= current.Attributes != oldAttributes; } - return type.Attributes != oldAttributes; + + return modified; } internal static bool PublicizeProperty(PropertyDef property, bool includeVirtual = true)