From 3719330a9ef9c1d95197bdf22497c4bb06a8b245 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 27 Jul 2026 21:45:20 -0700 Subject: [PATCH] Improve ILAssembler test coverage Add feature-focused metadata and PE validation tests and fix emitter issues exposed by them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4307156b-37a7-495d-9af6-fe9174c05b0b --- .../ilasm/src/ILAssembler/GrammarVisitor.cs | 366 +++++++++-- .../src/ILAssembler/VTableExportPEBuilder.cs | 106 ++-- .../tests/ILAssembler.Tests/AssemblyTests.cs | 85 +++ .../ILAssembler.Tests/CompilerOptionsTests.cs | 431 +++++++++++++ .../ILAssembler.Tests/CustomAttributeTests.cs | 575 ++++++++++++++++++ .../tests/ILAssembler.Tests/DataTests.cs | 140 +++++ .../DocumentCompilerTestHelpers.cs | 238 +++++++- .../DocumentCompilerTests.cs | 59 ++ .../tests/ILAssembler.Tests/EventTests.cs | 106 ++++ .../ExceptionHandlingTests.cs | 245 ++++++++ .../ILAssembler.Tests/ExportedTypeTests.cs | 91 +++ .../tests/ILAssembler.Tests/FieldTests.cs | 42 ++ .../ILAssembler.Tests/FunctionPointerTests.cs | 124 ++-- .../tests/ILAssembler.Tests/GenericTests.cs | 248 +++++++- .../ILAssembler.Tests/InstructionTests.cs | 393 ++++++++++++ .../tests/ILAssembler.Tests/InteropTests.cs | 447 ++++++++++++++ .../ManifestResourceTests.cs | 268 ++++++++ .../ILAssembler.Tests/MemberReferenceTests.cs | 235 ++++++- .../tests/ILAssembler.Tests/MethodTests.cs | 250 ++++++++ .../tests/ILAssembler.Tests/ModuleTests.cs | 126 ++++ .../ILAssembler.Tests/NativeExportTests.cs | 149 +++++ .../tests/ILAssembler.Tests/ParameterTests.cs | 32 + .../PreprocessorIntegrationTests.cs | 154 +++++ .../ILAssembler.Tests/PropertyEventTests.cs | 88 +++ .../tests/ILAssembler.Tests/PropertyTests.cs | 84 +++ .../tests/ILAssembler.Tests/SecurityTests.cs | 172 ++++++ .../ILAssembler.Tests/SourceDirectiveTests.cs | 224 ++++++- .../ILAssembler.Tests/TypeDefinitionTests.cs | 215 +++++++ .../ILAssembler.Tests/TypeReferenceTests.cs | 130 ++++ .../ILAssembler.Tests/TypeSignatureTests.cs | 86 +++ .../tests/ILAssembler.Tests/TypedefTests.cs | 46 ++ .../tests/ILAssembler.Tests/VTableTests.cs | 1 + 32 files changed, 5749 insertions(+), 207 deletions(-) create mode 100644 src/tools/ilasm/tests/ILAssembler.Tests/CompilerOptionsTests.cs create mode 100644 src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs create mode 100644 src/tools/ilasm/tests/ILAssembler.Tests/ManifestResourceTests.cs create mode 100644 src/tools/ilasm/tests/ILAssembler.Tests/NativeExportTests.cs create mode 100644 src/tools/ilasm/tests/ILAssembler.Tests/PreprocessorIntegrationTests.cs create mode 100644 src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs diff --git a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs index 7616d93c85ba82..8543239d09a5ca 100644 --- a/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs +++ b/src/tools/ilasm/src/ILAssembler/GrammarVisitor.cs @@ -162,9 +162,9 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC // Check for vtable fixups and exports - collect export info var exports = ImmutableArray.CreateBuilder(); - foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef)) + foreach (EntityRegistry.MethodDefinitionEntity method in GetParsedMethods()) { - if (entity is EntityRegistry.MethodDefinitionEntity method && method.ExportOrdinal >= 0) + if (method.ExportOrdinal >= 0) { exports.Add(new VTableExportPEBuilder.ExportInfo( method.ExportOrdinal, @@ -313,10 +313,9 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC } // Find methods that reference this vtable entry - foreach (var entity in _entityRegistry.GetSeenEntities(TableIndex.MethodDef)) + foreach (EntityRegistry.MethodDefinitionEntity method in GetParsedMethods()) { - if (entity is EntityRegistry.MethodDefinitionEntity method && - method.VTableEntry == entryIndex + 1 && // 1-based + if (method.VTableEntry == entryIndex + 1 && // 1-based method.VTableSlot > 0 && method.VTableSlot <= vtf.SlotCount) { @@ -334,6 +333,17 @@ private void ReportWarning(string id, string message, Antlr4.Runtime.ParserRuleC return builder.ToImmutable(); } + private IEnumerable GetParsedMethods() + { + foreach (EntityRegistry.TypeDefinitionEntity type in _entityRegistry.GetSeenEntities(TableIndex.TypeDef)) + { + foreach (EntityRegistry.MethodDefinitionEntity method in type.Methods) + { + yield return method; + } + } + } + private DebugDirectoryBuilder? BuildDebugDirectory(MethodDefinitionHandle entryPoint, out int debugDataSize) { debugDataSize = 0; @@ -453,8 +463,8 @@ private static BlobBuilder EncodeSequencePoints(List VisitCallConv(CILParser.CallConvContext conte } else if (context.EXPLICIT() is not null) { - return new((byte)(VisitCallConv(context.callConv()).Value | (byte)SignatureAttributes.ExplicitThis)); + return new((byte)( + VisitCallConv(context.callConv()).Value | + (byte)(SignatureAttributes.ExplicitThis | SignatureAttributes.Instance))); } return new(0); } @@ -1072,9 +1085,18 @@ public CurrentMethodContext(EntityRegistry.MethodDefinitionEntity definition) } private CurrentMethodContext? _currentMethod; + private EntityRegistry.EntityBase? _pendingClassCustomAttributeOwner; public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) { + bool isStandaloneCustomAttribute = + context.customAttrDecl().Length == 1 && + context.PARAM() is null; + if (!isStandaloneCustomAttribute) + { + _pendingClassCustomAttributeOwner = null; + } + if (context.classHead() is CILParser.ClassHeadContext classHead) { _currentTypeDefinition.Push(VisitClassHead(classHead).Value); @@ -1106,10 +1128,98 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) var declarativeSecurity = VisitSecDecl(secDecl).Value; declarativeSecurity?.Parent = _currentTypeDefinition.PeekOrDefault(); } + else if (context.TYPE() is not null && + context.typeSpec().Length == 1 && + context.customDescr() is { } interfaceAttribute) + { + var currentType = _currentTypeDefinition.PeekOrDefault(); + if (currentType is not null) + { + EntityRegistry.TypeEntity interfaceType = VisitTypeSpec(context.typeSpec()[0]).Value; + EntityRegistry.InterfaceImplementationEntity? implementation = + currentType.InterfaceImplementations.FirstOrDefault( + candidate => candidate.InterfaceType == interfaceType); + if (implementation is null) + { + implementation = + EntityRegistry.CreateUnrecordedInterfaceImplementation(currentType, interfaceType); + currentType.InterfaceImplementations.Add(implementation); + } + + VisitCustomDescr(interfaceAttribute).Value.Owner = implementation; + } + } else if (context.fieldDecl() is {} fieldDecl) { _ = VisitFieldDecl(fieldDecl); } + else if (context.dataDecl() is { } dataDecl) + { + _ = VisitDataDecl(dataDecl); + } + else if (context.extSourceSpec() is { } extSourceSpec) + { + _ = VisitExtSourceSpec(extSourceSpec); + } + else if (context.languageDecl() is { } languageDecl) + { + _ = VisitLanguageDecl(languageDecl); + } + else if (context.OVERRIDE() is not null) + { + var currentType = _currentTypeDefinition.PeekOrDefault(); + if (currentType is not null) + { + var typeSpecs = context.typeSpec(); + var methodNames = context.methodName(); + var callConventions = context.callConv(); + var returnTypes = context.type(); + var signatureArguments = context.sigArgs(); + var genericArities = context.genArity(); + + int bodySignatureIndex = context.METHOD().Length == 0 ? 0 : 1; + BlobBuilder declarationSignature = BuildMethodReferenceSignature( + callConventions[0], + returnTypes[0], + signatureArguments[0], + genericArities.Length > 0 ? VisitGenArity(genericArities[0]).Value : 0); + BlobBuilder bodySignature = context.METHOD().Length == 0 + ? declarationSignature + : BuildMethodReferenceSignature( + callConventions[bodySignatureIndex], + returnTypes[bodySignatureIndex], + signatureArguments[bodySignatureIndex], + genericArities.Length > bodySignatureIndex + ? VisitGenArity(genericArities[bodySignatureIndex]).Value + : 0); + + string bodyName = VisitMethodName(methodNames[1]).Value; + EntityRegistry.MethodDefinitionEntity[] bodyMethods = currentType.Methods + .Where(method => + method.Name == bodyName && + method.MethodSignature is not null && + method.MethodSignature.ContentEquals(bodySignature)) + .Take(2) + .ToArray(); + if (bodyMethods.Length != 1) + { + ReportError( + DiagnosticIds.InvalidMetadataToken, + $"Override body method '{bodyName}' could not be resolved uniquely", + context); + return GrammarResult.SentinelValue.Result; + } + + EntityRegistry.MethodDefinitionEntity bodyMethod = bodyMethods[0]; + EntityRegistry.MemberReferenceEntity declaration = + _entityRegistry.CreateLazilyRecordedMemberReference( + VisitTypeSpec(typeSpecs[0]).Value, + VisitMethodName(methodNames[0]).Value, + declarationSignature); + currentType.MethodImplementations.Add( + EntityRegistry.CreateUnrecordedMethodImplementation(bodyMethod, declaration)); + } + } else if (context.int32() is {} int32) { // .pack or .size @@ -1176,12 +1286,13 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) } } } - else if (context.customAttrDecl().Length == 1 && context.PARAM() is null) + else if (isStandaloneCustomAttribute) { - // Custom attribute directly on the type if (VisitCustomAttrDecl(context.customAttrDecl()[0]).Value is { } customAttr) { - customAttr.Owner = _currentTypeDefinition.PeekOrDefault(); + customAttr.Owner = + _pendingClassCustomAttributeOwner ?? + _currentTypeDefinition.PeekOrDefault(); } } else if (context.PARAM() is not null) @@ -1198,6 +1309,13 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) { param = currentType.GenericParameters[index]; } + else + { + ReportError( + DiagnosticIds.GenericParameterIndexOutOfRange, + string.Format(DiagnosticMessageTemplates.GenericParameterIndexOutOfRange, index), + context); + } } else if (context.dottedName() is { } dn) { @@ -1218,6 +1336,7 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) var customAttrDecl = VisitCustomAttrDecl(attr).Value; customAttrDecl?.Owner = param; } + _pendingClassCustomAttributeOwner = param; } } else if (currentType is not null && context.CONSTRAINT() is not null) @@ -1230,6 +1349,13 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) { param = currentType.GenericParameters[index]; } + else + { + ReportError( + DiagnosticIds.GenericParameterIndexOutOfRange, + string.Format(DiagnosticMessageTemplates.GenericParameterIndexOutOfRange, index), + context); + } } else if (context.dottedName() is { } dn) { @@ -1246,21 +1372,58 @@ public GrammarResult VisitClassDecl(CILParser.ClassDeclContext context) if (param is not null) { var baseType = VisitTypeSpec(context.typeSpec()[0]).Value; - var constraint = new EntityRegistry.GenericParameterConstraintEntity(baseType); - constraint.Owner = param; - param.Constraints.Add(constraint); - currentType.GenericParameterConstraints.Add(constraint); + EntityRegistry.GenericParameterConstraintEntity? constraint = + param.Constraints.FirstOrDefault(entity => entity.BaseType == baseType); + if (constraint is null) + { + constraint = EntityRegistry.CreateGenericConstraint(baseType); + constraint.Owner = param; + param.Constraints.Add(constraint); + currentType.GenericParameterConstraints.Add(constraint); + } foreach (var attr in customAttrDeclarations ?? Array.Empty()) { var customAttrDecl = VisitCustomAttrDecl(attr).Value; customAttrDecl?.Owner = constraint; } + _pendingClassCustomAttributeOwner = constraint; } } } return GrammarResult.SentinelValue.Result; } + + private BlobBuilder BuildMethodReferenceSignature( + CILParser.CallConvContext callConvention, + CILParser.TypeContext returnType, + CILParser.SigArgsContext signatureArguments, + int genericArity) + { + var signature = new BlobBuilder(); + byte header = VisitCallConv(callConvention).Value; + if (genericArity > 0) + { + header |= (byte)SignatureAttributes.Generic; + } + + signature.WriteByte(header); + if (genericArity > 0) + { + signature.WriteCompressedInteger(genericArity); + } + + ImmutableArray arguments = VisitSigArgs(signatureArguments).Value; + signature.WriteCompressedInteger(arguments.Count(argument => !argument.IsSentinel)); + VisitType(returnType).Value.WriteContentTo(signature); + foreach (SignatureArg argument in arguments) + { + argument.SignatureBlob.WriteContentTo(signature); + } + + return signature; + } + public GrammarResult VisitClassDecls(CILParser.ClassDeclsContext context) => VisitChildren(context); @@ -1635,11 +1798,10 @@ EntityRegistry.TypeEntity ResolveTypeDef() GrammarResult ICILVisitor.VisitClassSeq(CILParser.ClassSeqContext context) => VisitClassSeq(context); public GrammarResult.FormattedBlob VisitClassSeq(CILParser.ClassSeqContext context) { - // We're going to add all of the elements in the sequence as prefix blobs to this blob. BlobBuilder objSeqBlob = new(0); foreach (var item in context.classSeqElement()) { - objSeqBlob.LinkPrefix(VisitClassSeqElement(item).Value); + objSeqBlob.LinkSuffix(VisitClassSeqElement(item).Value); } return new(objSeqBlob); } @@ -1662,7 +1824,10 @@ public GrammarResult.FormattedBlob VisitClassSeqElement(CILParser.ClassSeqElemen return new(blob); } - blob.WriteSerializedString(context.SQSTRING()?.Symbol.Text); + blob.WriteSerializedString( + context.SQSTRING() is { } stringNode + ? StringHelpers.ParseQuotedString(stringNode.Symbol.Text) + : null); return new(blob); } public GrammarResult VisitCompControl(CILParser.CompControlContext context) @@ -1813,7 +1978,9 @@ public GrammarResult.FormattedBlob VisitCustomBlobNVPairs(CILParser.CustomBlobNV } else { - throw new UnreachableException(); + value = new(); + value.WriteUInt16(CustomAttributeBlobFormatVersion); + value.WriteUInt16(0); } return new(_entityRegistry.CreateCustomAttribute(ctor, value)); @@ -1842,7 +2009,9 @@ public GrammarResult.FormattedBlob VisitCustomBlobNVPairs(CILParser.CustomBlobNV } else { - throw new UnreachableException(); + value = new(); + value.WriteUInt16(CustomAttributeBlobFormatVersion); + value.WriteUInt16(0); } var attr = _entityRegistry.CreateCustomAttribute(ctor, value); @@ -2686,13 +2855,36 @@ public GrammarResult VisitExtSourceSpec(CILParser.ExtSourceSpecContext context) // 0xFEEFEE indicates a hidden sequence point if (startLine == 0xFEEFEE) { - _currentMethod.Definition.DebugInfo.SequencePoints.Add( - EntityRegistry.SequencePoint.Hidden(ilOffset)); + AddSequencePoint(EntityRegistry.SequencePoint.Hidden(ilOffset)); } else { - _currentMethod.Definition.DebugInfo.SequencePoints.Add( - new EntityRegistry.SequencePoint(ilOffset, startLine, startColumn, endLine, endColumn)); + if (endLine == startLine && endColumn == startColumn) + { + endColumn++; + } + + AddSequencePoint( + new EntityRegistry.SequencePoint( + ilOffset, + startLine, + startColumn, + endLine, + endColumn)); + } + + void AddSequencePoint(EntityRegistry.SequencePoint sequencePoint) + { + List sequencePoints = + _currentMethod.Definition.DebugInfo.SequencePoints; + if (sequencePoints.Count > 0 && sequencePoints[^1].ILOffset == ilOffset) + { + sequencePoints[^1] = sequencePoint; + } + else + { + sequencePoints.Add(sequencePoint); + } } } @@ -2713,7 +2905,7 @@ public GrammarResult.FormattedBlob VisitF32seq(CILParser.F32seqContext context) _ => throw new UnreachableException() })); } - return new(builder.MoveToImmutable().SerializeSequence()); + return new(builder.ToImmutable().SerializeSequence()); } GrammarResult ICILVisitor.VisitF64seq(CILParser.F64seqContext context) => VisitF64seq(context); public GrammarResult.FormattedBlob VisitF64seq(CILParser.F64seqContext context) @@ -2729,7 +2921,7 @@ public GrammarResult.FormattedBlob VisitF64seq(CILParser.F64seqContext context) _ => throw new UnreachableException() })); } - return new(builder.MoveToImmutable().SerializeSequence()); + return new(builder.ToImmutable().SerializeSequence()); } public GrammarResult VisitFaultClause(CILParser.FaultClauseContext context) => throw new UnreachableException(NodeShouldNeverBeDirectlyVisited); @@ -2989,7 +3181,18 @@ public GrammarResult.FormattedBlob VisitFieldSerInit(CILParser.FieldSerInitConte { if (context.float64() is CILParser.Float64Context float64) { - builder.WriteSingle((float)VisitFloat64(float64).Value); + string text = float64.GetText(); + if (!text.Contains('.') && + text.IndexOf('e') < 0 && + text.IndexOf('E') < 0 && + ParseIntegerValue(text.AsSpan(), out long rawValue)) + { + builder.WriteSingle(BitConverter.Int32BitsToSingle((int)rawValue)); + } + else + { + builder.WriteSingle((float)VisitFloat64(float64).Value); + } } if (context.int32() is CILParser.Int32Context int32) { @@ -2998,11 +3201,22 @@ public GrammarResult.FormattedBlob VisitFieldSerInit(CILParser.FieldSerInitConte } break; } - case CILParser.FLOAT64: + case CILParser.FLOAT64_: { if (context.float64() is CILParser.Float64Context float64) { - builder.WriteDouble(VisitFloat64(float64).Value); + string text = float64.GetText(); + if (!text.Contains('.') && + text.IndexOf('e') < 0 && + text.IndexOf('E') < 0 && + ParseIntegerValue(text.AsSpan(), out long rawValue)) + { + builder.WriteDouble(BitConverter.Int64BitsToDouble(rawValue)); + } + else + { + builder.WriteDouble(VisitFloat64(float64).Value); + } } if (context.int64() is CILParser.Int64Context int64) { @@ -3027,8 +3241,8 @@ public GrammarResult.Literal VisitFileAttr(CILParser.FileAttrContext conte var hashBlob = hash is not null ? new BlobBuilder() : null; hashBlob?.WriteBytes(hash!.Value); - bool hasMetadata = context.fileAttr().Aggregate(true, (acc, attr) => acc || VisitFileAttr(attr).Value); - bool isEntrypoint = context.fileEntry().Aggregate(true, (acc, attr) => acc || VisitFileEntry(attr).Value); + bool hasMetadata = context.fileAttr().Aggregate(true, (acc, attr) => acc && VisitFileAttr(attr).Value); + bool isEntrypoint = context.fileEntry().Aggregate(false, (acc, attr) => acc || VisitFileEntry(attr).Value); var entity = _entityRegistry.GetOrCreateFile(dottedName, hasMetadata, hashBlob); if (isEntrypoint) { @@ -3451,6 +3665,7 @@ public GrammarResult VisitInstr(CILParser.InstrContext context) { arg.SignatureBlob.WriteContentTo(signature); } + _currentMethod!.Definition.MethodBody.OpCode(opcode); _currentMethod!.Definition.MethodBody.Token(_entityRegistry.GetOrCreateStandaloneSignature(signature).Handle); } break; @@ -4584,7 +4799,10 @@ public GrammarResult VisitModuleHead(CILParser.ModuleHeadContext context) return GrammarResult.SentinelValue.Result; } - _entityRegistry.Module.Name = VisitDottedName(context.dottedName()).Value; + if (context.dottedName() is { } moduleName) + { + _entityRegistry.Module.Name = VisitDottedName(moduleName).Value; + } return GrammarResult.SentinelValue.Result; } @@ -4644,20 +4862,20 @@ public GrammarResult.FormattedBlob VisitNativeType(CILParser.NativeTypeContext c { if (arrayPointerInfo[i] is CILParser.PointerArrayTypeSizeContext size) { + suffix.WriteCompressedInteger(0); suffix.WriteCompressedInteger(VisitInt32(size.int32()).Value); + suffix.WriteCompressedInteger(0); } else if (arrayPointerInfo[i] is CILParser.PointerArrayTypeSizeParamIndexContext sizeParamIndex) { var ints = sizeParamIndex.int32(); suffix.WriteCompressedInteger(VisitInt32(ints[1]).Value); - suffix.WriteCompressedInteger(VisitInt32(ints[2]).Value); + suffix.WriteCompressedInteger(VisitInt32(ints[0]).Value); suffix.WriteCompressedInteger(1); // Write that the paramIndex parameter was specified } else if (arrayPointerInfo[i] is CILParser.PointerArrayTypeParamIndexContext paramIndex) { - suffix.WriteCompressedInteger(0); suffix.WriteCompressedInteger(VisitInt32(paramIndex.int32()).Value); - suffix.WriteCompressedInteger(0); // Write that the paramIndex parameter was not specified } } @@ -4669,7 +4887,7 @@ public GrammarResult.FormattedBlob VisitNativeType(CILParser.NativeTypeContext c GrammarResult ICILVisitor.VisitNativeTypeElement(CILParser.NativeTypeElementContext context) => VisitNativeTypeElement(context); public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeElementContext context) { - var blob = new BlobBuilder(5); + var blob = new BlobBuilder(); if (context.dottedName() is CILParser.DottedNameContext typedef) { // Native type typedefs are not yet fully supported @@ -4717,7 +4935,7 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl case CILParser.ARRAY: blob.WriteByte((byte)UnmanagedType.ByValArray); blob.WriteCompressedInteger(VisitInt32(context.int32()).Value); - blob.LinkSuffix(VisitNativeType(context.nativeType()).Value); + VisitNativeType(context.nativeType()).Value.WriteContentTo(blob); break; case CILParser.VARIANT: ReportWarning(DiagnosticIds.DeprecatedNativeType, @@ -4920,11 +5138,14 @@ public GrammarResult.FormattedBlob VisitNativeTypeElement(CILParser.NativeTypeEl GrammarResult ICILVisitor.VisitObjSeq(CILParser.ObjSeqContext context) => VisitObjSeq(context); public GrammarResult.FormattedBlob VisitObjSeq(CILParser.ObjSeqContext context) { - // We're going to add all of the elements in the sequence as prefix blobs to this blob. - BlobBuilder objSeqBlob = new(0); + BlobBuilder objSeqBlob = new(); foreach (var item in context.serInit()) { - objSeqBlob.LinkPrefix(VisitSerInit(item).Value); + if (item.serInit() is null) + { + WriteCustomAttributeFieldOrPropType(objSeqBlob, item); + } + objSeqBlob.LinkSuffix(VisitSerInit(item).Value); } return new(objSeqBlob); } @@ -5306,7 +5527,7 @@ public GrammarResult.FormattedBlob VisitSerializTypeElement(CILParser.SerializTy blob.WriteByte((byte)SerializationTypeCode.Enum); if (context.SQSTRING() is ITerminalNode sqString) { - blob.WriteSerializedString(sqString.GetText()); + blob.WriteSerializedString(StringHelpers.ParseQuotedString(sqString.GetText())); } else { @@ -5323,46 +5544,79 @@ public GrammarResult.FormattedBlob VisitSerInit(CILParser.SerInitContext context { if (context.fieldSerInit() is CILParser.FieldSerInitContext fieldSerInit) { - return VisitFieldSerInit(fieldSerInit); + if (fieldSerInit.bytes() is not null) + { + ReportError( + DiagnosticIds.InvalidMetadataToken, + "bytearray is not a valid structured custom attribute value", + context); + var invalidValue = new BlobBuilder(); + invalidValue.WriteSerializedString(null); + return new(invalidValue); + } + + ImmutableArray encodedValue = VisitFieldSerInit(fieldSerInit).Value.ToImmutableArray(); + var value = new BlobBuilder(Math.Max(0, encodedValue.Length - 1)); + if (encodedValue.Length > 1) + { + value.WriteBytes(encodedValue.AsSpan().Slice(1).ToArray()); + } + return new(value); } if (context.serInit() is CILParser.SerInitContext serInit) { Debug.Assert(context.OBJECT() is not null); - BlobBuilder taggedObjectBlob = new(1); - taggedObjectBlob.WriteByte((byte)SerializationTypeCode.TaggedObject); + BlobBuilder taggedObjectBlob = new(); + WriteCustomAttributeFieldOrPropType(taggedObjectBlob, serInit); taggedObjectBlob.LinkSuffix(VisitSerInit(serInit).Value); return new(taggedObjectBlob); } if (context.int32() is not CILParser.Int32Context arrLength) { - // The only cases where there is no int32 node is when the value is a string or type. BlobBuilder blob = new(); - blob.WriteByte((byte)GetTypeCodeForToken(((ITerminalNode)context.GetChild(0)).Symbol.Type)); if (context.className() is CILParser.ClassNameContext className) { blob.WriteSerializedString(VisitClassName(className).Value is EntityRegistry.IHasReflectionNotation reflection ? reflection.ReflectionNotation : string.Empty); } else { - blob.WriteSerializedString(context.SQSTRING()?.Symbol.Text); + blob.WriteSerializedString( + context.SQSTRING() is { } stringNode + ? StringHelpers.ParseQuotedString(stringNode.Symbol.Text) + : null); } return new(blob); } - int tokenType = ((ITerminalNode)context.GetChild(0)).Symbol.Type; - - // 1 byte for ELEMENT_TYPE_SZARRAY, 1 byte for the array element type, 4 bytes for the length. - BlobBuilder arrayHeader = new(6); - arrayHeader.WriteByte((byte)SerializationTypeCode.SZArray); - arrayHeader.WriteByte((byte)GetTypeCodeForToken(tokenType)); + BlobBuilder arrayHeader = new(sizeof(int)); arrayHeader.WriteInt32(VisitInt32(arrLength).Value); var sequenceResult = (GrammarResult.FormattedBlob)Visit(context.GetRuleContext(1)); arrayHeader.LinkSuffix(sequenceResult.Value); return new(arrayHeader); } + private static void WriteCustomAttributeFieldOrPropType( + BlobBuilder builder, + CILParser.SerInitContext context) + { + int tokenType = context.fieldSerInit() is { } fieldSerInit + ? ((ITerminalNode)fieldSerInit.GetChild(0)).Symbol.Type + : ((ITerminalNode)context.GetChild(0)).Symbol.Type; + if (context.fieldSerInit()?.bytes() is not null) + { + builder.WriteByte((byte)SerializationTypeCode.String); + return; + } + if (context.int32() is not null) + { + builder.WriteByte((byte)SerializationTypeCode.SZArray); + } + + builder.WriteByte((byte)GetTypeCodeForToken(tokenType)); + } + private static SerializationTypeCode GetTypeCodeForToken(int tokenType) { return tokenType switch @@ -5516,7 +5770,7 @@ public static GrammarResult.FormattedBlob VisitSqstringSeq(CILParser.SqstringSeq if (child is ITerminalNode { Symbol: { Type: CILParser.SQSTRING, Text: string stringValue } }) { - str = stringValue; + str = StringHelpers.ParseQuotedString(stringValue); } strings.Add(str); diff --git a/src/tools/ilasm/src/ILAssembler/VTableExportPEBuilder.cs b/src/tools/ilasm/src/ILAssembler/VTableExportPEBuilder.cs index b59c2c7f44eb37..3139e5aadb0e9a 100644 --- a/src/tools/ilasm/src/ILAssembler/VTableExportPEBuilder.cs +++ b/src/tools/ilasm/src/ILAssembler/VTableExportPEBuilder.cs @@ -190,16 +190,15 @@ protected override BlobBuilder SerializeSection(string name, SectionLocation loc /// private void PatchCorHeaderVTableFixups(BlobBuilder textSection, int _) { - // The COR header is at offset SizeOfImportAddressTable in the text section - // VTableFixups directory is at offset 52 within the COR header (after CodeManagerTable at 44) + // The COR header is at offset SizeOfImportAddressTable in the text section. bool is32Bit = Header.Machine == Machine.I386 || Header.Machine == 0; - int sizeOfImportAddressTable = (is32Bit || Header.Machine == 0) ? 8 : 0; + int sizeOfImportAddressTable = is32Bit ? 8 : 0; // COR header offset in text section int corHeaderOffset = sizeOfImportAddressTable; - // VTableFixups directory entry is at offset 52 within COR header - const int vtableFixupsOffset = 52; + // VTableFixups follows the 8-byte CodeManagerTable directory at offset 40. + const int vtableFixupsOffset = 48; int patchOffset = corHeaderOffset + vtableFixupsOffset; // Find the blob containing this offset and patch it @@ -371,8 +370,18 @@ private BlobBuilder SerializeSDataSection(SectionLocation location) _sdataRva = location.RelativeVirtualAddress; - // Calculate sizes for VTableFixups directory - int vtfDirSize = _vtableFixups.Length * 8; // 8 bytes per IMAGE_COR_VTABLEFIXUP entry + var vtableEntryBuilder = + ImmutableArray.CreateBuilder(_vtableFixups.Length); + foreach (VTableFixupInfo info in _vtableFixups) + { + vtableEntryBuilder.Add(new VTableFixupSupport.VTableFixupEntry( + info.SlotCount, + info.Flags, + info.DataLabel)); + } + ImmutableArray vtableEntries = + vtableEntryBuilder.MoveToImmutable(); + int vtfDirSize = VTableFixupSupport.CalculateVTableFixupsDirectorySize(vtableEntries); // Calculate slot data size and build slot offset map var slotOffsets = new Dictionary<(int EntryIndex, int SlotIndex), int>(); @@ -391,13 +400,27 @@ private BlobBuilder SerializeSDataSection(SectionLocation location) slotDataOffset += vtf.SlotCount * slotSize; } - int slotDataEndOffset = slotDataOffset; + int slotDataEndOffset = + vtfDirSize + + VTableFixupSupport.CalculateVTableSlotDataSize(vtableEntries); // Calculate export-related sizes int exportStubsOffset = slotDataEndOffset; int numExports = _exports.Length; int exportStubSize = GetExportStubSize(); int exportStubsTotalSize = numExports * exportStubSize; + int baseOrdinal = int.MaxValue; + int maxOrdinal = 0; + foreach (ExportInfo export in _exports) + { + baseOrdinal = Math.Min(baseOrdinal, export.Ordinal); + maxOrdinal = Math.Max(maxOrdinal, export.Ordinal); + } + if (baseOrdinal == int.MaxValue) + { + baseOrdinal = 1; + } + int numFunctions = numExports > 0 ? maxOrdinal - baseOrdinal + 1 : 0; // Export directory comes after export stubs int exportDirOffset = Align(exportStubsOffset + exportStubsTotalSize, 4); @@ -410,7 +433,7 @@ private BlobBuilder SerializeSDataSection(SectionLocation location) // - Export names (null-terminated strings) // - DLL name (null-terminated string) int exportAddrTableOffset = exportDirOffset + 40; - int exportNamePtrTableOffset = exportAddrTableOffset + numExports * 4; + int exportNamePtrTableOffset = exportAddrTableOffset + numFunctions * 4; int exportOrdinalTableOffset = exportNamePtrTableOffset + numExports * 4; // Calculate name table size @@ -428,38 +451,26 @@ private BlobBuilder SerializeSDataSection(SectionLocation location) // Store total size for COR header patching (only vtfixup directory, not stubs/exports) _sdataSize = vtfDirSize; - // Write VTableFixups directory (array of IMAGE_COR_VTABLEFIXUP structures) + var slotDataRvas = new int[_vtableFixups.Length]; int currentSlotDataOffset = vtfDirSize; - foreach (var vtf in _vtableFixups) + for (int i = 0; i < _vtableFixups.Length; i++) { - int slotDataRva = location.RelativeVirtualAddress + currentSlotDataOffset; - builder.WriteInt32(slotDataRva); // RVA to slot data - builder.WriteUInt16((ushort)vtf.SlotCount); // Count - builder.WriteUInt16(vtf.Flags); // Type/Flags - + VTableFixupInfo vtf = _vtableFixups[i]; + slotDataRvas[i] = location.RelativeVirtualAddress + currentSlotDataOffset; bool is64Bit = (vtf.Flags & VTableFixupSupport.COR_VTABLE_64BIT) != 0; int slotSize = is64Bit ? 8 : 4; currentSlotDataOffset += vtf.SlotCount * slotSize; } - // Write slot data (method tokens that get patched by the runtime) - foreach (var vtf in _vtableFixups) - { - bool is64Bit = (vtf.Flags & VTableFixupSupport.COR_VTABLE_64BIT) != 0; - - for (int i = 0; i < vtf.SlotCount; i++) + VTableFixupSupport.WriteVTableFixupsDirectory(builder, vtableEntries, slotDataRvas); + VTableFixupSupport.WriteVTableSlotData( + builder, + vtableEntries, + (entryIndex, slotIndex) => { - int token = i < vtf.MethodTokens.Length ? vtf.MethodTokens[i] : 0; - if (is64Bit) - { - builder.WriteInt64(token); - } - else - { - builder.WriteInt32(token); - } - } - } + ImmutableArray tokens = _vtableFixups[entryIndex - 1].MethodTokens; + return slotIndex <= tokens.Length ? tokens[slotIndex - 1] : 0; + }); // Write export stubs if we have exports if (numExports > 0) @@ -491,46 +502,33 @@ private BlobBuilder SerializeSDataSection(SectionLocation location) _exportDirectoryRva = location.RelativeVirtualAddress + builder.Count; // Write IMAGE_EXPORT_DIRECTORY - int baseOrdinal = int.MaxValue; - int maxOrdinal = 0; - foreach (var export in _exports) - { - if (export.Ordinal < baseOrdinal) baseOrdinal = export.Ordinal; - if (export.Ordinal > maxOrdinal) maxOrdinal = export.Ordinal; - } - if (baseOrdinal == int.MaxValue) baseOrdinal = 1; - int numFunctions = maxOrdinal - baseOrdinal + 1; - int exportDirStart = builder.Count; builder.WriteUInt32(0); // Characteristics builder.WriteUInt32(0); // TimeDateStamp (filled later or 0) builder.WriteUInt16(0); // MajorVersion builder.WriteUInt16(0); // MinorVersion - builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + - numExports * 4 + numExports * 4 + numExports * 2 + nameTableSize); // Name RVA (DLL name) + builder.WriteInt32(location.RelativeVirtualAddress + dllNameOffset); // Name RVA (DLL name) builder.WriteInt32(baseOrdinal); // Base - builder.WriteInt32(numExports); // NumberOfFunctions + builder.WriteInt32(numFunctions); // NumberOfFunctions builder.WriteInt32(numExports); // NumberOfNames builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40); // AddressOfFunctions - builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + numExports * 4); // AddressOfNames - builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + numExports * 4 * 2); // AddressOfNameOrdinals + builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + numFunctions * 4); // AddressOfNames + builder.WriteInt32(location.RelativeVirtualAddress + exportDirStart + 40 + numFunctions * 4 + numExports * 4); // AddressOfNameOrdinals // Sort exports by name for binary search var sortedExports = _exports.AsSpan().ToArray(); Array.Sort(sortedExports, (a, b) => string.CompareOrdinal(a.Name, b.Name)); - // Write Export Address Table (RVAs to stubs) var exportsArray = _exports.AsSpan().ToArray(); - foreach (var export in sortedExports) + for (int ordinal = baseOrdinal; ordinal <= maxOrdinal; ordinal++) { - int stubIndex = Array.FindIndex(exportsArray, e => e.Ordinal == export.Ordinal); - builder.WriteInt32(exportStubRvas[stubIndex]); + int stubIndex = Array.FindIndex(exportsArray, export => export.Ordinal == ordinal); + builder.WriteInt32(stubIndex >= 0 ? exportStubRvas[stubIndex] : 0); } // Write Export Name Pointer Table (RVAs to names) - int nameOffset = location.RelativeVirtualAddress + exportDirStart + 40 + - numExports * 4 + numExports * 4 + numExports * 2; + int nameOffset = location.RelativeVirtualAddress + exportNamesOffset; foreach (var export in sortedExports) { builder.WriteInt32(nameOffset); diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs index 542bfe2e2c225d..c08a72c8533efc 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/AssemblyTests.cs @@ -634,5 +634,90 @@ leave.s DONE var catchRegion = ehRegions.First(r => r.Kind == ExceptionRegionKind.Catch); Assert.Equal(HandleKind.TypeDefinition, catchRegion.CatchType.Kind); } + + [Fact] + public void AssemblyReferenceMetadata_EmitsExpectedIdentity() + { + string source = """ + .assembly extern Sample.Dependency + { + .ver 2:5:0:7 + .locale "en-US" + .publickeytoken = (01 23 45 67 89 AB CD EF) + .hash = (AA BB CC) + } + .assembly TestAssembly { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var assemblyRef = reader.GetAssemblyReference(MetadataTokens.AssemblyReferenceHandle(1)); + Assert.Equal("Sample.Dependency", reader.GetString(assemblyRef.Name)); + Assert.Equal(new Version(2, 5, 0, 7), assemblyRef.Version); + Assert.Equal("en-US", reader.GetString(assemblyRef.Culture)); + Assert.Equal([0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF], reader.GetBlobBytes(assemblyRef.PublicKeyOrToken)); + Assert.Equal([0xAA, 0xBB, 0xCC], reader.GetBlobBytes(assemblyRef.HashValue)); + } + + [Fact] + public void AssemblyAndAliasedReferenceDeclarations_EmitKeysWildcardsLocalesAndAttributes() + { + string source = """ + .assembly extern mscorlib { } + .assembly extern Original.Dependency as Alias + { + .publicKey = (01 02 03 04) + .ver *:2:*:4 + .locale = (65 00 6E 00) + .hash = (AA BB) + auto + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + ; + } + .assembly test + { + .publicKey = (10 20 30 40) + .ver *:5:*:7 + .locale = (66 00 72 00) + .custom instance void [mscorlib]System.CLSCompliantAttribute::.ctor(bool) = (01 00 01 00 00) + ; + } + .class public auto ansi Test + { + .field public class [Alias]Contoso.External Value + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var assembly = reader.GetAssemblyDefinition(); + var dependencyHandle = reader.AssemblyReferences + .Single(handle => reader.GetString(reader.GetAssemblyReference(handle).Name) == "Original.Dependency"); + var dependency = reader.GetAssemblyReference(dependencyHandle); + var externalType = reader.TypeReferences + .Select(reader.GetTypeReference) + .Single(type => reader.GetString(type.Name) == "External"); + + Assert.Equal(new Version(0, 5, 0, 7), assembly.Version); + Assert.Equal("fr", reader.GetString(assembly.Culture)); + Assert.True(assembly.Flags.HasFlag(AssemblyFlags.PublicKey)); + Assert.Equal([0x10, 0x20, 0x30, 0x40], reader.GetBlobBytes(assembly.PublicKey)); + CustomAttributeValue assemblyAttribute = reader + .GetCustomAttribute(Assert.Single(reader.GetAssemblyDefinition().GetCustomAttributes())) + .DecodeValue(DocumentCompilerTestHelpers.Decoder); + Assert.Equal(true, Assert.Single(assemblyAttribute.FixedArguments).Value); + + Assert.Equal(new Version(0, 2, 0, 4), dependency.Version); + Assert.Equal("en", reader.GetString(dependency.Culture)); + Assert.True(dependency.Flags.HasFlag(AssemblyFlags.PublicKey)); + Assert.Equal([0x01, 0x02, 0x03, 0x04], reader.GetBlobBytes(dependency.PublicKeyOrToken)); + Assert.Equal([0xAA, 0xBB], reader.GetBlobBytes(dependency.HashValue)); + Assert.Single(reader.GetCustomAttributes(dependencyHandle)); + Assert.Equal(dependencyHandle, externalType.ResolutionScope); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/CompilerOptionsTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/CompilerOptionsTests.cs new file mode 100644 index 00000000000000..ef1ba7797b18a6 --- /dev/null +++ b/src/tools/ilasm/tests/ILAssembler.Tests/CompilerOptionsTests.cs @@ -0,0 +1,431 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using Xunit; +using DocumentCompilerTestHelpers = ILAssembler.Tests.DocumentCompilerTestHelpers; + +namespace ILAssembler.Tests +{ + public class CompilerOptionsTests + { + [Fact] + public void AssemblyNameMetadataVersionAndModuleNameOptions_AreApplied() + { + string source = """ + .assembly SourceAssembly { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + AssemblyName = "Overridden.Assembly", + MetadataVersion = "vTestMetadata", + OutputFileName = "override.dll" + }); + var reader = pe.GetMetadataReader(); + + Assert.Equal("Overridden.Assembly", reader.GetString(reader.GetAssemblyDefinition().Name)); + Assert.Equal("vTestMetadata", reader.MetadataVersion); + Assert.Equal("override.dll", reader.GetString(reader.GetModuleDefinition().Name)); + } + + [Fact] + public void PeHeaderAndCorFlagsOptions_AreApplied() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void Main() cil managed + { + .entrypoint + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + Machine = Machine.Amd64, + FileAlignment = 0x200, + ImageBase = 0x140000000, + Subsystem = Subsystem.WindowsCui, + SubsystemVersion = (6, 1), + StackReserve = 0x200000, + CorFlags = CorFlags.ILOnly + }); + + var peHeader = pe.PEHeaders.PEHeader!; + Assert.Equal(0x200, peHeader.FileAlignment); + Assert.Equal((ulong)0x140000000, peHeader.ImageBase); + Assert.Equal(Subsystem.WindowsCui, peHeader.Subsystem); + Assert.Equal((ushort)6, peHeader.MajorSubsystemVersion); + Assert.Equal((ushort)1, peHeader.MinorSubsystemVersion); + Assert.Equal((ulong)0x200000, peHeader.SizeOfStackReserve); + Assert.Equal(Machine.Amd64, pe.PEHeaders.CoffHeader.Machine); + Assert.Equal(CorFlags.ILOnly, pe.PEHeaders.CorHeader!.Flags); + } + + [Fact] + public void Prefer32BitOption_AddsPreferredCorFlag() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + CorFlags = CorFlags.ILOnly | CorFlags.Requires32Bit, + Prefer32Bit = true + }); + + CorFlags flags = pe.PEHeaders.CorHeader!.Flags; + Assert.True(flags.HasFlag(CorFlags.Requires32Bit)); + Assert.True(flags.HasFlag(CorFlags.Prefers32Bit)); + } + + [Fact] + public void NoAutoInheritOption_SuppressesImplicitObjectBaseType() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options { NoAutoInherit = true }); + var reader = pe.GetMetadataReader(); + var typeDef = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .First(type => reader.GetString(type.Name) == "Test"); + + Assert.True(typeDef.BaseType.IsNil); + } + + [Fact] + public void DebugModeOpt_EmitsDebuggableAttributeBlob() + { + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + Debug = true, + DebugMode = DebugMode.Opt + }); + var reader = pe.GetMetadataReader(); + + var assemblyAttributes = reader.GetAssemblyDefinition().GetCustomAttributes().ToArray(); + var attributeHandle = Assert.Single(assemblyAttributes); + var attribute = reader.GetCustomAttribute(attributeHandle); + var constructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + var attributeType = reader.GetTypeReference((TypeReferenceHandle)constructor.Parent); + + Assert.Equal(".ctor", reader.GetString(constructor.Name)); + Assert.Equal("DebuggableAttribute", reader.GetString(attributeType.Name)); + AssertDebuggableAttribute(attribute, expectedMode: 0x03); + } + + [Fact] + public void DllCharacteristicsOptions_EmitExpectedPeBits() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + Machine = Machine.Amd64, + AppContainer = true, + HighEntropyVA = true, + StripReloc = true + }); + + DllCharacteristics characteristics = pe.PEHeaders.PEHeader!.DllCharacteristics; + Assert.True(characteristics.HasFlag(DllCharacteristics.AppContainer)); + Assert.True(characteristics.HasFlag(DllCharacteristics.HighEntropyVirtualAddressSpace)); + Assert.True(characteristics.HasFlag(DllCharacteristics.NxCompatible)); + Assert.True(characteristics.HasFlag(DllCharacteristics.NoSeh)); + Assert.True(characteristics.HasFlag(DllCharacteristics.TerminalServerAware)); + Assert.False(characteristics.HasFlag(DllCharacteristics.DynamicBase)); + } + + [Fact] + public void DeterministicOption_ProducesValidImageAndMvid() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void Main() cil managed + { + .entrypoint + ret + } + } + """; + + var image = DocumentCompilerTestHelpers.CompileAndGetImageBytes(source, new Options { Deterministic = true }); + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + Assert.NotEqual(Guid.Empty, reader.GetGuid(reader.GetModuleDefinition().Mvid)); + Assert.NotEqual(0, pe.PEHeaders.PEHeader!.SizeOfImage); + } + + [Fact] + public void PdbOption_EmitsEmbeddedPortablePdbWithoutLineDirectives() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void M() cil managed + { + nop + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options { Pdb = true }); + + var debugDirectory = pe.ReadDebugDirectory(); + var embeddedPdbEntry = debugDirectory.Single(entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry); + var pdbReader = pdbProvider.GetMetadataReader(); + + Assert.Empty(pdbReader.Documents); + Assert.NotEmpty(pdbReader.MethodDebugInformation); + } + + [Fact] + public void DebugModeImpl_EmitsDebuggableAttributeBlob() + { + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + Debug = true, + DebugMode = DebugMode.Impl + }); + var reader = pe.GetMetadataReader(); + + var assemblyAttributes = reader.GetAssemblyDefinition().GetCustomAttributes().ToArray(); + var attributeHandle = Assert.Single(assemblyAttributes); + var attribute = reader.GetCustomAttribute(attributeHandle); + var constructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + var attributeType = reader.GetTypeReference((TypeReferenceHandle)constructor.Parent); + + Assert.Equal(".ctor", reader.GetString(constructor.Name)); + Assert.Equal("DebuggableAttribute", reader.GetString(attributeType.Name)); + AssertDebuggableAttribute(attribute, expectedMode: 0x103); + } + + [Fact] + public void DebugOption_WithoutExplicitMode_EmitsDefaultDebuggableAttributeBlob() + { + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + Debug = true + }); + var reader = pe.GetMetadataReader(); + + var assemblyAttributes = reader.GetAssemblyDefinition().GetCustomAttributes().ToArray(); + var attributeHandle = Assert.Single(assemblyAttributes); + var attribute = reader.GetCustomAttribute(attributeHandle); + var constructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); + var attributeType = reader.GetTypeReference((TypeReferenceHandle)constructor.Parent); + + Assert.Equal(".ctor", reader.GetString(constructor.Name)); + Assert.Equal("DebuggableAttribute", reader.GetString(attributeType.Name)); + AssertDebuggableAttribute(attribute, expectedMode: 0x101); + } + + [Fact] + public void ValidKeyFile_EmbedsPublicKeyAndSetsAssemblyFlag() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + string tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDirectory); + string keyFile = Path.Combine(tempDirectory, "test.snk"); + byte[] expectedKeyBytes = [0x06, 0x02, 0x23, 0x29, 0x47, 0x6B, 0x8D, 0xAF]; + + try + { + File.WriteAllBytes(keyFile, expectedKeyBytes); + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options + { + KeyFile = keyFile + }); + var reader = pe.GetMetadataReader(); + var assemblyDefinition = reader.GetAssemblyDefinition(); + + Assert.True(assemblyDefinition.Flags.HasFlag(AssemblyFlags.PublicKey)); + Assert.Equal(expectedKeyBytes, reader.GetBlobBytes(assemblyDefinition.PublicKey)); + } + finally + { + if (File.Exists(keyFile)) + { + File.Delete(keyFile); + } + + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory); + } + } + } + + [Fact] + public void InvalidKeyFile_WithErrorTolerantOption_EmitsAssemblyAndReportsDiagnostic() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + } + """; + string missingKeyFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing.snk"); + var sourceText = new SourceText(source, "test.il"); + var compiler = new DocumentCompiler(); + + var (diagnostics, image) = compiler.Compile( + sourceText, + _ => throw new InvalidOperationException("Unexpected include"), + _ => throw new InvalidOperationException("Unexpected resource"), + new Options + { + ErrorTolerant = true, + KeyFile = missingKeyFile + }); + + var diagnostic = Assert.Single(diagnostics, d => d.Id == DiagnosticIds.KeyFileError); + Assert.Contains(missingKeyFile, diagnostic.Message); + Assert.NotNull(image); + + var blobBuilder = new BlobBuilder(); + image!.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + var assemblyDefinition = reader.GetAssemblyDefinition(); + + Assert.False(assemblyDefinition.Flags.HasFlag(AssemblyFlags.PublicKey)); + Assert.True(assemblyDefinition.PublicKey.IsNil); + } + + [Fact] + public void ErrorTolerantOption_ReturnsImageForStructuralMetadataError() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class extern public MissingType + { + .assembly extern MissingAssembly + } + """; + var sourceText = new SourceText(source, "test.il"); + var compiler = new DocumentCompiler(); + + var (strictDiagnostics, strictImage) = compiler.Compile( + sourceText, + _ => throw new InvalidOperationException("Unexpected include"), + _ => throw new InvalidOperationException("Unexpected resource"), + new Options()); + var strictDiagnostic = Assert.Single(strictDiagnostics, d => d.Id == DiagnosticIds.AssemblyNotFound); + Assert.Equal(DiagnosticSeverity.Error, strictDiagnostic.Severity); + Assert.Null(strictImage); + + var (tolerantDiagnostics, tolerantImage) = compiler.Compile( + sourceText, + _ => throw new InvalidOperationException("Unexpected include"), + _ => throw new InvalidOperationException("Unexpected resource"), + new Options { ErrorTolerant = true }); + var tolerantDiagnostic = Assert.Single(tolerantDiagnostics, d => d.Id == DiagnosticIds.AssemblyNotFound); + Assert.Equal(DiagnosticSeverity.Error, tolerantDiagnostic.Severity); + Assert.NotNull(tolerantImage); + + var blobBuilder = new BlobBuilder(); + tolerantImage!.Serialize(blobBuilder); + using var pe = new PEReader(blobBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + Assert.True(reader.TypeDefinitions.Count >= 1); + } + + [Fact] + public void PeDirectives_EmitSpecifiedHeaderValues() + { + string source = """ + .assembly test { } + .subsystem 0x0002 + .corflags 0x00000003 + .file alignment 0x00000400 + .imagebase 0x10000000 + .stackreserve 0x00200000 + .class public auto ansi Test { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var peHeader = pe.PEHeaders.PEHeader!; + var corHeader = pe.PEHeaders.CorHeader!; + + Assert.Equal(Subsystem.WindowsGui, peHeader.Subsystem); + Assert.Equal(0x400, peHeader.FileAlignment); + Assert.Equal(0x10000000UL, peHeader.ImageBase); + Assert.Equal(0x00200000UL, peHeader.SizeOfStackReserve); + Assert.Equal(CorFlags.ILOnly | CorFlags.Requires32Bit, corHeader.Flags); + } + + private static void AssertDebuggableAttribute(CustomAttribute attribute, int expectedMode) + { + CustomAttributeValue value = + attribute.DecodeValue(DocumentCompilerTestHelpers.Decoder); + var argument = Assert.Single(value.FixedArguments); + Assert.Equal(expectedMode, argument.Value); + Assert.Empty(value.NamedArguments); + } + } +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs index 2511c4be007d41..465533cb7357bf 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/CustomAttributeTests.cs @@ -178,5 +178,580 @@ .method public static void TestMethod() cil managed Assert.Equal(0x00, blobBytes[3]); // named arg count high } + [Fact] + public void CustomAttribute_LocalAttributeConstructor_UsesMethodDefinitionHandleAndBlob() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed beforefieldinit LocalAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 value) cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void LocalAttribute::.ctor(int32) = ( 01 00 2A 00 00 00 00 00 ) + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var testTypeHandle = reader.TypeDefinitions + .First(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var customAttributeHandle = Assert.Single(reader.GetCustomAttributes(testTypeHandle)); + var customAttribute = reader.GetCustomAttribute(customAttributeHandle); + + Assert.Equal(HandleKind.MethodDefinition, customAttribute.Constructor.Kind); + + var constructorHandle = (MethodDefinitionHandle)customAttribute.Constructor; + var constructor = reader.GetMethodDefinition(constructorHandle); + Assert.Equal(".ctor", reader.GetString(constructor.Name)); + + var attributeTypeHandle = reader.TypeDefinitions + .First(handle => reader.GetTypeDefinition(handle).GetMethods().Contains(constructorHandle)); + Assert.Equal("LocalAttribute", reader.GetString(reader.GetTypeDefinition(attributeTypeHandle).Name)); + + Assert.Equal([0x01, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00], reader.GetBlobBytes(customAttribute.Value)); + } + + [Fact] + public void CustomAttribute_VerbalPrimitiveArrays_EmitExpectedBlob() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed ArrayAttribute extends [mscorlib]System.Attribute + { + .method public specialname rtspecialname instance void .ctor( + float32[] singles, + float64[] doubles, + int64[] int64s, + int32[] int32s, + int16[] int16s, + int8[] int8s, + uint64[] uint64s, + uint32[] uint32s, + uint16[] uint16s, + uint8[] uint8s, + char[] chars, + bool[] bools, + string[] strings) cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void ArrayAttribute::.ctor( + float32[], + float64[], + int64[], + int32[], + int16[], + int8[], + uint64[], + uint32[], + uint16[], + uint8[], + char[], + bool[], + string[]) = { + float32[2](1.5 2) + float64[2](3.5 4) + int64[2](5 6) + int32[2](7 8) + int16[2](9 10) + int8[2](11 12) + uint64[2](13 14) + uint32[2](15 16) + uint16[2](17 18) + uint8[2](19 20) + char[2](65 66) + bool[2](true false) + string[2]('alpha' nullref) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var type = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var attribute = reader.GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(type))); + CustomAttributeValue value = attribute.DecodeValue(DocumentCompilerTestHelpers.Decoder); + + Assert.Empty(value.NamedArguments); + Assert.Equal(13, value.FixedArguments.Length); + AssertArrayArgument(value.FixedArguments[0], "float32[]", 1.5f, 2f); + AssertArrayArgument(value.FixedArguments[1], "float64[]", 3.5, 4d); + AssertArrayArgument(value.FixedArguments[2], "int64[]", 5L, 6L); + AssertArrayArgument(value.FixedArguments[3], "int32[]", 7, 8); + AssertArrayArgument(value.FixedArguments[4], "int16[]", (short)9, (short)10); + AssertArrayArgument(value.FixedArguments[5], "int8[]", (sbyte)11, (sbyte)12); + AssertArrayArgument(value.FixedArguments[6], "uint64[]", 13UL, 14UL); + AssertArrayArgument(value.FixedArguments[7], "uint32[]", 15U, 16U); + AssertArrayArgument(value.FixedArguments[8], "uint16[]", (ushort)17, (ushort)18); + AssertArrayArgument(value.FixedArguments[9], "uint8[]", (byte)19, (byte)20); + AssertArrayArgument(value.FixedArguments[10], "char[]", 'A', 'B'); + AssertArrayArgument(value.FixedArguments[11], "bool[]", true, false); + AssertArrayArgument(value.FixedArguments[12], "string[]", "alpha", null); + } + + [Fact] + public void CustomAttribute_VerbalScalarArguments_DecodeExpectedValues() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed ScalarAttribute extends [mscorlib]System.Attribute + { + .method public specialname rtspecialname instance void .ctor( + bool booleanValue, + int8 int8Value, + uint8 uint8Value, + int16 int16Value, + uint16 uint16Value, + int32 int32Value, + uint32 uint32Value, + int64 int64Value, + uint64 uint64Value, + char charValue, + float32 singleValue, + float32 singleBits, + float64 doubleValue, + float64 doubleBits, + string text, + class [mscorlib]System.Type typeValue) cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void ScalarAttribute::.ctor( + bool, + int8, + uint8, + int16, + uint16, + int32, + uint32, + int64, + uint64, + char, + float32, + float32, + float64, + float64, + string, + class [mscorlib]System.Type) = { + bool(true) + int8(-1) + uint8(255) + int16(-2) + uint16(65535) + int32(-3) + uint32(4294967295) + int64(-4) + uint64(9223372036854775807) + char(65) + float32(1.5) + float32(1065353216) + float64(2.5) + float64(4607182418800017408) + string('text') + type(class 'Contoso.Target') + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var type = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + CustomAttributeValue value = reader + .GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(type))) + .DecodeValue(DocumentCompilerTestHelpers.Decoder); + + Assert.Equal( + new object?[] + { + true, + (sbyte)-1, + (byte)255, + (short)-2, + (ushort)65535, + -3, + uint.MaxValue, + -4L, + 9223372036854775807UL, + 'A', + 1.5f, + 1f, + 2.5d, + 1d, + "text", + "Contoso.Target", + }, + value.FixedArguments.Select(argument => argument.Value)); + Assert.Empty(value.NamedArguments); + } + + [Fact] + public void CustomAttribute_VerbalNamedArguments_EmitExpectedBlob() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed NamedAttribute extends [mscorlib]System.Attribute + { + .method public specialname rtspecialname instance void .ctor() cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void NamedAttribute::.ctor() = { + field int32 Number = int32(42) + property string Message = string('hello') + property type Target = type(class 'Contoso.Target') + property object Boxed = object(int32(7)) + property int32[] Values = int32[2](1 2) + property enum class 'Contoso.Kind' Kind = int32(3) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var type = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var attribute = reader.GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(type))); + CustomAttributeValue value = attribute.DecodeValue(DocumentCompilerTestHelpers.Decoder); + + Assert.Empty(value.FixedArguments); + Assert.Equal(6, value.NamedArguments.Length); + AssertNamedArgument(value.NamedArguments[0], CustomAttributeNamedArgumentKind.Field, "Number", "int32", 42); + AssertNamedArgument(value.NamedArguments[1], CustomAttributeNamedArgumentKind.Property, "Message", "string", "hello"); + AssertNamedArgument(value.NamedArguments[2], CustomAttributeNamedArgumentKind.Property, "Target", "System.Type", "Contoso.Target"); + AssertNamedArgument(value.NamedArguments[3], CustomAttributeNamedArgumentKind.Property, "Boxed", "int32", 7); + Assert.Equal("Values", value.NamedArguments[4].Name); + Assert.Equal("int32[]", value.NamedArguments[4].Type); + AssertArrayValue(value.NamedArguments[4].Value, 1, 2); + AssertNamedArgument(value.NamedArguments[5], CustomAttributeNamedArgumentKind.Property, "Kind", "Contoso.Kind", 3); + } + + [Fact] + public void CustomAttribute_VerbalTypeArgumentFromUnversionedAssemblyReference_EmitsAssemblyQualifiedTypeName() + { + string source = """ + .assembly extern mscorlib { } + .assembly extern External.Assembly { } + .assembly test { } + .class public auto ansi sealed TypeAttribute extends [mscorlib]System.Attribute + { + .method public specialname rtspecialname instance void .ctor(class [mscorlib]System.Type value) cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void TypeAttribute::.ctor(class [mscorlib]System.Type) = { + type([External.Assembly]Contoso.ExternalType) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var externalAssembly = reader.AssemblyReferences + .Select(reader.GetAssemblyReference) + .Single(reference => reader.GetString(reference.Name) == "External.Assembly"); + var testType = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var attribute = reader.GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(testType))); + + Assert.Equal(new Version(0, 0, 0, 0), externalAssembly.Version); + CustomAttributeValue value = attribute.DecodeValue(DocumentCompilerTestHelpers.Decoder); + + var argument = Assert.Single(value.FixedArguments); + Assert.Equal("System.Type", argument.Type); + Assert.Equal( + "Contoso.ExternalType, External.Assembly, PublicKeyToken=null", + argument.Value); + Assert.Empty(value.NamedArguments); + } + + [Fact] + public void CustomAttribute_VerbalTypeAndObjectArrays_PreserveElementOrder() + { + string source = """ + .assembly extern mscorlib { } + .assembly extern External.Assembly { } + .assembly test { } + .class public auto ansi sealed ArrayAttribute extends [mscorlib]System.Attribute + { + .method public specialname rtspecialname instance void .ctor( + class [mscorlib]System.Type[] types, + object[] values) cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void ArrayAttribute::.ctor(class [mscorlib]System.Type[], object[]) = { + type[3]([External.Assembly]Contoso.ExternalType nullref class 'Quoted.Type') + object[3](int32(1) string('text') object(bool(true))) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var type = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var attribute = reader.GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(type))); + CustomAttributeValue value = attribute.DecodeValue(DocumentCompilerTestHelpers.Decoder); + + Assert.Equal(2, value.FixedArguments.Length); + Assert.Equal("System.Type[]", value.FixedArguments[0].Type); + AssertArrayValue( + value.FixedArguments[0].Value, + "Contoso.ExternalType, External.Assembly, PublicKeyToken=null", + null, + "Quoted.Type"); + + Assert.Equal("object[]", value.FixedArguments[1].Type); + ImmutableArray> boxedValues = + Assert.IsType>>(value.FixedArguments[1].Value); + Assert.Collection( + boxedValues, + item => + { + Assert.Equal("int32", item.Type); + Assert.Equal(1, item.Value); + }, + item => + { + Assert.Equal("string", item.Type); + Assert.Equal("text", item.Value); + }, + item => + { + Assert.Equal("bool", item.Type); + Assert.Equal(true, item.Value); + }); + Assert.Empty(value.NamedArguments); + } + + [Fact] + public void CustomAttribute_ExplicitOwners_AttachToTypeFieldAndMethod() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .field public int32 Value + .method public static void M() cil managed + { + ret + } + } + + .custom (Test) instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + .custom (field int32 Test::Value) instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + .custom (method void Test::M()) instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var typeHandle = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var type = reader.GetTypeDefinition(typeHandle); + var fieldHandle = Assert.Single(type.GetFields()); + var methodHandle = Assert.Single(type.GetMethods()); + + AssertCustomAttributeBlob(reader, Assert.Single(reader.GetCustomAttributes(typeHandle))); + AssertCustomAttributeBlob(reader, Assert.Single(reader.GetCustomAttributes(fieldHandle))); + AssertCustomAttributeBlob(reader, Assert.Single(reader.GetCustomAttributes(methodHandle))); + } + + [Fact] + public void CustomAttribute_NoValueAndExplicitOwnerVerbalForms_DecodeExpectedValues() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed ValueAttribute extends [mscorlib]System.Attribute + { + .method public specialname rtspecialname instance void .ctor(int32 value) cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() + } + .custom (Test) instance void ValueAttribute::.ctor(int32) = { int32(42) } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var testType = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var attributes = reader.GetCustomAttributes(testType) + .Select(reader.GetCustomAttribute) + .Select(attribute => attribute.DecodeValue(DocumentCompilerTestHelpers.Decoder)) + .ToArray(); + + Assert.Equal(2, attributes.Length); + Assert.Contains( + attributes, + attribute => attribute.FixedArguments.Length == 0 && attribute.NamedArguments.Length == 0); + Assert.Contains( + attributes, + attribute => + attribute.FixedArguments.Length == 1 && + attribute.FixedArguments[0].Type == "int32" && + Equals(attribute.FixedArguments[0].Value, 42)); + } + + [Fact] + public void CustomAttribute_BoxedByteArray_ReportsErrorAndEmitsDecodableFallback() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi sealed ArrayAttribute extends [mscorlib]System.Attribute + { + .method public specialname rtspecialname instance void .ctor(object[] values) cil managed + { + ldarg.0 + call instance void [mscorlib]System.Attribute::.ctor() + ret + } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .custom instance void ArrayAttribute::.ctor(object[]) = { + object[1](bytearray(01)) + } + } + """; + + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText(source, "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options { ErrorTolerant = true }); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.InvalidMetadataToken); + Assert.NotNull(result); + + var image = new BlobBuilder(); + result!.Serialize(image); + using var pe = new PEReader(image.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + var testType = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + CustomAttributeValue attribute = reader + .GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(testType))) + .DecodeValue(DocumentCompilerTestHelpers.Decoder); + var values = Assert.IsType>>( + Assert.Single(attribute.FixedArguments).Value); + + var value = Assert.Single(values); + Assert.Equal("string", value.Type); + Assert.Null(value.Value); + } + + [Fact] + public void AssemblyCustomAttribute_PreservesConstructorAndBlob() + { + string source = """ + .assembly extern System.Runtime { } + .assembly TestAssembly + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) + } + .class public auto ansi beforefieldinit Test + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var customAttributeHandle = Assert.Single(reader.GetAssemblyDefinition().GetCustomAttributes()); + var customAttribute = reader.GetCustomAttribute(customAttributeHandle); + var constructor = reader.GetMemberReference((MemberReferenceHandle)customAttribute.Constructor); + var attributeType = reader.GetTypeReference((TypeReferenceHandle)constructor.Parent); + + Assert.Equal(".ctor", reader.GetString(constructor.Name)); + Assert.Equal("CompilationRelaxationsAttribute", reader.GetString(attributeType.Name)); + Assert.Equal([0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00], reader.GetBlobBytes(customAttribute.Value)); + } + + private static void AssertArrayArgument( + CustomAttributeTypedArgument argument, + string expectedType, + params object?[] expectedValues) + { + Assert.Equal(expectedType, argument.Type); + AssertArrayValue(argument.Value, expectedValues); + } + + private static void AssertArrayValue(object? value, params object?[] expectedValues) + { + ImmutableArray> values = + Assert.IsType>>(value); + Assert.Equal(expectedValues, values.Select(item => item.Value)); + } + + private static void AssertNamedArgument( + CustomAttributeNamedArgument argument, + CustomAttributeNamedArgumentKind kind, + string name, + string type, + object? value) + { + Assert.Equal(kind, argument.Kind); + Assert.Equal(name, argument.Name); + Assert.Equal(type, argument.Type); + Assert.Equal(value, argument.Value); + } + + private static void AssertCustomAttributeBlob(MetadataReader reader, CustomAttributeHandle handle) + { + var attribute = reader.GetCustomAttribute(handle); + Assert.Equal( + [0x01, 0x00, 0x00, 0x00], + reader.GetBlobBytes(attribute.Value)); + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/DataTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/DataTests.cs index 7cbc689ac330b9..f0cd4a376925a9 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/DataTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/DataTests.cs @@ -11,6 +11,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Internal.IL; @@ -21,6 +22,138 @@ namespace ILAssembler.Tests { public class DataTests { + [Fact] + public void DataItems_EmitExpectedTypedBytesAndRepeatCounts() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .data D_STRING = char*("Hi") + .data D_BYTES = bytearray(01 02 03 04) + .data D_BRACES = { int32(0x11223344), int16(0x5566), int8(0x77) } + .data D_REPEAT = int32(7)[3] + .data D_F32 = float32(1.5) + .data D_F64 = float64(2.25) + .data D_ZERO8 = int8[3] + .data D_ZERO16 = int16[2] + .data D_ZERO32 = int32[2] + .data D_ZERO64 = int64[2] + .data D_ZEROF32 = float32[2] + .data D_ZEROF64 = float64[2] + + .class public explicit ansi sealed DataHolder extends [mscorlib]System.ValueType + { + .size 64 + .field [0] public static int8 StringData at D_STRING + .field [4] public static int8 ByteData at D_BYTES + .field [8] public static int8 BracedData at D_BRACES + .field [16] public static int8 RepeatedData at D_REPEAT + .field [28] public static int8 Float32Data at D_F32 + .field [32] public static int8 Float64Data at D_F64 + .field [40] public static int8 Zero8Data at D_ZERO8 + .field [44] public static int8 Zero16Data at D_ZERO16 + .field [48] public static int8 Zero32Data at D_ZERO32 + .field [52] public static int8 Zero64Data at D_ZERO64 + .field [56] public static int8 ZeroFloat32Data at D_ZEROF32 + .field [60] public static int8 ZeroFloat64Data at D_ZEROF64 + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var fields = reader.FieldDefinitions + .Select(reader.GetFieldDefinition) + .ToDictionary(field => reader.GetString(field.Name)); + + Assert.Equal( + [0x48, 0x00, 0x69, 0x00], + ReadData(pe, fields["StringData"], 4)); + Assert.Equal( + [0x01, 0x02, 0x03, 0x04], + ReadData(pe, fields["ByteData"], 4)); + Assert.Equal( + [0x44, 0x33, 0x22, 0x11, 0x66, 0x55, 0x77], + ReadData(pe, fields["BracedData"], 7)); + + byte[] repeatedData = ReadData(pe, fields["RepeatedData"], 12); + Assert.Equal(new[] { 7, 7, 7 }, MemoryMarshal.Cast(repeatedData).ToArray()); + Assert.Equal(1.5f, BitConverter.ToSingle(ReadData(pe, fields["Float32Data"], 4))); + Assert.Equal(2.25, BitConverter.ToDouble(ReadData(pe, fields["Float64Data"], 8))); + + Assert.All(ReadData(pe, fields["Zero8Data"], 3), value => Assert.Equal(0, value)); + Assert.All(ReadData(pe, fields["Zero16Data"], 4), value => Assert.Equal(0, value)); + Assert.All(ReadData(pe, fields["Zero32Data"], 8), value => Assert.Equal(0, value)); + Assert.All(ReadData(pe, fields["Zero64Data"], 16), value => Assert.Equal(0, value)); + Assert.All(ReadData(pe, fields["ZeroFloat32Data"], 8), value => Assert.Equal(0, value)); + Assert.All(ReadData(pe, fields["ZeroFloat64Data"], 16), value => Assert.Equal(0, value)); + } + + [Fact] + public void ClassScopedDataDirective_EmitsFieldRvaValue() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public explicit ansi sealed DataHolder extends [mscorlib]System.ValueType + { + .size 4 + .data ClassData = int32(1234) + .field [0] public static int32 Value at ClassData + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var field = reader.GetFieldDefinition(Assert.Single(reader.FieldDefinitions)); + + Assert.Equal( + 1234, + BitConverter.ToInt32( + pe.GetSectionData(field.GetRelativeVirtualAddress()).GetContent().AsSpan(0, sizeof(int)))); + } + + [Fact] + public void DataDirectives_EmitExpectedMappedFieldDataBytes() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .data IntData = int32(0x12345678) + .data ByteData = bytearray (AA BB CC DD) + .data FloatData = float32(3.5) + + .class public explicit ansi sealed beforefieldinit DataHolder extends [mscorlib]System.ValueType + { + .size 16 + .field [0] public static int32 IntField at IntData + .field [4] public static int32 ByteField at ByteData + .field [8] public static float32 FloatField at FloatData + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var type = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .First(definition => reader.GetString(definition.Name) == "DataHolder"); + var fields = new Dictionary(); + + foreach (var fieldHandle in type.GetFields()) + { + var field = reader.GetFieldDefinition(fieldHandle); + fields.Add(reader.GetString(field.Name), field); + } + + int intRva = fields["IntField"].GetRelativeVirtualAddress(); + int byteRva = fields["ByteField"].GetRelativeVirtualAddress(); + int floatRva = fields["FloatField"].GetRelativeVirtualAddress(); + + Assert.Equal([0x78, 0x56, 0x34, 0x12], pe.GetSectionData(intRva).GetContent().Take(4).ToArray()); + Assert.Equal([0xAA, 0xBB, 0xCC, 0xDD], pe.GetSectionData(byteRva).GetContent().Take(4).ToArray()); + Assert.Equal([0x00, 0x00, 0x60, 0x40], pe.GetSectionData(floatRva).GetContent().Take(4).ToArray()); + } + [Fact] public void Diagnostic_InvalidMetadataToken() { @@ -42,5 +175,12 @@ .assembly extern ForwardedAssembly Assert.Equal(DiagnosticIds.InvalidMetadataToken, error.Id); Assert.Equal(DiagnosticSeverity.Error, error.Severity); } + + private static byte[] ReadData(PEReader pe, FieldDefinition field, int length) + { + int rva = field.GetRelativeVirtualAddress(); + Assert.NotEqual(0, rva); + return pe.GetSectionData(rva).GetContent().AsSpan(0, length).ToArray(); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTestHelpers.cs b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTestHelpers.cs index 9b61bd9a7ed5e5..6906d6e3409a0e 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTestHelpers.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTestHelpers.cs @@ -10,6 +10,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Text; using Internal.IL; using Xunit; @@ -17,7 +18,43 @@ namespace ILAssembler.Tests { internal static class DocumentCompilerTestHelpers { + internal static MetadataStringDecoder Decoder { get; } = new(); + internal static PEReader CompileAndGetReader(string source, Options options) + { + var sourceText = new SourceText(source, "test.il"); + return CompileAndGetReader(sourceText, _ => + { + Assert.Fail("Expected no includes"); + return default; + }, _ => + { + Assert.Fail("Expected no resources"); + return default; + }, options); + } + + internal static PEReader CompileAndGetReader(SourceText sourceText, Func includedDocumentLoader, Options options) + { + return CompileAndGetReader(sourceText, includedDocumentLoader, _ => + { + Assert.Fail("Expected no resources"); + return default; + }, options); + } + + internal static PEReader CompileAndGetReader(SourceText sourceText, Func includedDocumentLoader, Func resourceLocator, Options options) + { + var documentCompiler = new DocumentCompiler(); + var (diagnostics, result) = documentCompiler.Compile(sourceText, includedDocumentLoader, resourceLocator, options); + Assert.Empty(diagnostics); + Assert.NotNull(result); + var blobBuilder = new BlobBuilder(); + result!.Serialize(blobBuilder); + return new PEReader(blobBuilder.ToImmutableArray()); + } + + internal static ImmutableArray CompileAndGetImageBytes(string source, Options options) { var sourceText = new SourceText(source, "test.il"); var documentCompiler = new DocumentCompiler(); @@ -30,7 +67,7 @@ internal static PEReader CompileAndGetReader(string source, Options options) Assert.NotNull(result); var blobBuilder = new BlobBuilder(); result!.Serialize(blobBuilder); - return new PEReader(blobBuilder.ToImmutableArray()); + return blobBuilder.ToImmutableArray(); } internal static int GetFirstTokenOperand(PEReader pe, MetadataReader reader, string methodName, ILOpcode targetOpcode) @@ -73,6 +110,48 @@ internal static int GetFirstTokenOperand(PEReader pe, MetadataReader reader, str return 0; } + internal static ImmutableArray GetTokenOperands( + PEReader pe, + MetadataReader reader, + string methodName, + ILOpcode targetOpcode) + { + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == methodName); + byte[] il = pe.GetMethodBody(method.RelativeVirtualAddress).GetILBytes()!; + var operands = ImmutableArray.CreateBuilder(); + + int offset = 0; + while (offset < il.Length) + { + var opcode = (ILOpcode)il[offset]; + int operandStart = offset + 1; + if (opcode == ILOpcode.prefix1) + { + opcode = (ILOpcode)(0x100 + il[offset + 1]); + operandStart = offset + 2; + } + + if (opcode == targetOpcode) + { + operands.Add(BinaryPrimitives.ReadInt32LittleEndian(il.AsSpan(operandStart))); + } + + if (opcode == ILOpcode.switch_) + { + uint count = BinaryPrimitives.ReadUInt32LittleEndian(il.AsSpan(operandStart)); + offset = operandStart + 4 + checked((int)count * 4); + } + else + { + offset += opcode.GetSize(); + } + } + + return operands.ToImmutable(); + } + internal static void AssertTypeDefToken(MetadataReader reader, int token, string expectedName) { var handle = MetadataTokens.EntityHandle(token); @@ -123,5 +202,162 @@ internal static ImmutableArray CompileAndGetDiagnostics(string sourc }, _ => { Assert.Fail("Expected no resources"); return default; }, options); return diagnostics; } + + internal sealed class MetadataStringDecoder : + ISignatureTypeProvider, + ICustomAttributeTypeProvider + { + public string GetArrayType(string elementType, ArrayShape shape) + { + var builder = new StringBuilder(elementType); + builder.Append('['); + for (int i = 0; i < shape.Rank; i++) + { + if (i > 0) + { + builder.Append(','); + } + + int lowerBound = i < shape.LowerBounds.Length ? shape.LowerBounds[i] : 0; + if (i < shape.LowerBounds.Length) + { + builder.Append(lowerBound); + } + + builder.Append("..."); + if (i < shape.Sizes.Length) + { + builder.Append(lowerBound + shape.Sizes[i] - 1); + } + } + + builder.Append(']'); + return builder.ToString(); + } + + public string GetByReferenceType(string elementType) => elementType + "&"; + + public string GetFunctionPointerType(MethodSignature signature) + { + var builder = new StringBuilder("method "); + builder.Append(signature.ReturnType); + builder.Append(" *("); + for (int i = 0; i < signature.ParameterTypes.Length; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + if (i == signature.RequiredParameterCount) + { + builder.Append("..., "); + } + + builder.Append(signature.ParameterTypes[i]); + } + + builder.Append(')'); + return builder.ToString(); + } + + public string GetGenericInstantiation(string genericType, ImmutableArray typeArguments) => + genericType + "<" + string.Join(",", typeArguments) + ">"; + + public string GetGenericMethodParameter(object? genericContext, int index) => "!!" + index; + + public string GetGenericTypeParameter(object? genericContext, int index) => "!" + index; + + public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired) => + unmodifiedType + (isRequired ? " modreq(" : " modopt(") + modifier + ")"; + + public string GetPinnedType(string elementType) => elementType + " pinned"; + + public string GetPointerType(string elementType) => elementType + "*"; + + public string GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode switch + { + PrimitiveTypeCode.Boolean => "bool", + PrimitiveTypeCode.Byte => "uint8", + PrimitiveTypeCode.Char => "char", + PrimitiveTypeCode.Double => "float64", + PrimitiveTypeCode.Int16 => "int16", + PrimitiveTypeCode.Int32 => "int32", + PrimitiveTypeCode.Int64 => "int64", + PrimitiveTypeCode.IntPtr => "native int", + PrimitiveTypeCode.Object => "object", + PrimitiveTypeCode.SByte => "int8", + PrimitiveTypeCode.Single => "float32", + PrimitiveTypeCode.String => "string", + PrimitiveTypeCode.TypedReference => "typedref", + PrimitiveTypeCode.UInt16 => "uint16", + PrimitiveTypeCode.UInt32 => "uint32", + PrimitiveTypeCode.UInt64 => "uint64", + PrimitiveTypeCode.UIntPtr => "native uint", + PrimitiveTypeCode.Void => "void", + _ => throw new ArgumentOutOfRangeException(nameof(typeCode)), + }; + + public string GetSZArrayType(string elementType) => elementType + "[]"; + + public string GetSystemType() => "System.Type"; + + public string GetTypeFromDefinition( + MetadataReader reader, + TypeDefinitionHandle handle, + byte rawTypeKind) + { + TypeDefinition definition = reader.GetTypeDefinition(handle); + string name = definition.Namespace.IsNil + ? reader.GetString(definition.Name) + : reader.GetString(definition.Namespace) + "." + reader.GetString(definition.Name); + + TypeDefinitionHandle declaringType = definition.GetDeclaringType(); + return declaringType.IsNil + ? name + : GetTypeFromDefinition(reader, declaringType, 0) + "/" + reader.GetString(definition.Name); + } + + public string GetTypeFromReference( + MetadataReader reader, + TypeReferenceHandle handle, + byte rawTypeKind) + { + TypeReference reference = reader.GetTypeReference(handle); + string name = reference.Namespace.IsNil + ? reader.GetString(reference.Name) + : reader.GetString(reference.Namespace) + "." + reader.GetString(reference.Name); + + if (name == "System.Type") + { + return name; + } + + return reference.ResolutionScope.Kind switch + { + HandleKind.AssemblyReference => + "[" + reader.GetString(reader.GetAssemblyReference((AssemblyReferenceHandle)reference.ResolutionScope).Name) + "]" + name, + HandleKind.ModuleReference => + "[.module " + reader.GetString(reader.GetModuleReference((ModuleReferenceHandle)reference.ResolutionScope).Name) + "]" + name, + HandleKind.TypeReference => + GetTypeFromReference(reader, (TypeReferenceHandle)reference.ResolutionScope, 0) + "/" + name, + _ => name, + }; + } + + public string GetTypeFromSerializedName(string name) => name; + + public string GetTypeFromSpecification( + MetadataReader reader, + object? genericContext, + TypeSpecificationHandle handle, + byte rawTypeKind) => + reader.GetTypeSpecification(handle).DecodeSignature(this, genericContext); + + public PrimitiveTypeCode GetUnderlyingEnumType(string type) => PrimitiveTypeCode.Int32; + + public bool IsSystemType(string type) => + type is "System.Type" or "[mscorlib]System.Type" or "[System.Runtime]System.Type"; + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs new file mode 100644 index 00000000000000..9ccf33c124cf93 --- /dev/null +++ b/src/tools/ilasm/tests/ILAssembler.Tests/DocumentCompilerTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace ILAssembler.Tests +{ + public class DocumentCompilerTests + { + [Fact] + public void ParserDiagnosticInLaterDocument_UsesDocumentPath_AndErrorTolerantStillEmitsImage() + { + var documents = ImmutableArray.Create( + new SourceText(""" + .assembly test { } + .class public auto ansi beforefieldinit ValidFromDoc1 + { + } + """, "valid.il"), + new SourceText(""" + .class public auto ansi beforefieldinit Broken + { + .method public static void M(int32 int32 int32) cil managed + { + ret + } + } + """, "broken.il")); + + var compiler = new DocumentCompiler(); + var (diagnostics, image) = compiler.Compile( + documents, + _ => throw new InvalidOperationException("Unexpected include"), + _ => throw new InvalidOperationException("Unexpected resource"), + new Options { ErrorTolerant = true }); + var parserDiagnostic = Assert.Single(diagnostics.Where(diagnostic => diagnostic.Id == "Parser")); + + Assert.Equal("broken.il", parserDiagnostic.Location.Source.Path); + Assert.True(parserDiagnostic.Location.Span.Start >= 0); + Assert.True(parserDiagnostic.Location.Span.Length > 0); + Assert.NotNull(image); + + var imageBuilder = new BlobBuilder(); + image!.Serialize(imageBuilder); + using var pe = new PEReader(imageBuilder.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + var typeNames = reader.TypeDefinitions + .Select(handle => reader.GetString(reader.GetTypeDefinition(handle).Name)) + .ToArray(); + + Assert.Contains("ValidFromDoc1", typeNames); + } + } +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/EventTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/EventTests.cs index 4e897e6d1fd3ae..6ea3a47b34bece 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/EventTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/EventTests.cs @@ -68,5 +68,111 @@ .event MyDelegate MyEvent var attrs = reader.GetCustomAttributes(eventHandle); Assert.True(attrs.Count >= 1, $"Event should have at least 1 custom attribute, got {attrs.Count}"); } + + [Fact] + public void EventWithRaiseAccessor_EmitsAccessorMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi MyDelegate extends [mscorlib]System.MulticastDelegate + { + .method public specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed + { + } + .method public virtual instance void Invoke() runtime managed + { + } + } + .class public auto ansi MyClass extends [mscorlib]System.Object + { + .method public hidebysig specialname instance void add_MyEvent(class MyDelegate value) cil managed + { + ret + } + .method public hidebysig specialname instance void remove_MyEvent(class MyDelegate value) cil managed + { + ret + } + .method public hidebysig specialname instance void raise_MyEvent() cil managed + { + ret + } + .event MyDelegate MyEvent + { + .addon instance void MyClass::add_MyEvent(class MyDelegate) + .removeon instance void MyClass::remove_MyEvent(class MyDelegate) + .fire instance void MyClass::raise_MyEvent() + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var eventHandle = reader.EventDefinitions.Single(); + var @event = reader.GetEventDefinition(eventHandle); + var accessors = @event.GetAccessors(); + string eventTypeName = @event.Type.Kind switch + { + HandleKind.TypeDefinition => reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)@event.Type).Name), + HandleKind.TypeReference => reader.GetString(reader.GetTypeReference((TypeReferenceHandle)@event.Type).Name), + _ => throw new InvalidOperationException($"Unexpected event type handle kind '{@event.Type.Kind}'."), + }; + + Assert.Equal("MyEvent", reader.GetString(@event.Name)); + Assert.Equal("MyDelegate", eventTypeName); + Assert.Equal("add_MyEvent", reader.GetString(reader.GetMethodDefinition(accessors.Adder).Name)); + Assert.Equal("remove_MyEvent", reader.GetString(reader.GetMethodDefinition(accessors.Remover).Name)); + Assert.Equal("raise_MyEvent", reader.GetString(reader.GetMethodDefinition(accessors.Raiser).Name)); + Assert.Equal(3, reader.GetTableRowCount(TableIndex.MethodSemantics)); + } + + [Fact] + public void EventAttributesOtherAccessorAndCustomAttribute_EmitMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi MyDelegate extends [mscorlib]System.MulticastDelegate + { + .method public specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed { } + .method public virtual instance void Invoke() runtime managed { } + } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public specialname instance void add_Changed(class MyDelegate value) cil managed { ret } + .method public specialname instance void remove_Changed(class MyDelegate value) cil managed { ret } + .method public specialname instance void raise_Changed() cil managed { ret } + .method public specialname instance void other_Changed() cil managed { ret } + + .event specialname rtspecialname MyDelegate Changed + { + .addon instance void Test::add_Changed(class MyDelegate) + .removeon instance void Test::remove_Changed(class MyDelegate) + .fire instance void Test::raise_Changed() + .other instance void Test::other_Changed() + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var eventHandle = Assert.Single(reader.EventDefinitions); + var @event = reader.GetEventDefinition(eventHandle); + var accessors = @event.GetAccessors(); + + Assert.True(@event.Attributes.HasFlag(EventAttributes.SpecialName)); + Assert.Equal("add_Changed", reader.GetString(reader.GetMethodDefinition(accessors.Adder).Name)); + Assert.Equal("remove_Changed", reader.GetString(reader.GetMethodDefinition(accessors.Remover).Name)); + Assert.Equal("raise_Changed", reader.GetString(reader.GetMethodDefinition(accessors.Raiser).Name)); + Assert.Equal( + "other_Changed", + reader.GetString(reader.GetMethodDefinition(Assert.Single(accessors.Others)).Name)); + Assert.Equal( + [0x01, 0x00, 0x00, 0x00], + reader.GetBlobBytes(reader.GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(eventHandle))).Value)); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ExceptionHandlingTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/ExceptionHandlingTests.cs index 0096d504f27497..892136bf565412 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/ExceptionHandlingTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ExceptionHandlingTests.cs @@ -196,5 +196,250 @@ leave.s IL_5 // 1-2 Assert.True(methodDef.RelativeVirtualAddress != 0); } + [Fact] + public void OffsetBasedCatchRegion_EmitsExactExceptionRegionBounds() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .maxstack 1 + .try 0 to 5 catch [mscorlib]System.Exception handler 5 to 8 + nop + nop + nop + leave.s IL_8 + IL_5: + pop + leave.s IL_8 + IL_8: + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == "TestMethod"); + var body = pe.GetMethodBody(method.RelativeVirtualAddress); + var region = Assert.Single(body.ExceptionRegions); + + Assert.Equal(ExceptionRegionKind.Catch, region.Kind); + Assert.Equal(0, region.TryOffset); + Assert.Equal(5, region.TryLength); + Assert.Equal(5, region.HandlerOffset); + Assert.Equal(3, region.HandlerLength); + Assert.Equal("Exception", reader.GetString(reader.GetTypeReference((TypeReferenceHandle)region.CatchType).Name)); + } + + [Fact] + public void OffsetBasedFinallyRegion_EmitsExactExceptionRegionBounds() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .maxstack 1 + .try 0 to 3 finally handler 3 to 4 + nop + leave.s IL_4 + IL_3: + endfinally + IL_4: + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == "TestMethod"); + var body = pe.GetMethodBody(method.RelativeVirtualAddress); + var region = Assert.Single(body.ExceptionRegions); + + Assert.Equal(ExceptionRegionKind.Finally, region.Kind); + Assert.Equal(0, region.TryOffset); + Assert.Equal(3, region.TryLength); + Assert.Equal(3, region.HandlerOffset); + Assert.Equal(1, region.HandlerLength); + } + + [Fact] + public void FilterHandler_EmitsFilterExceptionRegion() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .maxstack 1 + .try + { + ldnull + throw + } + filter + { + pop + ldc.i4.1 + endfilter + } + { + pop + leave.s DONE + } + DONE: + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "TestMethod"); + var body = pe.GetMethodBody(method.RelativeVirtualAddress); + var region = Assert.Single(body.ExceptionRegions); + + Assert.Equal(ExceptionRegionKind.Filter, region.Kind); + Assert.True(region.TryLength > 0); + Assert.True(region.FilterOffset >= 0); + Assert.True(region.HandlerOffset >= 0); + Assert.True(region.HandlerLength > 0); + } + + [Fact] + public void FaultHandler_EmitsFaultExceptionRegion() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void TestMethod() cil managed + { + .maxstack 1 + .try + { + nop + leave.s DONE + } + fault + { + endfinally + } + DONE: + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "TestMethod"); + var body = pe.GetMethodBody(method.RelativeVirtualAddress); + var region = Assert.Single(body.ExceptionRegions); + + Assert.Equal(ExceptionRegionKind.Fault, region.Kind); + Assert.True(region.TryLength > 0); + Assert.True(region.HandlerOffset >= region.TryOffset + region.TryLength); + Assert.True(region.HandlerLength > 0); + } + + [Fact] + public void CatchBlock_EmitsCatchExceptionRegion() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .maxstack 1 + .try + { + nop + leave.s DONE + } + catch [mscorlib]System.Exception + { + pop + leave.s DONE + } + DONE: + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == "M"); + var body = pe.GetMethodBody(method.RelativeVirtualAddress); + var region = Assert.Single(body.ExceptionRegions); + + Assert.Equal(ExceptionRegionKind.Catch, region.Kind); + Assert.True(region.TryLength > 0); + Assert.True(region.HandlerLength > 0); + Assert.Equal("Exception", reader.GetString(reader.GetTypeReference((TypeReferenceHandle)region.CatchType).Name)); + } + + [Fact] + public void FinallyBlock_EmitsFinallyExceptionRegion() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .maxstack 1 + .try + { + nop + leave.s DONE + } + finally + { + endfinally + } + DONE: + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == "M"); + var body = pe.GetMethodBody(method.RelativeVirtualAddress); + var region = Assert.Single(body.ExceptionRegions); + + Assert.Equal(ExceptionRegionKind.Finally, region.Kind); + Assert.True(region.TryLength > 0); + Assert.True(region.HandlerLength > 0); + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ExportedTypeTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/ExportedTypeTests.cs index cf80e241dd8f1c..22eb18c3b8b165 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/ExportedTypeTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ExportedTypeTests.cs @@ -153,5 +153,96 @@ .class extern forwarder System.ForwardedType Assert.Equal(DiagnosticSeverity.Warning, warning.Severity); } + [Fact] + public void ExportedTypeAttributesAndImplementations_EmitExpectedMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly extern ForwardedAssembly { } + .assembly test { } + .file Module.netmodule .hash = (01 02 03 04) + + .class extern public Outer + { + .assembly extern ForwardedAssembly + } + .class extern private PrivateType + { + .assembly extern ForwardedAssembly + } + .class extern forwarder ForwardedType + { + .assembly extern ForwardedAssembly + } + .class extern nested public NestedPublic + { + .class extern Outer + } + .class extern nested private NestedPrivate + { + .class extern Outer + } + .class extern nested family NestedFamily + { + .class extern Outer + } + .class extern nested assembly NestedAssembly + { + .class extern Outer + } + .class extern nested famandassem NestedFamAndAssem + { + .class extern Outer + } + .class extern nested famorassem NestedFamOrAssem + { + .class extern Outer + } + .class extern public FileBackedType + { + .file Module.netmodule + } + .class extern public TypeDefIdType + { + .assembly extern ForwardedAssembly + .class 42 + } + .class extern public AttributedType + { + .assembly extern ForwardedAssembly + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + ; + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var exportedTypes = reader.ExportedTypes + .ToDictionary( + handle => reader.GetString(reader.GetExportedType(handle).Name), + handle => (Handle: handle, Type: reader.GetExportedType(handle))); + + Assert.Equal(TypeAttributes.Public, exportedTypes["Outer"].Type.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal(TypeAttributes.NotPublic, exportedTypes["PrivateType"].Type.Attributes & TypeAttributes.VisibilityMask); + Assert.True(exportedTypes["ForwardedType"].Type.IsForwarder); + Assert.Equal(TypeAttributes.NestedPublic, exportedTypes["NestedPublic"].Type.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal(TypeAttributes.NestedPrivate, exportedTypes["NestedPrivate"].Type.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal(TypeAttributes.NestedFamily, exportedTypes["NestedFamily"].Type.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal(TypeAttributes.NestedAssembly, exportedTypes["NestedAssembly"].Type.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal(TypeAttributes.NestedFamANDAssem, exportedTypes["NestedFamAndAssem"].Type.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal(TypeAttributes.NestedFamORAssem, exportedTypes["NestedFamOrAssem"].Type.Attributes & TypeAttributes.VisibilityMask); + + Assert.Equal(HandleKind.AssemblyReference, exportedTypes["Outer"].Type.Implementation.Kind); + Assert.Equal(HandleKind.AssemblyFile, exportedTypes["FileBackedType"].Type.Implementation.Kind); + Assert.Equal(HandleKind.ExportedType, exportedTypes["NestedPublic"].Type.Implementation.Kind); + Assert.Equal(42, exportedTypes["TypeDefIdType"].Type.GetTypeDefinitionId()); + + var attribute = reader.GetCustomAttribute( + Assert.Single(reader.GetCustomAttributes(exportedTypes["AttributedType"].Handle))); + Assert.Equal( + [0x01, 0x00, 0x00, 0x00], + reader.GetBlobBytes(attribute.Value)); + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs index 3d0a13705cea38..ef47a470cc1596 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/FieldTests.cs @@ -59,6 +59,48 @@ .assembly extern System.Runtime { } Assert.Equal(0, fields[2].GetOffset()); } + [Fact] + public void FieldAttributes_EmitExpectedFlagsAndNullConstant() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .field family initonly int32 FamilyInitOnly + .field assembly int32 AssemblyField + .field famandassem int32 FamAndAssemField + .field famorassem int32 FamOrAssemField + .field privatescope int32 PrivateScopeField + .field public notserialized int32 NotSerializedField + .field flags(0x36) int32 FlaggedField + .field public volatile int32 VolatileField + .field public static literal string NullString = nullref + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var fields = reader.FieldDefinitions + .Select(reader.GetFieldDefinition) + .ToDictionary(field => reader.GetString(field.Name)); + + Assert.Equal(FieldAttributes.Family, fields["FamilyInitOnly"].Attributes & FieldAttributes.FieldAccessMask); + Assert.True(fields["FamilyInitOnly"].Attributes.HasFlag(FieldAttributes.InitOnly)); + Assert.Equal(FieldAttributes.Assembly, fields["AssemblyField"].Attributes & FieldAttributes.FieldAccessMask); + Assert.Equal(FieldAttributes.FamANDAssem, fields["FamAndAssemField"].Attributes & FieldAttributes.FieldAccessMask); + Assert.Equal(FieldAttributes.FamORAssem, fields["FamOrAssemField"].Attributes & FieldAttributes.FieldAccessMask); + Assert.Equal(FieldAttributes.PrivateScope, fields["PrivateScopeField"].Attributes & FieldAttributes.FieldAccessMask); +#pragma warning disable SYSLIB0050 + Assert.True(fields["NotSerializedField"].Attributes.HasFlag(FieldAttributes.NotSerialized)); +#pragma warning restore SYSLIB0050 + Assert.Equal((FieldAttributes)0x36, fields["FlaggedField"].Attributes); + Assert.Equal(FieldAttributes.Public, fields["VolatileField"].Attributes & FieldAttributes.FieldAccessMask); + + var nullConstant = reader.GetConstant(fields["NullString"].GetDefaultValue()); + Assert.Equal(ConstantTypeCode.NullReference, nullConstant.TypeCode); + } + [Fact] public void FieldRVA_MultipleDataSections() diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/FunctionPointerTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/FunctionPointerTests.cs index 0a3e83b1c0b39f..de68c84f921465 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/FunctionPointerTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/FunctionPointerTests.cs @@ -40,16 +40,9 @@ .class public auto ansi Test extends [mscorlib]System.Object var reader = pe.GetMetadataReader(); var field = reader.GetFieldDefinition(MetadataTokens.FieldDefinitionHandle(1)); - var sigBytes = reader.GetBlobBytes(field.Signature); - - // Field signature: 0x06 (FIELD), 0x1B (FNPTR), ... - Assert.Equal(0x06, sigBytes[0]); // FIELD calling convention - Assert.Equal(0x1B, sigBytes[1]); // ELEMENT_TYPE_FNPTR - // After FNPTR: calling convention byte, param count, return type, param types - Assert.Equal(0x00, sigBytes[2]); // DEFAULT calling convention - Assert.Equal(0x01, sigBytes[3]); // 1 parameter - Assert.Equal(0x01, sigBytes[4]); // return type: void - Assert.Equal(0x08, sigBytes[5]); // param type: int32 + Assert.Equal( + "method void *(int32)", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); } @@ -76,13 +69,10 @@ .method public static void Invoke(method void *(int32) callback) cil managed .Select(h => reader.GetMethodDefinition(h)) .First(m => reader.GetString(m.Name) == "Invoke"); - var sigBytes = reader.GetBlobBytes(method.Signature); - // Method signature: 0x00 (DEFAULT), 0x01 (1 param), 0x01 (void ret), ... - Assert.Equal(0x00, sigBytes[0]); // DEFAULT calling convention - Assert.Equal(0x01, sigBytes[1]); // 1 parameter - Assert.Equal(0x01, sigBytes[2]); // return type: void - // Parameter should be ELEMENT_TYPE_FNPTR (0x1B) - Assert.Equal(0x1B, sigBytes[3]); // ELEMENT_TYPE_FNPTR + MethodSignature signature = + method.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + Assert.Equal("void", signature.ReturnType); + Assert.Equal(new[] { "method void *(int32)" }, signature.ParameterTypes); } @@ -111,18 +101,10 @@ .class public auto ansi Test extends [mscorlib]System.Object .Select(h => reader.GetMethodDefinition(h)) .First(m => reader.GetString(m.Name) == "GetAdder"); - var sigBytes = reader.GetBlobBytes(method.Signature); - // Method signature: 0x00 (DEFAULT), 0x00 (0 params), return type... - Assert.Equal(0x00, sigBytes[0]); // DEFAULT calling convention - Assert.Equal(0x00, sigBytes[1]); // 0 parameters - // Return type should be ELEMENT_TYPE_FNPTR (0x1B) - Assert.Equal(0x1B, sigBytes[2]); // ELEMENT_TYPE_FNPTR - // After FNPTR: calling convention, param count, return type (int32), param types (int32, int32) - Assert.Equal(0x00, sigBytes[3]); // DEFAULT calling convention for inner sig - Assert.Equal(0x02, sigBytes[4]); // 2 parameters in inner sig - Assert.Equal(0x08, sigBytes[5]); // inner return type: int32 - Assert.Equal(0x08, sigBytes[6]); // inner param 1: int32 - Assert.Equal(0x08, sigBytes[7]); // inner param 2: int32 + MethodSignature signature = + method.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + Assert.Equal("method int32 *(int32, int32)", signature.ReturnType); + Assert.Empty(signature.ParameterTypes); } @@ -143,13 +125,9 @@ .class public auto ansi Test extends [mscorlib]System.Object var reader = pe.GetMetadataReader(); var field = reader.GetFieldDefinition(MetadataTokens.FieldDefinitionHandle(1)); - var sigBytes = reader.GetBlobBytes(field.Signature); - - Assert.Equal(0x06, sigBytes[0]); // FIELD calling convention - Assert.Equal(0x1B, sigBytes[1]); // ELEMENT_TYPE_FNPTR - Assert.Equal(0x00, sigBytes[2]); // DEFAULT calling convention - Assert.Equal(0x00, sigBytes[3]); // 0 parameters - Assert.Equal(0x01, sigBytes[4]); // return type: void + Assert.Equal( + "method void *()", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); } @@ -172,17 +150,9 @@ .class public auto ansi Test extends [mscorlib]System.Object var reader = pe.GetMetadataReader(); var field = reader.GetFieldDefinition(MetadataTokens.FieldDefinitionHandle(1)); - var sigBytes = reader.GetBlobBytes(field.Signature); - - // Field sig: 0x06 (FIELD), 0x1B (FNPTR), 0x00 (DEFAULT), 0x01 (1 param), - // 0x0F (PTR), 0x01 (VOID) [= void* return type], 0x08 (int32 param) - Assert.Equal(0x06, sigBytes[0]); // FIELD calling convention - Assert.Equal(0x1B, sigBytes[1]); // ELEMENT_TYPE_FNPTR - Assert.Equal(0x00, sigBytes[2]); // DEFAULT calling convention - Assert.Equal(0x01, sigBytes[3]); // 1 parameter - Assert.Equal(0x0F, sigBytes[4]); // return type: ELEMENT_TYPE_PTR - Assert.Equal(0x01, sigBytes[5]); // return type inner: VOID (making void*) - Assert.Equal(0x08, sigBytes[6]); // param type: int32 + Assert.Equal( + "method void* *(int32)", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); } @@ -203,16 +173,9 @@ .class public auto ansi Test extends [mscorlib]System.Object var reader = pe.GetMetadataReader(); var field = reader.GetFieldDefinition(MetadataTokens.FieldDefinitionHandle(1)); - var sigBytes = reader.GetBlobBytes(field.Signature); - - // Field sig: 0x06 (FIELD), 0x1B (FNPTR), 0x00 (DEFAULT), 0x00 (0 params), - // 0x0F (PTR), 0x01 (VOID) [= void* return type] - Assert.Equal(0x06, sigBytes[0]); // FIELD calling convention - Assert.Equal(0x1B, sigBytes[1]); // ELEMENT_TYPE_FNPTR - Assert.Equal(0x00, sigBytes[2]); // DEFAULT calling convention - Assert.Equal(0x00, sigBytes[3]); // 0 parameters - Assert.Equal(0x0F, sigBytes[4]); // return type: ELEMENT_TYPE_PTR - Assert.Equal(0x01, sigBytes[5]); // return type inner: VOID (making void*) + Assert.Equal( + "method void* *()", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); } @@ -233,17 +196,9 @@ .class public auto ansi Test extends [mscorlib]System.Object var reader = pe.GetMetadataReader(); var field = reader.GetFieldDefinition(MetadataTokens.FieldDefinitionHandle(1)); - var sigBytes = reader.GetBlobBytes(field.Signature); - - // Field sig: 0x06 (FIELD), 0x1B (FNPTR), 0x00 (DEFAULT), 0x01 (1 param), - // 0x0F (PTR), 0x08 (I4) [= int32* return type], 0x08 (int32 param) - Assert.Equal(0x06, sigBytes[0]); // FIELD calling convention - Assert.Equal(0x1B, sigBytes[1]); // ELEMENT_TYPE_FNPTR - Assert.Equal(0x00, sigBytes[2]); // DEFAULT calling convention - Assert.Equal(0x01, sigBytes[3]); // 1 parameter - Assert.Equal(0x0F, sigBytes[4]); // return type: ELEMENT_TYPE_PTR - Assert.Equal(0x08, sigBytes[5]); // return type inner: int32 (making int32*) - Assert.Equal(0x08, sigBytes[6]); // param type: int32 + Assert.Equal( + "method int32* *(int32)", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); } @@ -265,15 +220,30 @@ .class public auto ansi Test extends [mscorlib]System.Object var reader = pe.GetMetadataReader(); var field = reader.GetFieldDefinition(MetadataTokens.FieldDefinitionHandle(1)); - var sigBytes = reader.GetBlobBytes(field.Signature); - - // Field sig: 0x06 (FIELD), then the type is FNPTR(void(int32)) with PTR modifier - // The modifier ordering means: 0x06, 0x1B (FNPTR), ..., then 0x0F (PTR) wraps it - // But in practice ECMA-335 encodes: 0x06, 0x0F (PTR), 0x1B (FNPTR), ... - Assert.Equal(0x06, sigBytes[0]); // FIELD calling convention - // The next two bytes must contain both PTR and FNPTR - Assert.Contains((byte)0x1B, sigBytes.Skip(1).ToArray()); // Must have ELEMENT_TYPE_FNPTR - Assert.Contains((byte)0x0F, sigBytes.Skip(1).ToArray()); // Must have ELEMENT_TYPE_PTR + Assert.Equal( + "method void *(int32)*", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + [Fact] + public void FunctionPointer_VarargSignature_EmitsSentinel() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .field public static method vararg void *(int32, ..., object) fnPtrField + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var field = reader.GetFieldDefinition(MetadataTokens.FieldDefinitionHandle(1)); + Assert.Equal( + "method void *(int32, ..., object)", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/GenericTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/GenericTests.cs index 561c1bf0d4a713..2f475ab9ccfe34 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/GenericTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/GenericTests.cs @@ -126,6 +126,148 @@ .field public !1 fieldU Assert.Equal(2, genericParams.Count); } + [Fact] + public void DuplicateGenericParameterNames_PreserveFirstNameBindingInFieldSignature() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit DuplicateName`2 extends [mscorlib]System.Object + { + .field public !T Value + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var typeDef = reader.TypeDefinitions + .Select(h => reader.GetTypeDefinition(h)) + .First(t => reader.GetString(t.Name) == "DuplicateName`2"); + + var genericParameters = typeDef.GetGenericParameters() + .Select(reader.GetGenericParameter) + .ToArray(); + Assert.Equal(2, genericParameters.Length); + Assert.All(genericParameters, parameter => Assert.Equal("T", reader.GetString(parameter.Name))); + + var field = reader.GetFieldDefinition(typeDef.GetFields().Single()); + Assert.Equal("!0", field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + [Fact] + public void GenericParameterAttributesAndBounds_EmitExpectedMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class interface public abstract auto ansi IVariant`5<+T, -U, class .ctor V, valuetype byreflike W, flags(0x0004) X> + { + } + .class public auto ansi Constrained`1<(class [mscorlib]System.IDisposable) T> extends [mscorlib]System.Object + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var variantType = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Single(type => reader.GetString(type.Name) == "IVariant`5"); + var parameters = variantType.GetGenericParameters() + .Select(reader.GetGenericParameter) + .ToDictionary(parameter => reader.GetString(parameter.Name)); + + Assert.Equal(GenericParameterAttributes.Covariant, parameters["T"].Attributes & GenericParameterAttributes.VarianceMask); + Assert.Equal(GenericParameterAttributes.Contravariant, parameters["U"].Attributes & GenericParameterAttributes.VarianceMask); + Assert.True(parameters["V"].Attributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); + Assert.True(parameters["V"].Attributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint)); + Assert.True(parameters["W"].Attributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint)); + Assert.True(((int)parameters["W"].Attributes & 0x20) != 0); + Assert.Equal((GenericParameterAttributes)0x0004, parameters["X"].Attributes); + + var constrainedType = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Single(type => reader.GetString(type.Name) == "Constrained`1"); + var constrainedParameter = reader.GetGenericParameter(Assert.Single(constrainedType.GetGenericParameters())); + var constraint = reader.GetGenericParameterConstraint(Assert.Single(constrainedParameter.GetConstraints())); + + Assert.Equal(HandleKind.TypeSpecification, constraint.Type.Kind); + Assert.Equal( + "[mscorlib]System.IDisposable", + reader.GetTypeSpecification((TypeSpecificationHandle)constraint.Type) + .DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + [Fact] + public void ClassGenericParameterAndConstraintDirectives_AttachCustomAttributes() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi ByName`1<([mscorlib]System.ICloneable) T> extends [mscorlib]System.Object + { + .param type T + .custom instance void [mscorlib]System.CLSCompliantAttribute::.ctor(bool) = (01 00 01 00 00) + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + .param constraint T, [mscorlib]System.ICloneable + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + .custom instance void [mscorlib]System.CLSCompliantAttribute::.ctor(bool) = (01 00 01 00 00) + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var types = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Where(definition => reader.GetString(definition.Name) == "ByName`1") + .ToDictionary(definition => reader.GetString(definition.Name)); + + AssertGenericParameterAnnotation(reader, types["ByName`1"], "ICloneable"); + } + + [Fact] + public void MethodGenericParameterAndConstraintDirectives_AttachCustomAttributes() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .param type [0] + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + .param type T + .custom instance void [mscorlib]System.CLSCompliantAttribute::.ctor(bool) = (01 00 01 00 00) + .param constraint [0], [mscorlib]System.IDisposable + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + .param constraint T, [mscorlib]System.ICloneable + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.GetMethodDefinition(Assert.Single(reader.MethodDefinitions)); + var parameterHandle = Assert.Single(method.GetGenericParameters()); + var parameter = reader.GetGenericParameter(parameterHandle); + var constraints = parameter.GetConstraints().ToArray(); + + Assert.Equal(2, reader.GetCustomAttributes(parameterHandle).Count); + Assert.Equal(2, constraints.Length); + Assert.All(constraints, constraint => Assert.Single(reader.GetCustomAttributes(constraint))); + Assert.Equal( + new[] { "ICloneable", "IDisposable" }, + constraints + .Select(reader.GetGenericParameterConstraint) + .Select(constraint => reader.GetString(reader.GetTypeReference((TypeReferenceHandle)constraint.Type).Name)) + .OrderBy(name => name)); + } + [Fact] public void GenericMethod_UsesNamedElementList() @@ -300,13 +442,10 @@ .method public hidebysig instance void M() cil managed // NOT a TypeRef to System.Object. Assert.Equal(HandleKind.TypeSpecification, constraintType.Kind); - // Decode the TypeSpec blob to verify it's a generic instantiation of IMinusT`1 var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)constraintType); - var sigBytes = reader.GetBlobBytes(typeSpec.Signature); - - // Expected: GENERICINST (0x15), CLASS (0x12), , - // 1 (generic arg count), VAR 1 (type parameter !U which is index 1) - Assert.Equal(0x15, sigBytes[0]); // ELEMENT_TYPE_GENERICINST + Assert.Equal( + "IMinusT`1", + typeSpec.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); } @@ -365,11 +504,100 @@ .method public hidebysig newslot abstract virtual instance void Assert.Equal(HandleKind.TypeSpecification, constraintType.Kind); var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)constraintType); - var sigBytes = reader.GetBlobBytes(typeSpec.Signature); + Assert.Equal( + "IMinusT`1", + typeSpec.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + [Fact] + public void VariantGenericParameters_EmitVarianceFlags() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .class interface public abstract auto ansi IVariant`2<+([mscorlib]System.Object) TOut, -([mscorlib]System.Object) TIn> + { + } + """; - // Expected: GENERICINST (0x15), CLASS (0x12), , - // 1 (generic arg count), VAR 0 (type parameter !PlusT at index 0) - Assert.Equal(0x15, sigBytes[0]); // ELEMENT_TYPE_GENERICINST + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var type = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .First(definition => reader.GetString(definition.Name) == "IVariant`2"); + var parameters = type.GetGenericParameters() + .Select(reader.GetGenericParameter) + .ToArray(); + + Assert.Equal(2, parameters.Length); + Assert.Equal("TOut", reader.GetString(parameters[0].Name)); + Assert.Equal("TIn", reader.GetString(parameters[1].Name)); + Assert.True((parameters[0].Attributes & GenericParameterAttributes.Covariant) != 0); + Assert.True((parameters[1].Attributes & GenericParameterAttributes.Contravariant) != 0); + } + + [Fact] + public void GenericFieldAndMethod_EmitVarAndMVarSignatures() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit GenericBox`1 extends [mscorlib]System.Object + { + .field public !0 Value + + .method public static !!0 Identity(!!0 arg) cil managed + { + ldarg.0 + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var type = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .First(definition => reader.GetString(definition.Name) == "GenericBox`1"); + var field = reader.GetFieldDefinition(type.GetFields().Single()); + var method = type.GetMethods() + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == "Identity"); + + Assert.Equal("!0", field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + + var typeParameter = reader.GetGenericParameter(type.GetGenericParameters().Single()); + var methodParameter = reader.GetGenericParameter(method.GetGenericParameters().Single()); + Assert.Equal("T", reader.GetString(typeParameter.Name)); + Assert.Equal("U", reader.GetString(methodParameter.Name)); + + MethodSignature methodSignature = + method.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + Assert.True(methodSignature.Header.IsGeneric); + Assert.Equal(1, methodSignature.GenericParameterCount); + Assert.Equal("!!0", methodSignature.ReturnType); + Assert.Equal(new[] { "!!0" }, methodSignature.ParameterTypes); + } + + private static void AssertGenericParameterAnnotation( + MetadataReader reader, + TypeDefinition type, + string expectedConstraint) + { + var parameterHandle = Assert.Single(type.GetGenericParameters()); + var parameter = reader.GetGenericParameter(parameterHandle); + var constraintHandle = Assert.Single(parameter.GetConstraints()); + var constraint = reader.GetGenericParameterConstraint(constraintHandle); + + Assert.Equal(2, reader.GetCustomAttributes(parameterHandle).Count); + Assert.Equal(2, reader.GetCustomAttributes(constraintHandle).Count); + Assert.Equal(HandleKind.TypeReference, constraint.Type.Kind); + Assert.Equal( + expectedConstraint, + reader.GetString(reader.GetTypeReference((TypeReferenceHandle)constraint.Type).Name)); } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/InstructionTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/InstructionTests.cs index 73c90be93c88da..e3fc4667e05b6f 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/InstructionTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/InstructionTests.cs @@ -281,6 +281,368 @@ .method public static void M() cil managed Assert.Empty(diagnostics); } + [Fact] + public void FloatingPointInstruction_Float64BitPattern_EmitsExpectedDouble() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static float64 GetPi() cil managed + { + ldc.r8 float64(0x400921FB54442D18) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + byte[] il = GetMethodIL(pe, "GetPi"); + + Assert.Equal(0x23, il[0]); + Assert.Equal(Math.PI, BitConverter.ToDouble(il, 1), 14); + } + + [Fact] + public void FloatingPointInstruction_ByteForms_EmitExpectedConstants() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static float32 GetSingle() cil managed + { + ldc.r4 (00 00 80 3F) + ret + } + + .method public static float64 GetDouble() cil managed + { + ldc.r8 bytearray(00 00 00 00 00 00 F0 3F) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + byte[] singleIL = GetMethodIL(pe, "GetSingle"); + byte[] doubleIL = GetMethodIL(pe, "GetDouble"); + + Assert.Equal(0x22, singleIL[0]); + Assert.Equal(1f, BitConverter.ToSingle(singleIL, 1)); + Assert.Equal(0x23, doubleIL[0]); + Assert.Equal(1d, BitConverter.ToDouble(doubleIL, 1)); + } + + [Fact] + public void CalliInstruction_EmitsStandaloneSignatureToken() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static int32 M(native int functionPointer) cil managed + { + ldarg.0 + calli default int32() + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.StandAloneSig)); + + var signature = reader.GetStandaloneSignature(MetadataTokens.StandaloneSignatureHandle(1)); + MethodSignature decodedSignature = + signature.DecodeMethodSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + Assert.Equal(SignatureCallingConvention.Default, decodedSignature.Header.CallingConvention); + Assert.Equal("int32", decodedSignature.ReturnType); + Assert.Empty(decodedSignature.ParameterTypes); + + int token = DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "M", ILOpcode.calli); + Assert.Equal(MetadataTokens.GetToken(MetadataTokens.StandaloneSignatureHandle(1)), token); + } + + [Theory] + [InlineData("unmanaged cdecl", (int)SignatureCallingConvention.CDecl, false, false)] + [InlineData("unmanaged stdcall", (int)SignatureCallingConvention.StdCall, false, false)] + [InlineData("unmanaged thiscall", (int)SignatureCallingConvention.ThisCall, false, false)] + [InlineData("unmanaged fastcall", (int)SignatureCallingConvention.FastCall, false, false)] + [InlineData("unmanaged", (int)SignatureCallingConvention.Unmanaged, false, false)] + [InlineData("vararg", (int)SignatureCallingConvention.VarArgs, false, false)] + [InlineData("instance default", (int)SignatureCallingConvention.Default, true, false)] + [InlineData("callconv(0x05)", (int)SignatureCallingConvention.VarArgs, false, false)] + public void CalliInstruction_CallingConvention_EmitsDecodedSignature( + string callingConvention, + int expectedCallingConvention, + bool expectedInstance, + bool expectedExplicitThis) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + ldc.i4.0 + conv.i + calli {{callingConvention}} void() + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var signature = reader.GetStandaloneSignature(Assert.Single( + Enumerable.Range(1, reader.GetTableRowCount(TableIndex.StandAloneSig)) + .Select(MetadataTokens.StandaloneSignatureHandle))); + MethodSignature decoded = + signature.DecodeMethodSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + + Assert.Equal((SignatureCallingConvention)expectedCallingConvention, decoded.Header.CallingConvention); + Assert.Equal(expectedInstance, decoded.Header.IsInstance); + Assert.True( + decoded.Header.HasExplicitThis == expectedExplicitThis, + $"Expected explicit-this={expectedExplicitThis}, header=0x{decoded.Header.RawValue:X2}"); + Assert.Equal("void", decoded.ReturnType); + Assert.Empty(decoded.ParameterTypes); + } + + [Fact] + public void LdstrInstruction_ByteArrayAndAnsiForms_EmitExpectedUserStrings() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static string GetUtf16() cil managed + { + ldstr bytearray(48 00 69 00) + ret + } + + .method public static string GetAnsi() cil managed + { + ldstr ansi("AB") + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Equal("Hi", ReadLdstrValue(pe, reader, "GetUtf16")); + Assert.Equal("\u4241", ReadLdstrValue(pe, reader, "GetAnsi")); + } + + [Fact] + public void Ldstr_WithControlAndQuotedEscapes_EmitsExpectedUserStrings() + { + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + .class public auto ansi beforefieldinit Test extends [System.Runtime]System.Object + { + .method public static string ControlEscapes() cil managed + { + ldstr "A\bB\fC\vD\aE\?F\'G" + ret + } + + .method public static string QuotedLiteral() cil managed + { + ldstr "double\"quoted\?" + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Equal("A\bB\fC\vD\aE?F'G", ReadLdstrValue(pe, reader, "ControlEscapes")); + Assert.Equal("double\"quoted?", ReadLdstrValue(pe, reader, "QuotedLiteral")); + } + + [Fact] + public void Ldstr_WithLineContinuationAndShortOrHighOctalEscapes_EmitsExpectedUserString() + { + string source = + ".assembly extern System.Runtime { }\n" + + ".assembly test { }\n" + + ".class public auto ansi beforefieldinit Test extends [System.Runtime]System.Object\n" + + "{\n" + + " .method public static string FallbackEscapes() cil managed\n" + + " {\n" + + " ldstr \"line\\\n" + + " continued\\12\\400!\"\n" + + " ret\n" + + " }\n" + + "}\n"; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Equal("linecontinued12400!", ReadLdstrValue(pe, reader, "FallbackEscapes")); + } + + [Fact] + public void SwitchInstruction_IntegerOffsets_EmitsExpectedBranchTable() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static int32 M(int32 value) cil managed + { + ldarg.0 + switch (3, 6) + ldc.i4.0 + ret + ldc.i4.1 + ret + ldc.i4.2 + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + byte[] il = GetMethodIL(pe, "M"); + int switchOffset = Array.IndexOf(il, (byte)0x45); + + Assert.True(switchOffset >= 0); + Assert.Equal(2, BitConverter.ToInt32(il, switchOffset + 1)); + Assert.Equal(3, BitConverter.ToInt32(il, switchOffset + 5)); + Assert.Equal(6, BitConverter.ToInt32(il, switchOffset + 9)); + } + + [Fact] + public void LdtokenInstruction_TypeReference_EmitsTypeReferenceToken() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static valuetype [mscorlib]System.RuntimeTypeHandle GetHandle() cil managed + { + ldtoken [mscorlib]System.Int32 + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + byte[] il = GetMethodIL(pe, "GetHandle"); + + Assert.Equal(0xD0, il[0]); + int token = BitConverter.ToInt32(il, 1); + var handle = MetadataTokens.EntityHandle(token); + Assert.Equal(HandleKind.TypeReference, handle.Kind); + Assert.Equal("Int32", reader.GetString(reader.GetTypeReference((TypeReferenceHandle)handle).Name)); + } + + [Fact] + public void InstructionOperandForms_EmitExpectedTokensAndRawOperands() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .field public static int32 Value + + .method public specialname rtspecialname instance void .ctor() cil managed + { + ldarg.0 + call instance void [mscorlib]System.Object::.ctor() + ret + } + + .method public static void Exercise(object value) cil managed + { + br 0 + ldsfld mdtoken(0x04000001) + pop + ldsfld int32 Test::Value + pop + ldsfld mdtoken(0x01000001) + pop + ldarg.0 + unaligned. 1 + ldind.i4 + pop + ldarg.0 + callvirt instance string [mscorlib]System.Object::ToString() + pop + newobj instance void Test::.ctor() + pop + ldc.i4.0 + ldnull + ldc.i4.0 + conv.i + calli default void(int32, string) + ldtoken Test + pop + switch () + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + byte[] il = GetMethodIL(pe, "Exercise"); + + ImmutableArray fieldTokens = + DocumentCompilerTestHelpers.GetTokenOperands(pe, reader, "Exercise", ILOpcode.ldsfld); + Assert.Equal(3, fieldTokens.Length); + Assert.Equal(HandleKind.FieldDefinition, MetadataTokens.EntityHandle(fieldTokens[0]).Kind); + Assert.Equal(HandleKind.FieldDefinition, MetadataTokens.EntityHandle(fieldTokens[1]).Kind); + Assert.Equal(HandleKind.TypeReference, MetadataTokens.EntityHandle(fieldTokens[2]).Kind); + + int callvirtToken = + DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "Exercise", ILOpcode.callvirt); + Assert.Equal( + "ToString", + reader.GetString(reader.GetMemberReference((MemberReferenceHandle)MetadataTokens.EntityHandle(callvirtToken)).Name)); + + int newobjToken = + DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "Exercise", ILOpcode.newobj); + Assert.Equal( + ".ctor", + reader.GetString(reader.GetMethodDefinition((MethodDefinitionHandle)MetadataTokens.EntityHandle(newobjToken)).Name)); + + int typeToken = + DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "Exercise", ILOpcode.ldtoken); + Assert.Equal( + "Test", + reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)MetadataTokens.EntityHandle(typeToken)).Name)); + + var calliSignature = reader.GetStandaloneSignature( + MetadataTokens.StandaloneSignatureHandle(reader.GetTableRowCount(TableIndex.StandAloneSig))); + MethodSignature decodedCalli = + calliSignature.DecodeMethodSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + Assert.Equal("void", decodedCalli.ReturnType); + Assert.Equal(new[] { "int32", "string" }, decodedCalli.ParameterTypes); + + Assert.True(ContainsSequence(il, [0x38, 0x00, 0x00, 0x00, 0x00])); + Assert.True(ContainsSequence(il, [0xFE, 0x12, 0x01])); + Assert.True(ContainsSequence(il, [0x45, 0x00, 0x00, 0x00, 0x00])); + } + [Fact] public void FieldRVA_DataLabelEmitted() @@ -303,5 +665,36 @@ .field public static int32 myData at D_1 Assert.True(fieldRvaCount >= 1, $"FieldRVA table should have at least 1 entry, has {fieldRvaCount}"); } + private static byte[] GetMethodIL(PEReader pe, string methodName) + { + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == methodName); + return pe.GetMethodBody(method.RelativeVirtualAddress).GetILBytes()!; + } + + private static string ReadLdstrValue(PEReader pe, MetadataReader reader, string methodName) + { + byte[] il = GetMethodIL(pe, methodName); + Assert.Equal(0x72, il[0]); + int token = BitConverter.ToInt32(il, 1); + Assert.Equal(0x70, (token >> 24) & 0xFF); + return reader.GetUserString(MetadataTokens.UserStringHandle(token & 0x00FFFFFF)); + } + + private static bool ContainsSequence(byte[] bytes, ReadOnlySpan sequence) + { + for (int i = 0; i <= bytes.Length - sequence.Length; i++) + { + if (bytes.AsSpan(i, sequence.Length).SequenceEqual(sequence)) + { + return true; + } + } + + return false; + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs index 6f5981510ca25e..1d5984a1372812 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/InteropTests.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Immutable; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; using Internal.IL; using Xunit; using DocumentCompilerTestHelpers = ILAssembler.Tests.DocumentCompilerTestHelpers; @@ -104,5 +106,450 @@ int32 GetCurrentProcessId() cil managed preservesig var import = method.GetImport(); Assert.False(import.Module.IsNil); } + + [Fact] + public void MarshalLpStrParameter_EmitsMarshallingDescriptorBlob() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void Print(string marshal(lpstr) text) cil managed + { + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var method = reader.MethodDefinitions + .Select(h => reader.GetMethodDefinition(h)) + .First(m => reader.GetString(m.Name) == "Print"); + var parameterHandle = Assert.Single(method.GetParameters()); + var parameter = reader.GetParameter(parameterHandle); + var descriptor = parameter.GetMarshallingDescriptor(); + + Assert.False(descriptor.IsNil); + Assert.Equal("text", reader.GetString(parameter.Name)); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.FieldMarshal)); + BlobReader descriptorReader = reader.GetBlobReader(descriptor); + Assert.Equal((byte)UnmanagedType.LPStr, descriptorReader.ReadByte()); + Assert.Equal(0, descriptorReader.RemainingBytes); + } + + [Fact] + public void PinvokeMethod_WithAlias_EmitsImportNameAndModuleReference() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .module test.dll + .class public auto ansi beforefieldinit NativeMethods extends [mscorlib]System.Object + { + .method public static pinvokeimpl("kernel32.dll" as "GetTickCount" winapi) + int32 GetTickCountManaged() cil managed preservesig + { + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == "GetTickCountManaged"); + var import = method.GetImport(); + var moduleRef = reader.GetModuleReference(import.Module); + + Assert.True(method.Attributes.HasFlag(MethodAttributes.PinvokeImpl)); + Assert.Equal("GetTickCount", reader.GetString(import.Name)); + Assert.Equal("kernel32.dll", reader.GetString(moduleRef.Name)); + } + + [Fact] + public void PinvokeMethod_EmitsImportMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .module test.dll + .class public auto ansi beforefieldinit NativeMethods extends [mscorlib]System.Object + { + .method public static pinvokeimpl("kernel32.dll" winapi) + int32 GetCurrentProcessId() cil managed preservesig + { + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == "GetCurrentProcessId"); + var import = method.GetImport(); + var moduleRef = reader.GetModuleReference(import.Module); + + Assert.True(method.Attributes.HasFlag(MethodAttributes.PinvokeImpl)); + Assert.Equal("kernel32.dll", reader.GetString(moduleRef.Name)); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.ModuleRef)); + } + + [Theory] + [InlineData("nomangle", (int)MethodImportAttributes.ExactSpelling)] + [InlineData("ansi", (int)MethodImportAttributes.CharSetAnsi)] + [InlineData("unicode", (int)MethodImportAttributes.CharSetUnicode)] + [InlineData("autochar", (int)MethodImportAttributes.CharSetAuto)] + [InlineData("lasterr", (int)MethodImportAttributes.SetLastError)] + [InlineData("cdecl", (int)MethodImportAttributes.CallingConventionCDecl)] + [InlineData("stdcall", (int)MethodImportAttributes.CallingConventionStdCall)] + [InlineData("thiscall", (int)MethodImportAttributes.CallingConventionThisCall)] + [InlineData("fastcall", (int)MethodImportAttributes.CallingConventionFastCall)] + [InlineData("bestfit:on", (int)MethodImportAttributes.BestFitMappingEnable)] + [InlineData("bestfit:off", (int)MethodImportAttributes.BestFitMappingDisable)] + [InlineData("charmaperror:on", (int)MethodImportAttributes.ThrowOnUnmappableCharEnable)] + [InlineData("charmaperror:off", (int)MethodImportAttributes.ThrowOnUnmappableCharDisable)] + [InlineData("flags(0x1234)", 0x1234)] + public void PinvokeAttribute_EmitsExpectedImportFlags(string attribute, int expectedFlags) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi NativeMethods extends [mscorlib]System.Object + { + .method public static pinvokeimpl("native.dll" {{attribute}}) + void M() cil managed preservesig + { + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.GetMethodDefinition(Assert.Single(reader.MethodDefinitions)); + var import = method.GetImport(); + + Assert.Equal((MethodImportAttributes)expectedFlags, import.Attributes); + Assert.Equal("M", reader.GetString(method.Name)); + Assert.Equal(string.Empty, reader.GetString(import.Name)); + Assert.Equal("native.dll", reader.GetString(reader.GetModuleReference(import.Module).Name)); + } + + [Theory] + [InlineData("bool", 0x02)] + [InlineData("int8", 0x03)] + [InlineData("uint8", 0x04)] + [InlineData("int16", 0x05)] + [InlineData("uint16", 0x06)] + [InlineData("int32", 0x07)] + [InlineData("uint32", 0x08)] + [InlineData("int64", 0x09)] + [InlineData("uint64", 0x0A)] + [InlineData("float32", 0x0B)] + [InlineData("float64", 0x0C)] + [InlineData("currency", 0x0F)] + [InlineData("bstr", 0x13)] + [InlineData("lpstr", 0x14)] + [InlineData("lpwstr", 0x15)] + [InlineData("lptstr", 0x16)] + [InlineData("iunknown", 0x19)] + [InlineData("idispatch", 0x1A)] + [InlineData("struct", 0x1B)] + [InlineData("interface", 0x1C)] + [InlineData("int", 0x1F)] + [InlineData("uint", 0x20)] + [InlineData("byvalstr", 0x22)] + [InlineData("ansi bstr", 0x23)] + [InlineData("tbstr", 0x24)] + [InlineData("method", 0x26)] + [InlineData("as any", 0x28)] + [InlineData("lpstruct", 0x2B)] + [InlineData("error", 0x2D)] + public void MarshalSimpleNativeType_EmitsExpectedDescriptor(string nativeType, int expectedTypeCode) + { + var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); + Assert.Empty(diagnostics); + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); + + Assert.Equal((byte)expectedTypeCode, descriptor.ReadByte()); + Assert.Equal(0, descriptor.RemainingBytes); + } + + [Theory] + [InlineData("fixed systring[5]")] + [InlineData("fixed array[3] int16")] + [InlineData("iunknown(iidparam=2)")] + [InlineData("idispatch(iidparam=3)")] + [InlineData("interface(iidparam=4)")] + [InlineData("int32*")] + [InlineData("int32[]")] + [InlineData("int32[3]")] + [InlineData("int32[3+2]")] + [InlineData("int32[+2]")] + public void MarshalNativeTypeWithPayload_EmitsExpectedDescriptor(string nativeType) + { + var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); + if (nativeType == "int32*") + { + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedNativeType); + } + else + { + Assert.Empty(diagnostics); + } + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); + + switch (nativeType) + { + case "fixed systring[5]": + Assert.Equal((byte)UnmanagedType.ByValTStr, descriptor.ReadByte()); + Assert.Equal(5, descriptor.ReadCompressedInteger()); + break; + + case "fixed array[3] int16": + Assert.Equal((byte)UnmanagedType.ByValArray, descriptor.ReadByte()); + Assert.Equal(3, descriptor.ReadCompressedInteger()); + Assert.Equal((byte)UnmanagedType.I2, descriptor.ReadByte()); + break; + + case "iunknown(iidparam=2)": + Assert.Equal((byte)UnmanagedType.IUnknown, descriptor.ReadByte()); + Assert.Equal(2, descriptor.ReadCompressedInteger()); + break; + + case "idispatch(iidparam=3)": + Assert.Equal((byte)UnmanagedType.IDispatch, descriptor.ReadByte()); + Assert.Equal(3, descriptor.ReadCompressedInteger()); + break; + + case "interface(iidparam=4)": + Assert.Equal((byte)UnmanagedType.Interface, descriptor.ReadByte()); + Assert.Equal(4, descriptor.ReadCompressedInteger()); + break; + + case "int32*": + Assert.Equal(0x10, descriptor.ReadByte()); + Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); + break; + + case "int32[]": + Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); + Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); + break; + + case "int32[3]": + Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); + Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); + Assert.Equal(0, descriptor.ReadCompressedInteger()); + Assert.Equal(3, descriptor.ReadCompressedInteger()); + Assert.Equal(0, descriptor.ReadCompressedInteger()); + break; + + case "int32[3+2]": + Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); + Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); + Assert.Equal(2, descriptor.ReadCompressedInteger()); + Assert.Equal(3, descriptor.ReadCompressedInteger()); + Assert.Equal(1, descriptor.ReadCompressedInteger()); + break; + + case "int32[+2]": + Assert.Equal((byte)UnmanagedType.LPArray, descriptor.ReadByte()); + Assert.Equal((byte)UnmanagedType.I4, descriptor.ReadByte()); + Assert.Equal(2, descriptor.ReadCompressedInteger()); + break; + } + + Assert.Equal(0, descriptor.RemainingBytes); + } + + [Theory] + [InlineData("custom(\"Marshaller.Type\", \"cookie\")", false)] + [InlineData("custom(\"guid\", \"native\", \"Marshaller.Type\", \"cookie\")", true)] + public void MarshalCustomMarshaller_EmitsStructuredDescriptor( + string nativeType, + bool usesDeprecatedForm) + { + var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); + if (usesDeprecatedForm) + { + Assert.Contains( + diagnostics, + diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedCustomMarshaller); + } + else + { + Assert.Empty(diagnostics); + } + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); + + Assert.Equal((byte)UnmanagedType.CustomMarshaler, descriptor.ReadByte()); + if (usesDeprecatedForm) + { + Assert.Equal("guid", descriptor.ReadSerializedString()); + Assert.Equal("native", descriptor.ReadSerializedString()); + } + else + { + Assert.Equal(0, descriptor.ReadCompressedInteger()); + Assert.Equal(0, descriptor.ReadCompressedInteger()); + } + + Assert.Equal("Marshaller.Type", descriptor.ReadSerializedString()); + Assert.Equal("cookie", descriptor.ReadSerializedString()); + Assert.Equal(0, descriptor.RemainingBytes); + } + + [Theory] + [InlineData("variant", (int)VarEnum.VT_VARIANT)] + [InlineData("currency", (int)VarEnum.VT_CY)] + [InlineData("void", (int)VarEnum.VT_VOID)] + [InlineData("bool", (int)VarEnum.VT_BOOL)] + [InlineData("int8", (int)VarEnum.VT_I1)] + [InlineData("int16", (int)VarEnum.VT_I2)] + [InlineData("int32", (int)VarEnum.VT_I4)] + [InlineData("int64", (int)VarEnum.VT_I8)] + [InlineData("float32", (int)VarEnum.VT_R4)] + [InlineData("float64", (int)VarEnum.VT_R8)] + [InlineData("uint8", (int)VarEnum.VT_UI1)] + [InlineData("uint16", (int)VarEnum.VT_UI2)] + [InlineData("uint32", (int)VarEnum.VT_UI4)] + [InlineData("uint64", (int)VarEnum.VT_UI8)] + [InlineData("decimal", (int)VarEnum.VT_DECIMAL)] + [InlineData("date", (int)VarEnum.VT_DATE)] + [InlineData("bstr", (int)VarEnum.VT_BSTR)] + [InlineData("lpstr", (int)VarEnum.VT_LPSTR)] + [InlineData("lpwstr", (int)VarEnum.VT_LPWSTR)] + [InlineData("iunknown", (int)VarEnum.VT_UNKNOWN)] + [InlineData("idispatch", (int)VarEnum.VT_DISPATCH)] + [InlineData("safearray", (int)VarEnum.VT_SAFEARRAY)] + [InlineData("int", (int)VarEnum.VT_INT)] + [InlineData("uint", (int)VarEnum.VT_UINT)] + [InlineData("error", (int)VarEnum.VT_ERROR)] + [InlineData("hresult", (int)VarEnum.VT_HRESULT)] + [InlineData("carray", (int)VarEnum.VT_CARRAY)] + [InlineData("userdefined", (int)VarEnum.VT_USERDEFINED)] + [InlineData("record", (int)VarEnum.VT_RECORD)] + [InlineData("filetime", (int)VarEnum.VT_FILETIME)] + [InlineData("blob", (int)VarEnum.VT_BLOB)] + [InlineData("stream", (int)VarEnum.VT_STREAM)] + [InlineData("storage", (int)VarEnum.VT_STORAGE)] + [InlineData("streamed_object", (int)VarEnum.VT_STREAMED_OBJECT)] + [InlineData("stored_object", (int)VarEnum.VT_STORED_OBJECT)] + [InlineData("blob_object", (int)VarEnum.VT_BLOB_OBJECT)] + [InlineData("cf", (int)VarEnum.VT_CF)] + [InlineData("clsid", (int)VarEnum.VT_CLSID)] + [InlineData("int32[]", (int)(VarEnum.VT_I4 | VarEnum.VT_ARRAY))] + [InlineData("int32 vector", (int)(VarEnum.VT_I4 | VarEnum.VT_VECTOR))] + [InlineData("int32&", (int)(VarEnum.VT_I4 | VarEnum.VT_BYREF))] + public void MarshalSafeArrayVariantType_EmitsExpectedDescriptor(string variantType, int expectedVariantType) + { + var (diagnostics, image) = CompileParameterMarshallingDescriptor($"safearray {variantType}"); + Assert.Empty(diagnostics); + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + BlobHandle descriptorHandle = GetOnlyMarshallingDescriptor(reader); + BlobReader descriptor = reader.GetBlobReader(descriptorHandle); + + Assert.True( + descriptor.ReadByte() == (byte)UnmanagedType.SafeArray, + Convert.ToHexString(reader.GetBlobBytes(descriptorHandle))); + Assert.Equal(expectedVariantType, descriptor.ReadCompressedInteger()); + Assert.Equal(0, descriptor.ReadCompressedInteger()); + Assert.Equal(0, descriptor.RemainingBytes); + } + + [Theory] + [InlineData("variant", 0x0E)] + [InlineData("syschar", 0x0D)] + [InlineData("void", 0x01)] + [InlineData("decimal", 0x11)] + [InlineData("date", 0x12)] + [InlineData("objectref", 0x18)] + [InlineData("nested struct", 0x21)] + public void DeprecatedMarshalNativeType_EmitsDescriptorAndWarning( + string nativeType, + int expectedTypeCode) + { + var (diagnostics, image) = CompileParameterMarshallingDescriptor(nativeType); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.DeprecatedNativeType); + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + BlobReader descriptor = reader.GetBlobReader(GetOnlyMarshallingDescriptor(reader)); + + Assert.Equal((byte)expectedTypeCode, descriptor.ReadByte()); + Assert.Equal(0, descriptor.RemainingBytes); + } + + [Fact] + public void MarshalSafeArrayWithUserDefinedType_EmitsVariantAndTypeName() + { + var (diagnostics, image) = + CompileParameterMarshallingDescriptor("safearray int32, \"Contoso.Element\""); + Assert.Empty(diagnostics); + + using var pe = new PEReader(image); + var reader = pe.GetMetadataReader(); + BlobHandle descriptorHandle = GetOnlyMarshallingDescriptor(reader); + BlobReader descriptor = reader.GetBlobReader(descriptorHandle); + + Assert.True( + descriptor.ReadByte() == (byte)UnmanagedType.SafeArray, + Convert.ToHexString(reader.GetBlobBytes(descriptorHandle))); + Assert.Equal((int)VarEnum.VT_I4, descriptor.ReadCompressedInteger()); + Assert.Equal("Contoso.Element", descriptor.ReadSerializedString()); + Assert.Equal(0, descriptor.RemainingBytes); + } + + private static (ImmutableArray Diagnostics, ImmutableArray Image) + CompileParameterMarshallingDescriptor(string nativeType) + { + string source = $$""" + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public static void M(object marshal({{nativeType}}) value) cil managed + { + ret + } + } + """; + + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText(source, "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options()); + + Assert.NotNull(result); + var image = new BlobBuilder(); + result!.Serialize(image); + return (diagnostics, image.ToImmutableArray()); + } + + private static BlobHandle GetOnlyMarshallingDescriptor(MetadataReader reader) + { + var method = reader.GetMethodDefinition(Assert.Single(reader.MethodDefinitions)); + var parameter = reader.GetParameter(Assert.Single(method.GetParameters())); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.FieldMarshal)); + Assert.Equal("value", reader.GetString(parameter.Name)); + return parameter.GetMarshallingDescriptor(); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ManifestResourceTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/ManifestResourceTests.cs new file mode 100644 index 00000000000000..afd14a7d827443 --- /dev/null +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ManifestResourceTests.cs @@ -0,0 +1,268 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace ILAssembler.Tests +{ + public class ManifestResourceTests + { + [Fact] + public void EmbeddedManifestResource_UsesResourceLocatorAliasAndEmitsResourceBytes() + { + string source = """ + .assembly test { } + .mresource public Test.Resource as ResourceAlias + { + } + .class public auto ansi beforefieldinit Test + { + } + """; + byte[] expectedResourceBytes = [0x10, 0x20, 0x30, 0x40]; + List requestedAliases = new(); + + var (diagnostics, imageBytes) = CompileAndGetImageBytes( + source, + new Options(), + resourceLocator: alias => + { + requestedAliases.Add(alias); + return expectedResourceBytes; + }); + + Assert.Empty(diagnostics); + Assert.Equal(new[] { "ResourceAlias" }, requestedAliases); + Assert.False(imageBytes.IsDefault); + + using var pe = new PEReader(imageBytes); + var reader = pe.GetMetadataReader(); + + var resource = reader.GetManifestResource(Assert.Single(reader.ManifestResources)); + Assert.Equal("Test.Resource", reader.GetString(resource.Name)); + Assert.Equal(ManifestResourceAttributes.Public, resource.Attributes); + Assert.True(resource.Implementation.IsNil); + Assert.Equal(0u, resource.Offset); + Assert.Equal(0, reader.GetTableRowCount(TableIndex.File)); + Assert.Equal(expectedResourceBytes, ReadEmbeddedResource(pe, resource)); + } + + [Fact] + public void AssemblyBackedManifestResource_EmitsAssemblyImplementation() + { + string source = """ + .assembly extern ResourceAssembly { } + .assembly test { } + .mresource private External.Resource + { + .assembly extern ResourceAssembly + } + .class public auto ansi beforefieldinit Test + { + } + """; + int resourceLocatorCallCount = 0; + + var (diagnostics, imageBytes) = CompileAndGetImageBytes( + source, + new Options(), + resourceLocator: _ => + { + resourceLocatorCallCount++; + return new byte[] { 0xFF }; + }); + + Assert.Empty(diagnostics); + Assert.Equal(0, resourceLocatorCallCount); + Assert.False(imageBytes.IsDefault); + + using var pe = new PEReader(imageBytes); + var reader = pe.GetMetadataReader(); + + var resource = reader.GetManifestResource(Assert.Single(reader.ManifestResources)); + Assert.Equal("External.Resource", reader.GetString(resource.Name)); + Assert.Equal(ManifestResourceAttributes.Private, resource.Attributes); + Assert.Equal(HandleKind.AssemblyReference, resource.Implementation.Kind); + Assert.Equal("ResourceAssembly", reader.GetString(reader.GetAssemblyReference((AssemblyReferenceHandle)resource.Implementation).Name)); + Assert.Equal(0u, resource.Offset); + } + + [Fact] + public void MissingEmbeddedManifestResource_WithErrorTolerantOption_StillEmitsMetadataRow() + { + string source = """ + .assembly test { } + .mresource public Missing.Resource as MissingAlias + { + } + .class public auto ansi beforefieldinit Test + { + } + """; + + var (strictDiagnostics, strictImageBytes) = CompileAndGetImageBytes( + source, + new Options(), + resourceLocator: alias => + { + Assert.Equal("MissingAlias", alias); + return null!; + }); + + var strictDiagnostic = Assert.Single(strictDiagnostics.Where(d => d.Id == DiagnosticIds.FileNotFound)); + Assert.Equal(DiagnosticSeverity.Error, strictDiagnostic.Severity); + Assert.Equal("test.il", strictDiagnostic.Location.Source.Path); + Assert.True(strictImageBytes.IsDefault); + + var (tolerantDiagnostics, tolerantImageBytes) = CompileAndGetImageBytes( + source, + new Options { ErrorTolerant = true }, + resourceLocator: alias => + { + Assert.Equal("MissingAlias", alias); + return null!; + }); + + var tolerantDiagnostic = Assert.Single(tolerantDiagnostics.Where(d => d.Id == DiagnosticIds.FileNotFound)); + Assert.Equal(DiagnosticSeverity.Error, tolerantDiagnostic.Severity); + Assert.False(tolerantImageBytes.IsDefault); + + using var pe = new PEReader(tolerantImageBytes); + var reader = pe.GetMetadataReader(); + var resource = reader.GetManifestResource(Assert.Single(reader.ManifestResources)); + + Assert.Equal("Missing.Resource", reader.GetString(resource.Name)); + Assert.True(resource.Implementation.IsNil); + Assert.Equal(0u, resource.Offset); + Assert.Equal(0, pe.PEHeaders.CorHeader!.ResourcesDirectory.Size); + } + + [Fact] + public void DeterministicOption_WithEmbeddedManifestResource_ProducesValidResourceImage() + { + string source = """ + .assembly test { } + .mresource public Stable.Resource as StableAlias + { + } + .class public auto ansi beforefieldinit Test + { + } + """; + byte[] expectedResourceBytes = [0x01, 0x23, 0x45, 0x67]; + + var (firstDiagnostics, firstImageBytes) = CompileAndGetImageBytes( + source, + new Options { Deterministic = true }, + resourceLocator: alias => + { + Assert.Equal("StableAlias", alias); + return expectedResourceBytes; + }); + Assert.Empty(firstDiagnostics); + Assert.False(firstImageBytes.IsDefault); + + using var firstPe = new PEReader(firstImageBytes); + var firstReader = firstPe.GetMetadataReader(); + var firstResource = firstReader.GetManifestResource(Assert.Single(firstReader.ManifestResources)); + + Assert.NotEqual(Guid.Empty, firstReader.GetGuid(firstReader.GetModuleDefinition().Mvid)); + Assert.Equal("Stable.Resource", firstReader.GetString(firstResource.Name)); + Assert.Equal(expectedResourceBytes, ReadEmbeddedResource(firstPe, firstResource)); + } + + [Fact] + public void FileBackedManifestResource_EmitsFileImplementationOffsetAndCustomAttribute() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .file nometadata Data.resources .hash = (01 02 03 04) + .mresource public File.Resource + { + .file Data.resources at 12 + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + } + .class public auto ansi Test extends [mscorlib]System.Object { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var resourceHandle = Assert.Single(reader.ManifestResources); + var resource = reader.GetManifestResource(resourceHandle); + + Assert.Equal("File.Resource", reader.GetString(resource.Name)); + Assert.Equal(ManifestResourceAttributes.Public, resource.Attributes); + Assert.Equal(12u, resource.Offset); + Assert.Equal(HandleKind.AssemblyFile, resource.Implementation.Kind); + + var file = reader.GetAssemblyFile((AssemblyFileHandle)resource.Implementation); + Assert.Equal("Data.resources", reader.GetString(file.Name)); + Assert.False(file.ContainsMetadata); + Assert.Equal([0x01, 0x02, 0x03, 0x04], reader.GetBlobBytes(file.HashValue)); + + var attribute = reader.GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(resourceHandle))); + Assert.Equal( + [0x01, 0x00, 0x00, 0x00], + reader.GetBlobBytes(attribute.Value)); + } + + private static (ImmutableArray Diagnostics, ImmutableArray ImageBytes) CompileAndGetImageBytes( + string source, + Options options, + Func? includedDocumentLoader = null, + Func? resourceLocator = null) + { + return CompileAndGetImageBytes( + ImmutableArray.Create(new SourceText(source, "test.il")), + options, + includedDocumentLoader, + resourceLocator); + } + + private static (ImmutableArray Diagnostics, ImmutableArray ImageBytes) CompileAndGetImageBytes( + ImmutableArray documents, + Options options, + Func? includedDocumentLoader = null, + Func? resourceLocator = null) + { + var compiler = new DocumentCompiler(); + var (diagnostics, image) = compiler.Compile( + documents, + includedDocumentLoader ?? (_ => throw new InvalidOperationException("Unexpected include")), + resourceLocator ?? (_ => throw new InvalidOperationException("Unexpected resource")), + options); + + if (image is null) + { + return (diagnostics, default); + } + + var blobBuilder = new BlobBuilder(); + image.Serialize(blobBuilder); + return (diagnostics, blobBuilder.ToImmutableArray()); + } + + private static byte[] ReadEmbeddedResource(PEReader pe, ManifestResource resource) + { + Assert.True(resource.Implementation.IsNil); + + int resourcesRva = pe.PEHeaders.CorHeader!.ResourcesDirectory.RelativeVirtualAddress; + Assert.NotEqual(0, resourcesRva); + + var resourceBlob = pe.GetSectionData(resourcesRva).GetContent(); + int offset = checked((int)resource.Offset); + int length = BinaryPrimitives.ReadInt32LittleEndian(resourceBlob.AsSpan(offset, sizeof(int))); + return resourceBlob.AsSpan(offset + sizeof(int), length).ToArray(); + } + } +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/MemberReferenceTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/MemberReferenceTests.cs index e7fe59d280196d..8b2279a59f730b 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/MemberReferenceTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/MemberReferenceTests.cs @@ -1,18 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - using System; -using System.Buffers.Binary; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; -using System.Reflection.PortableExecutable; -using System.Text; -using System.Threading.Tasks; using Internal.IL; using Xunit; using DocumentCompilerTestHelpers = ILAssembler.Tests.DocumentCompilerTestHelpers; @@ -21,5 +14,233 @@ namespace ILAssembler.Tests { public class MemberReferenceTests { + [Fact] + public void LocalVarargCallSite_WithLocalOptionalType_EmitsSentinelMemberRefSignature() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Payload extends [mscorlib]System.Object + { + } + .class public auto ansi beforefieldinit Printer extends [mscorlib]System.Object + { + .method public static vararg void Print(string format) cil managed + { + ret + } + + .method public static void Caller() cil managed + { + ldstr "payload" + ldnull + call vararg void Printer::Print(string, ..., class [test]Payload) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var memberRefHandle = Assert.Single(reader.MemberReferences); + var memberRef = reader.GetMemberReference(memberRefHandle); + Assert.Equal("Print", reader.GetString(memberRef.Name)); + Assert.Equal(HandleKind.MethodDefinition, memberRef.Parent.Kind); + + // The optional vararg parameter is carried by the MemberRef signature only. + Assert.Equal(1, reader.GetTableRowCount(TableIndex.Param)); + + MethodSignature signature = + memberRef.DecodeMethodSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + Assert.Equal(SignatureCallingConvention.VarArgs, signature.Header.CallingConvention); + Assert.Equal(1, signature.RequiredParameterCount); + Assert.Equal("void", signature.ReturnType); + Assert.Equal(new[] { "string", "Payload" }, signature.ParameterTypes); + + int callToken = DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "Caller", ILOpcode.call); + Assert.Equal(MetadataTokens.GetToken(memberRefHandle), callToken); + } + + [Fact] + public void FieldReference_OnLocalGenericInstantiation_UsesTypeSpecParent() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Box`1 extends [mscorlib]System.Object + { + .field public !0 Value + } + .class public auto ansi beforefieldinit Caller extends [mscorlib]System.Object + { + .method public static int32 Read(class Box`1 boxInstance) cil managed + { + ldarg.0 + ldfld int32 class Box`1::Value + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var memberRefHandle = Assert.Single(reader.MemberReferences); + var memberRef = reader.GetMemberReference(memberRefHandle); + Assert.Equal("Value", reader.GetString(memberRef.Name)); + Assert.Equal(HandleKind.TypeSpecification, memberRef.Parent.Kind); + Assert.Equal( + "int32", + memberRef.DecodeFieldSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + + var parentTypeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memberRef.Parent); + Assert.Equal( + "Box`1", + parentTypeSpec.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + + int fieldToken = DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "Read", ILOpcode.ldfld); + Assert.Equal(MetadataTokens.GetToken(memberRefHandle), fieldToken); + } + + [Fact] + public void MethodReference_OnLocalGenericInstantiation_UsesTypeSpecParent() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Box`1 extends [mscorlib]System.Object + { + .method public hidebysig instance void Touch() cil managed + { + ret + } + } + .class public auto ansi beforefieldinit Caller extends [mscorlib]System.Object + { + .method public static void Invoke(class Box`1 boxInstance) cil managed + { + ldarg.0 + call instance void class Box`1::Touch() + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var memberRefHandle = Assert.Single(reader.MemberReferences); + var memberRef = reader.GetMemberReference(memberRefHandle); + Assert.Equal("Touch", reader.GetString(memberRef.Name)); + Assert.Equal(HandleKind.TypeSpecification, memberRef.Parent.Kind); + + MethodSignature signature = + memberRef.DecodeMethodSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + Assert.True(signature.Header.IsInstance); + Assert.Equal("void", signature.ReturnType); + Assert.Empty(signature.ParameterTypes); + + var parentTypeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memberRef.Parent); + Assert.Equal( + "Box`1", + parentTypeSpec.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + + int callToken = DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "Invoke", ILOpcode.call); + Assert.Equal(MetadataTokens.GetToken(memberRefHandle), callToken); + } + + [Fact] + public void MethodReferenceForms_EmitResolvableCallTokensAndMethodSpecification() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .typedef method void Test::Target() as TargetAlias + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void Target() cil managed + { + ret + } + + .method public static void GenericTarget() cil managed + { + ret + } + + .method public static void Caller() cil managed + { + call void Target() + call void Test::GenericTarget<[1]>() + call void Test::GenericTarget() + call mdtoken(0x06000001) + call TargetAlias + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + ImmutableArray callTokens = + DocumentCompilerTestHelpers.GetTokenOperands(pe, reader, "Caller", ILOpcode.call); + + Assert.Equal(5, callTokens.Length); + Assert.Equal("Target", GetMethodName(reader, MetadataTokens.EntityHandle(callTokens[0]))); + Assert.Equal("GenericTarget", GetMethodName(reader, MetadataTokens.EntityHandle(callTokens[1]))); + Assert.Equal(HandleKind.MethodSpecification, MetadataTokens.EntityHandle(callTokens[2]).Kind); + Assert.Equal("Target", GetMethodName(reader, MetadataTokens.EntityHandle(callTokens[3]))); + Assert.Equal("Target", GetMethodName(reader, MetadataTokens.EntityHandle(callTokens[4]))); + + var methodSpecification = reader.GetMethodSpecification( + (MethodSpecificationHandle)MetadataTokens.EntityHandle(callTokens[2])); + Assert.Equal( + new[] { "int32" }, + methodSpecification.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + [Fact] + public void ExternalFieldReference_EmitsMemberRefFieldSignature() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Reader extends [mscorlib]System.Object + { + .method public static string GetEmpty() cil managed + { + ldsfld string [mscorlib]System.String::Empty + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var memberRefHandle = Assert.Single(reader.MemberReferences); + var memberRef = reader.GetMemberReference(memberRefHandle); + var parent = reader.GetTypeReference((TypeReferenceHandle)memberRef.Parent); + + Assert.Equal("Empty", reader.GetString(memberRef.Name)); + Assert.Equal("String", reader.GetString(parent.Name)); + Assert.Equal( + "string", + memberRef.DecodeFieldSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + private static string GetMethodName(MetadataReader reader, EntityHandle handle) + { + return handle.Kind switch + { + HandleKind.MethodDefinition => + reader.GetString(reader.GetMethodDefinition((MethodDefinitionHandle)handle).Name), + HandleKind.MemberReference => + reader.GetString(reader.GetMemberReference((MemberReferenceHandle)handle).Name), + _ => throw new InvalidOperationException($"Unexpected method handle kind {handle.Kind}."), + }; + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs index 0490d709aaf742..97805fb7ee60fa 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/MethodTests.cs @@ -96,6 +96,144 @@ .method public static vararg void VarargMethod(int32 x) cil managed Assert.Equal(0x5, header & 0x0F); } + [Fact] + public void MethodAndImplementationAttributes_EmitExpectedFlags() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public abstract auto ansi Test extends [mscorlib]System.Object + { + .method private static void PrivateMethod() cil managed { ret } + .method family static void FamilyMethod() cil managed { ret } + .method assembly static void AssemblyMethod() cil managed { ret } + .method famandassem static void FamAndAssemMethod() cil managed { ret } + .method famorassem static void FamOrAssemMethod() cil managed { ret } + .method privatescope static void PrivateScopeMethod() cil managed { ret } + .method public virtual strict instance void StrictMethod() cil managed { ret } + .method public static unmanagedexp void UnmanagedExportMethod() cil managed { ret } + .method public static reqsecobj void RequireSecurityObjectMethod() cil managed { ret } + .method flags(0x16) void FlaggedMethod() cil managed { ret } + + .method public static void NativeMethod() native unmanaged { } + .method public static void OptilMethod() optil managed { } + .method public static void RuntimeMethod() runtime managed internalcall { } + .method public static void ForwardMethod() cil managed forwardref preservesig synchronized noinlining aggressiveinlining nooptimization aggressiveoptimization async { } + .method public static void FlaggedImplMethod() flags(0x1000) { } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var methods = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .ToDictionary(method => reader.GetString(method.Name)); + + Assert.Equal(MethodAttributes.Private, methods["PrivateMethod"].Attributes & MethodAttributes.MemberAccessMask); + Assert.Equal(MethodAttributes.Family, methods["FamilyMethod"].Attributes & MethodAttributes.MemberAccessMask); + Assert.Equal(MethodAttributes.Assembly, methods["AssemblyMethod"].Attributes & MethodAttributes.MemberAccessMask); + Assert.Equal(MethodAttributes.FamANDAssem, methods["FamAndAssemMethod"].Attributes & MethodAttributes.MemberAccessMask); + Assert.Equal(MethodAttributes.FamORAssem, methods["FamOrAssemMethod"].Attributes & MethodAttributes.MemberAccessMask); + Assert.Equal(MethodAttributes.PrivateScope, methods["PrivateScopeMethod"].Attributes & MethodAttributes.MemberAccessMask); + Assert.True(methods["StrictMethod"].Attributes.HasFlag(MethodAttributes.CheckAccessOnOverride)); + Assert.True(methods["UnmanagedExportMethod"].Attributes.HasFlag(MethodAttributes.UnmanagedExport)); + Assert.True(methods["RequireSecurityObjectMethod"].Attributes.HasFlag(MethodAttributes.RequireSecObject)); + Assert.Equal((MethodAttributes)0x16, methods["FlaggedMethod"].Attributes); + + Assert.Equal( + MethodImplAttributes.Native | MethodImplAttributes.Unmanaged, + methods["NativeMethod"].ImplAttributes & (MethodImplAttributes.CodeTypeMask | MethodImplAttributes.ManagedMask)); + Assert.Equal(MethodImplAttributes.OPTIL, methods["OptilMethod"].ImplAttributes & MethodImplAttributes.CodeTypeMask); + Assert.Equal(MethodImplAttributes.Runtime, methods["RuntimeMethod"].ImplAttributes & MethodImplAttributes.CodeTypeMask); + Assert.True(methods["RuntimeMethod"].ImplAttributes.HasFlag(MethodImplAttributes.InternalCall)); + + MethodImplAttributes forwardFlags = methods["ForwardMethod"].ImplAttributes; + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.ForwardRef)); + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.PreserveSig)); + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.Synchronized)); + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.NoInlining)); + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.AggressiveInlining)); + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.NoOptimization)); + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.AggressiveOptimization)); + Assert.True(forwardFlags.HasFlag(MethodImplAttributes.Async)); + Assert.Equal((MethodImplAttributes)0x1000, methods["FlaggedImplMethod"].ImplAttributes); + } + + [Fact] + public void MethodBodyDirectives_EmitRawInstructionLocalsInitializationAndMappedData() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public explicit ansi sealed DataHolder extends [mscorlib]System.ValueType + { + .size 4 + .field [0] public static int32 Value at MethodData + .method public static void M() cil managed + { + .maxstack 1 + .zeroinit + .locals (int32 local) + .emitbyte 0x00 + { + nop + } + .data MethodData = int32(42) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var type = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Single(definition => reader.GetString(definition.Name) == "DataHolder"); + var method = type.GetMethods() + .Select(reader.GetMethodDefinition) + .Single(definition => reader.GetString(definition.Name) == "M"); + var field = reader.GetFieldDefinition(Assert.Single(type.GetFields())); + var body = pe.GetMethodBody(method.RelativeVirtualAddress); + + Assert.Equal([0x00, 0x00, 0x2A], body.GetILBytes()); + Assert.True(body.LocalVariablesInitialized); + Assert.Equal( + new[] { "int32" }, + reader.GetStandaloneSignature(body.LocalSignature) + .DecodeLocalSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + Assert.Equal( + 42, + BitConverter.ToInt32( + pe.GetSectionData(field.GetRelativeVirtualAddress()).GetContent().AsSpan(0, sizeof(int)))); + } + + [Fact] + public void EntryPointMethod_SetsCorHeaderEntryPointToken() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Program extends [mscorlib]System.Object + { + .method public static int32 Main() cil managed + { + .entrypoint + ldc.i4.0 + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var mainHandle = reader.MethodDefinitions + .First(handle => reader.GetString(reader.GetMethodDefinition(handle).Name) == "Main"); + + Assert.Equal( + MetadataTokens.GetToken(mainHandle), + pe.PEHeaders.CorHeader!.EntryPointTokenOrRelativeVirtualAddress); + } + [Fact] public void ArrayType_InMethodSignature_ParsedCorrectly() @@ -241,6 +379,118 @@ .method public hidebysig newslot virtual instance object Func2(string& res) cil Assert.Equal(2, methodImplCount); } + [Fact] + public void ClassLevelOverrideForms_EmitMethodImplementations() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Base extends [mscorlib]System.Object + { + .method public virtual instance void M() cil managed { ret } + .method public virtual instance void G() cil managed { ret } + } + .class public auto ansi Derived extends Base + { + .method public virtual instance void BodyM() cil managed { ret } + .method public virtual instance void BodyG() cil managed { ret } + + .override Base::M with instance void Derived::BodyM() + .override method instance void Base::G<[1]>() with method instance void Derived::BodyG<[1]>() + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var derived = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Single(definition => reader.GetString(definition.Name) == "Derived"); + var implementations = derived.GetMethodImplementations() + .Select(reader.GetMethodImplementation) + .ToArray(); + + Assert.Equal(2, implementations.Length); + Assert.Equal( + new[] { "BodyG", "BodyM" }, + implementations + .Select(implementation => + reader.GetString(reader.GetMethodDefinition((MethodDefinitionHandle)implementation.MethodBody).Name)) + .OrderBy(name => name)); + + var declarations = implementations + .Select(implementation => implementation.MethodDeclaration) + .ToDictionary(handle => GetMethodName(reader, handle)); + Assert.Equal( + "void", + DecodeMethodSignature(reader, declarations["M"]).ReturnType); + MethodSignature genericSignature = + DecodeMethodSignature(reader, declarations["G"]); + Assert.True(genericSignature.Header.IsGeneric); + Assert.Equal(1, genericSignature.GenericParameterCount); + } + + [Fact] + public void ClassLevelOverrideWithMissingBody_ReportsErrorAndEmitsTypeMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Base extends [mscorlib]System.Object + { + .method public virtual instance void M() cil managed { ret } + } + .class public auto ansi Derived extends Base + { + .override Base::M with instance void Derived::Missing() + } + """; + + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText(source, "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options { ErrorTolerant = true }); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.InvalidMetadataToken); + Assert.NotNull(result); + + var image = new BlobBuilder(); + result!.Serialize(image); + using var pe = new PEReader(image.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + var derived = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Single(definition => reader.GetString(definition.Name) == "Derived"); + + Assert.Empty(derived.GetMethodImplementations()); + Assert.Equal("Base", reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)derived.BaseType).Name)); + } + + private static string GetMethodName(MetadataReader reader, EntityHandle handle) => handle.Kind switch + { + HandleKind.MethodDefinition => + reader.GetString(reader.GetMethodDefinition((MethodDefinitionHandle)handle).Name), + HandleKind.MemberReference => + reader.GetString(reader.GetMemberReference((MemberReferenceHandle)handle).Name), + _ => throw new InvalidOperationException($"Unexpected method handle kind {handle.Kind}."), + }; + + private static MethodSignature DecodeMethodSignature( + MetadataReader reader, + EntityHandle handle) => handle.Kind switch + { + HandleKind.MethodDefinition => + reader.GetMethodDefinition((MethodDefinitionHandle)handle).DecodeSignature( + DocumentCompilerTestHelpers.Decoder, + genericContext: null), + HandleKind.MemberReference => + reader.GetMemberReference((MemberReferenceHandle)handle).DecodeMethodSignature( + DocumentCompilerTestHelpers.Decoder, + genericContext: null), + _ => throw new InvalidOperationException($"Unexpected method handle kind {handle.Kind}."), + }; + [Fact] public void MethodRtSpecialName_Preserved() diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ModuleTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/ModuleTests.cs index ce7251c02c6ae4..d4df8e471d4737 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/ModuleTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ModuleTests.cs @@ -123,5 +123,131 @@ .class public auto ansi beforefieldinit Test var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); Assert.Empty(diagnostics); } + + [Fact] + public void FileDeclaration_WithHash_EmitsMetadataFileRow() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .file NetModule.netmodule .hash = (AB CD EF 01) + .class public auto ansi Test extends [mscorlib]System.Object { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var file = reader.GetAssemblyFile(Assert.Single(reader.AssemblyFiles)); + + Assert.Equal("NetModule.netmodule", reader.GetString(file.Name)); + Assert.Equal([0xAB, 0xCD, 0xEF, 0x01], reader.GetBlobBytes(file.HashValue)); + Assert.True(file.ContainsMetadata); + Assert.Equal(0, pe.PEHeaders.CorHeader!.EntryPointTokenOrRelativeVirtualAddress); + } + + [Fact] + public void FileDeclaration_NoMetadata_EmitsResourceFileRow() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .file nometadata DataFile.resources .hash = (FF FE) + .class public auto ansi Test extends [mscorlib]System.Object { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var file = reader.GetAssemblyFile(Assert.Single(reader.AssemblyFiles)); + + Assert.Equal("DataFile.resources", reader.GetString(file.Name)); + Assert.Equal([0xFF, 0xFE], reader.GetBlobBytes(file.HashValue)); + Assert.False(file.ContainsMetadata); + Assert.Equal(0, pe.PEHeaders.CorHeader!.EntryPointTokenOrRelativeVirtualAddress); + } + + [Fact] + public void ModuleExtern_EmitsModuleReference() + { + string source = """ + .assembly test { } + .module extern Native.netmodule + .class public auto ansi Test { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.ModuleRef)); + var moduleReference = reader.GetModuleReference(MetadataTokens.ModuleReferenceHandle(1)); + + Assert.Equal("Native.netmodule", reader.GetString(moduleReference.Name)); + Assert.True(reader.GetModuleDefinition().Name.IsNil); + } + + [Fact] + public void BareModuleDirective_PreservesDefaultOutputModuleName() + { + string source = """ + .assembly test { } + .module + .class public auto ansi Test { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + source, + new Options { OutputFileName = "Output.dll" }); + var reader = pe.GetMetadataReader(); + + Assert.Equal("Output.dll", reader.GetString(reader.GetModuleDefinition().Name)); + Assert.Equal(0, reader.GetTableRowCount(TableIndex.ModuleRef)); + } + + [Fact] + public void GlobalMethodAndModuleAttribute_EmitModuleMetadataAndPdb() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() + .line 1 "global.cs" + .method public static int32 Global() cil managed + { + .locals init (int32 value) + .line 10 + ldc.i4.s 42 + stloc.0 + ldloc.0 + ret + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var moduleType = reader.GetTypeDefinition(MetadataTokens.TypeDefinitionHandle(1)); + var globalMethod = reader.GetMethodDefinition(Assert.Single(moduleType.GetMethods())); + var body = pe.GetMethodBody(globalMethod.RelativeVirtualAddress); + + Assert.Equal("", reader.GetString(moduleType.Name)); + Assert.Equal("Global", reader.GetString(globalMethod.Name)); + Assert.True(body.LocalVariablesInitialized); + Assert.Equal( + new[] { "int32" }, + reader.GetStandaloneSignature(body.LocalSignature) + .DecodeLocalSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + Assert.Equal(42, body.GetILBytes()![1]); + + CustomAttributeValue attribute = reader + .GetCustomAttribute(Assert.Single(reader.GetModuleDefinition().GetCustomAttributes())) + .DecodeValue(DocumentCompilerTestHelpers.Decoder); + Assert.Empty(attribute.FixedArguments); + Assert.Empty(attribute.NamedArguments); + + var embeddedPdb = Assert.Single( + pe.ReadDebugDirectory(), + entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + using var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdb); + var pdbReader = pdbProvider.GetMetadataReader(); + Assert.Contains( + "global.cs", + pdbReader.GetString(pdbReader.GetDocument(Assert.Single(pdbReader.Documents)).Name)); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/NativeExportTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/NativeExportTests.cs new file mode 100644 index 00000000000000..76022ff15e1199 --- /dev/null +++ b/src/tools/ilasm/tests/ILAssembler.Tests/NativeExportTests.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Text; +using Xunit; +using DocumentCompilerTestHelpers = ILAssembler.Tests.DocumentCompilerTestHelpers; + +namespace ILAssembler.Tests +{ + public class NativeExportTests + { + [Fact] + public void ExportDirective_WithoutVTableFixup_DoesNotEmitNativeExportArtifacts() + { + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void ExportedMethod() cil managed + { + .export [1] as MyExport + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + + Assert.False(pe.PEHeaders.SectionHeaders.Any(section => section.Name == ".sdata")); + + DirectoryEntry exportTableDirectory = pe.PEHeaders.PEHeader!.ExportTableDirectory; + Assert.Equal(0, exportTableDirectory.RelativeVirtualAddress); + Assert.Equal(0, exportTableDirectory.Size); + } + + [Fact] + public void VTableFixupAndExport_EmitClrAndPeDirectories() + { + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .data VT = int32(0) int32(0) + .vtfixup [2] int32 fromunmanaged at VT + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void Zed() cil managed + { + .vtentry 1 : 1 + .export [1] as Zed + ret + } + + .method public static void Alpha() cil managed + { + .vtentry 1 : 2 + .export [3] as Alpha + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var sdataSection = pe.PEHeaders.SectionHeaders.Single(section => section.Name == ".sdata"); + DirectoryEntry vtableFixups = pe.PEHeaders.CorHeader!.VtableFixupsDirectory; + DirectoryEntry exports = pe.PEHeaders.PEHeader!.ExportTableDirectory; + + Assert.Equal(sdataSection.VirtualAddress, vtableFixups.RelativeVirtualAddress); + Assert.Equal(8, vtableFixups.Size); + Assert.NotEqual(0, exports.RelativeVirtualAddress); + Assert.True(exports.Size > 0); + + int dllNameRva = ReadInt32(pe, exports.RelativeVirtualAddress + 12); + int baseOrdinal = ReadInt32(pe, exports.RelativeVirtualAddress + 16); + int numberOfFunctions = ReadInt32(pe, exports.RelativeVirtualAddress + 20); + int numberOfNames = ReadInt32(pe, exports.RelativeVirtualAddress + 24); + int addressTableRva = ReadInt32(pe, exports.RelativeVirtualAddress + 28); + int namePointerTableRva = ReadInt32(pe, exports.RelativeVirtualAddress + 32); + int ordinalTableRva = ReadInt32(pe, exports.RelativeVirtualAddress + 36); + + Assert.Equal(1, baseOrdinal); + Assert.Equal(3, numberOfFunctions); + Assert.Equal(2, numberOfNames); + Assert.Equal("output.dll", ReadAsciiString(pe, dllNameRva)); + Assert.Equal("Alpha", ReadAsciiString(pe, ReadInt32(pe, namePointerTableRva))); + Assert.Equal("Zed", ReadAsciiString(pe, ReadInt32(pe, namePointerTableRva + 4))); + Assert.Equal(2, ReadUInt16(pe, ordinalTableRva)); + Assert.Equal(0, ReadUInt16(pe, ordinalTableRva + 2)); + Assert.Equal(sdataSection.VirtualAddress + 16, ReadInt32(pe, addressTableRva)); + Assert.Equal(0, ReadInt32(pe, addressTableRva + 4)); + Assert.Equal(sdataSection.VirtualAddress + 22, ReadInt32(pe, addressTableRva + 8)); + } + + [Theory] + [InlineData(Machine.Amd64, "48A1")] + [InlineData(Machine.Arm, "DFF800F0")] + [InlineData(Machine.Arm64, "50000058")] + public void ExportStub_TargetMachine_EmitsExpectedInstructionPrefix( + Machine machine, + string expectedPrefix) + { + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .data VT = int32(0) + .vtfixup [1] int32 fromunmanaged at VT + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void Exported() cil managed + { + .vtentry 1 : 1 + .export [1] as Exported + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader( + source, + new Options { Machine = machine }); + DirectoryEntry exports = pe.PEHeaders.PEHeader!.ExportTableDirectory; + int addressTableRva = ReadInt32(pe, exports.RelativeVirtualAddress + 28); + int stubRva = ReadInt32(pe, addressTableRva); + byte[] prefix = Convert.FromHexString(expectedPrefix); + + Assert.Equal(machine, pe.PEHeaders.CoffHeader.Machine); + Assert.NotEqual(0, exports.RelativeVirtualAddress); + Assert.True( + pe.GetSectionData(stubRva).GetContent().AsSpan(0, prefix.Length).SequenceEqual(prefix)); + } + + private static int ReadInt32(PEReader pe, int rva) => + BinaryPrimitives.ReadInt32LittleEndian(pe.GetSectionData(rva).GetContent().AsSpan(0, sizeof(int))); + + private static ushort ReadUInt16(PEReader pe, int rva) => + BinaryPrimitives.ReadUInt16LittleEndian(pe.GetSectionData(rva).GetContent().AsSpan(0, sizeof(ushort))); + + private static string ReadAsciiString(PEReader pe, int rva) + { + var content = pe.GetSectionData(rva).GetContent(); + int length = content.AsSpan().IndexOf((byte)0); + return Encoding.ASCII.GetString(content.AsSpan(0, length)); + } + + } +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs index 32bb09e3bc0c55..3931daef2d36e1 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/ParameterTests.cs @@ -197,6 +197,38 @@ .method public hidebysig static void M([in] int32& x) cil managed Assert.True(param.Attributes.HasFlag(ParameterAttributes.In)); } + [Fact] + public void ParameterAttributeForms_EmitExpectedFlags() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M( + [in] int32 input, + [out] int32 output, + [opt] int32 optional, + [7] int32 rawFlags) cil managed + { + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var method = reader.GetMethodDefinition(Assert.Single(reader.MethodDefinitions)); + var parameters = method.GetParameters() + .Select(reader.GetParameter) + .ToDictionary(parameter => reader.GetString(parameter.Name)); + + Assert.Equal(ParameterAttributes.In, parameters["input"].Attributes); + Assert.Equal(ParameterAttributes.Out, parameters["output"].Attributes); + Assert.Equal(ParameterAttributes.Optional, parameters["optional"].Attributes); + Assert.Equal((ParameterAttributes)8, parameters["rawFlags"].Attributes); + } + [Fact] public void UnnamedInstanceParam_EmitsParamRow() { diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/PreprocessorIntegrationTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/PreprocessorIntegrationTests.cs new file mode 100644 index 00000000000000..005433345bf790 --- /dev/null +++ b/src/tools/ilasm/tests/ILAssembler.Tests/PreprocessorIntegrationTests.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using Xunit; +using DocumentCompilerTestHelpers = ILAssembler.Tests.DocumentCompilerTestHelpers; + +namespace ILAssembler.Tests +{ + public class PreprocessorIntegrationTests + { + [Fact] + public void NestedIncludes_EmitExpectedTypesAndUserString() + { + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + #include "level1.il" + .class public auto ansi beforefieldinit AfterInclude extends [System.Runtime]System.Object + { + .method public static string GetIncludedLiteral() cil managed + { + ldstr INCLUDED_LITERAL + ret + } + } + """; + + using var pe = CompileWithIncludesAndGetReader(source, new Dictionary + { + ["level1.il"] = """ + #define INCLUDED_LITERAL "\"double-quoted-from-include\"" + #include "level2.il" + .class public auto ansi beforefieldinit Included.Namespace.IncludedType extends [System.Runtime]System.Object + { + } + """, + ["level2.il"] = """ + .class public auto ansi beforefieldinit Nested.DeepType extends [System.Runtime]System.Object + { + } + """ + }); + + var reader = pe.GetMetadataReader(); + var typeNames = reader.TypeDefinitions + .Select(handle => + { + var type = reader.GetTypeDefinition(handle); + return (Namespace: reader.GetString(type.Namespace), Name: reader.GetString(type.Name)); + }) + .ToArray(); + + Assert.Contains(("Included.Namespace", "IncludedType"), typeNames); + Assert.Contains(("Nested", "DeepType"), typeNames); + Assert.Contains((string.Empty, "AfterInclude"), typeNames); + Assert.Equal("double-quoted-from-include", GetLdstrUserString(pe, reader, "GetIncludedLiteral")); + } + + [Fact] + public void ConditionalCompilation_EmitsOnlyActiveTypes() + { + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + #define OUTER + #ifdef OUTER + .class public auto ansi beforefieldinit OuterSelected extends [System.Runtime]System.Object { } + #else + .class public auto ansi beforefieldinit WrongOuter extends [System.Runtime]System.Object { } + #endif + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var typeNames = reader.TypeDefinitions + .Select(handle => reader.GetString(reader.GetTypeDefinition(handle).Name)) + .ToHashSet(); + + Assert.Equal(2, typeNames.Count); + Assert.Contains("OuterSelected", typeNames); + Assert.DoesNotContain("WrongOuter", typeNames); + } + + [Fact] + public void IncludedMultiTokenMacro_EmitsExpectedFloatOperand() + { + string source = """ + .assembly extern System.Runtime { } + .assembly test { } + #include "constants.il" + .class public auto ansi beforefieldinit Test extends [System.Runtime]System.Object + { + .method public static float32 GetValue() cil managed + { + ldc.r4 NEG_INF + ret + } + } + """; + + using var pe = CompileWithIncludesAndGetReader(source, new Dictionary + { + ["constants.il"] = """ + #define NEG_INF "float32(0xFF800000)" + """ + }); + + var reader = pe.GetMetadataReader(); + byte[] il = GetMethodBody(pe, reader, "GetValue"); + + Assert.Equal(6, il.Length); + Assert.True(float.IsNegativeInfinity(BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(il.AsSpan(1, 4))))); + } + + private static PEReader CompileWithIncludesAndGetReader(string source, IReadOnlyDictionary includes) + { + return DocumentCompilerTestHelpers.CompileAndGetReader( + new SourceText(source, "root.il"), + path => + { + Assert.True(includes.TryGetValue(path, out string? includedSource), $"Unexpected include path '{path}'"); + return new SourceText(includedSource!, path); + }, + new Options()); + } + + private static string GetLdstrUserString(PEReader pe, MetadataReader reader, string methodName) + { + int token = DocumentCompilerTestHelpers.GetFirstTokenOperand( + pe, + reader, + methodName, + Internal.IL.ILOpcode.ldstr); + return reader.GetUserString(MetadataTokens.UserStringHandle(token & 0x00FFFFFF)); + } + + private static byte[] GetMethodBody(PEReader pe, MetadataReader reader, string methodName) + { + var method = reader.MethodDefinitions + .Select(reader.GetMethodDefinition) + .First(definition => reader.GetString(definition.Name) == methodName); + + Assert.True(method.RelativeVirtualAddress > 0, $"Method '{methodName}' should have a body"); + return pe.GetMethodBody(method.RelativeVirtualAddress).GetILBytes()!; + } + } +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs new file mode 100644 index 00000000000000..468c1d7aedd50b --- /dev/null +++ b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyEventTests.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Xunit; +using DocumentCompilerTestHelpers = ILAssembler.Tests.DocumentCompilerTestHelpers; + +namespace ILAssembler.Tests +{ + public class PropertyEventTests + { + [Fact] + public void PropertyAndEvent_EmitMapsAndMethodSemantics() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi MyDelegate extends [mscorlib]System.MulticastDelegate + { + .method public specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed + { + } + .method public virtual instance void Invoke() runtime managed + { + } + } + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .field private int32 _value + + .method public hidebysig specialname instance int32 get_Value() cil managed + { + ldarg.0 + ldfld int32 Test::_value + ret + } + + .method public hidebysig specialname instance void set_Value(int32 value) cil managed + { + ldarg.0 + ldarg.1 + stfld int32 Test::_value + ret + } + + .method public hidebysig specialname instance void add_Changed(class MyDelegate value) cil managed + { + ret + } + + .method public hidebysig specialname instance void remove_Changed(class MyDelegate value) cil managed + { + ret + } + + .property int32 Value() + { + .get instance int32 Test::get_Value() + .set instance void Test::set_Value(int32) + } + + .event MyDelegate Changed + { + .addon instance void Test::add_Changed(class MyDelegate) + .removeon instance void Test::remove_Changed(class MyDelegate) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + Assert.Equal(1, reader.GetTableRowCount(TableIndex.Property)); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.Event)); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.PropertyMap)); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.EventMap)); + Assert.Equal(4, reader.GetTableRowCount(TableIndex.MethodSemantics)); + + var property = reader.GetPropertyDefinition(reader.PropertyDefinitions.Single()); + var @event = reader.GetEventDefinition(reader.EventDefinitions.Single()); + + Assert.Equal("Value", reader.GetString(property.Name)); + Assert.Equal("Changed", reader.GetString(@event.Name)); + } + } +} diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyTests.cs index e53e81f0e589f5..12cf95fd7b0ee2 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/PropertyTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/PropertyTests.cs @@ -66,6 +66,46 @@ .method public hidebysig specialname instance int32 get_Value() cil managed Assert.True(propCount > 0, $"Expected at least 1 property, got {propCount}"); } + [Fact] + public void PropertyAttributesOtherAccessorAndCustomAttribute_EmitMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public specialname instance int32 get_Value() cil managed { ldc.i4.0 ret } + .method public specialname instance void set_Value(int32 value) cil managed { ret } + .method public specialname instance void other_Value() cil managed { ret } + + .property specialname rtspecialname instance int32 Value() + { + .get instance int32 Test::get_Value() + .set instance void Test::set_Value(int32) + .other instance void Test::other_Value() + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var propertyHandle = Assert.Single(reader.PropertyDefinitions); + var property = reader.GetPropertyDefinition(propertyHandle); + var accessors = property.GetAccessors(); + + Assert.True(property.Attributes.HasFlag(PropertyAttributes.SpecialName)); + Assert.Equal("get_Value", reader.GetString(reader.GetMethodDefinition(accessors.Getter).Name)); + Assert.Equal("set_Value", reader.GetString(reader.GetMethodDefinition(accessors.Setter).Name)); + Assert.Equal( + "other_Value", + reader.GetString(reader.GetMethodDefinition(Assert.Single(accessors.Others)).Name)); + Assert.Equal( + [0x01, 0x00, 0x00, 0x00], + reader.GetBlobBytes(reader.GetCustomAttribute(Assert.Single(reader.GetCustomAttributes(propertyHandle))).Value)); + } + + [Fact] public void PropertyInitOpt_WithConstantValue_CreatesConstantEntry() { @@ -217,5 +257,49 @@ .property instance int32 Value() var attrs = reader.GetCustomAttributes(propHandle); Assert.True(attrs.Count >= 1, $"Property should have at least 1 custom attribute, got {attrs.Count}"); } + + [Fact] + public void IndexedProperty_EmitsSignatureAndAccessorMetadata() + { + string source = """ + .assembly test { } + .assembly extern mscorlib { } + .class public auto ansi beforefieldinit Test + { + .property instance int32 Item(int32) + { + .get instance int32 Test::get_Item(int32) + .set instance void Test::set_Item(int32, int32) + } + + .method public hidebysig specialname instance int32 get_Item(int32 index) cil managed + { + ldarg.1 + ret + } + + .method public hidebysig specialname instance void set_Item(int32 index, int32 value) cil managed + { + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var propertyHandle = reader.PropertyDefinitions.Single(); + var property = reader.GetPropertyDefinition(propertyHandle); + var accessors = property.GetAccessors(); + MethodSignature signature = + property.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null); + + Assert.Equal("Item", reader.GetString(property.Name)); + Assert.Equal("get_Item", reader.GetString(reader.GetMethodDefinition(accessors.Getter).Name)); + Assert.Equal("set_Item", reader.GetString(reader.GetMethodDefinition(accessors.Setter).Name)); + Assert.True(signature.Header.IsInstance); + Assert.Equal("int32", signature.ReturnType); + Assert.Equal(new[] { "int32" }, signature.ParameterTypes); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/SecurityTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/SecurityTests.cs index d28234893aaf90..aa07bba04f7267 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/SecurityTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/SecurityTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; @@ -54,5 +55,176 @@ .class public auto ansi Test { } // Verify assembly compiled successfully with the permission set Assert.True(reader.GetTableRowCount(TableIndex.DeclSecurity) >= 1); } + + [Fact] + public void PermissionSet_WithDemand_EmitsDeclSecurityBlob() + { + string source = """ + .assembly test + { + .permissionset demand = (2E) + .ver 1:0:0:0 + } + .class public auto ansi Test { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var securityHandle = MetadataTokens.DeclarativeSecurityAttributeHandle(1); + var security = reader.GetDeclarativeSecurityAttribute(securityHandle); + + Assert.Equal(1, reader.GetTableRowCount(TableIndex.DeclSecurity)); + Assert.Equal(2, (int)security.Action); + Assert.Equal(HandleKind.AssemblyDefinition, security.Parent.Kind); + Assert.Equal([0x2E], reader.GetBlobBytes(security.PermissionSet)); + } + + [Theory] + [InlineData("request", 1)] + [InlineData("demand", 2)] + [InlineData("assert", 3)] + [InlineData("deny", 4)] + [InlineData("permitonly", 5)] + [InlineData("linkcheck", 6)] + [InlineData("inheritcheck", 7)] + [InlineData("reqmin", 8)] + [InlineData("reqopt", 9)] + [InlineData("reqrefuse", 10)] + [InlineData("prejitgrant", 11)] + [InlineData("prejitdeny", 12)] + [InlineData("noncasdemand", 13)] + [InlineData("noncaslinkdemand", 14)] + [InlineData("noncasinheritance", 15)] + public void PermissionSet_Action_EmitsExpectedDeclSecurityAction(string action, int expectedAction) + { + string source = $$""" + .assembly test + { + .permissionset {{action}} = (2E) + } + .class public auto ansi Test { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var security = reader.GetDeclarativeSecurityAttribute( + Assert.Single(reader.GetAssemblyDefinition().GetDeclarativeSecurityAttributes())); + + Assert.Equal((DeclarativeSecurityAction)expectedAction, security.Action); + Assert.Equal(HandleKind.AssemblyDefinition, security.Parent.Kind); + Assert.Equal([0x2E], reader.GetBlobBytes(security.PermissionSet)); + } + + [Fact] + public void PermissionSet_VerbalForm_EmitsSerializedAttributeAndNamedProperty() + { + string source = """ + .assembly test + { + .permissionset deny = { + class 'My.Permission' = { + property bool Enabled = bool(true) + } + } + } + .class public auto ansi Test { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var security = reader.GetDeclarativeSecurityAttribute( + Assert.Single(reader.GetAssemblyDefinition().GetDeclarativeSecurityAttributes())); + + Assert.Equal(DeclarativeSecurityAction.Deny, security.Action); + BlobReader permissionSet = reader.GetBlobReader(security.PermissionSet); + Assert.Equal((byte)'.', permissionSet.ReadByte()); + Assert.Equal(1, permissionSet.ReadCompressedInteger()); + Assert.Equal("My.Permission", permissionSet.ReadSerializedString()); + Assert.Equal(1, permissionSet.ReadUInt16()); + Assert.Equal((byte)CustomAttributeNamedArgumentKind.Property, permissionSet.ReadByte()); + Assert.Equal((byte)SerializationTypeCode.Boolean, permissionSet.ReadByte()); + Assert.Equal("Enabled", permissionSet.ReadSerializedString()); + Assert.True(permissionSet.ReadBoolean()); + Assert.Equal(0, permissionSet.RemainingBytes); + } + + [Fact] + public void UnsupportedPermission_NameValueForms_ErrorTolerantImageRemainsValid() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .permission demand [mscorlib]System.Security.Permissions.SecurityPermissionAttribute( + "Boolean" = true, + "Integer" = 123, + "WrappedInteger" = int32(456), + "Text" = "hello", + "Enum8" = [mscorlib]Contoso.Kind(int8:1), + "Enum16" = [mscorlib]Contoso.Kind(int16:2), + "Enum32" = [mscorlib]Contoso.Kind(int32:3), + "EnumDefault" = [mscorlib]Contoso.Kind(4) + ) + .class public auto ansi Test extends [mscorlib]System.Object { } + """; + + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText(source, "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options { ErrorTolerant = true }); + + Assert.Contains( + diagnostics, + diagnostic => diagnostic.Id == DiagnosticIds.UnsupportedSecurityDeclaration); + Assert.NotNull(result); + + var image = new BlobBuilder(); + result!.Serialize(image); + using var pe = new PEReader(image.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + + Assert.Contains( + reader.TypeDefinitions, + handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + Assert.Equal(0, reader.GetTableRowCount(TableIndex.DeclSecurity)); + } + + [Fact] + public void TypeAndMethodPermissionSets_EmitExpectedParentsAndActions() + { + string source = """ + .assembly test { } + .class public auto ansi Test + { + .permissionset demand = (2E) + .method public static void M() cil managed + { + .permissionset assert = (2F) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var securityDeclarations = Enumerable.Range(1, reader.GetTableRowCount(TableIndex.DeclSecurity)) + .Select(row => reader.GetDeclarativeSecurityAttribute(MetadataTokens.DeclarativeSecurityAttributeHandle(row))) + .ToArray(); + + Assert.Contains( + securityDeclarations, + security => + security.Parent.Kind == HandleKind.TypeDefinition && + security.Action == DeclarativeSecurityAction.Demand && + reader.GetBlobBytes(security.PermissionSet).SequenceEqual((byte[])[0x2E])); + Assert.Contains( + securityDeclarations, + security => + security.Parent.Kind == HandleKind.MethodDefinition && + security.Action == DeclarativeSecurityAction.Assert && + reader.GetBlobBytes(security.PermissionSet).SequenceEqual((byte[])[0x2F])); + } } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/SourceDirectiveTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/SourceDirectiveTests.cs index 30c39a9c3a0542..475a436b7921c9 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/SourceDirectiveTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/SourceDirectiveTests.cs @@ -207,8 +207,11 @@ .method public static void M() cil managed } """; - var diagnostics = DocumentCompilerTestHelpers.CompileAndGetDiagnostics(source, new Options()); - Assert.Empty(diagnostics); + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + int token = DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "M", ILOpcode.ldstr); + string value = reader.GetUserString(MetadataTokens.UserStringHandle(token & 0x00FFFFFF)); + Assert.Equal("Hello\nWorld\t!", value); } @@ -247,5 +250,222 @@ .class public auto ansi beforefieldinit ASSEMBLY_NAME extends [mscorlib]System.O Assert.Equal("TestAssembly", reader.GetString(typeDef.Name)); } + [Fact] + public void LineDirective_WithRange_EmitsPortablePdbSequencePointWithSpecifiedStartPosition() + { + string source = """ + .assembly test { } + .class public auto ansi beforefieldinit Test + { + .method public static void TestMethod() cil managed + { + .line 10, 10 : 5, 6 "test.cs" + nop + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var methodHandle = reader.MethodDefinitions.Single(handle => reader.GetString(reader.GetMethodDefinition(handle).Name) == "TestMethod"); + + var embeddedPdbEntry = pe.ReadDebugDirectory().Single(entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry); + var pdbReader = pdbProvider.GetMetadataReader(); + var debugHandle = MetadataTokens.MethodDebugInformationHandle(MetadataTokens.GetRowNumber(methodHandle)); + var debugInfo = pdbReader.GetMethodDebugInformation(debugHandle); + var sequencePoint = debugInfo.GetSequencePoints().First(point => !point.IsHidden); + + Assert.Equal(0, sequencePoint.Offset); + Assert.Equal(10, sequencePoint.StartLine); + Assert.Equal(5, sequencePoint.StartColumn); + + var document = pdbReader.GetDocument(sequencePoint.Document); + Assert.Contains("test.cs", pdbReader.GetString(document.Name)); + } + + [Theory] + [InlineData(".line 10 'single.cs'", "single.cs")] + [InlineData(".line 11", "default.cs")] + [InlineData(".line 12 : 3 'single.cs'", "single.cs")] + [InlineData(".line 13 : 4", "default.cs")] + [InlineData(".line 14 : 5, 6 'single.cs'", "single.cs")] + [InlineData(".line 15 : 6, 7", "default.cs")] + [InlineData(".line 16, 17 : 8 'single.cs'", "single.cs")] + [InlineData(".line 18, 19 : 9", "default.cs")] + [InlineData(".line 20, 21 : 10, 11 'single.cs'", "single.cs")] + [InlineData(".line 22, 23 : 12, 13", "default.cs")] + [InlineData(".line 24 \"double.cs\"", "double.cs")] + [InlineData(".line 25 : 14 \"double.cs\"", "double.cs")] + [InlineData(".line 26 : 15, 16 \"double.cs\"", "double.cs")] + [InlineData(".line 27, 28 : 17 \"double.cs\"", "double.cs")] + [InlineData(".line 29, 30 : 18, 19 \"double.cs\"", "double.cs")] + public void LineDirective_SyntaxVariant_EmitsPortablePdbMethodDebugInformation( + string directive, + string expectedDocument) + { + string initialDirective = + directive.Contains('\'') || directive.Contains('"') + ? string.Empty + : ".line 1, 1 : 1, 2 \"default.cs\""; + string source = $$""" + .assembly test { } + .class public auto ansi Test + { + .method public static void M() cil managed + { + {{initialDirective}} + {{directive}} + nop + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var methodHandle = reader.MethodDefinitions + .Single(handle => reader.GetString(reader.GetMethodDefinition(handle).Name) == "M"); + var embeddedPdb = Assert.Single( + pe.ReadDebugDirectory(), + entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + using var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdb); + var pdbReader = pdbProvider.GetMetadataReader(); + var debugInformation = pdbReader.GetMethodDebugInformation( + MetadataTokens.MethodDebugInformationHandle(MetadataTokens.GetRowNumber(methodHandle))); + SequencePoint[] sequencePoints = debugInformation.GetSequencePoints().ToArray(); + + Assert.False(debugInformation.SequencePointsBlob.IsNil); + Assert.NotEmpty(sequencePoints); + Assert.Contains( + expectedDocument, + pdbReader.GetString(pdbReader.GetDocument(debugInformation.Document).Name)); + } + + [Theory] + [InlineData(".language '3f5162f8-07c6-11d3-9053-00c04fa302a1'")] + [InlineData(".language '3f5162f8-07c6-11d3-9053-00c04fa302a1', '994b45c4-e6e9-11d2-903f-00c04fa302a1'")] + [InlineData(".language '3f5162f8-07c6-11d3-9053-00c04fa302a1', '994b45c4-e6e9-11d2-903f-00c04fa302a1', '5a869d0b-6611-11d3-bd2a-0000f80849bd'")] + [InlineData(".language \"3f5162f8-07c6-11d3-9053-00c04fa302a1\" \"994b45c4-e6e9-11d2-903f-00c04fa302a1\"")] + [InlineData(".language \"3f5162f8-07c6-11d3-9053-00c04fa302a1\" \"994b45c4-e6e9-11d2-903f-00c04fa302a1\" \"5a869d0b-6611-11d3-bd2a-0000f80849bd\"")] + public void LanguageDirective_SyntaxVariant_EmitsDocumentLanguage(string languageDirective) + { + string source = $$""" + .assembly test { } + .class public auto ansi Test + { + .method public static void M() cil managed + { + {{languageDirective}} + .line 10 "document.cs" + nop + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var embeddedPdb = Assert.Single( + pe.ReadDebugDirectory(), + entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + using var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdb); + var pdbReader = pdbProvider.GetMetadataReader(); + var document = pdbReader.GetDocument(Assert.Single(pdbReader.Documents)); + + Assert.Equal( + new Guid("3f5162f8-07c6-11d3-9053-00c04fa302a1"), + pdbReader.GetGuid(document.Language)); + Assert.Contains("document.cs", pdbReader.GetString(document.Name)); + } + + [Fact] + public void ClassScopedLanguageAndLineDirectives_ApplyToMethodSequencePoint() + { + string source = """ + .assembly test { } + .class public auto ansi Test + { + .language '3f5162f8-07c6-11d3-9053-00c04fa302a1' + .line 1 "class.cs" + .method public static void M() cil managed + { + .line 10 + nop + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var embeddedPdb = Assert.Single( + pe.ReadDebugDirectory(), + entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + using var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdb); + var pdbReader = pdbProvider.GetMetadataReader(); + var document = pdbReader.GetDocument(Assert.Single(pdbReader.Documents)); + + Assert.Equal( + new Guid("3f5162f8-07c6-11d3-9053-00c04fa302a1"), + pdbReader.GetGuid(document.Language)); + Assert.Contains("class.cs", pdbReader.GetString(document.Name)); + } + + [Fact] + public void MultiDocumentCompile_WithLineDirectivesAcrossDocuments_EmitsPdbDocumentsForEachSource() + { + var documents = ImmutableArray.Create( + new SourceText(""" + .assembly test { } + .class public auto ansi beforefieldinit First + { + .method public static void M1() cil managed + { + .line 10 "doc1.cs" + nop + ret + } + } + """, "doc1.il"), + new SourceText(""" + .class public auto ansi beforefieldinit Second + { + .method public static void M2() cil managed + { + .line 20 "doc2.cs" + nop + ret + } + } + """, "doc2.il")); + + var compiler = new DocumentCompiler(); + var (diagnostics, image) = compiler.Compile( + documents, + _ => throw new InvalidOperationException("Unexpected include"), + _ => throw new InvalidOperationException("Unexpected resource"), + new Options { Pdb = true }); + + Assert.Empty(diagnostics); + Assert.NotNull(image); + + var imageBuilder = new BlobBuilder(); + image!.Serialize(imageBuilder); + using var pe = new PEReader(imageBuilder.ToImmutableArray()); + var embeddedPdbEntry = Assert.Single( + pe.ReadDebugDirectory(), + entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb); + using var pdbProvider = pe.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry); + var pdbReader = pdbProvider.GetMetadataReader(); + var documentNames = pdbReader.Documents + .Select(handle => pdbReader.GetString(pdbReader.GetDocument(handle).Name)) + .ToArray(); + + Assert.Equal(2, documentNames.Length); + Assert.Contains(documentNames, name => name.Contains("doc1.cs", StringComparison.Ordinal)); + Assert.Contains(documentNames, name => name.Contains("doc2.cs", StringComparison.Ordinal)); + Assert.Equal(pe.GetMetadataReader().MethodDefinitions.Count, pdbReader.MethodDebugInformation.Count); + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/TypeDefinitionTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/TypeDefinitionTests.cs index dfcdea81056e29..05f55224e514f1 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/TypeDefinitionTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/TypeDefinitionTests.cs @@ -571,5 +571,220 @@ .field [4] public int32 y Assert.Equal(16, layout.Size); } + [Fact] + public void QuotedTypeName_WithConsecutiveDots_SplitsAtLastDot() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit 'Contoso.Tools..ctor' extends [mscorlib]System.Object + { + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var typeDef = reader.TypeDefinitions + .Select(handle => reader.GetTypeDefinition(handle)) + .Single(type => reader.GetString(type.Name) == "ctor"); + + Assert.Equal("ctor", reader.GetString(typeDef.Name)); + Assert.Equal("Contoso.Tools.", reader.GetString(typeDef.Namespace)); + } + + [Fact] + public void ClassAttributes_EmitExpectedFlagsAndImplicitBaseTypes() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + + .class public auto ansi Outer extends [mscorlib]System.Object + { + .class nested private auto ansi NestedPrivate extends [mscorlib]System.Object { } + .class nested family auto ansi NestedFamily extends [mscorlib]System.Object { } + .class nested assembly auto ansi NestedAssembly extends [mscorlib]System.Object { } + .class nested famandassem auto ansi NestedFamAndAssem extends [mscorlib]System.Object { } + .class nested famorassem auto ansi NestedFamOrAssem extends [mscorlib]System.Object { } + } + + .class public auto unicode import serializable windowsruntime UnicodeImported extends [mscorlib]System.Object { } + .class public value ValueTypeByKeyword { } + .class public enum EnumTypeByKeyword { } + .class flags(0x00100001) FlaggedType extends [mscorlib]System.Object { } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var types = reader.TypeDefinitions + .Select(handle => (Handle: handle, Definition: reader.GetTypeDefinition(handle))) + .ToDictionary(item => reader.GetString(item.Definition.Name)); + + Assert.Equal( + TypeAttributes.NestedPrivate, + types["NestedPrivate"].Definition.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal( + TypeAttributes.NestedFamily, + types["NestedFamily"].Definition.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal( + TypeAttributes.NestedAssembly, + types["NestedAssembly"].Definition.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal( + TypeAttributes.NestedFamANDAssem, + types["NestedFamAndAssem"].Definition.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal( + TypeAttributes.NestedFamORAssem, + types["NestedFamOrAssem"].Definition.Attributes & TypeAttributes.VisibilityMask); + + TypeAttributes unicodeImported = types["UnicodeImported"].Definition.Attributes; + Assert.Equal(TypeAttributes.UnicodeClass, unicodeImported & TypeAttributes.StringFormatMask); + Assert.True(unicodeImported.HasFlag(TypeAttributes.Import)); + Assert.True(unicodeImported.HasFlag(TypeAttributes.Serializable)); + Assert.True(unicodeImported.HasFlag(TypeAttributes.WindowsRuntime)); + + Assert.True(types["ValueTypeByKeyword"].Definition.Attributes.HasFlag(TypeAttributes.Sealed)); + Assert.Equal("ValueType", GetBaseTypeName(reader, types["ValueTypeByKeyword"].Definition)); + Assert.Equal("Enum", GetBaseTypeName(reader, types["EnumTypeByKeyword"].Definition)); + + Assert.Equal((TypeAttributes)0x00100001, types["FlaggedType"].Definition.Attributes); + } + + [Fact] + public void LegacyClassFlagSentinels_EmitImplicitValueTypeAndEnumBases() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class flags(0x80100001) FlagValueType { } + .class flags(0x40100001) FlagEnumType { } + """; + + var compiler = new DocumentCompiler(); + var (diagnostics, result) = compiler.Compile( + new SourceText(source, "test.il"), + _ => { Assert.Fail("Expected no includes"); return default; }, + _ => { Assert.Fail("Expected no resources"); return default; }, + new Options { ErrorTolerant = true }); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == DiagnosticIds.UnsealedValueType); + Assert.NotNull(result); + + var image = new BlobBuilder(); + result!.Serialize(image); + using var pe = new PEReader(image.ToImmutableArray()); + var reader = pe.GetMetadataReader(); + var types = reader.TypeDefinitions + .Select(handle => reader.GetTypeDefinition(handle)) + .ToDictionary(type => reader.GetString(type.Name)); + + Assert.True(types["FlagValueType"].Attributes.HasFlag(TypeAttributes.BeforeFieldInit)); + Assert.True(types["FlagValueType"].Attributes.HasFlag(TypeAttributes.Sealed)); + Assert.Equal("ValueType", GetBaseTypeName(reader, types["FlagValueType"])); + Assert.True(types["FlagEnumType"].Attributes.HasFlag(TypeAttributes.BeforeFieldInit)); + Assert.Equal("Enum", GetBaseTypeName(reader, types["FlagEnumType"])); + } + + [Fact] + public void NestedType_EmitsDeclaringTypeMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Outer extends [mscorlib]System.Object + { + .class nested public auto ansi beforefieldinit Inner extends [mscorlib]System.Object + { + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var outerHandle = reader.TypeDefinitions + .First(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Outer"); + var innerHandle = reader.TypeDefinitions + .First(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Inner"); + + var inner = reader.GetTypeDefinition(innerHandle); + Assert.Equal(TypeAttributes.NestedPublic, inner.Attributes & TypeAttributes.VisibilityMask); + Assert.Equal("Outer", reader.GetString(reader.GetTypeDefinition(outerHandle).Name)); + Assert.Equal(1, reader.GetTableRowCount(TableIndex.NestedClass)); + } + + [Fact] + public void InterfaceImplementation_EmitsInterfaceImplRow() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class interface public abstract auto ansi IWorker + { + .method public hidebysig newslot abstract virtual instance void Run() cil managed { } + } + .class public auto ansi beforefieldinit Worker extends [mscorlib]System.Object implements IWorker + { + .method public hidebysig newslot virtual instance void Run() cil managed + { + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var workerHandle = reader.TypeDefinitions + .First(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Worker"); + var worker = reader.GetTypeDefinition(workerHandle); + var interfaceImplHandle = Assert.Single(worker.GetInterfaceImplementations()); + var interfaceImpl = reader.GetInterfaceImplementation(interfaceImplHandle); + + Assert.Equal(HandleKind.TypeDefinition, interfaceImpl.Interface.Kind); + Assert.Equal("IWorker", reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)interfaceImpl.Interface).Name)); + } + + [Fact] + public void InterfaceImplementationDirective_EmitsCustomAttributeOnInterfaceImpl() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class interface public abstract auto ansi IWorker + { + } + .class public auto ansi Worker extends [mscorlib]System.Object implements IWorker + { + .interfaceimpl type IWorker + .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var worker = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Single(definition => reader.GetString(definition.Name) == "Worker"); + var implementationHandle = Assert.Single(worker.GetInterfaceImplementations()); + var implementation = reader.GetInterfaceImplementation(implementationHandle); + var attribute = reader.GetCustomAttribute( + Assert.Single(reader.GetCustomAttributes(implementationHandle))); + + Assert.Equal(HandleKind.TypeDefinition, implementation.Interface.Kind); + Assert.Equal( + "IWorker", + reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)implementation.Interface).Name)); + Assert.Equal( + [0x01, 0x00, 0x00, 0x00], + reader.GetBlobBytes(attribute.Value)); + } + + private static string GetBaseTypeName(MetadataReader reader, TypeDefinition type) + { + Assert.Equal(HandleKind.TypeReference, type.BaseType.Kind); + return reader.GetString(reader.GetTypeReference((TypeReferenceHandle)type.BaseType).Name); + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/TypeReferenceTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/TypeReferenceTests.cs index ec6a3a3e26ada5..cf01434024961f 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/TypeReferenceTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/TypeReferenceTests.cs @@ -289,5 +289,135 @@ ldtoken [test]MyType DocumentCompilerTestHelpers.AssertTypeDefToken(reader, token, "MyType"); } + [Fact] + public void ThisBaseNesterAndMetadataTokenTypeReferences_EmitExpectedTypeTokens() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Base extends [mscorlib]System.Object { } + .class public auto ansi Outer extends Base + { + .method public static void UseThisAndBase(object value) cil managed + { + ldarg.0 + box .this + pop + ldarg.0 + box .base + pop + ldarg.0 + box mdtoken(0x02000002) + pop + ret + } + + .class nested public auto ansi Inner extends [mscorlib]System.Object + { + .method public static void UseNester(object value) cil managed + { + ldarg.0 + box .nester + pop + ret + } + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + ImmutableArray outerTokens = + DocumentCompilerTestHelpers.GetTokenOperands(pe, reader, "UseThisAndBase", ILOpcode.box); + ImmutableArray innerTokens = + DocumentCompilerTestHelpers.GetTokenOperands(pe, reader, "UseNester", ILOpcode.box); + + Assert.Equal( + new[] { "Outer", "Base", "Base" }, + outerTokens.Select(token => GetTypeName(reader, MetadataTokens.EntityHandle(token)))); + Assert.Equal( + "Outer", + GetTypeName(reader, MetadataTokens.EntityHandle(Assert.Single(innerTokens)))); + } + + [Fact] + public void ModuleScopedTypeReference_EmitsModuleReferenceResolutionScope() + { + string source = """ + .assembly test { } + .module extern Native.netmodule + .class public auto ansi Test + { + .field public class [.module Native.netmodule]Contoso.External Value + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var field = reader.GetFieldDefinition(Assert.Single(reader.FieldDefinitions)); + var typeReference = reader.TypeReferences + .Select(handle => (Handle: handle, Type: reader.GetTypeReference(handle))) + .Single(item => reader.GetString(item.Type.Name) == "External"); + + Assert.Equal( + "[.module Native.netmodule]Contoso.External", + field.DecodeSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + Assert.Equal(HandleKind.ModuleReference, typeReference.Type.ResolutionScope.Kind); + Assert.Equal( + "Native.netmodule", + reader.GetString(reader.GetModuleReference((ModuleReferenceHandle)typeReference.Type.ResolutionScope).Name)); + } + + [Fact] + public void TypelistMscorlibAndSemicolonControls_EmitExpectedMetadata() + { + string source = """ + .assembly extern External.Assembly { } + .assembly test { } + .mscorlib + .typelist { + [External.Assembly]Contoso.First + [External.Assembly]Contoso.Outer/Nested + } + ; + .class public auto ansi Test + { + ; + .method public static void M() cil managed + { + ; + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var typeReferences = reader.TypeReferences + .Select(reader.GetTypeReference) + .Select(type => (Namespace: reader.GetString(type.Namespace), Name: reader.GetString(type.Name))) + .ToArray(); + + Assert.Contains(("Contoso", "First"), typeReferences); + Assert.Contains(("Contoso", "Outer"), typeReferences); + Assert.Contains((string.Empty, "Nested"), typeReferences); + + var testType = reader.TypeDefinitions + .Select(reader.GetTypeDefinition) + .Single(type => reader.GetString(type.Name) == "Test"); + var method = reader.GetMethodDefinition(Assert.Single(testType.GetMethods())); + Assert.Equal("M", reader.GetString(method.Name)); + Assert.Equal([0x2A], pe.GetMethodBody(method.RelativeVirtualAddress).GetILBytes()); + } + + private static string GetTypeName(MetadataReader reader, EntityHandle handle) => handle.Kind switch + { + HandleKind.TypeDefinition => + reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)handle).Name), + HandleKind.TypeReference => + reader.GetString(reader.GetTypeReference((TypeReferenceHandle)handle).Name), + _ => throw new InvalidOperationException($"Unexpected type handle kind {handle.Kind}."), + }; + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/TypeSignatureTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/TypeSignatureTests.cs index fa2e2253354490..185ac34cb11f47 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/TypeSignatureTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/TypeSignatureTests.cs @@ -42,5 +42,91 @@ .locals init (int32[0...,0...] V_0) Assert.Empty(diagnostics); } + [Fact] + public void MultiDimArrayLocal_EmitsArraySignatureShape() + { + string source = """ + .assembly extern System.Runtime { } + .assembly TestAssembly { } + .class public auto ansi beforefieldinit Test + { + .method public static void M() cil managed + { + .locals init (int32[0...,0...] V_0) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var signature = reader.GetStandaloneSignature(MetadataTokens.StandaloneSignatureHandle(1)); + Assert.Equal( + new[] { "int32[0...,0...]" }, + signature.DecodeLocalSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + [Fact] + public void ArrayBoundVariants_DecodeExpectedShapes() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .locals init ( + int32[] szArray, + int32[,] unspecified, + int32[5] sized, + int32[2...] lowerBoundOnly, + int32[2...5] bounded) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var signature = reader.GetStandaloneSignature(MetadataTokens.StandaloneSignatureHandle(1)); + + Assert.Equal( + new[] + { + "int32[]", + "int32[..., ...]".Replace(" ", string.Empty), + "int32[0...4]", + "int32[2...]", + "int32[2...5]", + }, + signature.DecodeLocalSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + + [Fact] + public void MultiDimensionalLocalSignature_EmitsArrayShape() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object + { + .method public static void M() cil managed + { + .locals init (int32[0...,0...] matrix) + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + + var signature = reader.GetStandaloneSignature(MetadataTokens.StandaloneSignatureHandle(1)); + Assert.Equal( + new[] { "int32[0...,0...]" }, + signature.DecodeLocalSignature(DocumentCompilerTestHelpers.Decoder, genericContext: null)); + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/TypedefTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/TypedefTests.cs index 94605d871dd495..5f4f3b8fc37d29 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/TypedefTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/TypedefTests.cs @@ -143,5 +143,51 @@ .method public static void TestMethod() cil managed Assert.Empty(diagnostics); } + [Fact] + public void Typedef_FieldAndCustomAttributeForms_EmitResolvableMetadata() + { + string source = """ + .assembly extern mscorlib { } + .assembly test { } + .typedef field int32 Test::Value as ValueAlias + .typedef .custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = (01 00 00 00) as AttributeAlias + .typedef .custom (Test) instance void [mscorlib]System.CLSCompliantAttribute::.ctor(bool) = (01 00 01 00 00) as OwnedAttributeAlias + .class public auto ansi Test extends [mscorlib]System.Object + { + .field public static int32 Value + AttributeAlias + .method public static int32 Read() cil managed + { + ldsfld ValueAlias + ret + } + } + """; + + using var pe = DocumentCompilerTestHelpers.CompileAndGetReader(source, new Options()); + var reader = pe.GetMetadataReader(); + var testTypeHandle = reader.TypeDefinitions + .Single(handle => reader.GetString(reader.GetTypeDefinition(handle).Name) == "Test"); + var testType = reader.GetTypeDefinition(testTypeHandle); + var fieldHandle = Assert.Single(testType.GetFields()); + int fieldToken = + DocumentCompilerTestHelpers.GetFirstTokenOperand(pe, reader, "Read", ILOpcode.ldsfld); + + Assert.Equal(MetadataTokens.GetToken(fieldHandle), fieldToken); + + var attributes = reader.GetCustomAttributes(testTypeHandle) + .Select(reader.GetCustomAttribute) + .Select(attribute => attribute.DecodeValue(DocumentCompilerTestHelpers.Decoder)) + .ToArray(); + Assert.Equal(2, attributes.Length); + Assert.Contains(attributes, attribute => attribute.FixedArguments.Length == 0); + Assert.Contains( + attributes, + attribute => + attribute.FixedArguments.Length == 1 && + attribute.FixedArguments[0].Type == "bool" && + Equals(attribute.FixedArguments[0].Value, true)); + } + } } diff --git a/src/tools/ilasm/tests/ILAssembler.Tests/VTableTests.cs b/src/tools/ilasm/tests/ILAssembler.Tests/VTableTests.cs index 4cf7bc6ef9ff3f..d8ed874821e152 100644 --- a/src/tools/ilasm/tests/ILAssembler.Tests/VTableTests.cs +++ b/src/tools/ilasm/tests/ILAssembler.Tests/VTableTests.cs @@ -185,5 +185,6 @@ .export [2] as Export2 Assert.Contains("Method1", methodNames); Assert.Contains("Method2", methodNames); } + } }