diff --git a/README.md b/README.md index 177932d..e7472c8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You could download refasmer from GitHub: https://github.com/JetBrains/Refasmer/r ## Usage: ``` -refasmer [options] [ ...] +refasmer [options] [<**/*.dll> ...] Options: -v increase verbosity -q, --quiet be quiet @@ -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``` diff --git a/src/Refasmer/Filters/AllowAll.cs b/src/Refasmer/Filters/AllowAll.cs index 802b4f7..1b5c7fd 100644 --- a/src/Refasmer/Filters/AllowAll.cs +++ b/src/Refasmer/Filters/AllowAll.cs @@ -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; } -} \ No newline at end of file +} diff --git a/src/Refasmer/Filters/AllowPublic.cs b/src/Refasmer/Filters/AllowPublic.cs index d3c46f4..eacb5f3 100644 --- a/src/Refasmer/Filters/AllowPublic.cs +++ b/src/Refasmer/Filters/AllowPublic.cs @@ -3,10 +3,13 @@ namespace JetBrains.Refasmer.Filters { - public class AllowPublic: IImportFilter + public class AllowPublic(bool omitNonApiMembers) : PartialTypeFilterBase(omitNonApiMembers) { - 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: @@ -22,7 +25,7 @@ public virtual bool AllowImport( TypeDefinition type, MetadataReader reader ) } } - public virtual bool AllowImport( MethodDefinition method, MetadataReader reader ) + public override bool AllowImport( MethodDefinition method, MetadataReader reader ) { switch (method.Attributes & MethodAttributes.MemberAccessMask) { @@ -37,7 +40,7 @@ public virtual bool AllowImport( MethodDefinition method, MetadataReader reader } } - public virtual bool AllowImport( FieldDefinition field, MetadataReader reader ) + public override bool AllowImport( FieldDefinition field, MetadataReader reader ) { switch (field.Attributes & FieldAttributes.FieldAccessMask) { diff --git a/src/Refasmer/Filters/AllowPublicAndInternals.cs b/src/Refasmer/Filters/AllowPublicAndInternals.cs index 45d28df..d075412 100644 --- a/src/Refasmer/Filters/AllowPublicAndInternals.cs +++ b/src/Refasmer/Filters/AllowPublicAndInternals.cs @@ -3,16 +3,17 @@ namespace JetBrains.Refasmer.Filters { - public class AllowPublicAndInternals: IImportFilter + public class AllowPublicAndInternals(bool omitNonApiMembers) : PartialTypeFilterBase(omitNonApiMembers) { - 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: @@ -28,14 +29,14 @@ public virtual bool AllowImport( TypeDefinition type, MetadataReader reader ) } } - 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: @@ -49,12 +50,12 @@ public virtual bool AllowImport( MethodDefinition method, MetadataReader reader } } - 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; diff --git a/src/Refasmer/Filters/IImportFilter.cs b/src/Refasmer/Filters/IImportFilter.cs index 64ed1cb..4fff6d6 100644 --- a/src/Refasmer/Filters/IImportFilter.cs +++ b/src/Refasmer/Filters/IImportFilter.cs @@ -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 } } \ No newline at end of file diff --git a/src/Refasmer/Filters/PartialTypeFilterBase.cs b/src/Refasmer/Filters/PartialTypeFilterBase.cs new file mode 100644 index 0000000..92f3429 --- /dev/null +++ b/src/Refasmer/Filters/PartialTypeFilterBase.cs @@ -0,0 +1,21 @@ +using System.Reflection.Metadata; + +namespace JetBrains.Refasmer.Filters; + +/// Base type for a filter that doesn't pass all types. +/// Whether the non-API types should be hidden when possible. +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); +} diff --git a/src/Refasmer/Importer/ImportLogic.cs b/src/Refasmer/Importer/ImportLogic.cs index a398b4c..0bc1a57 100644 --- a/src/Refasmer/Importer/ImportLogic.cs +++ b/src/Refasmer/Importer/ImportLogic.cs @@ -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); @@ -18,22 +41,35 @@ 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 importedInstanceFields = null; + List 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; } @@ -41,10 +77,16 @@ private TypeDefinitionHandle ImportTypeDefinitionSkeleton( TypeDefinitionHandle 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(); @@ -389,9 +431,50 @@ public ReservedBlob 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 type + if (srcHandle.GetHashCode() == 1 && _reader.GetString(src.Name) == "") + { + 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"); @@ -464,5 +547,21 @@ public ReservedBlob Import() return mvidBlob; } + /// + /// The point of this method is to make a value type non-empty in case we've decided to skip all its fields. + /// + private void PostProcessSkippedValueTypeFields( + List skippedFields, + List 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(""), + _builder.GetOrAddBlob(new[] { (byte)SignatureKind.Field, (byte)SignatureTypeCode.Int32 })); + } } } \ No newline at end of file diff --git a/src/Refasmer/Importer/MetadataImporter.cs b/src/Refasmer/Importer/MetadataImporter.cs index e6901f6..b6b3b27 100644 --- a/src/Refasmer/Importer/MetadataImporter.cs +++ b/src/Refasmer/Importer/MetadataImporter.cs @@ -4,6 +4,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using JetBrains.Refasmer.Filters; @@ -27,9 +28,38 @@ public MetadataImporter( MetadataReader reader, MetadataBuilder builder, BlobBui _ilStream = ilStream; } - - public static byte[] MakeRefasm(MetadataReader metaReader, PEReader peReader, LoggerBase logger, IImportFilter filter = null, - bool makeMock = false, bool omitReferenceAssemblyAttr = false ) + /// Produces a reference assembly for the passed input. + /// Input assembly's metadata reader. + /// Input file's PE structure reader. + /// Logger to write the process information to. + /// + /// Filter to apply to the assembly members. If null then will be auto-detected from the assembly + /// contents: for an assembly that has a applied to it, use + /// , otherwise use . + /// + /// + /// Omit private members and types not participating in the public API (will preserve the empty vs + /// non-empty struct semantics, but might affect the unmanaged struct constraint). + /// + /// Mandatory if the is not passed. Ignored otherwise. + /// + /// + /// Whether to make a mock assembly instead of a reference assembly. A mock assembly throws + /// in each imported method, while a reference assembly follows the + /// reference assembly specification. + /// + /// + /// Whether to omit the reference assembly attribute in the generated assembly. + /// + /// Bytes of the generated assembly. + public static byte[] MakeRefasm( + MetadataReader metaReader, + PEReader peReader, + LoggerBase logger, + IImportFilter filter, + bool? omitNonApiMembers, + bool makeMock = false, + bool omitReferenceAssemblyAttr = false) { var metaBuilder = new MetadataBuilder(); var ilStream = new BlobBuilder(); @@ -48,12 +78,16 @@ public static byte[] MakeRefasm(MetadataReader metaReader, PEReader peReader, Lo } else if (importer.IsInternalsVisible()) { - importer.Filter = new AllowPublicAndInternals(); + importer.Filter = new AllowPublicAndInternals( + omitNonApiMembers ?? throw new Exception( + $"{nameof(omitNonApiMembers)} should be specified for the current filter mode.")); logger.Info?.Invoke("InternalsVisibleTo attributes found, using AllowPublicAndInternals entity filter"); } else { - importer.Filter = new AllowPublic(); + importer.Filter = new AllowPublic( + omitNonApiMembers ?? throw new Exception( + $"{nameof(omitNonApiMembers)} should be specified for the current filter mode.")); logger.Info?.Invoke("Using AllowPublic entity filter"); } @@ -114,7 +148,7 @@ public static void MakeRefasm( string inputPath, string outputPath, LoggerBase l if (!metaReader.IsAssembly) throw new Exception("File format is not supported"); - var result = MakeRefasm(metaReader, peReader, logger, filter, makeMock); + var result = MakeRefasm(metaReader, peReader, logger, filter, omitNonApiMembers: false, makeMock); logger.Debug?.Invoke($"Writing result to {outputPath}"); if (File.Exists(outputPath)) @@ -123,4 +157,4 @@ public static void MakeRefasm( string inputPath, string outputPath, LoggerBase l File.WriteAllBytes(outputPath, result); } } -} \ No newline at end of file +} diff --git a/src/RefasmerExe/Program.cs b/src/RefasmerExe/Program.cs index 80554c2..c6c227b 100644 --- a/src/RefasmerExe/Program.cs +++ b/src/RefasmerExe/Program.cs @@ -7,12 +7,9 @@ using System.Reflection.PortableExecutable; using System.Text; using System.Xml; - using JetBrains.Refasmer.Filters; - using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; - using Mono.Options; namespace JetBrains.Refasmer @@ -34,6 +31,7 @@ enum Operation private static bool _public; private static bool _internals; private static bool _all; + private static bool? _omitNonApiMembers; private static bool _makeMock; private static bool _omitReferenceAssemblyAttr; @@ -89,6 +87,7 @@ public static int Main(string[] args) { "p|public", "drop non-public types even with InternalsVisibleTo", v => _public = v != null }, { "i|internals", "import public and internal types", v => _internals = v != null }, { "all", "ignore visibility and import all", v => _all = v != null }, + { "omit-non-api-members=", "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)", x => _omitNonApiMembers = string.Equals(x, "true", StringComparison.OrdinalIgnoreCase) }, { "m|mock", "make mock assembly instead of reference assembly", p => _makeMock = p != null }, { "n|noattr", "omit reference assembly attribute", p => _omitReferenceAssemblyAttr = p != null }, @@ -135,6 +134,12 @@ public static int Main(string[] args) return 2; } + if (!_all && _omitNonApiMembers == null) + { + Console.Error.WriteLine("Either specify --all to emit all private types, or set --omit-non-api-members to either true or false."); + return 2; + } + _logger = new LoggerBase(new VerySimpleLogger(Console.Error, verbosity)); try @@ -256,15 +261,24 @@ private static void MakeRefasm((string Path, string RelativeForOutput) input) IImportFilter filter = null; if (_public) - filter = new AllowPublic(); + filter = new AllowPublic(_omitNonApiMembers ?? throw new Exception("--omit-non-api-members should be specified for the passed filter type.")); else if (_internals) - filter = new AllowPublicAndInternals(); + filter = new AllowPublicAndInternals(_omitNonApiMembers ?? throw new Exception("--omit-non-api-members should be specified for the passed filter type.")); else if (_all) filter = new AllowAll(); byte[] result; using (var peReader = ReadAssembly(input.Path, out var metaReader)) - result = MetadataImporter.MakeRefasm(metaReader, peReader, _logger, filter, _makeMock, _omitReferenceAssemblyAttr); + { + result = MetadataImporter.MakeRefasm( + metaReader, + peReader, + _logger, + filter, + _omitNonApiMembers, + _makeMock, + _omitReferenceAssemblyAttr); + } string output; @@ -349,4 +363,4 @@ private static PEReader ReadAssembly(string input, out MetadataReader metaReader return expanded; } } -} \ No newline at end of file +} diff --git a/tests/Refasmer.Tests/ExitCodeTests.cs b/tests/Refasmer.Tests/ExitCodeTests.cs new file mode 100644 index 0000000..ee847f8 --- /dev/null +++ b/tests/Refasmer.Tests/ExitCodeTests.cs @@ -0,0 +1,29 @@ +namespace JetBrains.Refasmer.Tests; + +public class ExitCodeTests : IntegrationTestBase +{ + [TestCase] + public Task OmitNonApiTypesTrue() => + DoTest(0, "--omit-non-api-members", "true"); + + [TestCase] + public Task OmitNonApiTypesFalse() => + DoTest(0, "--omit-non-api-members", "false"); + + [TestCase] + public Task OmitNonApiTypesMissing() => + DoTest(2); + + private async Task DoTest(int expectedCode, params string[] additionalArgs) + { + var assemblyPath = await BuildTestAssembly(); + var outputPath = Path.GetTempFileName(); + using var collector = CollectConsoleOutput(); + var exitCode = ExecuteRefasmAndGetExitCode(assemblyPath, outputPath, additionalArgs); + Assert.That( + exitCode, + Is.EqualTo(expectedCode), + $"Refasmer returned exit code {exitCode}, while {expectedCode} was expected." + + $" StdOut:\n{collector.StdOut}\nStdErr: {collector.StdErr}"); + } +} diff --git a/tests/Refasmer.Tests/IntegrationTestBase.cs b/tests/Refasmer.Tests/IntegrationTestBase.cs new file mode 100644 index 0000000..02e96b9 --- /dev/null +++ b/tests/Refasmer.Tests/IntegrationTestBase.cs @@ -0,0 +1,106 @@ +using System.Globalization; +using System.Text; +using Medallion.Shell; +using Mono.Cecil; + +namespace JetBrains.Refasmer.Tests; + +public abstract class IntegrationTestBase +{ + protected static async Task BuildTestAssembly() + { + var root = FindSourceRoot(); + var testProject = Path.Combine(root, "tests/RefasmerTestAssembly/RefasmerTestAssembly.csproj"); + Console.WriteLine($"Building project {testProject}…"); + var result = await Command.Run("dotnet", "build", testProject, "--configuration", "Release").Task; + + Assert.That( + result.ExitCode, + Is.EqualTo(0), + $"Failed to build test assembly, exit code {result.ExitCode}. StdOut:\n{result.StandardOutput}\nStdErr: {result.StandardError}"); + + return Path.Combine(root, "tests/RefasmerTestAssembly/bin/Release/net6.0/RefasmerTestAssembly.dll"); + } + + private static string FindSourceRoot() + { + var current = Directory.GetCurrentDirectory(); + while (current != null) + { + if (File.Exists(Path.Combine(current, "README.md"))) + return current; + current = Path.GetDirectoryName(current); + } + throw new Exception("Cannot find source root."); + } + + protected static int ExecuteRefasmAndGetExitCode( + string assemblyPath, + string outputPath, + params string[] additionalOptions) + { + var args = new List + { + "-v", + $"--output={outputPath}" + }; + args.AddRange(additionalOptions); + args.Add(assemblyPath); + return Program.Main(args.ToArray()); + } + + protected static string RefasmTestAssembly(string assemblyPath, bool omitNonApiMembers = false) + { + var outputPath = Path.GetTempFileName(); + var options = new[]{ "--omit-non-api-members", omitNonApiMembers.ToString(CultureInfo.InvariantCulture) }; + using var collector = CollectConsoleOutput(); + var exitCode = ExecuteRefasmAndGetExitCode(assemblyPath, outputPath, options); + Assert.That( + exitCode, + Is.EqualTo(0), + $"Refasmer returned exit code {exitCode}. StdOut:\n{collector.StdOut}\nStdErr: {collector.StdErr}"); + + return outputPath; + } + + protected static Task VerifyTypeContent(string assemblyPath, string typeName) + { + var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); + var type = assembly.MainModule.GetType(typeName); + Assert.That(assembly.MainModule.GetType(typeName), Is.Not.Null); + + var printout = new StringBuilder(); + Printer.PrintType(type, printout); + + var verifySettings = new VerifySettings(); + verifySettings.DisableDiff(); + verifySettings.UseDirectory("data"); + verifySettings.UseParameters(typeName); + return Verify(printout, verifySettings); + } + + protected class Outputs : IDisposable + { + public StringBuilder StdOut { get; } = new(); + public StringBuilder StdErr { get; } = new(); + + private readonly TextWriter _oldStdOut; + private readonly TextWriter _oldStdErr; + + public Outputs() + { + _oldStdOut = Console.Out; + _oldStdErr = Console.Error; + Console.SetOut(new StringWriter(StdOut)); + Console.SetError(new StringWriter(StdErr)); + } + + public void Dispose() + { + Console.SetError(_oldStdErr); + Console.SetOut(_oldStdOut); + } + } + + protected static Outputs CollectConsoleOutput() => new(); +} diff --git a/tests/Refasmer.Tests/IntegrationTests.cs b/tests/Refasmer.Tests/IntegrationTests.cs index 40a94a1..086d9e1 100644 --- a/tests/Refasmer.Tests/IntegrationTests.cs +++ b/tests/Refasmer.Tests/IntegrationTests.cs @@ -1,15 +1,15 @@ -using System.Text; -using Medallion.Shell; -using Mono.Cecil; - namespace JetBrains.Refasmer.Tests; -public class IntegrationTests +public class IntegrationTests : IntegrationTestBase { [TestCase("RefasmerTestAssembly.PublicClassWithPrivateFields")] [TestCase("RefasmerTestAssembly.PublicStructWithPrivateFields")] [TestCase("RefasmerTestAssembly.UnsafeClassWithFunctionPointer")] [TestCase("RefasmerTestAssembly.StructWithNestedPrivateTypes")] + [TestCase("RefasmerTestAssembly.BlittableGraph")] + [TestCase("RefasmerTestAssembly.BlittableStructWithPrivateFields")] + [TestCase("RefasmerTestAssembly.NonBlittableStructWithPrivateFields")] + [TestCase("RefasmerTestAssembly.NonBlittableGraph")] public async Task CheckRefasmedType(string typeName) { var assemblyPath = await BuildTestAssembly(); @@ -17,53 +17,20 @@ public async Task CheckRefasmedType(string typeName) await VerifyTypeContent(resultAssembly, typeName); } - private static async Task BuildTestAssembly() - { - var root = FindSourceRoot(); - var testProject = Path.Combine(root, "tests/RefasmerTestAssembly/RefasmerTestAssembly.csproj"); - Console.WriteLine($"Building project {testProject}…"); - var result = await Command.Run("dotnet", "build", testProject, "--configuration", "Release").Task; - - Assert.That( - result.ExitCode, - Is.EqualTo(0), - $"Failed to build test assembly, exit code {result.ExitCode}. StdOut:\n{result.StandardOutput}\nStdErr: {result.StandardError}"); - - return Path.Combine(root, "tests/RefasmerTestAssembly/bin/Release/net6.0/RefasmerTestAssembly.dll"); - } - - private static string FindSourceRoot() - { - var current = Directory.GetCurrentDirectory(); - while (current != null) - { - if (File.Exists(Path.Combine(current, "README.md"))) - return current; - current = Path.GetDirectoryName(current); - } - throw new Exception("Cannot find source root."); - } - - private static string RefasmTestAssembly(string assemblyPath) - { - var tempLocation = Path.GetTempFileName(); - var exitCode = Program.Main(new[] { $"-v", $"--output={tempLocation}", assemblyPath }); - Assert.That(exitCode, Is.EqualTo(0)); - - return tempLocation; - } - - private static Task VerifyTypeContent(string assemblyPath, string typeName) + [TestCase("RefasmerTestAssembly.PublicClassWithPrivateFields")] + [TestCase("RefasmerTestAssembly.PublicStructWithPrivateFields")] + [TestCase("RefasmerTestAssembly.UnsafeClassWithFunctionPointer")] + [TestCase("RefasmerTestAssembly.StructWithNestedPrivateTypes")] + [TestCase("RefasmerTestAssembly.BlittableGraph")] + [TestCase("RefasmerTestAssembly.BlittableStructWithPrivateFields")] + [TestCase("RefasmerTestAssembly.NonBlittableStructWithPrivateFields")] + [TestCase("RefasmerTestAssembly.NonBlittableGraph")] + [TestCase("RefasmerTestAssembly.EmptyStructWithStaticMember")] + [TestCase("RefasmerTestAssembly.NonEmptyStructWithStaticMember")] + public async Task CheckRefasmedTypeOmitNonApi(string typeName) { - var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); - var type = assembly.MainModule.GetType(typeName); - var printout = new StringBuilder(); - Printer.PrintType(type, printout); - - var verifySettings = new VerifySettings(); - verifySettings.DisableDiff(); - verifySettings.UseDirectory("data"); - verifySettings.UseParameters(typeName); - return Verify(printout, verifySettings); + var assemblyPath = await BuildTestAssembly(); + var resultAssembly = RefasmTestAssembly(assemblyPath, omitNonApiMembers: true); + await VerifyTypeContent(resultAssembly, typeName); } -} \ No newline at end of file +} diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.BlittableGraph.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.BlittableGraph.verified.txt new file mode 100644 index 0000000..5d625ae --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.BlittableGraph.verified.txt @@ -0,0 +1,3 @@ +public type: RefasmerTestAssembly.BlittableGraph +fields: +- : System.Int32 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.BlittableStructWithPrivateFields.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.BlittableStructWithPrivateFields.verified.txt new file mode 100644 index 0000000..2d4e000 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.BlittableStructWithPrivateFields.verified.txt @@ -0,0 +1,3 @@ +public type: RefasmerTestAssembly.BlittableStructWithPrivateFields +fields: +- : System.Int32 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.EmptyStructWithStaticMember.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.EmptyStructWithStaticMember.verified.txt new file mode 100644 index 0000000..cb9c757 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.EmptyStructWithStaticMember.verified.txt @@ -0,0 +1 @@ +public type: RefasmerTestAssembly.EmptyStructWithStaticMember diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonBlittableGraph.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonBlittableGraph.verified.txt new file mode 100644 index 0000000..2b70304 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonBlittableGraph.verified.txt @@ -0,0 +1,3 @@ +public type: RefasmerTestAssembly.NonBlittableGraph +fields: +- : System.Int32 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonBlittableStructWithPrivateFields.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonBlittableStructWithPrivateFields.verified.txt new file mode 100644 index 0000000..3e5a629 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonBlittableStructWithPrivateFields.verified.txt @@ -0,0 +1,3 @@ +public type: RefasmerTestAssembly.NonBlittableStructWithPrivateFields +fields: +- : System.Int32 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonEmptyStructWithStaticMember.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonEmptyStructWithStaticMember.verified.txt new file mode 100644 index 0000000..a5eb51c --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.NonEmptyStructWithStaticMember.verified.txt @@ -0,0 +1,3 @@ +public type: RefasmerTestAssembly.NonEmptyStructWithStaticMember +fields: +- : System.Int32 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.PublicClassWithPrivateFields.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.PublicClassWithPrivateFields.verified.txt new file mode 100644 index 0000000..b3096b0 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.PublicClassWithPrivateFields.verified.txt @@ -0,0 +1,5 @@ +public type: RefasmerTestAssembly.PublicClassWithPrivateFields +fields: +- PublicInt: System.Int32 +methods: +- .ctor(): System.Void: diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.PublicStructWithPrivateFields.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.PublicStructWithPrivateFields.verified.txt new file mode 100644 index 0000000..ed60ee4 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.PublicStructWithPrivateFields.verified.txt @@ -0,0 +1,3 @@ +public type: RefasmerTestAssembly.PublicStructWithPrivateFields +fields: +- PublicInt: System.Int32 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.StructWithNestedPrivateTypes.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.StructWithNestedPrivateTypes.verified.txt new file mode 100644 index 0000000..08cefb4 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.StructWithNestedPrivateTypes.verified.txt @@ -0,0 +1,7 @@ +public type: RefasmerTestAssembly.StructWithNestedPrivateTypes +fields: +- : System.Int32 +types: + internal type: RefasmerTestAssembly.StructWithNestedPrivateTypes/UnusedPublicStruct + fields: + - : System.Int32 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.UnsafeClassWithFunctionPointer.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.UnsafeClassWithFunctionPointer.verified.txt new file mode 100644 index 0000000..679ec12 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedTypeOmitNonApi_typeName=RefasmerTestAssembly.UnsafeClassWithFunctionPointer.verified.txt @@ -0,0 +1,4 @@ +public type: RefasmerTestAssembly.UnsafeClassWithFunctionPointer +methods: +- MethodWithFunctionPointer(method System.Void *() functionPointer): System.Void: +- .ctor(): System.Void: diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.BlittableGraph.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.BlittableGraph.verified.txt new file mode 100644 index 0000000..cdab51f --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.BlittableGraph.verified.txt @@ -0,0 +1,11 @@ +public type: RefasmerTestAssembly.BlittableGraph +fields: +- x: RefasmerTestAssembly.BlittableGraph/BlittableType +types: + private type: RefasmerTestAssembly.BlittableGraph/BlittableType + fields: + - x: RefasmerTestAssembly.BlittableGraph/BlittableType/BlittableType2 + types: + private type: RefasmerTestAssembly.BlittableGraph/BlittableType/BlittableType2 + fields: + - x: System.Int64 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.BlittableStructWithPrivateFields.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.BlittableStructWithPrivateFields.verified.txt new file mode 100644 index 0000000..f0c9424 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.BlittableStructWithPrivateFields.verified.txt @@ -0,0 +1,5 @@ +public type: RefasmerTestAssembly.BlittableStructWithPrivateFields +fields: +- x: System.Int64 +- y: System.Int64 +- z: System.Int64 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.NonBlittableGraph.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.NonBlittableGraph.verified.txt new file mode 100644 index 0000000..940bc52 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.NonBlittableGraph.verified.txt @@ -0,0 +1,11 @@ +public type: RefasmerTestAssembly.NonBlittableGraph +fields: +- x: RefasmerTestAssembly.NonBlittableGraph/DubiouslyBlittableType +types: + private type: RefasmerTestAssembly.NonBlittableGraph/DubiouslyBlittableType + fields: + - x: RefasmerTestAssembly.NonBlittableGraph/DubiouslyBlittableType/NonBlittableType + types: + private type: RefasmerTestAssembly.NonBlittableGraph/DubiouslyBlittableType/NonBlittableType + fields: + - x: System.String diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.NonBlittableStructWithPrivateFields.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.NonBlittableStructWithPrivateFields.verified.txt new file mode 100644 index 0000000..ff3da32 --- /dev/null +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.NonBlittableStructWithPrivateFields.verified.txt @@ -0,0 +1,4 @@ +public type: RefasmerTestAssembly.NonBlittableStructWithPrivateFields +fields: +- x: System.String +- y: System.Int64 diff --git a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.StructWithNestedPrivateTypes.verified.txt b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.StructWithNestedPrivateTypes.verified.txt index 71576b1..0d0b218 100644 --- a/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.StructWithNestedPrivateTypes.verified.txt +++ b/tests/Refasmer.Tests/data/IntegrationTests.CheckRefasmedType_typeName=RefasmerTestAssembly.StructWithNestedPrivateTypes.verified.txt @@ -5,3 +5,9 @@ types: private type: RefasmerTestAssembly.StructWithNestedPrivateTypes/NestedPrivateStruct fields: - Field: System.Int32 + private type: RefasmerTestAssembly.StructWithNestedPrivateTypes/UnusedPrivateStruct + fields: + - Field: System.Int32 + internal type: RefasmerTestAssembly.StructWithNestedPrivateTypes/UnusedPublicStruct + fields: + - Field: System.Int32 diff --git a/tests/RefasmerTestAssembly/BlittableGraph.cs b/tests/RefasmerTestAssembly/BlittableGraph.cs new file mode 100644 index 0000000..d3a2331 --- /dev/null +++ b/tests/RefasmerTestAssembly/BlittableGraph.cs @@ -0,0 +1,15 @@ +namespace RefasmerTestAssembly; + +public struct BlittableGraph +{ + private BlittableType x; + private struct BlittableType + { + private BlittableType2 x; + + private struct BlittableType2 + { + private long x; + } + } +} diff --git a/tests/RefasmerTestAssembly/BlittableStructWithPrivateFields.cs b/tests/RefasmerTestAssembly/BlittableStructWithPrivateFields.cs new file mode 100644 index 0000000..f563617 --- /dev/null +++ b/tests/RefasmerTestAssembly/BlittableStructWithPrivateFields.cs @@ -0,0 +1,8 @@ +namespace RefasmerTestAssembly; + +public struct BlittableStructWithPrivateFields +{ + private long x; + private long y; + private long z; +} \ No newline at end of file diff --git a/tests/RefasmerTestAssembly/ClassWithLambda.cs b/tests/RefasmerTestAssembly/ClassWithLambda.cs new file mode 100644 index 0000000..bcb9c19 --- /dev/null +++ b/tests/RefasmerTestAssembly/ClassWithLambda.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Linq; + +namespace RefasmerTestAssembly; + +public class ClassWithLambda +{ + public int Method() + { + return new List{1,2,3}.Select(x => x * 2).Sum(); + } +} \ No newline at end of file diff --git a/tests/RefasmerTestAssembly/EmptyStructWithStaticMember.cs b/tests/RefasmerTestAssembly/EmptyStructWithStaticMember.cs new file mode 100644 index 0000000..b5ae8dc --- /dev/null +++ b/tests/RefasmerTestAssembly/EmptyStructWithStaticMember.cs @@ -0,0 +1,8 @@ +// ReSharper disable InconsistentNaming +#pragma warning disable CS0169 +namespace RefasmerTestAssembly; + +public struct EmptyStructWithStaticMember +{ + private static int StaticPrivateInt; +} diff --git a/tests/RefasmerTestAssembly/NonBlittableGraph.cs b/tests/RefasmerTestAssembly/NonBlittableGraph.cs new file mode 100644 index 0000000..4adc61e --- /dev/null +++ b/tests/RefasmerTestAssembly/NonBlittableGraph.cs @@ -0,0 +1,15 @@ +namespace RefasmerTestAssembly; + +public struct NonBlittableGraph +{ + private DubiouslyBlittableType x; + private struct DubiouslyBlittableType + { + private NonBlittableType x; + + private struct NonBlittableType + { + private string x; + } + } +} diff --git a/tests/RefasmerTestAssembly/NonBlittableStructWithPrivateFields.cs b/tests/RefasmerTestAssembly/NonBlittableStructWithPrivateFields.cs new file mode 100644 index 0000000..40c8307 --- /dev/null +++ b/tests/RefasmerTestAssembly/NonBlittableStructWithPrivateFields.cs @@ -0,0 +1,7 @@ +namespace RefasmerTestAssembly; + +public struct NonBlittableStructWithPrivateFields +{ + private string x; + private long y; +} \ No newline at end of file diff --git a/tests/RefasmerTestAssembly/NonEmptyStructWithStaticMember.cs b/tests/RefasmerTestAssembly/NonEmptyStructWithStaticMember.cs new file mode 100644 index 0000000..534b431 --- /dev/null +++ b/tests/RefasmerTestAssembly/NonEmptyStructWithStaticMember.cs @@ -0,0 +1,9 @@ +// ReSharper disable InconsistentNaming +#pragma warning disable CS0169 +namespace RefasmerTestAssembly; + +public struct NonEmptyStructWithStaticMember +{ + private static int StaticPrivateInt; + private int PrivateInt; +} \ No newline at end of file diff --git a/tests/RefasmerTestAssembly/RefasmerTestAssembly.csproj b/tests/RefasmerTestAssembly/RefasmerTestAssembly.csproj index b3cc109..0fbe834 100644 --- a/tests/RefasmerTestAssembly/RefasmerTestAssembly.csproj +++ b/tests/RefasmerTestAssembly/RefasmerTestAssembly.csproj @@ -4,6 +4,7 @@ net6.0 Latest true + true diff --git a/tests/RefasmerTestAssembly/StructWithNestedPrivateTypes.cs b/tests/RefasmerTestAssembly/StructWithNestedPrivateTypes.cs index b311a0c..038fde6 100644 --- a/tests/RefasmerTestAssembly/StructWithNestedPrivateTypes.cs +++ b/tests/RefasmerTestAssembly/StructWithNestedPrivateTypes.cs @@ -8,4 +8,14 @@ private struct NestedPrivateStruct } private NestedPrivateStruct PrivateField; + + private struct UnusedPrivateStruct + { + private int Field; + } + + public struct UnusedPublicStruct + { + private int Field; + } } \ No newline at end of file