mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
98fe7a86fc
Unbreaks the **Dead Code** gate on main, red since #663 merged. ## What happened #663 branched before the knip workflow existed (#572), so its own PR checks never ran the gate. Its `component-names.ts` exports three types with no importers — `StrippedName`, `ComponentAnnotation`, `ComponentNameResolution` — which pushed the knip second pass to **218** parked issues against the **215** ceiling. Every open PR now inherits the red check (e.g. #687, whose own commit adds no parked items). Local runs hid this: counted the way `knip.jsonc` prescribes — against a tree with **no build output** — the failure reproduces exactly. ## The fix Import the types where the tests naturally need them, which also removes a wart: the two `renderComponentNameMiss(r as never)` casts existed precisely because the narrowing `ComponentNameResolution` enables was never written. The new `expectMiss` helper narrows properly and fails loudly if the resolver ever stops returning a miss. No ceiling change: 47 + 156 + 12 = **215**, exactly at the limit. Note the budget now has zero headroom — the next PR that parks an export will trip it, which is the gate working as designed. ## Checks - clean-tree `npm run knip` exit 0 (both passes) - component-names tests 18/18, tool-server test typecheck clean, prettier clean 🤖 Generated with [Claude Code](https://claude.com/claude-code)
196 lines
7.7 KiB
TypeScript
196 lines
7.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type {
|
|
ComponentAnnotation,
|
|
ComponentNameResolution,
|
|
StrippedName,
|
|
} from "../../src/utils/react-profiler/component-names";
|
|
import {
|
|
stripComponentWrappers,
|
|
annotateComponentName,
|
|
resolveComponentName,
|
|
renderComponentNameMiss,
|
|
astLookupCandidates,
|
|
} from "../../src/utils/react-profiler/component-names";
|
|
|
|
/**
|
|
* Names taken verbatim from a real Expo SDK 54 profiling session (RN 0.81.5,
|
|
* React Compiler on) — the session that produced issue #632. Includes the one
|
|
* genuine collision it contained and a router name whose inner text has
|
|
* parentheses, dots and slashes.
|
|
*/
|
|
const REAL_SESSION_NAMES = [
|
|
"Forget(ParallaxScrollView)",
|
|
"Forget(Collapsible)",
|
|
"Forget(IconSymbol)",
|
|
"Forget(ThemedText)",
|
|
"Forget(BottomTabItem)",
|
|
"Forget(TabTwoScreen(./(tabs)/explore.tsx))",
|
|
"StaticContainer",
|
|
"Memo(StaticContainer)",
|
|
"Anonymous",
|
|
"Animated(Anonymous)",
|
|
"Animated(View)",
|
|
"View",
|
|
"Context.Provider",
|
|
"InnerScreen",
|
|
"ForwardRef(InnerScreen)",
|
|
];
|
|
|
|
describe("stripComponentWrappers", () => {
|
|
it("strips the three wrappers React DevTools adds, including nested ones", () => {
|
|
expect(stripComponentWrappers("Forget(Foo)").baseName).toBe("Foo");
|
|
expect(stripComponentWrappers("Memo(Foo)").baseName).toBe("Foo");
|
|
expect(stripComponentWrappers("ForwardRef(Foo)").baseName).toBe("Foo");
|
|
expect(stripComponentWrappers("Memo(Forget(Foo))").baseName).toBe("Foo");
|
|
expect(stripComponentWrappers("Forget(Memo(ForwardRef(Memo(Deep))))").baseName).toBe("Deep");
|
|
});
|
|
|
|
it("handles an inner name containing parens, dots and slashes", () => {
|
|
// expo-router names a screen after its file, so the greedy anchored match
|
|
// must not stop at the first inner ")".
|
|
expect(stripComponentWrappers("Forget(TabTwoScreen(./(tabs)/explore.tsx))").baseName).toBe(
|
|
"TabTwoScreen(./(tabs)/explore.tsx)"
|
|
);
|
|
});
|
|
|
|
it("leaves wrappers the report never strips alone", () => {
|
|
// Stripping these would make a query for `View` ambiguous in almost every
|
|
// session, and no agent ever holds a stripped form of them.
|
|
for (const n of ["Animated(View)", "AnimatedComponent(View)", "Motion(View)", "Anonymous"]) {
|
|
expect(stripComponentWrappers(n).baseName).toBe(n);
|
|
}
|
|
expect(stripComponentWrappers("Context.Provider").baseName).toBe("Context.Provider");
|
|
});
|
|
|
|
it("reports which wrappers were present", () => {
|
|
const s: StrippedName = stripComponentWrappers("Memo(Forget(Foo))");
|
|
expect(s.hasMemo).toBe(true);
|
|
expect(s.hasForget).toBe(true);
|
|
expect(s.hasForwardRef).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("annotateComponentName", () => {
|
|
it("keeps the tag wording and ordering the report has always used", () => {
|
|
const ann: ComponentAnnotation = annotateComponentName("Forget(Foo)");
|
|
expect(ann).toEqual({
|
|
displayName: "Foo",
|
|
tag: " [React Compiler]",
|
|
rawName: "Forget(Foo)",
|
|
});
|
|
expect(annotateComponentName("Memo(Forget(ForwardRef(Foo)))").tag).toBe(
|
|
" [React.memo + React Compiler + forwardRef]"
|
|
);
|
|
expect(annotateComponentName("View").tag).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("resolveComponentName", () => {
|
|
it("resolves the display name the report prints — the issue", () => {
|
|
const r = resolveComponentName("ParallaxScrollView", REAL_SESSION_NAMES);
|
|
expect(r.kind).toBe("display");
|
|
expect(r.kind === "display" && r.rawName).toBe("Forget(ParallaxScrollView)");
|
|
});
|
|
|
|
it("still resolves the raw name unchanged", () => {
|
|
const r = resolveComponentName("Forget(ParallaxScrollView)", REAL_SESSION_NAMES);
|
|
expect(r.kind).toBe("exact");
|
|
expect(r.kind === "exact" && r.rawName).toBe("Forget(ParallaxScrollView)");
|
|
});
|
|
|
|
it("lets an exact match win over a wrapped sibling, and never widens it", () => {
|
|
// Both `StaticContainer` and `Memo(StaticContainer)` were recorded in one
|
|
// real session. Asking for the bare one must return the bare one.
|
|
const r = resolveComponentName("StaticContainer", REAL_SESSION_NAMES);
|
|
expect(r.kind).toBe("exact");
|
|
expect(r.kind === "exact" && r.rawName).toBe("StaticContainer");
|
|
// ...but the caller is told the other fiber exists, instead of silently
|
|
// seeing one of two.
|
|
expect(r.kind === "exact" && r.alsoMatching).toEqual(["Memo(StaticContainer)"]);
|
|
});
|
|
|
|
it("does not cross-resolve one wrapper into another", () => {
|
|
// Matching on the recorded name's DISPLAY form (not a stripped query)
|
|
// means asking for a Forget() fiber never silently returns a Memo() one.
|
|
const r = resolveComponentName("Forget(StaticContainer)", REAL_SESSION_NAMES);
|
|
expect(r.kind).toBe("missing");
|
|
});
|
|
|
|
it("does not resolve a non-stripped wrapper from its inner name", () => {
|
|
const r = resolveComponentName("View", REAL_SESSION_NAMES);
|
|
expect(r.kind).toBe("exact");
|
|
expect(r.kind === "exact" && r.rawName).toBe("View");
|
|
// `Animated(View)` is a different component and must not be pulled in.
|
|
expect(r.kind === "exact" && r.alsoMatching).toEqual([]);
|
|
});
|
|
|
|
it("refuses rather than merging when several fibers share a display name", () => {
|
|
const names = ["Memo(Widget)", "ForwardRef(Widget)"];
|
|
const r = resolveComponentName("Widget", names);
|
|
expect(r.kind).toBe("ambiguous");
|
|
expect(r.kind === "ambiguous" && r.candidates.sort()).toEqual([
|
|
"ForwardRef(Widget)",
|
|
"Memo(Widget)",
|
|
]);
|
|
});
|
|
|
|
it("reports a miss with re-queryable suggestions", () => {
|
|
const r = resolveComponentName("ParallaxScroll", REAL_SESSION_NAMES);
|
|
expect(r.kind).toBe("missing");
|
|
expect(r.kind === "missing" && r.suggestions).toContain("Forget(ParallaxScrollView)");
|
|
});
|
|
});
|
|
|
|
// Narrow a resolution to the two outcomes renderComponentNameMiss accepts,
|
|
// failing the test if the resolver returned anything else.
|
|
function expectMiss(
|
|
r: ComponentNameResolution
|
|
): Extract<ComponentNameResolution, { kind: "ambiguous" | "missing" }> {
|
|
if (r.kind !== "ambiguous" && r.kind !== "missing") {
|
|
throw new Error(`expected a non-resolving outcome, got "${r.kind}"`);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
describe("renderComponentNameMiss", () => {
|
|
it("hands back exact strings to retry with on an ambiguity", () => {
|
|
const r = resolveComponentName("Widget", ["Memo(Widget)", "ForwardRef(Widget)"]);
|
|
const out = renderComponentNameMiss(expectMiss(r));
|
|
expect(out).toContain("ambiguous");
|
|
expect(out).toContain("`Memo(Widget)`");
|
|
expect(out).toContain("`ForwardRef(Widget)`");
|
|
expect(out).toContain("are not merged");
|
|
});
|
|
|
|
it("quantifies the session so a miss can be told from an empty capture", () => {
|
|
const r = resolveComponentName("Nope", REAL_SESSION_NAMES);
|
|
const out = renderComponentNameMiss(expectMiss(r), { fiberRenders: 313, commits: 16 });
|
|
expect(out).toContain("313 fiber renders across 16 commits");
|
|
expect(out).toContain("profiler-commit-query");
|
|
});
|
|
});
|
|
|
|
describe("astLookupCandidates", () => {
|
|
it("offers the bare source identifier for a wrapped name", () => {
|
|
expect(astLookupCandidates("Forget(IconSymbol)")).toEqual(["Forget(IconSymbol)", "IconSymbol"]);
|
|
});
|
|
|
|
it("offers the declared identifier for an expo-router screen name", () => {
|
|
// The report shows `TabTwoScreen(./(tabs)/explore.tsx)`; the source declares
|
|
// `export default function TabTwoScreen()`.
|
|
expect(astLookupCandidates("Forget(TabTwoScreen(./(tabs)/explore.tsx))")).toEqual([
|
|
"Forget(TabTwoScreen(./(tabs)/explore.tsx))",
|
|
"TabTwoScreen(./(tabs)/explore.tsx)",
|
|
"TabTwoScreen",
|
|
]);
|
|
});
|
|
|
|
it("does not invent variants for a plain name", () => {
|
|
expect(astLookupCandidates("IconSymbol")).toEqual(["IconSymbol"]);
|
|
});
|
|
|
|
it("only drops a suffix when what remains is a valid identifier", () => {
|
|
expect(astLookupCandidates("Context.Provider")).toEqual(["Context.Provider"]);
|
|
});
|
|
});
|