Files
Aiden Bai 8c2f03aea9 feat: make React cleanup first-class (#1624)
* feat: make React cleanup first-class

* refactor: remove editor integrations

* fix: harden React cleanup analysis

* fix: detect default export duplication roots

* fix: unwrap typed duplication roots

* feat: add opt-in project analysis rules

* fix: canonicalize project analysis paths

* fix: harden project analysis precision

* fix: recognize cross-platform project entries

* fix: eliminate project analysis false positives

* fix: harden project analysis reachability

* fix: canonicalize project analysis inputs

* fix: resolve project analysis review findings

* fix: eliminate residual project analysis false positives

* fix: ignore commented registry previews

* fix: eliminate project analysis false positives

* fix: normalize project analysis paths across platforms

* fix: normalize Nextra theme path identity

* test: canonicalize convention fixture paths

* fix: preserve project analysis provenance

* fix: harden project analysis precision

* fix: honor project analysis boundaries

* fix: recognize conditional config plugins

* fix: recognize executable project references

* fix: recognize Stencil tool contracts

* fix: recognize nested tool references

* fix: recognize project setup contracts

* fix: recognize generated and local package consumers

* fix: recognize static template package references

* fix: recognize nested package runtime contracts

* fix: close project analysis parser gaps

* fix: parse project conventions structurally

* refactor: replace structural scanners with parsers

* fix: recognize functional Next CSS config

* fix: close remaining project analysis gaps

* fix: apply tag filters to project analysis

* fix: preserve embedded source positions

* fix: validate static config helper bindings

* fix: bound runtime directory discovery

* fix: close final dependency analysis gaps

* fix: preserve declaration dependency references

* chore: refresh generated rule metadata

* fix: make project analysis portable and bounded

* test: stabilize cleanup scaling guard

* refactor: parse project syntax with oxc

* fix: normalize native filesystem paths

* fix: separate path identity from report paths

* fix: match project files by filesystem identity

* fix: match build glob files by package identity

* fix: use native path keys for file identity

* fix: canonicalize Windows file identities

* fix: canonicalize package ownership paths

* test: inspect Windows path identities

* test: trace Windows package ownership

* fix: keep Windows path identities consistent

* fix: classify test contracts by normalized path

* fix: scope test contracts by canonical package path

* fix: keep test package graphs conservative

* test: keep React complexity advisory
2026-08-13 16:26:08 -07:00

143 lines
4.9 KiB
TypeScript

import * as fs from "node:fs";
import * as path from "node:path";
import { PERCENT_MULTIPLIER } from "./constants.ts";
import { isRecordWithFields } from "./is-record-with-fields.ts";
import type { V8ProfileCallFrame } from "./types.ts";
export interface CollectProfilePathsInput {
directory: string;
extension: string;
}
export const collectProfilePaths = (input: CollectProfilePathsInput): string[] => {
const profilePaths: string[] = [];
const pendingDirectories = [input.directory];
while (pendingDirectories.length > 0) {
const currentDirectory = pendingDirectories.pop();
if (currentDirectory === undefined) continue;
for (const entry of fs.readdirSync(currentDirectory, { withFileTypes: true })) {
const entryPath = path.join(currentDirectory, entry.name);
if (entry.isDirectory()) pendingDirectories.push(entryPath);
else if (entry.isFile() && entry.name.endsWith(input.extension)) {
profilePaths.push(entryPath);
}
}
}
return profilePaths.toSorted();
};
export interface MutableFrameValue {
callFrame: V8ProfileCallFrame;
self: number;
total: number;
}
export interface RankedFrame {
functionName: string;
url: string;
lineNumber: number;
self: number;
total: number;
selfPercent: number;
totalPercent: number;
}
export interface ProfileFrameChainInput {
readonly startNode: { id: number; callFrame: V8ProfileCallFrame };
readonly amount: number;
readonly nodesById: ReadonlyMap<number, { id: number; callFrame: V8ProfileCallFrame }>;
readonly parentById: ReadonlyMap<number, number>;
readonly frameKeysByNodeId: ReadonlyMap<number, string>;
readonly frames: Map<string, MutableFrameValue>;
readonly profilePath: string;
}
export const isCallFrame = (value: unknown): value is V8ProfileCallFrame =>
isRecordWithFields(value, {
functionName: "string",
url: "string",
lineNumber: "number",
columnNumber: "number",
});
export const profileFrameKey = (callFrame: V8ProfileCallFrame): string =>
[
callFrame.functionName || "(anonymous)",
callFrame.url,
String(callFrame.lineNumber),
String(callFrame.columnNumber),
].join("::");
export const resolveProfileProcessRole = (
callFrames: ReadonlyArray<V8ProfileCallFrame>,
): string => {
const urls = callFrames.map((callFrame) => callFrame.url).join("\n");
if (urls.includes("packages/react-doctor/dist/cli.js")) return "react-doctor";
if (urls.includes("oxlint") || urls.includes("oxlint-plugin-react-doctor")) return "oxlint";
return "node";
};
export const getFrameValue = (
frames: Map<string, MutableFrameValue>,
key: string,
callFrame: V8ProfileCallFrame,
): MutableFrameValue => {
const existingFrame = frames.get(key);
if (existingFrame !== undefined) return existingFrame;
const createdFrame: MutableFrameValue = { callFrame, self: 0, total: 0 };
frames.set(key, createdFrame);
return createdFrame;
};
export const addFrameChainTotals = (input: ProfileFrameChainInput): void => {
const visitedFrameKeys = new Set<string>();
const visitedNodeIds = new Set<number>();
let currentNode: { id: number; callFrame: V8ProfileCallFrame } | undefined = input.startNode;
while (currentNode !== undefined) {
if (visitedNodeIds.has(currentNode.id)) {
throw new Error(`Invalid profile with cyclic nodes: ${input.profilePath}`);
}
visitedNodeIds.add(currentNode.id);
const currentFrameKey = input.frameKeysByNodeId.get(currentNode.id);
if (currentFrameKey !== undefined && !visitedFrameKeys.has(currentFrameKey)) {
getFrameValue(input.frames, currentFrameKey, currentNode.callFrame).total += input.amount;
visitedFrameKeys.add(currentFrameKey);
}
const parentId = input.parentById.get(currentNode.id);
currentNode = parentId === undefined ? undefined : input.nodesById.get(parentId);
}
};
export const aggregateFrameValues = (
frameMaps: ReadonlyArray<Map<string, MutableFrameValue>>,
): Map<string, MutableFrameValue> => {
const aggregateFrames = new Map<string, MutableFrameValue>();
for (const frames of frameMaps) {
for (const [key, frameValue] of frames) {
const aggregateFrame = getFrameValue(aggregateFrames, key, frameValue.callFrame);
aggregateFrame.self += frameValue.self;
aggregateFrame.total += frameValue.total;
}
}
return aggregateFrames;
};
export const toRankedFrames = (
frames: Map<string, MutableFrameValue>,
sampledTotal: number,
): RankedFrame[] =>
[...frames.values()]
.map((frame) => ({
functionName: frame.callFrame.functionName || "(anonymous)",
url: frame.callFrame.url,
lineNumber: frame.callFrame.lineNumber + 1,
self: frame.self,
total: frame.total,
selfPercent: sampledTotal === 0 ? 0 : (frame.self / sampledTotal) * PERCENT_MULTIPLIER,
totalPercent: sampledTotal === 0 ? 0 : (frame.total / sampledTotal) * PERCENT_MULTIPLIER,
}))
.toSorted(
(leftFrame, rightFrame) =>
rightFrame.self - leftFrame.self || rightFrame.total - leftFrame.total,
);