mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
abcbce991e
## Problem The AST component index (`buildAstIndexWithDiagnostics`) is keyed by component **name**. When two files define a component with the same name — most commonly platform variants, e.g. `List` in `List.tsx` and `List` in `List.web.tsx` — the index kept whichever file the directory walk happened to reach **first** (`if (!index.has(name))`) and threw the rest away. So a lookup for `List` could return the wrong platform variant's source, with no signal that other matches even existed. And because it depended on filesystem walk order, *which* `List` survived was effectively arbitrary. Raised in review on #287. ## Fix - **Collect every match per name** instead of keeping the first walked. - **Choose a deterministic primary**: base files are preferred over `.web` / `.ios` / `.android` / `.native` variants (those only override the base when bundling for that platform), then ties break on path → line → column. The result no longer depends on directory-walk order. - **Surface the rest** as `otherMatches` on the index entry. `react-profiler-component-source` now returns `otherMatches: [{ file, line, col }]` alongside the primary, so a caller asking for `List` can tell the locations apart instead of trusting a single arbitrary file. `react-profiler-analyze` needs no change — it enriches findings with the (now deterministic) primary location, which is strictly better than before; surfacing per-finding alternates there is out of scope. ## Tests `packages/tool-server/test/react-profiler/ast-index.test.ts` (index layer) and a new `component-source-duplicates.test.ts` (end-to-end through the tool's `execute`): - same name in `List.tsx` + `List.web.tsx` → primary is the base file, its source is returned, `List.web.tsx` is under `otherMatches`; - only platform variants (`.android` + `.ios`, no base) → deterministic primary by path; - uniquely-named component → no `otherMatches`. The two collision assertions fail on pre-fix code (no `otherMatches`, arbitrary primary) and pass with the fix. Verified locally with Node 20: `vitest run test/react-profiler/` — 80 passed; `tsc --build` clean; prettier + eslint clean.
49 lines
2.2 KiB
TypeScript
49 lines
2.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { mkdtempSync, mkdirSync, writeFileSync } from "fs";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { reactProfilerComponentSourceTool } from "../../src/tools/profiler/react/react-profiler-component-source";
|
|
|
|
/**
|
|
* End-to-end through the tool's execute(): when a component name resolves to
|
|
* several files (platform variants), the lookup must return one deterministic
|
|
* primary's source AND surface the rest under otherMatches — otherwise a caller
|
|
* asking for "List" is silently handed whichever file the directory walk
|
|
* happened to reach first, which may be the wrong platform variant.
|
|
*/
|
|
describe("react-profiler-component-source: duplicate component names", () => {
|
|
it("returns the base-file primary source and surfaces variants under otherMatches", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "component-source-dup-"));
|
|
mkdirSync(join(dir, "components"), { recursive: true });
|
|
const base = join(dir, "components", "List.tsx");
|
|
const web = join(dir, "components", "List.web.tsx");
|
|
writeFileSync(base, `export function List() { return <View>NATIVE_MARKER</View>; }\n`);
|
|
writeFileSync(web, `export function List() { return <div>WEB_MARKER</div>; }\n`);
|
|
|
|
const result = (await reactProfilerComponentSourceTool.execute(
|
|
{},
|
|
{ component_name: "List", project_root: dir }
|
|
)) as Record<string, unknown>;
|
|
|
|
expect(result.found).toBe(true);
|
|
expect(result.file).toBe(base);
|
|
// Source must come from the primary (base) file, not the web variant.
|
|
expect(result.source).toContain("NATIVE_MARKER");
|
|
expect(result.source).not.toContain("WEB_MARKER");
|
|
expect(result.otherMatches).toEqual([{ file: web, line: 1, col: 16 }]);
|
|
});
|
|
|
|
it("omits otherMatches for a uniquely-named component", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "component-source-uniq-"));
|
|
writeFileSync(join(dir, "Solo.tsx"), `export function Solo() { return <View />; }\n`);
|
|
|
|
const result = (await reactProfilerComponentSourceTool.execute(
|
|
{},
|
|
{ component_name: "Solo", project_root: dir }
|
|
)) as Record<string, unknown>;
|
|
|
|
expect(result.found).toBe(true);
|
|
expect(result.otherMatches).toBeUndefined();
|
|
});
|
|
});
|