Skip to content

Commit

Permalink
Add support for regular expressions in member names (#123)
Browse files Browse the repository at this point in the history
Publicize-items can now optionally have an atttribute or node called MemberPattern. MemberPattern accepts a regular expression for matching against members in the assembly.
  • Loading branch information
krafs authored Dec 7, 2024
1 parent e1ae057 commit 91f9f5a
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Set up .NET
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ Publicizer needs to be told what private members you want access to. You do this
<!-- All members in an assembly -->
<Publicize Include="MyAssemblyFileName" />

<!-- Type -->
<Publicize Include="MyAssemblyFileName:MyNamespace.MyType" />

<!-- Generic Type -->
<!-- The number represents the arity/number of generic type arguments -->
<Publicize Include="MyAssemblyFileName:MyNamespace.MyType`2" />

<!-- Field -->
<Publicize Include="MyAssemblyFileName:MyNamespace.MyType.myField" />

Expand All @@ -26,7 +33,7 @@ Publicizer needs to be told what private members you want access to. You do this

<!-- Method -->
<Publicize Include="MyAssemblyFileName:MyNamespace.MyType.MyMethod" />

<!-- Field in nested type -->
<Publicize Include="MyAssemblyFileName:MyNamespace.MyType+MyNestedType.myField" />

Expand All @@ -35,6 +42,15 @@ Publicizer needs to be told what private members you want access to. You do this
</ItemGroup>
```

### Regular expressions
Regular expressions are supported with the `MemberPattern` attribute.
```xml
<ItemGroup>
<!-- All members in a type -->
<Publicize Include="MyAssemblyFileName" MemberPattern="^MyNamespace\.MyType\..*" />
</ItemGroup>
```

Notes:
- Assemblies are referenced by their file name, excluding file extension.
So, given an assembly called `MyAssemblyFileName.dll`, you reference it as `MyAssemblyFileName`.
Expand Down
2 changes: 1 addition & 1 deletion src/Publicizer.Tests/Publicizer.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<NoWarn>NU1702</NoWarn>
Expand Down
82 changes: 81 additions & 1 deletion src/Publicizer.Tests/PublicizerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Publicizer.Tests;

public class PublicizerTests
{
private const string TestTargetFramework = "net8.0";
private const string TestTargetFramework = "net9.0";

[Test]
public void PublicizePrivateField_CompilesAndRunsWithExitCode0AndPrintsFieldValue()
Expand Down Expand Up @@ -757,4 +757,84 @@ public class LibraryClass
Assert.That(buildAppProcess.Output, Does.Match("CS0122: 'LibraryClass.VirtualProtectedProperty' is inaccessible due to its protection level"));
Assert.That(buildAppProcess.Output, Does.Not.Match("CS0122: 'LibraryClass.ProtectedProperty' is inaccessible due to its protection level"));
}

[Test]
public void PublicizePrivateMembersWithMemberPattern_CompilesAndRunsWithExitCode0AndPrintsExpectedValues()
{
using var libraryFolder = new TemporaryFolder();
string libraryCodePath = Path.Combine(libraryFolder.Path, "PrivateClass.cs");
string libraryCode = """"
namespace PrivateNamespace;
class PrivateClass
{
protected string PrivateFooField = "foo";
protected string PrivateBarField = "bar";
protected string PrivateFooProperty { get; } = "foo";
}
"""";
File.WriteAllText(libraryCodePath, libraryCode);

string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj");
string libraryCsproj = $"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>{TestTargetFramework}</TargetFramework>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<OutDir>{libraryFolder.Path}</OutDir>
</PropertyGroup>
<ItemGroup>
<Compile Include="{libraryCodePath}" />
</ItemGroup>
</Project>
""";

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");
string appCode = """
var instance = new PrivateNamespace.PrivateClass();
_ = instance.PrivateFooField;
_ = instance.PrivateBarField;
_ = instance.PrivateFooProperty;
""";
File.WriteAllText(appCodePath, appCode);
string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll");

string appCsproj = $"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>{TestTargetFramework}</TargetFramework>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<OutputType>exe</OutputType>
<OutDir>{appFolder.Path}</OutDir>
</PropertyGroup>
<ItemGroup>
<Compile Include="{appCodePath}" />
<Reference Include="PrivateAssembly" HintPath="{libraryPath}" />
<PackageReference Include="Krafs.Publicizer" Version="*" />
<Publicize Include="PrivateAssembly" MemberPattern=".*Foo.*" />
</ItemGroup>
</Project>
""";

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);
Assert.That(buildAppProcess.ExitCode, Is.Not.Zero, buildAppProcess.Output);
Assert.That(buildAppProcess.Output, Does.Match("CS0122: 'PrivateClass.PrivateBarField' is inaccessible due to its protection level"));
Assert.That(buildAppProcess.Output, Does.Not.Match("CS0122: 'PrivateClass.PrivateFooField' is inaccessible due to its protection level"));
Assert.That(buildAppProcess.Output, Does.Not.Match("CS0122: 'PrivateClass.PrivateFooProperty' is inaccessible due to its protection level"));
}
}
8 changes: 6 additions & 2 deletions src/Publicizer/Hasher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ internal static string ComputeHash(string assemblyPath, PublicizerAssemblyContex
{
sb.Append(doNotPublicizePattern);
}
if (assemblyContext.PublicizeMemberRegexPattern is not null)
{
sb.Append(assemblyContext.PublicizeMemberRegexPattern.ToString());
}

byte[] patternbytes = Encoding.UTF8.GetBytes(sb.ToString());
byte[] patternBytes = Encoding.UTF8.GetBytes(sb.ToString());
byte[] assemblyBytes = File.ReadAllBytes(assemblyPath);
byte[] allBytes = assemblyBytes.Concat(patternbytes).ToArray();
byte[] allBytes = assemblyBytes.Concat(patternBytes).ToArray();

return ComputeHash(allBytes);
}
Expand Down
29 changes: 27 additions & 2 deletions src/Publicizer/PublicizeAssemblies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ private static Dictionary<string, PublicizerAssemblyContext> GetPublicizerAssemb
assemblyContext.IncludeCompilerGeneratedMembers = item.IncludeCompilerGeneratedMembers();
assemblyContext.IncludeVirtualMembers = item.IncludeVirtualMembers();
assemblyContext.ExplicitlyPublicizeAssembly = true;
logger.Info($"Publicize: {item}, virtual members: {assemblyContext.IncludeVirtualMembers}, compiler-generated members: {assemblyContext.IncludeCompilerGeneratedMembers}");
assemblyContext.PublicizeMemberRegexPattern = item.MemberPattern();
logger.Info($"Publicize: {item}, virtual members: {assemblyContext.IncludeVirtualMembers}, compiler-generated members: {assemblyContext.IncludeCompilerGeneratedMembers}, member pattern: {assemblyContext.PublicizeMemberRegexPattern}");
}
else
{
Expand Down Expand Up @@ -237,7 +238,7 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex
string typeName = typeDef.ReflectionFullName;

bool explicitlyDoNotPublicizeType = assemblyContext.DoNotPublicizeMemberPatterns.Contains(typeName);

// PROPERTIES
foreach (PropertyDef? propertyDef in typeDef.Properties)
{
Expand Down Expand Up @@ -288,6 +289,12 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex
{
continue;
}

bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(propertyName) ?? true;
if (!isRegexPatternMatch)
{
continue;
}

if (AssemblyEditor.PublicizeProperty(propertyDef, assemblyContext.IncludeVirtualMembers))
{
Expand Down Expand Up @@ -346,6 +353,12 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex
{
continue;
}

bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(methodName) ?? true;
if (!isRegexPatternMatch)
{
continue;
}

if (AssemblyEditor.PublicizeMethod(methodDef, assemblyContext.IncludeVirtualMembers))
{
Expand Down Expand Up @@ -398,6 +411,12 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex
{
continue;
}

bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(fieldName) ?? true;
if (!isRegexPatternMatch)
{
continue;
}

if (AssemblyEditor.PublicizeField(fieldDef))
{
Expand Down Expand Up @@ -449,6 +468,12 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex
continue;
}

bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(typeName) ?? true;
if (!isRegexPatternMatch)
{
continue;
}

if (AssemblyEditor.PublicizeType(typeDef))
{
publicizedAnyMemberInAssembly = true;
Expand Down
4 changes: 2 additions & 2 deletions src/Publicizer/Publicizer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
<Title>Publicizer</Title>
<PackageId>Krafs.Publicizer</PackageId>
<Authors>Krafs</Authors>
<Copyright>© Krafs 2023</Copyright>
<Copyright>© Krafs 2024</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/krafs/Publicizer</PackageProjectUrl>
<RepositoryUrl>https://github.com/krafs/Publicizer.git</RepositoryUrl>
<Description>MSBuild library for allowing direct access to non-public members in .NET assemblies.</Description>
<PackageTags>msbuild accesschecks public publicizer</PackageTags>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Version>2.2.1</Version>
<Version>2.3.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/Publicizer/PublicizerAssemblyContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Publicizer;

Expand All @@ -15,5 +16,6 @@ internal PublicizerAssemblyContext(string assemblyName)
internal bool IncludeVirtualMembers { get; set; } = true;
internal bool ExplicitlyDoNotPublicizeAssembly { get; set; } = false;
internal HashSet<string> PublicizeMemberPatterns { get; } = new HashSet<string>();
internal Regex? PublicizeMemberRegexPattern { get; set; }
internal HashSet<string> DoNotPublicizeMemberPatterns { get; } = new HashSet<string>();
}
12 changes: 12 additions & 0 deletions src/Publicizer/TaskItemExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;

namespace Publicizer;
Expand Down Expand Up @@ -33,4 +34,15 @@ internal static bool IncludeVirtualMembers(this ITaskItem item)

return true;
}

internal static Regex? MemberPattern(this ITaskItem item)
{
string? memberPattern = item.GetMetadata("MemberPattern");
if (string.IsNullOrWhiteSpace(memberPattern))
{
return null;
}

return new Regex(memberPattern, RegexOptions.Compiled);
}
}

0 comments on commit 91f9f5a

Please sign in to comment.