From 87fa1b613d2b6bda1722855a00f1390f27731b64 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:11:41 +0800 Subject: [PATCH] fix(managed): retain nested names and ReadyToRun observations Resolve attribute display names from validated nesting and parsed member facts, admit composed names before allocation, and preserve unknown malformed chains. Keep ReadyToRun classification ahead of metadata hints while retaining each observed hint. Fixes #6815 Fixes #6810 --- docs/managed-code-analysis.md | 7 + src/dotnet/ManagedArtifactInspector.test.ts | 127 ++++++++++++ src/dotnet/ManagedArtifactInspector.ts | 91 ++++---- src/dotnet/ManagedMemberInspector.ts | 8 + src/dotnet/ManagedMetadataInventory.ts | 6 + src/dotnet/ManagedMetadataInventoryRows.ts | 217 +++++++++++++++----- tests/acceptance/managedCliOutput.test.ts | 64 ++++++ 7 files changed, 424 insertions(+), 96 deletions(-) diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index e349f6e9ad..c9f656d7ea 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -182,6 +182,9 @@ declares API compatibility across multiple .NET implementations. REA preserves that target in `target_frameworks` and leaves `runtime_family` as `unknown` unless independent markers identify a runtime family, such as a ReadyToRun signature. +When ReadyToRun is observed, `runtime_family` remains `modern-dotnet` even if +Unity or target-framework hints are also present; the separate hint evidence +is retained in the classification evidence list. ### Resource admission @@ -542,6 +545,10 @@ preserving readable member and attribute facts. The retained inverse indexes are admitted against the existing representation budget before allocation. +Custom-attribute `type_name` uses `Outer+Nested` when its local TypeDef or +TypeRef enclosing chain is readable. It remains `null` when the constructor's +type or required enclosing facts cannot be resolved. + `exported_types` reports ExportedType rows as declared. Facade assemblies such as .NET `System.Runtime.dll` provide their types this way: `forwarder: true` marks a type forwarder, and `implementation_token` names the AssemblyRef, File, diff --git a/src/dotnet/ManagedArtifactInspector.test.ts b/src/dotnet/ManagedArtifactInspector.test.ts index 50de3a39b1..814e15e991 100644 --- a/src/dotnet/ManagedArtifactInspector.test.ts +++ b/src/dotnet/ManagedArtifactInspector.test.ts @@ -479,6 +479,100 @@ describe("managed artifact coded indexes", () => { }); describe("managed artifact reference inventory", () => { + it("qualifies local custom-attribute names with their enclosing types", () => { + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: 1, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes: [[2, 1]], + typeName: "InnerAttribute", + typeNamespace: "", + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBe("Empty0+InnerAttribute"); + expect(result.coverage).toEqual({ state: "complete", issues: [] }); + }); + + it("qualifies referenced custom-attribute names from TypeRef scopes", () => { + const bytes = buildManagedPeFixture({ + customAttributeTypeRaw: (1 << 3) | 3, + memberRefParentRaw: (3 << 3) | 1, + typeReferences: [ + { namespace: "Fixture", name: "Outer" }, + { namespace: "", name: "InnerAttribute", enclosingRow: 2 }, + ], + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBe( + "Fixture.Outer+InnerAttribute", + ); + expect(result.coverage).toEqual({ state: "complete", issues: [] }); + }); + + it.each([ + { + label: "duplicate NestedClass rows", + additionalEmptyTypes: 1, + nestedTypes: [ + [2, 1], + [2, 1], + ] as const, + coverage: "partial", + }, + { + label: "a cyclic NestedClass chain", + additionalEmptyTypes: 2, + nestedTypes: [ + [2, 3], + [3, 2], + ] as const, + coverage: "complete", + }, + ])("leaves a nested attribute name unknown for $label", (fixture) => { + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: fixture.additionalEmptyTypes, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes: fixture.nestedTypes, + typeName: "InnerAttribute", + typeNamespace: "", + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBeNull(); + expect(result.coverage.state).toBe(fixture.coverage); + }); + + it("leaves a custom-attribute name unknown when nesting exceeds its cap", () => { + const nestedTypes: (readonly [number, number])[] = Array.from( + { length: 64 }, + (_, index) => [65 - index, 64 - index], + ); + const bytes = buildManagedPeFixture({ + additionalEmptyTypes: 64, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes, + typeName: "InnerAttribute", + typeNamespace: "", + }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.attributes[0]?.type_name).toBeNull(); + expect(result.coverage).toEqual({ state: "complete", issues: [] }); + }); + it("returns all references inline while classifying from the full inventory", () => { const bytes = buildManagedPeFixture({ references: ["System.Runtime", "UnityEngine.CoreModule"], @@ -548,6 +642,39 @@ describe("managed fixture data layout", () => { }); describe("managed implementation classification", () => { + it.each([ + { + hint: "Unity AssemblyRef", + options: { references: ["UnityEngine.CoreModule"] }, + evidence: "unity-reference-observed", + }, + { + hint: ".NET Framework target", + options: { targetFramework: ".NETFramework,Version=v4.8" }, + evidence: "target-framework-observed", + }, + ])( + "keeps ReadyToRun classification and evidence with a $hint", + ({ options, evidence }) => { + const bytes = buildManagedPeFixture({ readyToRun: true, ...options }); + const result = inspectManagedArtifactBytes( + bytes, + managedPeFixtureTarget(bytes), + ); + + expect(result.classification).toMatchObject({ + runtime_family: "modern-dotnet", + implementation: "cil-and-ready-to-run", + }); + expect(result.classification.evidence).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "ready-to-run-signature-observed" }), + expect.objectContaining({ code: evidence }), + ]), + ); + }, + ); + it("classifies crossgen2 ReadyToRun output without ILONLY as modern .NET", () => { // crossgen2 writes IL_LIBRARY | STRONGNAMESIGNED and clears ILONLY. const bytes = buildManagedPeFixture({ readyToRun: true, cliFlags: 0xc }); diff --git a/src/dotnet/ManagedArtifactInspector.ts b/src/dotnet/ManagedArtifactInspector.ts index e5e06f678c..b39d269fd5 100644 --- a/src/dotnet/ManagedArtifactInspector.ts +++ b/src/dotnet/ManagedArtifactInspector.ts @@ -189,9 +189,57 @@ const runtimeFamilyFor = ( const hasUnity = inventory.referenceNames.some( (name) => name === "UnityEngine" || name.startsWith("UnityEngine."), ); + const hasFrameworkTarget = frameworks.some((value) => + value.startsWith(".NETFramework,"), + ); + const hasModernTarget = frameworks.some((value) => + value.startsWith(".NETCoreApp,"), + ); + const hasStandardTarget = frameworks.some((value) => + value.startsWith(".NETStandard,"), + ); + if (cli.readyToRunSignature) + evidence.push({ + code: "ready-to-run-signature-observed", + detail: "The managed native header has the ReadyToRun signature.", + file_offset: layout.rvaToOffset( + cli.managedNativeHeader.rva, + 4, + "cli.managed-native-header", + ), + }); + if (hasUnity) + evidence.push({ + code: "unity-reference-observed", + detail: + "The admitted AssemblyRef table contains a UnityEngine reference.", + file_offset: metadata.table(35)?.offset ?? null, + }); + if (hasFrameworkTarget) + evidence.push({ + code: "target-framework-observed", + detail: + "An assembly-level TargetFrameworkAttribute names .NET Framework.", + file_offset: metadata.table(12)?.offset ?? null, + }); + if (hasModernTarget) + evidence.push({ + code: "target-framework-observed", + detail: "An assembly-level TargetFrameworkAttribute names modern .NET.", + file_offset: metadata.table(12)?.offset ?? null, + }); + if (hasStandardTarget) + evidence.push({ + code: "target-framework-observed", + detail: + "An assembly-level TargetFrameworkAttribute names .NET Standard, an API specification that does not identify a runtime family.", + file_offset: metadata.table(12)?.offset ?? null, + }); let runtimeFamily: ManagedArtifactInspection["classification"]["runtime_family"] = "unknown"; - if (runtimeFamilyFromCliHeader(cli) === "mixed-clr-native") { + if (cli.readyToRunSignature) { + runtimeFamily = "modern-dotnet"; + } else if (runtimeFamilyFromCliHeader(cli) === "mixed-clr-native") { runtimeFamily = "mixed-clr-native"; evidence.push({ code: "cli-il-only-clear", @@ -201,47 +249,10 @@ const runtimeFamilyFor = ( }); } else if (hasUnity) { runtimeFamily = "unity-mono"; - evidence.push({ - code: "unity-reference-observed", - detail: - "The admitted AssemblyRef table contains a UnityEngine reference.", - file_offset: metadata.table(35)?.offset ?? null, - }); - } else if (frameworks.some((value) => value.startsWith(".NETFramework,"))) { + } else if (hasFrameworkTarget) { runtimeFamily = "dotnet-framework"; - evidence.push({ - code: "target-framework-observed", - detail: - "An assembly-level TargetFrameworkAttribute names .NET Framework.", - file_offset: metadata.table(12)?.offset ?? null, - }); - } else if ( - frameworks.some((value) => value.startsWith(".NETCoreApp,")) || - cli.readyToRunSignature - ) { + } else if (hasModernTarget) { runtimeFamily = "modern-dotnet"; - evidence.push({ - code: cli.readyToRunSignature - ? "ready-to-run-signature-observed" - : "target-framework-observed", - detail: cli.readyToRunSignature - ? "The managed native header has the ReadyToRun signature." - : "An assembly-level TargetFrameworkAttribute names modern .NET.", - file_offset: cli.readyToRunSignature - ? layout.rvaToOffset( - cli.managedNativeHeader.rva, - 4, - "cli.managed-native-header", - ) - : (metadata.table(12)?.offset ?? null), - }); - } else if (frameworks.some((value) => value.startsWith(".NETStandard,"))) { - evidence.push({ - code: "target-framework-observed", - detail: - "An assembly-level TargetFrameworkAttribute names .NET Standard, an API specification that does not identify a runtime family.", - file_offset: metadata.table(12)?.offset ?? null, - }); } return { runtime_family: runtimeFamily, evidence }; }; diff --git a/src/dotnet/ManagedMemberInspector.ts b/src/dotnet/ManagedMemberInspector.ts index 1720e08e4f..68f5638a31 100644 --- a/src/dotnet/ManagedMemberInspector.ts +++ b/src/dotnet/ManagedMemberInspector.ts @@ -18,6 +18,7 @@ import { readManagedMetadataInventory, readManagedModuleIdentity, } from "./ManagedMetadataInventory.js"; +import { managedMetadataTypeNames } from "./ManagedMetadataInventoryRows.js"; import { managedPeInspectionEnvelope } from "./ManagedPeInspectionEnvelope.js"; import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { @@ -262,6 +263,13 @@ export const inspectManagedMembersBytes = ( readManagedResourceDirectory(pe, issues), memberLists, identity, + managedMetadataTypeNames( + bytes, + layout, + issues, + parsedTypes, + typeReferences, + ), ); appendManagedParseIssues(issues, inventory.issues); const exportedTypes = parseExportedTypes(bytes, layout); diff --git a/src/dotnet/ManagedMetadataInventory.ts b/src/dotnet/ManagedMetadataInventory.ts index 1ffec64051..3d55371cb1 100644 --- a/src/dotnet/ManagedMetadataInventory.ts +++ b/src/dotnet/ManagedMetadataInventory.ts @@ -13,6 +13,8 @@ import { readCustomAttribute, readModule, readResource, + managedMetadataTypeNames, + type ManagedMetadataTypeNameLookup, } from "./ManagedMetadataInventoryRows.js"; import { metadataToken } from "./ManagedMetadataHeaps.js"; import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js"; @@ -166,6 +168,7 @@ const readAttributes = ( layout: ManagedMetadataLayout, issues: ManagedParseIssue[], memberLists: TypeMemberListFacts, + typeNames: ManagedMetadataTypeNameLookup, ): { readonly attributes: readonly CustomAttribute[]; readonly targetFrameworks: readonly string[]; @@ -180,6 +183,7 @@ const readAttributes = ( heapExtent(layout), issues, memberLists, + typeNames, ), issues, ), @@ -207,6 +211,7 @@ export const readManagedMetadataInventory = ( ManagedMetadataInventory, "module" | "issues" > = readManagedModuleIdentity(bytes, layout), + typeNames?: ManagedMetadataTypeNameLookup, ): ManagedMetadataInventory => { const issues: ManagedParseIssue[] = [...memberLists.issues]; issues.push(...identity.issues); @@ -228,6 +233,7 @@ export const readManagedMetadataInventory = ( layout, issues, memberLists, + typeNames ?? managedMetadataTypeNames(bytes, layout, issues), ); return { module, diff --git a/src/dotnet/ManagedMetadataInventoryRows.ts b/src/dotnet/ManagedMetadataInventoryRows.ts index 4e3559388a..79563afb62 100644 --- a/src/dotnet/ManagedMetadataInventoryRows.ts +++ b/src/dotnet/ManagedMetadataInventoryRows.ts @@ -1,4 +1,8 @@ import { managedDecodeBudget } from "./ManagedDecodeBudget.js"; +import { managedTypeReferences } from "./ManagedTypeReferences.js"; +import { MAX_TYPE_NESTING_DEPTH } from "./ManagedTypeNameDigests.js"; +import { readManagedTypeNesting } from "./ManagedTypeNesting.js"; +import type { ManagedTypeNesting } from "./ManagedTypeNesting.js"; import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js"; import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js"; @@ -20,7 +24,9 @@ import { readMetadataString, } from "./ManagedMetadataHeaps.js"; import { managedFailure, readManagedValue } from "./ManagedReaderFailure.js"; +import type { ManagedTypeReferenceLookup } from "./ManagedTypeReferences.js"; import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js"; +import type { ManagedType } from "./ManagedMemberInspectorCore.js"; type ModuleIdentity = NonNullable; type AssemblyIdentity = NonNullable; @@ -229,48 +235,161 @@ export const readAssemblyReference = ( }; }; -interface ReadTypeNameContext { - readonly bytes: Buffer; - readonly layout: ManagedMetadataLayout; - readonly table: 1 | 2; - readonly row: number; - readonly heapExtent: number; -} +export type ManagedMetadataTypeNameLookup = ( + table: 1 | 2, + row: number, +) => string | null; -const readTypeName = ({ - bytes, - layout, - table, - row, - heapExtent, -}: ReadTypeNameContext): string | null => { - const descriptor = layout.table(table); - if (descriptor === undefined || row < 1 || row > descriptor.rowCount) - return null; - const cursor = metadataRowCursor(bytes, layout, table, row); - if (table === 1) cursor.readIndex(layout.codedIndexSize("ResolutionScope")); - else cursor.readUInt32(); - const name = readMetadataString( - bytes, - layout, - cursor.readIndex(layout.stringIndexSize), - heapExtent, - ); - const namespace = readMetadataString( - bytes, - layout, - cursor.readIndex(layout.stringIndexSize), - heapExtent, - ); - return namespace.length === 0 ? name : `${namespace}.${name}`; +/** Resolve the display name from the same validated nesting facts used by member parsing. */ +export const managedMetadataTypeNames = ( + bytes: Buffer, + layout: ManagedMetadataLayout, + issues: ManagedParseIssue[], + parsedTypes?: { + readonly types: readonly ManagedType[]; + readonly nesting: ManagedTypeNesting; + }, + parsedReferences?: ManagedTypeReferenceLookup, +): ManagedMetadataTypeNameLookup => { + const nesting = + parsedTypes?.nesting ?? readManagedTypeNesting(bytes, layout, issues); + const references = + parsedReferences ?? managedTypeReferences(bytes, layout, issues); + const definitions = new Map(); + if (parsedTypes !== undefined) { + managedDecodeBudget(layout).reserve(parsedTypes.types.length * 128, null); + for (const type of parsedTypes.types) + definitions.set(Number.parseInt(type.token.slice(4), 16), type); + } + const cache = new Map< + string, + { + readonly fullName: string; + readonly depth: number; + readonly utf8Bytes: number; + } | null + >(); + const depthLimit = Symbol("type-name-depth-limit"); + const resolve = ( + table: 1 | 2, + row: number, + active: ReadonlySet = new Set(), + ): + | { + readonly fullName: string; + readonly depth: number; + readonly utf8Bytes: number; + } + | null + | typeof depthLimit => { + const key = `${String(table)}:${String(row)}`; + if (cache.has(key)) return cache.get(key) ?? null; + const descriptor = layout.table(table); + if (descriptor === undefined || row < 1 || row > descriptor.rowCount) + return null; + if (active.has(key)) { + cache.set(key, null); + return null; + } + if (active.size >= MAX_TYPE_NESTING_DEPTH) return depthLimit; + managedDecodeBudget(layout).reserve(128, null); + let name: string; + let namespace: string; + let parentRow: number | null; + if (table === 1) { + const reference = references(row); + if (reference === null || !reference.scopeValid) { + cache.set(key, null); + return null; + } + ({ name, namespace, enclosingRow: parentRow } = reference); + } else { + if (!nesting.complete) { + cache.set(key, null); + return null; + } + const parsed = definitions.get(row); + if (parsedTypes !== undefined && parsed === undefined) { + cache.set(key, null); + return null; + } + let flags: number; + if (parsed !== undefined) { + name = parsed.name; + namespace = parsed.namespace; + flags = parsed.flags; + } else { + const cursor = metadataRowCursor(bytes, layout, table, row); + flags = cursor.readUInt32(); + name = readMetadataString( + bytes, + layout, + cursor.readIndex(layout.stringIndexSize), + layout.strings.size, + ); + namespace = readMetadataString( + bytes, + layout, + cursor.readIndex(layout.stringIndexSize), + layout.strings.size, + ); + } + parentRow = nesting.enclosing.get(row) ?? null; + const isNested = (flags & 0x7) >= 2; + if (isNested !== (parentRow !== null)) { + cache.set(key, null); + return null; + } + } + let fullName: string | null; + let depth = 1; + let utf8Bytes = 0; + const nameBytes = Buffer.byteLength(name, "utf8"); + if (parentRow === null) { + if (namespace.length === 0) { + fullName = name; + utf8Bytes = nameBytes; + } else { + utf8Bytes = Buffer.byteLength(namespace, "utf8") + 1 + nameBytes; + managedDecodeBudget(layout).reserveString( + utf8Bytes, + metadataRowOffset(layout, table, row), + ); + fullName = `${namespace}.${name}`; + } + } else { + const parents = new Set(active); + parents.add(key); + const parent = resolve(table, parentRow, parents); + if (parent === depthLimit) return depthLimit; + if (parent === null) fullName = null; + else { + depth = parent.depth + 1; + utf8Bytes = parent.utf8Bytes + 1 + nameBytes; + managedDecodeBudget(layout).reserveString( + utf8Bytes, + metadataRowOffset(layout, table, row), + ); + fullName = `${parent.fullName}+${name}`; + } + } + if (depth > MAX_TYPE_NESTING_DEPTH) fullName = null; + const named = fullName === null ? null : { fullName, depth, utf8Bytes }; + cache.set(key, named); + return named; + }; + return (table, row) => { + const named = resolve(table, row); + return named === null || named === depthLimit ? null : named.fullName; + }; }; const declaringTypeForMethod = ( bytes: Buffer, layout: ManagedMetadataLayout, methodRow: number, - heapExtent: number, memberLists: TypeMemberListFacts, + typeName: ManagedMetadataTypeNameLookup, ): string | null => { const owner = memberDeclaringTypeRowLookup( bytes, @@ -279,21 +398,21 @@ const declaringTypeForMethod = ( memberLists, )(methodRow); if (owner === 0) return null; - return readTypeName({ bytes, layout, table: 2, row: owner, heapExtent }); + return typeName(2, owner); }; const attributeTypeName = ( bytes: Buffer, layout: ManagedMetadataLayout, rawType: number, - heapExtent: number, memberLists: TypeMemberListFacts, + typeName: ManagedMetadataTypeNameLookup, ): string | null => { const tag = rawType & 7; const row = Math.floor(rawType / 8); if (row === 0) return null; if (tag === 2) - return declaringTypeForMethod(bytes, layout, row, heapExtent, memberLists); + return declaringTypeForMethod(bytes, layout, row, memberLists, typeName); if (tag !== 3) return null; const memberRefs = layout.table(10); if (memberRefs === undefined || row > memberRefs.rowCount) return null; @@ -301,29 +420,15 @@ const attributeTypeName = ( const parent = cursor.readIndex(layout.codedIndexSize("MemberRefParent")); const parentTag = parent & 7; const parentRow = Math.floor(parent / 8); - if (parentTag === 0) - return readTypeName({ - bytes, - layout, - table: 2, - row: parentRow, - heapExtent, - }); - if (parentTag === 1) - return readTypeName({ - bytes, - layout, - table: 1, - row: parentRow, - heapExtent, - }); + if (parentTag === 0) return typeName(2, parentRow); + if (parentTag === 1) return typeName(1, parentRow); if (parentTag === 3) return declaringTypeForMethod( bytes, layout, parentRow, - heapExtent, memberLists, + typeName, ); return null; }; @@ -389,6 +494,7 @@ export const readCustomAttribute = ( heapExtent: number, issues: ManagedParseIssue[], memberLists: TypeMemberListFacts, + typeNames: ManagedMetadataTypeNameLookup, ): CustomAttribute => { const cursor = metadataRowCursor(bytes, layout, 12, row); const parentRaw = cursor.readIndex( @@ -451,8 +557,7 @@ export const readCustomAttribute = ( if (constructor !== null) typeName = readManagedValue( - () => - attributeTypeName(bytes, layout, typeRaw, heapExtent, memberLists), + () => attributeTypeName(bytes, layout, typeRaw, memberLists, typeNames), issues, ) ?? null; return { diff --git a/tests/acceptance/managedCliOutput.test.ts b/tests/acceptance/managedCliOutput.test.ts index cefb178e0f..b83d237552 100644 --- a/tests/acceptance/managedCliOutput.test.ts +++ b/tests/acceptance/managedCliOutput.test.ts @@ -166,6 +166,70 @@ cliTest( }, }); } + + const hintedPath = join(root, "ready-to-run-with-runtime-hints.dll"); + const hintedTarget = ".NETFramework,Version=v4.8"; + await writeFile( + hintedPath, + buildManagedPeFixture({ + readyToRun: true, + targetFramework: hintedTarget, + references: ["UnityEngine.CoreModule"], + }), + ); + const hinted = await cli.run({ + arguments: ["inspect-managed-artifact", hintedPath, "--json"], + }); + expect(hinted.exitCode).toBe(0); + expect(hinted.json).toMatchObject({ + normalized_result: { + classification: { + runtime_family: "modern-dotnet", + implementation: "cil-and-ready-to-run", + evidence: expect.arrayContaining([ + expect.objectContaining({ + code: "ready-to-run-signature-observed", + }), + expect.objectContaining({ code: "unity-reference-observed" }), + expect.objectContaining({ code: "target-framework-observed" }), + ]), + }, + target_frameworks: [hintedTarget], + }, + }); + }, +); + +cliTest( + "managed CLI qualifies a local nested custom-attribute type name", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-managed-nested-attribute-"); + const path = join(root, "nested-attribute.dll"); + await writeFile( + path, + buildManagedPeFixture({ + additionalEmptyTypes: 1, + customAttributeTypeRaw: (1 << 3) | 2, + nestedTypes: [[2, 1]], + typeName: "InnerAttribute", + typeNamespace: "", + }), + ); + + const result = await cli.run({ + arguments: ["inspect-managed-artifact", path, "--json"], + }); + expect(result.exitCode).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: { + attributes: [ + expect.objectContaining({ + type_name: "Empty0+InnerAttribute", + }), + ], + coverage: { state: "complete", issues: [] }, + }, + }); }, );