diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 7bd4051a8..83c787d5f 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -511,6 +511,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `DYNAMIC_CODE_CALL` | javascript | `eval(…)` / `new Function(…)` — unknowable by construction. | | `DYNAMIC_IMPORT_CALL` | javascript | `import(…)` — a module load that is also a site. | | `JSX_ELEMENT` | javascript | `` — the renderer runs the component (a function component, or a class component's constructor and `render`) with the element's attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (`
`) is no site. | +| `JSX_ATTRIBUTE` | javascript | `
`, `` — a function handed over as a JSX attribute, on any tag; always tier callback_registered. Engine-authored: the site is the attribute. | | `SIMPLE_CALL` | python | `f(…)` — a bare name. | | `METHOD_CALL` | python | `obj.m(…)`. | | `CHAINED_CALL` | python | `a.b().c(…)` — the receiver is itself a call. | diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index 8467c9467..8ac1966cf 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -645,6 +645,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'DYNAMIC_CODE_CALL', languages: S, meaning: '`eval(…)` / `new Function(…)` — unknowable by construction.' }, { table: 'call_edges', column: 'kind', value: 'DYNAMIC_IMPORT_CALL', languages: S, meaning: '`import(…)` — a module load that is also a site.' }, { table: 'call_edges', column: 'kind', value: 'JSX_ELEMENT', languages: S, meaning: '`` — the renderer runs the component (a function component, or a class component\'s constructor and `render`) with the element\'s attributes as its props. Engine-authored: no written call; the site is the JSX element expression. An intrinsic tag (`
`) is no site.' }, + { table: 'call_edges', column: 'kind', value: 'JSX_ATTRIBUTE', languages: S, meaning: '``, `` — a function handed over as a JSX attribute, on any tag; always tier callback_registered. Engine-authored: the site is the attribute.' }, // — Python (the parser's callKind, plus engine-authored decorator/metaclass forms) { table: 'call_edges', column: 'kind', value: 'SIMPLE_CALL', languages: P, meaning: '`f(…)` — a bare name.' }, { table: 'call_edges', column: 'kind', value: 'METHOD_CALL', languages: P, meaning: '`obj.m(…)`.' }, diff --git a/graph/javascript/engine/call-edge-generation/jsx.dl b/graph/javascript/engine/call-edge-generation/jsx.dl index 4e1180cda..f3c31375a 100644 --- a/graph/javascript/engine/call-edge-generation/jsx.dl +++ b/graph/javascript/engine/call-edge-generation/jsx.dl @@ -144,3 +144,20 @@ call_chain_edge(je, caller, "-", m, "lib", "boundary_lib", "JSX_ELEMENT") :- jsx // A tag that names something the value layer could not follow: a declared unknown. call_chain_edge(je, caller, "-", "-", "-", "ambiguous_unknown", "JSX_ELEMENT") :- jsx_target_count(je, 0), jsx_caller(je, caller). + +// ── a function handed over as an attribute ────────────────────────────────── +// ``, ` + + label()}>hover + + + ); +} diff --git a/graph/test/javascript/cases/70-jsx-attribute-handler/src/UserForm.jsx b/graph/test/javascript/cases/70-jsx-attribute-handler/src/UserForm.jsx new file mode 100644 index 000000000..e0ba98471 --- /dev/null +++ b/graph/test/javascript/cases/70-jsx-attribute-handler/src/UserForm.jsx @@ -0,0 +1,3 @@ +export default function UserForm({ action }) { + return
; +} diff --git a/graph/test/javascript/cases/70-jsx-attribute-handler/src/actions.js b/graph/test/javascript/cases/70-jsx-attribute-handler/src/actions.js new file mode 100644 index 000000000..22631c0b9 --- /dev/null +++ b/graph/test/javascript/cases/70-jsx-attribute-handler/src/actions.js @@ -0,0 +1 @@ +export async function createUser(data) { return data; } diff --git a/graph/test/javascript/cases/70-jsx-attribute-handler/src/main.jsx b/graph/test/javascript/cases/70-jsx-attribute-handler/src/main.jsx new file mode 100644 index 000000000..51bd35b35 --- /dev/null +++ b/graph/test/javascript/cases/70-jsx-attribute-handler/src/main.jsx @@ -0,0 +1,2 @@ +import LoginForm from './LoginForm'; +export const root = ; diff --git a/graph/test/javascript/expected/62-jsx-component-render.edges b/graph/test/javascript/expected/62-jsx-component-render.edges index 05290ffbf..5ec30d2d7 100644 --- a/graph/test/javascript/expected/62-jsx-component-render.edges +++ b/graph/test/javascript/expected/62-jsx-component-render.edges @@ -5,12 +5,15 @@ App.jsx:20:27 FUNCTION_CALL format -> known_edge App.jsx:26:1 format App.jsx:20:7 JSX_ELEMENT {(item) => format(item)} -> known_edge components/ui.jsx:3:1 List App.jsx:25:30 FUNCTION_CALL div -> known_edge App.jsx:7:1 div App.jsx:26:32 FUNCTION_CALL String -> ambient_terminal - +components/Panel.jsx:5:21 JSX_ATTRIBUTE onClick={() => this.props.onClose()} -> callback_registered components/Panel.jsx:5:30 components/Panel.jsx:5:36 METHOD_CALL this.props.onClose -> known_edge App.jsx:25:1 closeAll +components/TodoItem.jsx:3:14 JSX_ATTRIBUTE onClick={() => onToggle(todo)} -> callback_registered components/TodoItem.jsx:3:23 components/TodoItem.jsx:3:29 FUNCTION_CALL onToggle -> known_edge components/TodoList.jsx:4:3 handleToggle components/TodoList.jsx:4:40 FUNCTION_CALL save -> known_edge components/TodoList.jsx:9:1 save components/TodoList.jsx:6:15 METHOD_CALL todos.map -> ambient_terminal - components/TodoList.jsx:6:15 METHOD_CALL todos.map -> callback_registered components/TodoList.jsx:6:25 components/TodoList.jsx:6:32 JSX_ELEMENT -> known_edge components/TodoItem.jsx:2:1 TodoItem +components/TodoList.jsx:6:64 JSX_ATTRIBUTE onToggle={handleToggle} -> callback_registered components/TodoList.jsx:4:3 handleToggle components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> ambient_terminal - components/ui.jsx:3:43 METHOD_CALL [1, 2].map -> callback_registered components/ui.jsx:3:54 components/ui.jsx:3:61 METHOD_CALL props.children -> known_edge App.jsx:20:17 diff --git a/graph/test/javascript/expected/70-jsx-attribute-handler.diag b/graph/test/javascript/expected/70-jsx-attribute-handler.diag new file mode 100644 index 000000000..a21ec6f5b --- /dev/null +++ b/graph/test/javascript/expected/70-jsx-attribute-handler.diag @@ -0,0 +1,2 @@ +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved LoginForm.jsx:8:27 METHOD_CALL preventDefault receiver_untyped diff --git a/graph/test/javascript/expected/70-jsx-attribute-handler.edges b/graph/test/javascript/expected/70-jsx-attribute-handler.edges new file mode 100644 index 000000000..1b0d890b6 --- /dev/null +++ b/graph/test/javascript/expected/70-jsx-attribute-handler.edges @@ -0,0 +1,10 @@ +LoginForm.jsx:11:11 JSX_ATTRIBUTE onSubmit={submit} -> callback_registered LoginForm.jsx:8:18 +LoginForm.jsx:12:29 JSX_ATTRIBUTE onClick={logout} -> callback_registered LoginForm.jsx:4:1 logout +LoginForm.jsx:13:53 FUNCTION_CALL label -> known_edge LoginForm.jsx:5:1 label +LoginForm.jsx:14:13 JSX_ATTRIBUTE onMouseEnter={() => label()} -> callback_registered LoginForm.jsx:14:27 +LoginForm.jsx:14:33 FUNCTION_CALL label -> known_edge LoginForm.jsx:5:1 label +LoginForm.jsx:15:17 JSX_ATTRIBUTE action={createUser} -> callback_registered actions.js:1:1 createUser +LoginForm.jsx:15:7 JSX_ELEMENT -> known_edge UserForm.jsx:1:1 UserForm +LoginForm.jsx:8:27 METHOD_CALL e.preventDefault -> ambiguous_unknown - +UserForm.jsx:2:16 JSX_ATTRIBUTE action={action} -> callback_registered actions.js:1:1 createUser +main.jsx:2:21 JSX_ELEMENT -> known_edge LoginForm.jsx:7:1 LoginForm diff --git a/graph/test/javascript/expected/70-jsx-attribute-handler.oracle b/graph/test/javascript/expected/70-jsx-attribute-handler.oracle new file mode 100644 index 000000000..b9e235e77 --- /dev/null +++ b/graph/test/javascript/expected/70-jsx-attribute-handler.oracle @@ -0,0 +1,3 @@ +LoginForm.jsx:13:53 FUNCTION_CALL label EXACT LoginForm.jsx:5:1 +LoginForm.jsx:14:33 FUNCTION_CALL label EXACT LoginForm.jsx:5:1 +# defects: 0 diff --git a/graph/test/javascript/tools/normalize_edges.py b/graph/test/javascript/tools/normalize_edges.py index 8c515c322..47679a645 100644 --- a/graph/test/javascript/tools/normalize_edges.py +++ b/graph/test/javascript/tools/normalize_edges.py @@ -51,8 +51,10 @@ def methods(d, tag): if h: i = {c: h.index(c) for c in ('jsExpressionUniqueHash', 'ownerModuleLinkHash', 'startLine', 'startColumn', 'expressionKind', 'text')} for r in exprs: - # A JSX element edge is keyed on the JSX_ELEMENT expression, likewise no call site. - if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS', 'JSX_ELEMENT'): + # A JSX element edge is keyed on the JSX_ELEMENT expression, likewise no call site; + # a handler handed over as an attribute, on the JSX_ATTRIBUTE_VALUE expression. + if r[i['expressionKind']] in ('PROPERTY_ACCESS', 'OPTIONAL_ACCESS', 'ELEMENT_ACCESS', 'JSX_ELEMENT', + 'JSX_ATTRIBUTE_VALUE'): access[r[i['jsExpressionUniqueHash']]] = '%s:%s:%s %%s %s' % ( modfile.get(r[i['ownerModuleLinkHash']], '?'), r[i['startLine']], r[i['startColumn']], r[i['text']].replace('\n', ' ')[:60])