// packages/ui-gen/src/harness/check/runtime-render/find-wiring.ts
//
// Source-AST wiring detection.
//
// Given a component's TSX source and a hook reference (e.g., `useAction('save')`),
// determine HOW the resulting callback is wired in JSX so the runtime probe knows
// which trigger to simulate — instead of guessing by clicking everything.
//
// Rules:
// - Handle ONE level of alias indirection:
// const save = useAction('save');
// const onSave = () => save(payload); // alias
// // alias used in JSX → click
// - Be CONSERVATIVE on non-native props. Native event names are deterministic;
// custom-component props (onValueChange, onSelect, onOpenChange) are NOT
// guaranteed to fire on a synthetic click/change → return `unverified`.
// - Anything we can't classify deterministically returns `unverified` with a
// reason. NEVER pretend.
//
// Returns ONE detection object summarizing the strongest wiring found, plus
// optional fallback hints. Callers use the kind to pick a simulator.
import ts from "typescript";
// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────
export type WiringKind =
| "click" // wired as native onClick prop on a host element / role-button
| "submit" // wired via onSubmit on a form, or button[type=submit]
| "change" // wired via onChange on native select/input
| "keyboard-enter" // wired via onKeyDown with Enter check
| "unverified" // wiring exists but trigger style is non-deterministic
| "missing"; // hook destructured but never referenced in JSX
export interface WiringDetection {
readonly kind: WiringKind;
/**
* For `unverified`: human-readable reason (e.g., "wired via Dropdown.onChange",
* "wired into custom-component prop onValueChange", "wired into onDrop handler").
* For `missing`: brief explanation.
* For verified kinds: optional element-locator hint (tag name, label).
*/
readonly reason?: string;
/**
* Pure observational: the JSX element types the callback flows into.
* Useful for richer feedback in the EvalIssue.
*/
readonly observedJsxElements: readonly string[];
/**
* Native event prop names where we observed the callback (e.g., onClick, onSubmit).
* Empty if only seen on non-native props.
*/
readonly observedNativeProps: readonly string[];
/**
* Non-native event prop names where we observed it (e.g., onValueChange, onSelect).
* Drives the unverified reason.
*/
readonly observedCustomProps: readonly string[];
}
export interface FindWiringInput {
readonly sourceCode: string;
/** "useAction" or "useStream" */
readonly hookName: string;
/** The hook's first-arg literal — the action/tool/event name. */
readonly hookArg: string;
}
// Native DOM event props we can simulate deterministically.
const NATIVE_CLICK_PROPS = new Set(["onClick"]);
const NATIVE_SUBMIT_PROPS = new Set(["onSubmit"]);
const NATIVE_CHANGE_PROPS = new Set(["onChange"]);
const NATIVE_KEY_PROPS = new Set(["onKeyDown", "onKeyUp", "onKeyPress"]);
// JSX element tags (lowercase) that count as host elements with native event behavior.
const HOST_CLICK_TAGS = new Set(["button", "a", "div", "span", "li", "input"]);
const HOST_CHANGE_TAGS = new Set(["select", "input", "textarea"]);
// ─────────────────────────────────────────────────────────────────────────────
// Public entry
// ─────────────────────────────────────────────────────────────────────────────
export function findWiring(input: FindWiringInput): WiringDetection {
const { sourceCode, hookName, hookArg } = input;
const sf = ts.createSourceFile("Component.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
// Step 1: find the destructured variable name from the hook call.
// const save = useAction('save')
// Returns the binding name(s) — multiple if the hook returns an object
// destructure.
const hookBindings = findHookBindings(sf, hookName, hookArg);
if (hookBindings.length === 0) {
return {
kind: "missing",
reason: `Hook ${hookName}('${hookArg}') is not destructured in the component`,
observedJsxElements: [],
observedNativeProps: [],
observedCustomProps: [],
};
}
// Step 2: collect every identifier that should "count" as a reference to the
// hook's callable. This includes:
// - The hook bindings themselves (e.g., `save`)
// - One-level aliases: `const onSave = () => save(...); const onClick = save;`
// → `onSave` and `onClick` count as references to `save`
const callableNames = expandAliases(sf, hookBindings);
// Step 3: scan JSX attributes. For each attribute whose value contains a
// reference to any callableName, classify by attribute name + element kind.
const observedJsxElements: string[] = [];
const observedNativeProps: string[] = [];
const observedCustomProps: string[] = [];
let sawClickOnHost = false;
let sawSubmitOnForm = false;
let sawChangeOnNativeInput = false;
let sawKeyOnAnything = false;
let sawNonNativeProp = false;
const customPropElements: string[] = [];
const submitButton = { found: false };
function visit(node: ts.Node): void {
//
if (ts.isJsxAttribute(node) && node.initializer) {
const attrName = node.name.getText(sf);
const initRefs = findReferencedNames(node.initializer, callableNames);
if (initRefs.size > 0) {
const parent = node.parent.parent; // JsxAttributes → opening element
const tagName = getTagName(parent);
if (tagName) observedJsxElements.push(tagName);
const isHostTag = isHostElementTag(tagName ?? "");
if (NATIVE_CLICK_PROPS.has(attrName)) {
observedNativeProps.push(attrName);
if (isHostTag && (HOST_CLICK_TAGS.has((tagName ?? "").toLowerCase()) || tagName === "button")) {
sawClickOnHost = true;
} else {
// onClick on a custom component (e.g.,