mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(apple): count Interface Builder hierarchy records beyond the object limit (#4162)
The ibtool dictionary parser cut the hierarchy array at a fixed 20,000 entries without counting what it discarded, so an identified record past that position vanished while the document still reported complete hierarchy coverage. The graph walker already owns the hierarchy limit; the parser now passes the decoded array through. The walker applies max_objects per identified record and keeps walking past the limit to count omitted records, instead of stopping on a pending structural entry. Coverage now reports the omitted count, and a hierarchy that exactly reaches the limit is no longer marked partial. Fixes #3950 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -481,6 +481,93 @@ describe("Interface Builder hierarchy completeness", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Interface Builder hierarchy record limits", () => {
|
||||
const analyze = (
|
||||
hierarchy: unknown[],
|
||||
maxObjects?: number,
|
||||
objects: Record<string, unknown> = {},
|
||||
) =>
|
||||
buildInterfaceBuilderAnalysis({
|
||||
targetSha256: hash,
|
||||
toolVersion: "test",
|
||||
documents: [
|
||||
{
|
||||
relativePath: "Main.nib",
|
||||
archiveSha256: hash,
|
||||
documentKind: "nib",
|
||||
raw: {
|
||||
"com.apple.ibtool.document.objects": objects,
|
||||
"com.apple.ibtool.document.hierarchy": hierarchy,
|
||||
},
|
||||
},
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse(
|
||||
maxObjects === undefined ? {} : { max_objects: maxObjects },
|
||||
),
|
||||
});
|
||||
const hierarchyFacet = (result: ReturnType<typeof analyze>) =>
|
||||
result.graph.coverage.find(({ facet }) => facet === "hierarchy:Main.nib");
|
||||
|
||||
it("keeps an identified record after more than 20,000 structural records", () => {
|
||||
const hierarchy: unknown[] = Array.from({ length: 20_001 }, () => ({
|
||||
children: [],
|
||||
}));
|
||||
hierarchy[20_000] = { objectID: "tail", children: [] };
|
||||
|
||||
const result = analyze(hierarchy);
|
||||
|
||||
expect(
|
||||
result.graph.nodes.map(
|
||||
({ attributes }) => attributes.interface_builder_object_id,
|
||||
),
|
||||
).toContain("tail");
|
||||
expect(result.graph.edges).toHaveLength(1);
|
||||
expect(result.graph.truncated).toBe(false);
|
||||
expect(hierarchyFacet(result)).toMatchObject({
|
||||
status: "complete",
|
||||
examined: 1,
|
||||
omitted: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("counts identified records beyond the object limit as omitted", () => {
|
||||
const result = analyze(
|
||||
["a", "b", "c", "d"].map((objectID) => ({ objectID, children: [] })),
|
||||
2,
|
||||
);
|
||||
|
||||
expect(result.documents[0]?.hierarchy_complete).toBe(false);
|
||||
expect(result.graph.truncated).toBe(true);
|
||||
expect(hierarchyFacet(result)).toMatchObject({
|
||||
status: "partial",
|
||||
reason: "graph_limit_reached",
|
||||
examined: 2,
|
||||
omitted: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("stays complete when the identified records exactly reach the object limit", () => {
|
||||
// The root and both objects fill the three graph nodes; the third
|
||||
// hierarchy record reaches the hierarchy limit and has a structural child.
|
||||
const result = analyze(
|
||||
[
|
||||
{ objectID: "a", children: [{ objectID: "b", children: [] }] },
|
||||
{ objectID: "b", children: [{ children: [] }] },
|
||||
],
|
||||
3,
|
||||
{ a: { class: "UIView" }, b: { class: "UIView" } },
|
||||
);
|
||||
|
||||
expect(result.documents[0]?.hierarchy_complete).toBe(true);
|
||||
expect(result.graph.truncated).toBe(false);
|
||||
expect(hierarchyFacet(result)).toMatchObject({
|
||||
status: "complete",
|
||||
examined: 3,
|
||||
omitted: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyed archive object references", () => {
|
||||
it("keeps referenced objects as stubs so shared and cyclic references stay finite", () => {
|
||||
// Each view references its window, superview, and next key view, so
|
||||
|
||||
@@ -211,6 +211,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
);
|
||||
};
|
||||
let hierarchyCount = 0;
|
||||
let documentOmittedHierarchyNodes = 0;
|
||||
const visitHierarchy = (
|
||||
initial: unknown,
|
||||
initialParentId: string,
|
||||
@@ -218,11 +219,6 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
const pending: { readonly value: unknown; readonly parentId: string }[] =
|
||||
[{ value: initial, parentId: initialParentId }];
|
||||
while (pending.length > 0) {
|
||||
if (hierarchyCount >= input.limits.max_objects) {
|
||||
truncated = true;
|
||||
hierarchyTruncated = true;
|
||||
return;
|
||||
}
|
||||
const entry = pending.pop();
|
||||
if (entry === undefined) break;
|
||||
const { value, parentId } = entry;
|
||||
@@ -242,7 +238,13 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
typeof item.archiveUID === "number" ? String(item.archiveUID) : null,
|
||||
);
|
||||
let nextParent = parentId;
|
||||
if (objectId !== null) {
|
||||
if (objectId !== null && hierarchyCount >= input.limits.max_objects) {
|
||||
// The walk continues over the decoded records so the omission is
|
||||
// counted rather than reported as an unknown remainder.
|
||||
truncated = true;
|
||||
hierarchyTruncated = true;
|
||||
documentOmittedHierarchyNodes += 1;
|
||||
} else if (objectId !== null) {
|
||||
const known = objectIds.has(objectId);
|
||||
const childId = known
|
||||
? objectNodeId(objectId)
|
||||
@@ -504,6 +506,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
: null,
|
||||
examined: hierarchyCount,
|
||||
omitted:
|
||||
documentOmittedHierarchyNodes +
|
||||
documentOmittedHierarchyEdges +
|
||||
parserOmittedHierarchyReferences +
|
||||
omittedSerializedHierarchyReferences,
|
||||
|
||||
@@ -131,7 +131,7 @@ export const parseInterfaceBuilderRecords = (
|
||||
return {
|
||||
objects,
|
||||
connections,
|
||||
hierarchy: Array.isArray(hierarchyRaw) ? hierarchyRaw.slice(0, 20_000) : [],
|
||||
hierarchy: Array.isArray(hierarchyRaw) ? hierarchyRaw : [],
|
||||
classes: classesRaw,
|
||||
objectCount: objectEntries.length,
|
||||
omittedObjects: Math.max(0, objectEntries.length - 20_000),
|
||||
|
||||
Reference in New Issue
Block a user