mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(managed): derive selected scope from parsed declarations
This commit is contained in:
@@ -229,9 +229,12 @@ rea inspect-managed-members /absolute/path/Assembly.dll --type Example.Parser
|
||||
MCP uses `inspect_managed_members` with
|
||||
`{"path":"/absolute/path/Assembly.dll","type_names":["Example.Parser"]}`.
|
||||
Repeat `--type` or supply multiple unique `type_names` for several types.
|
||||
Names match the reported `full_name` exactly. Top-level names are namespace
|
||||
qualified; nested types currently report bare names, which can select several
|
||||
types with that name. Each type's `enclosing_type_token` records its enclosing
|
||||
Names match the reported `full_name` exactly. Top-level names require that
|
||||
namespace-qualified spelling. Nested declarations also accept their bare
|
||||
`name`, including when their own namespace is nonempty; this can select several
|
||||
types with that name. Selection uses the parsed TypeDef facts and comparison
|
||||
uses the returned declaring types, rather than reconstructing scope from the
|
||||
caller's spelling. Each type's `enclosing_type_token` records its enclosing
|
||||
TypeDef. `nesting_read` distinguishes complete source rows (true), incomplete
|
||||
current rows (false), and historical Evidence (null). A null enclosing token
|
||||
alone does not establish that a type is top-level.
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import {
|
||||
buildComparisonCoverage,
|
||||
hasCompleteMethodRows,
|
||||
selectedDeclaringTypes,
|
||||
buildComparisonMatching,
|
||||
buildComparisonSummary,
|
||||
comparisonLimitations,
|
||||
@@ -216,11 +217,6 @@ export interface ManagedMemberComparisonSide {
|
||||
readonly result: ManagedMemberInspection;
|
||||
}
|
||||
|
||||
const selectedTypes = (
|
||||
result: ManagedMemberInspection,
|
||||
): ReadonlySet<string> | null =>
|
||||
result.selection === null ? null : new Set(result.selection.type_names);
|
||||
|
||||
const callEdgesByCaller = (
|
||||
result: ManagedMemberInspection,
|
||||
): ReadonlyMap<
|
||||
@@ -275,8 +271,8 @@ export const compareManagedMembers = (
|
||||
rightMethodsComplete:
|
||||
right.result.metadata.status === "complete" &&
|
||||
hasCompleteMethodRows(right.result),
|
||||
leftSelection: selectedTypes(left.result),
|
||||
rightSelection: selectedTypes(right.result),
|
||||
leftSelection: selectedDeclaringTypes(left.result),
|
||||
rightSelection: selectedDeclaringTypes(right.result),
|
||||
signatureResolutionExamined:
|
||||
hasSignatureResolution(left.result) &&
|
||||
hasSignatureResolution(right.result),
|
||||
|
||||
@@ -50,6 +50,14 @@ const sameMethodRows = (
|
||||
left.method_rows.count === right.method_rows.count &&
|
||||
left.method_rows.total === right.method_rows.total);
|
||||
|
||||
/** Returned type facts, rather than request spelling, own the observed scope. */
|
||||
export const selectedDeclaringTypes = (
|
||||
result: ManagedMemberInspection,
|
||||
): ReadonlySet<string> | null =>
|
||||
result.selection === null
|
||||
? null
|
||||
: new Set(result.types.map(({ full_name }) => full_name));
|
||||
|
||||
/** Whether both observations inspected the same declaring types. */
|
||||
const sameSelection = (
|
||||
left: ManagedMemberInspection,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { hasCompleteMethodRows } from "./managedMemberComparisonCoverage.js";
|
||||
import {
|
||||
hasCompleteMethodRows,
|
||||
selectedDeclaringTypes,
|
||||
} from "./managedMemberComparisonCoverage.js";
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import {
|
||||
managedMemberInspectionSchema,
|
||||
@@ -490,12 +493,15 @@ const groupBy = <Item>(
|
||||
};
|
||||
|
||||
/** Whether a side's inventory observed members of this declaring type. */
|
||||
const observedBy =
|
||||
(side: ManagedMemberComparisonSide) =>
|
||||
({ declaring_type }: { readonly declaring_type: string | null }): boolean =>
|
||||
side.result.selection === null ||
|
||||
(declaring_type !== null &&
|
||||
side.result.selection.type_names.includes(declaring_type));
|
||||
const observedBy = (side: ManagedMemberComparisonSide) => {
|
||||
const types = selectedDeclaringTypes(side.result);
|
||||
return ({
|
||||
declaring_type,
|
||||
}: {
|
||||
readonly declaring_type: string | null;
|
||||
}): boolean =>
|
||||
types === null || (declaring_type !== null && types.has(declaring_type));
|
||||
};
|
||||
|
||||
/**
|
||||
* Pair only members whose declaring type both sides observed. A member outside
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
import {
|
||||
parseTypes,
|
||||
readTypeDefinitions,
|
||||
type TypeRange,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
import { edgeTargets } from "./ManagedMethodSpecs.js";
|
||||
import { managedTypeNameDigests } from "./ManagedTypeNameDigests.js";
|
||||
@@ -124,26 +123,27 @@ const readMemberLayout = (bytes: Buffer, pe: ManagedPeLayout) => {
|
||||
};
|
||||
|
||||
const selectTypes = (
|
||||
ranges: readonly TypeRange[],
|
||||
types: ManagedMemberInspection["types"],
|
||||
typeNames: readonly string[] | undefined,
|
||||
): {
|
||||
readonly tokens: SelectedTypeTokens;
|
||||
readonly selection: ManagedMemberInspection["selection"];
|
||||
readonly unreadableNames: number;
|
||||
} => {
|
||||
if (typeNames === undefined)
|
||||
return { tokens: null, selection: null, unreadableNames: 0 };
|
||||
if (typeNames === undefined) return { tokens: null, selection: null };
|
||||
const requested = new Set(typeNames);
|
||||
const tokens = new Set<string>();
|
||||
const matched = new Set<string>();
|
||||
for (const range of ranges) {
|
||||
if (range.fullName === null || !requested.has(range.fullName)) continue;
|
||||
tokens.add(range.token);
|
||||
matched.add(range.fullName);
|
||||
for (const type of types) {
|
||||
const nested = (type.flags & 0x7) >= 2;
|
||||
const spellings = nested ? [type.full_name, type.name] : [type.full_name];
|
||||
for (const name of spellings) {
|
||||
if (!requested.has(name)) continue;
|
||||
tokens.add(type.token);
|
||||
matched.add(name);
|
||||
}
|
||||
}
|
||||
return {
|
||||
tokens,
|
||||
unreadableNames: ranges.filter(({ fullName }) => fullName === null).length,
|
||||
selection: {
|
||||
type_names: [...typeNames],
|
||||
unmatched_type_names: typeNames.filter((name) => !matched.has(name)),
|
||||
@@ -198,12 +198,13 @@ export const inspectManagedMembersBytes = (
|
||||
const identity = readManagedModuleIdentity(bytes, layout);
|
||||
appendManagedParseIssues(issues, identity.issues);
|
||||
const ranges = readTypeDefinitions(bytes, layout, issues, memberLists);
|
||||
const {
|
||||
tokens: selected,
|
||||
selection,
|
||||
unreadableNames,
|
||||
} = selectTypes(ranges, typeNames);
|
||||
const parsedTypes = parseTypes(bytes, layout, ranges);
|
||||
const { tokens: selected, selection } = selectTypes(
|
||||
parsedTypes.types,
|
||||
typeNames,
|
||||
);
|
||||
const unreadableNames =
|
||||
typeNames === undefined ? 0 : ranges.length - parsedTypes.types.length;
|
||||
appendManagedParseIssues(issues, parsedTypes.issues);
|
||||
const typeReferences = managedTypeReferences(bytes, layout, issues);
|
||||
const signatureTypeName = managedTypeNameDigests(
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { readManagedPeLayout } from "../../../../src/dotnet/ManagedPeReader.js";
|
||||
import {
|
||||
metadataRowOffset,
|
||||
readManagedMetadataLayout,
|
||||
} from "../../../../src/dotnet/ManagedMetadataLayout.js";
|
||||
import { buildManagedNestedTypeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -15,6 +21,59 @@ import {
|
||||
|
||||
import { buildManagedPeFixture as buildMultiMethodFixture } from "../../../../scripts/fixtures/managed/pe.mjs";
|
||||
|
||||
it("selects namespace-bearing nested declarations from parsed type facts and compares the observed scope", () => {
|
||||
const bytes = buildManagedNestedTypeFixture();
|
||||
const pe = readManagedPeLayout(bytes);
|
||||
if (pe.cli === null) throw new Error("Missing fixture CLI");
|
||||
const layout = readManagedMetadataLayout(
|
||||
bytes,
|
||||
pe.rvaToOffset(pe.cli.metadata.rva, pe.cli.metadata.size, "fixture"),
|
||||
pe.cli.metadata.size,
|
||||
);
|
||||
const column = 4 + layout.stringIndexSize;
|
||||
const namespace = bytes.readUInt16LE(
|
||||
metadataRowOffset(layout, 2, 2) + column,
|
||||
);
|
||||
bytes.writeUInt16LE(namespace, metadataRowOffset(layout, 2, 4) + column);
|
||||
const selected = inspect(bytes, "/fixture/nested-selection.dll", ["Inner"]);
|
||||
expect(selected.result.selection?.unmatched_type_names).toEqual([]);
|
||||
expect(selected.result.types.map(({ token }) => token)).toEqual([
|
||||
"0x02000004",
|
||||
"0x02000005",
|
||||
]);
|
||||
expect(selected.result.methods).toHaveLength(2);
|
||||
expect(selected.result.fields).toHaveLength(2);
|
||||
const comparison = compareManagedMembers(selected, selected);
|
||||
expect(
|
||||
comparison.methods.every(({ match }) => match.status === "matched"),
|
||||
).toBe(true);
|
||||
expect(comparison.summary).toMatchObject({ added: 0, removed: 0 });
|
||||
expect(
|
||||
inspect(bytes, "/fixture/nested-qualified.dll", [
|
||||
"Fixture.Inner",
|
||||
]).result.types.map(({ token }) => token),
|
||||
).toEqual(["0x02000004"]);
|
||||
const topLevel = buildManagedNestedTypeFixture({ topLevelTypes: true });
|
||||
const topPe = readManagedPeLayout(topLevel);
|
||||
if (topPe.cli === null) throw new Error("Missing top-level fixture CLI");
|
||||
const topLayout = readManagedMetadataLayout(
|
||||
topLevel,
|
||||
topPe.rvaToOffset(
|
||||
topPe.cli.metadata.rva,
|
||||
topPe.cli.metadata.size,
|
||||
"fixture",
|
||||
),
|
||||
topPe.cli.metadata.size,
|
||||
);
|
||||
topLevel.writeUInt16LE(
|
||||
topLevel.readUInt16LE(metadataRowOffset(topLayout, 2, 2) + column),
|
||||
metadataRowOffset(topLayout, 2, 4) + column,
|
||||
);
|
||||
expect(
|
||||
inspect(topLevel, "/fixture/top-level.dll", ["Inner"]).result.types,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
describe("managed comparison method row coverage", () => {
|
||||
it("keeps paged omissions unknown while preserving full and equal-page controls", () => {
|
||||
const bytes = buildMultiMethodFixture({
|
||||
|
||||
Reference in New Issue
Block a user