Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --omit-non-api-members option #29

Merged
merged 10 commits into from
Nov 2, 2024
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ You could download refasmer from GitHub: https://github.com/JetBrains/Refasmer/r

## Usage:
```
refasmer [options] <dll> [<dll> ...]
refasmer [options] <dll> [<**/*.dll> ...]
Options:
-v increase verbosity
-q, --quiet be quiet
Expand All @@ -29,17 +29,31 @@ Options:
-p, --public drop non-public types even with InternalsVisibleTo
-i, --internals import public and internal types
--all ignore visibility and import all
--omit-non-api-members=VALUE
omit private members and types not participating
in the public API (will preserve the empty vs
non-empty struct semantics, but might affect
unmanaged struct constraint)
-m, --mock make mock assembly instead of reference assembly
-n, --noattr omit reference assembly attribute
-l, --list make file list xml
-a, --attr=VALUE add FileList tag attribute
-g, --globs expand globs internally: ?, *, **
```

(note the executable is called `RefasmerExe.exe` if built locally; `refasmer` is a name of an executable installed by `dotnet tool install`)

Mock assembly throws System.NotImplementedException in each imported method.
Mock assembly throws `System.NotImplementedException` in each imported method.

Reference assembly contains only type definition and method signatures with no method bodies.

By default, if you don't specify any of `--public`, `--internals`, or `--all`, Refasmer will try to detect the refasming mode from the input assembly. If the assembly has an `InternalsVisibleTo` attribute applied to it, then `--internals` will be implicitly applied; otherwise, `--public` will.

> [!IMPORTANT]
> Note that `--omit-non-api-members` performs a nontrivial transformation on the resulting assembly. Normally, a reference assembly should include any types, including private and internal ones, because this is up to the spec. However, in some cases, it is possible to omit private and internal types from the reference assembly, because they are not part of the public API, while preserving some of the value type semantics. In these cases, Refasmer is able to remove these types from the assembly, sometimes emitting synthetic fields in the output type. This will preserve the difference of empty and non-empty struct types, but will not preserve the type blittability (i.e. some types after refasming might obtain the ability to follow the `unmanaged` constraint, even if this wasn't possible before refasming).

If you didn't specify the `--all` option, you must pass either `--omit-non-api-members true` or `--omit-non-api-members false`, to exactly identify the required behavior of refasming.

## Examples:

```refasmer -v -O ref -c a.dll b.dll c.dll```
Expand Down
6 changes: 4 additions & 2 deletions src/Refasmer/Filters/AllowAll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ namespace JetBrains.Refasmer.Filters
{
public class AllowAll : IImportFilter
{
public virtual bool AllowImport( TypeDefinition declaringType, MetadataReader reader ) => true;
public bool OmitNonApiMembers => false;

public virtual bool AllowImport(TypeDefinition declaringType, MetadataReader reader) => true;
public virtual bool AllowImport( MethodDefinition method, MetadataReader reader ) => true;
public virtual bool AllowImport( FieldDefinition field, MetadataReader reader ) => true;
}
}
}
11 changes: 7 additions & 4 deletions src/Refasmer/Filters/AllowPublic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

namespace JetBrains.Refasmer.Filters
{
public class AllowPublic: IImportFilter
public class AllowPublic(bool omitNonApiMembers) : PartialTypeFilterBase(omitNonApiMembers)

Check warning on line 6 in src/Refasmer/Filters/AllowPublic.cs

View workflow job for this annotation

GitHub Actions / main

Parameter 'bool omitNonApiMembers' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.

Check warning on line 6 in src/Refasmer/Filters/AllowPublic.cs

View workflow job for this annotation

GitHub Actions / main

Parameter 'bool omitNonApiMembers' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
{
public virtual bool AllowImport( TypeDefinition type, MetadataReader reader )
public override bool AllowImport(TypeDefinition type, MetadataReader reader)
{
if (!base.AllowImport(type, reader)) return false;
if (!omitNonApiMembers) return true;

switch (type.Attributes & TypeAttributes.VisibilityMask)
{
case TypeAttributes.Public:
Expand All @@ -22,7 +25,7 @@
}
}

public virtual bool AllowImport( MethodDefinition method, MetadataReader reader )
public override bool AllowImport( MethodDefinition method, MetadataReader reader )
{
switch (method.Attributes & MethodAttributes.MemberAccessMask)
{
Expand All @@ -37,7 +40,7 @@
}
}

public virtual bool AllowImport( FieldDefinition field, MetadataReader reader )
public override bool AllowImport( FieldDefinition field, MetadataReader reader )
{
switch (field.Attributes & FieldAttributes.FieldAccessMask)
{
Expand Down
19 changes: 10 additions & 9 deletions src/Refasmer/Filters/AllowPublicAndInternals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@

namespace JetBrains.Refasmer.Filters
{
public class AllowPublicAndInternals: IImportFilter
public class AllowPublicAndInternals(bool omitNonApiMembers) : PartialTypeFilterBase(omitNonApiMembers)

Check warning on line 6 in src/Refasmer/Filters/AllowPublicAndInternals.cs

View workflow job for this annotation

GitHub Actions / main

Parameter 'bool omitNonApiMembers' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.

Check warning on line 6 in src/Refasmer/Filters/AllowPublicAndInternals.cs

View workflow job for this annotation

GitHub Actions / main

Parameter 'bool omitNonApiMembers' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
{
private readonly CachedAttributeChecker _attrChecker = new();

public virtual bool AllowImport( TypeDefinition type, MetadataReader reader )
public override bool AllowImport(TypeDefinition type, MetadataReader reader)
{
if (!base.AllowImport(type, reader)) return false;
if (!omitNonApiMembers) return true;

switch (type.Attributes & TypeAttributes.VisibilityMask)
{
case TypeAttributes.NotPublic:
return !_attrChecker.HasAttribute(reader, type.GetCustomAttributes(), FullNames.CompilerGenerated);
return !AttributeCache.HasAttribute(reader, type.GetCustomAttributes(), FullNames.CompilerGenerated);
case TypeAttributes.Public:
return true;
case TypeAttributes.NestedPublic:
Expand All @@ -28,14 +29,14 @@
}
}

public virtual bool AllowImport( MethodDefinition method, MetadataReader reader )
public override bool AllowImport( MethodDefinition method, MetadataReader reader )
{
switch (method.Attributes & MethodAttributes.MemberAccessMask)
{
case MethodAttributes.Assembly:
if ((method.Attributes & MethodAttributes.SpecialName) != 0)
return true;
return !_attrChecker.HasAttribute(reader, method, FullNames.CompilerGenerated);
return !AttributeCache.HasAttribute(reader, method, FullNames.CompilerGenerated);

case MethodAttributes.Public:
case MethodAttributes.FamORAssem:
Expand All @@ -49,12 +50,12 @@
}
}

public virtual bool AllowImport( FieldDefinition field, MetadataReader reader )
public override bool AllowImport( FieldDefinition field, MetadataReader reader )
{
switch (field.Attributes & FieldAttributes.FieldAccessMask)
{
case FieldAttributes.Assembly:
return !_attrChecker.HasAttribute(reader, field, FullNames.CompilerGenerated);
return !AttributeCache.HasAttribute(reader, field, FullNames.CompilerGenerated);
case FieldAttributes.Public:
case FieldAttributes.FamORAssem:
return true;
Expand Down
5 changes: 3 additions & 2 deletions src/Refasmer/Filters/IImportFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ namespace JetBrains.Refasmer.Filters
{
public interface IImportFilter
{
public bool OmitNonApiMembers { get; }

bool AllowImport(TypeDefinition type, MetadataReader reader);
bool AllowImport( MethodDefinition method, MetadataReader reader );
bool AllowImport( FieldDefinition field, MetadataReader reader );

// TODO: others on demand
}
}
21 changes: 21 additions & 0 deletions src/Refasmer/Filters/PartialTypeFilterBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Reflection.Metadata;

namespace JetBrains.Refasmer.Filters;

/// <summary>Base type for a filter that doesn't pass all types.</summary>
/// <param name="omitNonApiMembers">Whether the non-API types should be hidden when possible.</param>
public abstract class PartialTypeFilterBase(bool omitNonApiMembers) : IImportFilter
{
public bool OmitNonApiMembers => omitNonApiMembers;

protected readonly CachedAttributeChecker AttributeCache = new();

public virtual bool AllowImport(TypeDefinition type, MetadataReader reader)
{
var isCompilerGenerated = AttributeCache.HasAttribute(reader, type, FullNames.CompilerGenerated);
return !isCompilerGenerated;
}

public abstract bool AllowImport(MethodDefinition method, MetadataReader reader);
public abstract bool AllowImport(FieldDefinition field, MetadataReader reader);
}
109 changes: 104 additions & 5 deletions src/Refasmer/Importer/ImportLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,34 @@
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using JetBrains.Refasmer.Filters;

namespace JetBrains.Refasmer
{
public partial class MetadataImporter
{
private bool AllowImportType( EntityHandle typeHandle )
{
if (typeHandle.IsNil)
return false;

if (Filter == null)
return true;

switch (typeHandle.Kind)
{
case HandleKind.TypeDefinition:
return true;
case HandleKind.TypeReference:
return true;
case HandleKind.TypeSpecification:
return AllowImportType(_reader.GetGenericType((TypeSpecificationHandle)typeHandle));

default:
throw new ArgumentOutOfRangeException(nameof (typeHandle));
}
}

private TypeDefinitionHandle ImportTypeDefinitionSkeleton( TypeDefinitionHandle srcHandle )
{
var src = _reader.GetTypeDefinition(srcHandle);
Expand All @@ -18,33 +41,52 @@ private TypeDefinitionHandle ImportTypeDefinitionSkeleton( TypeDefinitionHandle
Import(src.BaseType), NextFieldHandle(), NextMethodHandle());

Trace?.Invoke($"Imported {_reader.ToString(src)} -> {RowId(dstHandle):X}");

using var _ = WithLogPrefix($"[{_reader.ToString(src)}]");

var isValueType = _reader.GetFullname(src.BaseType) == "System::ValueType";
var forcePreservePrivateFields = isValueType && !Filter.OmitNonApiMembers;

if (isValueType)
Trace?.Invoke($"{_reader.ToString(src)} is ValueType, all fields should be imported");
List<FieldDefinition> importedInstanceFields = null;
List<FieldDefinition> skippedInstanceFields = null;

if (forcePreservePrivateFields)
Trace?.Invoke($"{_reader.ToString(src)} is ValueType, all fields should be imported");
else
{
importedInstanceFields = [];
skippedInstanceFields = [];
}

foreach (var srcFieldHandle in src.GetFields())
{
var srcField = _reader.GetFieldDefinition(srcFieldHandle);
var isStatic = (srcField.Attributes & FieldAttributes.Static) != 0;
var isForcedToInclude = forcePreservePrivateFields && !isStatic;

if (!isValueType && Filter?.AllowImport(srcField, _reader) == false)
if (!isForcedToInclude && Filter?.AllowImport(srcField, _reader) == false)
{
Trace?.Invoke($"Not imported {_reader.ToString(srcField)}");
if (!isStatic)
skippedInstanceFields?.Add(srcField);

continue;
}

var dstFieldHandle = _builder.AddFieldDefinition(srcField.Attributes, ImportValue(srcField.Name),
ImportSignatureWithHeader(srcField.Signature));
_fieldDefinitionCache.Add(srcFieldHandle, dstFieldHandle);
Trace?.Invoke($"Imported {_reader.ToString(srcFieldHandle)} -> {RowId(dstFieldHandle):X}");
if (!isStatic)
importedInstanceFields?.Add(srcField);
}

if (!forcePreservePrivateFields)
PostProcessSkippedValueTypeFields(skippedInstanceFields, importedInstanceFields);

var implementations = src.GetMethodImplementations()
.Select(_reader.GetMethodImplementation)
.Where(mi => AllowImportType(_reader.GetMethodClass(mi.MethodDeclaration)))
.Select(mi => (MethodDefinitionHandle)mi.MethodBody)
.ToImmutableHashSet();

Expand Down Expand Up @@ -389,9 +431,50 @@ public ReservedBlob<GuidHandle> Import()
var index = 1;
Debug?.Invoke("Preparing type list for import");

var checker = new CachedAttributeChecker();

foreach (var srcHandle in _reader.TypeDefinitions)
{
_typeDefinitionCache[srcHandle] = MetadataTokens.TypeDefinitionHandle(index++);
bool shouldImport;

var src = _reader.GetTypeDefinition(srcHandle);

// Special <Module> type
if (srcHandle.GetHashCode() == 1 && _reader.GetString(src.Name) == "<Module>")
{
shouldImport = true;
}
else if (checker.HasAttribute(_reader, src, FullNames.Embedded) &&
checker.HasAttribute(_reader, src, FullNames.CompilerGenerated))
{
Trace?.Invoke($"Embedded type found {_reader.ToString(srcHandle)}");
shouldImport = true;
}
else if (_reader.GetString(src.Namespace) == FullNames.CompilerServices &&
_reader.GetFullname(src.BaseType) == FullNames.Attribute)
{
Trace?.Invoke($"CompilerServices attribute found {_reader.ToString(srcHandle)}");
shouldImport = true;
}
else if (_reader.GetString(src.Namespace) == FullNames.CodeAnalysis &&
_reader.GetFullname(src.BaseType) == FullNames.Attribute)
{
Trace?.Invoke($"CodeAnalysis attribute found {_reader.ToString(srcHandle)}");
shouldImport = true;
}
else
{
shouldImport = Filter?.AllowImport(_reader.GetTypeDefinition(srcHandle), _reader) != false;
}

if (shouldImport)
{
_typeDefinitionCache[srcHandle] = MetadataTokens.TypeDefinitionHandle(index++);
}
else
{
Trace?.Invoke($"Type filtered and will not be imported {_reader.ToString(srcHandle)}");
}
}

Debug?.Invoke("Importing type definitions");
Expand Down Expand Up @@ -464,5 +547,21 @@ public ReservedBlob<GuidHandle> Import()
return mvidBlob;
}

/// <remarks>
/// The point of this method is to make a value type non-empty in case we've decided to skip all its fields.
/// </remarks>
private void PostProcessSkippedValueTypeFields(
List<FieldDefinition> skippedFields,
List<FieldDefinition> importedFields)
{
if (importedFields.Count > 0) return; // we have imported some fields, no need to make the struct non-empty
if (skippedFields.Count == 0) return; // we haven't skipped any fields; the struct was empty to begin with

// We have skipped all fields, so we need to add a dummy field to make the struct non-empty.
_builder.AddFieldDefinition(
FieldAttributes.Private,
_builder.GetOrAddString("<SyntheticNonEmptyStructMarker>"),
_builder.GetOrAddBlob(new[] { (byte)SignatureKind.Field, (byte)SignatureTypeCode.Int32 }));
}
}
}
Loading
Loading