Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion graph/javascript/engine/call-edge-generation/jsx.dl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
// A tag bound to a component WRAPPER (`memo(C)`, `forwardRef(fn)`, `observer(C)`,
// `withRouter(C)`) or a LOADER (`lazy(() => import("./x"))`) renders what the wrapper
// was handed or what the loader settles to — see "wrapped and lazy components" below.
// Not modelled: `connect(…)(C)` and other curried wrappers.
// Redux's curried `connect(…)(C)` is such a wrapper, and it also hands C the props its
// map functions return; React context hands `useContext(Ctx)` the provider's `value`
// — see "props that come from outside the element" below. Not modelled: other curried
// wrappers.
// ============================================================================

// ── the element and what its tag holds ──────────────────────────────────────
Expand Down Expand Up @@ -67,13 +70,24 @@ jsx_component_loader(w) :- expr_kind(_, "CALL", _, w), call_site("client", _, n,
jsx_vue_definer_name("defineComponent").
jsx_vue_definer_name("defineNuxtComponent").
jsx_vue_definer(w) :- expr_kind(_, "CALL", _, w), call_site("client", _, n, _, _, _, w, _, _), jsx_vue_definer_name(n).
// Redux `connect(mapState, mapDispatch)(C)`: the OUTER call is the wrapper, its argument
// 0 the component. Keyed on the callee being a call of `connect`, so a plain
// `connect(db)` or a `connect(…)` whose result is not called again stays out.
jsx_connect_call(w, inner) :- expr_kind(_, "CALL", _, w), expr_child(_, w, "CALLEE", _, inner),
expr_kind(_, "CALL", _, inner), call_site("client", _, "connect", _, _, _, inner, _, _).
jsx_component_wrapper(w) :- jsx_connect_call(w, _).
// React `createContext(…)`: not a wrapper, but it reaches `<Ctx.Provider>` and
// `useContext(Ctx)` the ways a component reaches a tag, so jsx_holds follows it too.
// No jsx_call_renders rule reads it: holding a context renders nothing.
jsx_context_create(c) :- expr_kind(_, "CALL", _, c), call_site("client", _, "createContext", _, _, _, c, _, _).

// jsx_holds(Expr, Call) — the wrapper or loader call an expression holds, followed the
// ways a component reaches a tag: the call itself, a const initialised with it, a named
// or default import of an export that holds it, and a re-export of one.
jsx_holds(w, w) :- jsx_component_wrapper(w).
jsx_holds(w, w) :- jsx_component_loader(w).
jsx_holds(w, w) :- jsx_vue_definer(w).
jsx_holds(c, c) :- jsx_context_create(c).
jsx_holds(e, w) :- expr_binding(_, v, e), jsx_var_holds(v, w).
jsx_var_holds(v, w) :- var_init(_, _, e, v), !var_binding_form(_, "OBJECT_PATTERN", v), !var_binding_form(_, "ARRAY_PATTERN", v),
jsx_holds(e, w).
Expand Down Expand Up @@ -147,6 +161,57 @@ param_value(p, "obj", je) :- jsx_tag_value(je, "ctor", t), type_construct_target
param_decl(_, _, "0", m, p), !param_is_rest(_, p).
prop_value("inst", t, "props", "obj", je) :- jsx_tag_value(je, "ctor", t), type_decl("client", _, _, _, _, t).

// ── props that come from outside the element ────────────────────────────────
// Two library conventions hand a component values no JSX attribute names, so a
// handler reached through them had no use: `onClick={props.onClickLogout}` and
// `const { logout } = useContext(Ctx); … onClick={logout}` registered nothing.
//
// connect(mapState, mapDispatch)(C): C's props also hold what each map function
// returns, and the object form of mapDispatch (`connect(null, { onSave })`) itself.
// Every returned object is merged into one props value, as the library does.
jsx_connect_props(w, k, i) :- jsx_connect_call(w, inner), call_arg(inner, pos, a), pos <= 1,
expr_value(a, "func", f), return_value(f, k, i), k = "obj".
jsx_connect_props(w, "obj", o) :- jsx_connect_call(w, inner), call_arg(inner, 1, a), expr_value(a, "obj", o).
param_value(p, k, i) :- jsx_connect_props(w, k, i), jsx_call_renders(w, kc, ic), kc != "ctor", callable_value(kc, ic, m),
method_decl("client", _, _, _, _, _, _, m), !method_is_hot(m, _),
param_decl(_, _, "0", m, p), !param_is_rest(_, p).
// A class component reads them as `this.props` in its OWN methods. Not a prop_value of
// the instance: that is inherited (value-flow.dl), and a subclass connected on its own
// (`class Favorites extends Profile`, each with its connect) would read the parent's
// handlers too.
jsx_connect_class_props(t, k, i) :- jsx_connect_props(w, k, i), jsx_call_renders(w, "ctor", t), type_decl("client", _, _, _, _, t).
expr_value(e, k, i) :- expr_kind(_, "PROPERTY_ACCESS", _, e), expr_name(_, "props", e),
expr_child(_, e, "ACCESS_TARGET", _, r), expr_kind(_, "THIS", _, r), expr_owner(_, m, _, r),
this_value(m, "inst", t), jsx_connect_class_props(t, k, i).

// React context: `useContext(Ctx)` (and React 19's `use(Ctx)`) evaluates to the `value`
// of every `<Ctx.Provider value={…}>` (React 19: `<Ctx value={…}>`) of the same
// createContext call, and to that call's default argument. "Same" is the call the
// argument and the tag hold, followed through consts, imports and re-exports — never
// the name — so two contexts in one app keep their values apart.
jsx_context_tag(je, c) :- jsx_element_tag(je, tag), expr_kind(_, "PROPERTY_ACCESS", _, tag), expr_name(_, "Provider", tag),
expr_child(_, tag, "ACCESS_TARGET", _, r), jsx_holds(r, c), jsx_context_create(c).
jsx_context_tag(je, c) :- jsx_element_tag(je, tag), jsx_holds(tag, c), jsx_context_create(c).
jsx_context_value(c, k, i) :- jsx_context_tag(je, c), prop_value("obj", je, "value", k, i).
jsx_context_value(c, k, i) :- jsx_context_create(c), call_arg(c, 0, a), expr_value(a, k, i).
// The provider's value is usually memoised — `useMemo(() => ({ user, logout }), [user])`
// with its handlers in `useCallback(fn, deps)` — and React's library is not staged, so
// both calls had no value. useMemo evaluates to what its argument 0 returns,
// useCallback to argument 0 itself.
expr_value(u, k, i) :- expr_kind(_, "CALL", _, u), call_site("client", _, "useMemo", _, _, _, u, _, _),
call_arg(u, 0, a), expr_value(a, "func", f), return_value(f, k, i).
expr_value(u, "func", f) :- expr_kind(_, "CALL", _, u), call_site("client", _, "useCallback", _, _, _, u, _, _),
call_arg(u, 0, a), expr_value(a, "func", f).
jsx_context_reader_name("useContext").
jsx_context_reader_name("use").
expr_value(u, k, i) :- expr_kind(_, "CALL", _, u), call_site("client", _, n, _, _, _, u, _, _), jsx_context_reader_name(n),
call_arg(u, 0, a), jsx_holds(a, c), jsx_context_create(c), jsx_context_value(c, k, i).
// `<Ctx.Consumer>{(value) => …}</Ctx.Consumer>` — the render function receives it.
param_value(p, k, i) :- jsx_element_tag(je, tag), expr_kind(_, "PROPERTY_ACCESS", _, tag), expr_name(_, "Consumer", tag),
expr_child(_, tag, "ACCESS_TARGET", _, r), jsx_holds(r, c), jsx_context_create(c),
prop_value("obj", je, "children", "func", m), method_decl("client", _, _, _, _, _, _, m),
param_decl(_, _, "0", m, p), !param_is_rest(_, p), jsx_context_value(c, k, i).

// ── the rows ────────────────────────────────────────────────────────────────
// The caller is the callable whose body holds the markup; top-level markup
// (`root.render(<App/>)`) belongs to the module initializer, as a top-level call does.
Expand Down
7 changes: 7 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@
.decl jsx_call_renders(c0:symbol, c1:symbol, c2:symbol)
.decl jsx_loader_settles(c0:symbol, c1:symbol, c2:symbol)
.decl jsx_component_kind(c0:symbol)
.decl jsx_connect_call(c0:symbol, c1:symbol)
.decl jsx_connect_props(c0:symbol, c1:symbol, c2:symbol)
.decl jsx_connect_class_props(c0:symbol, c1:symbol, c2:symbol)
.decl jsx_context_create(c0:symbol)
.decl jsx_context_tag(c0:symbol, c1:symbol)
.decl jsx_context_value(c0:symbol, c1:symbol, c2:symbol)
.decl jsx_context_reader_name(c0:symbol)

// ── call-edge-generation/calls.dl ──
.decl method_prov(c0:symbol, c1:symbol)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createContext, useCallback, useContext, useMemo } from 'react';

export const AuthCtx = createContext(null);
export const ThemeCtx = createContext({ toggle: () => 'default-toggle' });

export function AuthProvider({ children }) {
const logout = () => 'out';
return <AuthCtx.Provider value={{ logout }}>{children}</AuthCtx.Provider>;
}

export function ThemeProvider({ children }) {
const toggle = useCallback(() => 'dark', []);
return <ThemeCtx value={useMemo(() => ({ toggle }), [toggle])}>{children}</ThemeCtx>;
}

// context in the same file, destructured
export function LogoutButton() {
const { logout } = useContext(AuthCtx);
return <button onClick={logout}>out</button>;
}

// a hook that returns the context value
export const useAuth = () => useContext(AuthCtx);

// the render-prop consumer
export function LogoutLink() {
return <AuthCtx.Consumer>{(auth) => <a onClick={auth.logout}>out</a>}</AuthCtx.Consumer>;
}

// CONTROL: another context — its `logout` is not AuthCtx's, only toggle (and the default) are here
export function ThemeButton() {
const { toggle, logout } = useContext(ThemeCtx);
return <button onClick={toggle} onBlur={logout}>theme</button>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useAuth } from './Auth';

// through an imported hook, member read
export default function Menu() {
const auth = useAuth();
return <li onClick={auth.logout}>out</li>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { connect } from './db';

// CONTROL: not wrapped by connect(...)(C) — its props come from nobody the graph sees
export function Plain(props) {
return <button onClick={props.onClickLogout}>out</button>;
}

// CONTROL: a one-shot connect(...) that is not curried is not a component wrapper
export const conn = connect({ onClickLogout: () => 'db' });
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { connect } from 'react-redux';

function saveProfile() { return { type: 'SAVE' }; }

// a class component reads this.props; mapDispatch in its object form
class Profile extends React.Component {
render() {
const { onSave } = this.props;
return <form onSubmit={onSave}><button onClick={this.props.onSave}>save</button></form>;
}
}

export default connect(null, { onSave: saveProfile })(Profile);

function openFavorites() { return { type: 'OPEN' }; }

// CONTROL: a subclass connected on its own reads its own props, not Profile's onSave
class Favorites extends Profile {
footer() {
return <p onClick={this.props.onOpen} onBlur={this.props.onSave}>more</p>;
}
}

export const ConnectedFavorites = connect(null, { onOpen: openFavorites })(Favorites);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { connect } from 'react-redux';

function Settings(props) {
return <button onClick={props.onClickLogout}>out</button>;
}

const mapStateToProps = (state) => ({ user: state.user });
const mapDispatchToProps = (dispatch) => ({
onClickLogout: () => dispatch({ type: 'LOGOUT' }),
});

export default connect(mapStateToProps, mapDispatchToProps)(Settings);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function connect(opts) { return opts; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AuthProvider, ThemeProvider, LogoutButton, LogoutLink, ThemeButton } from './Auth';
import Menu from './Menu';
import ConnectedSettings from './Settings';
import ConnectedProfile from './Profile';
import { Plain } from './Plain';

export function App() {
return (
<AuthProvider>
<ThemeProvider>
<LogoutButton /><LogoutLink /><ThemeButton /><Menu />
<ConnectedSettings /><ConnectedProfile /><Plain />
</ThemeProvider>
</AuthProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import_cause Auth.jsx:1:10 react not_staged
import_cause Auth.jsx:1:25 react not_staged
import_cause Auth.jsx:1:38 react not_staged
import_cause Auth.jsx:1:50 react not_staged
import_cause Profile.jsx:1:1 react not_staged
import_cause Profile.jsx:2:10 react-redux not_staged
import_cause Settings.jsx:1:10 react-redux not_staged
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
unresolved Auth.jsx:12:18 FUNCTION_CALL useCallback callee_untyped
unresolved Auth.jsx:13:27 FUNCTION_CALL useMemo callee_untyped
unresolved Auth.jsx:18:22 FUNCTION_CALL useContext callee_untyped
unresolved Auth.jsx:23:30 FUNCTION_CALL useContext callee_untyped
unresolved Auth.jsx:32:30 FUNCTION_CALL useContext callee_untyped
unresolved Auth.jsx:3:24 FUNCTION_CALL createContext callee_untyped
unresolved Auth.jsx:4:25 FUNCTION_CALL createContext callee_untyped
unresolved Profile.jsx:14:16 FUNCTION_CALL callee_untyped
unresolved Profile.jsx:14:16 FUNCTION_CALL connect callee_untyped
unresolved Profile.jsx:25:35 FUNCTION_CALL callee_untyped
unresolved Profile.jsx:25:35 FUNCTION_CALL connect callee_untyped
unresolved Settings.jsx:12:16 FUNCTION_CALL callee_untyped
unresolved Settings.jsx:12:16 FUNCTION_CALL connect callee_untyped
unresolved Settings.jsx:9:24 FUNCTION_CALL dispatch callee_untyped
value_callee Profile.jsx:14:16 connect(null, { onSave: saveProfile }) expression
value_callee Profile.jsx:25:35 connect(null, { onOpen: openFavorites }) expression
value_callee Settings.jsx:12:16 connect(mapStateToProps, mapDispatchToProps) expression
value_callee Settings.jsx:9:24 dispatch parameter
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Auth.jsx:12:18 FUNCTION_CALL useCallback -> ambiguous_unknown -
Auth.jsx:12:18 FUNCTION_CALL useCallback -> callback_registered Auth.jsx:12:30 <arrow>
Auth.jsx:13:10 JSX_ELEMENT <ThemeCtx value={useMemo(() => ({ toggle }), [toggle])}>{chi -> ambiguous_unknown -
Auth.jsx:13:27 FUNCTION_CALL useMemo -> ambiguous_unknown -
Auth.jsx:13:27 FUNCTION_CALL useMemo -> callback_registered Auth.jsx:13:35 <arrow>
Auth.jsx:18:22 FUNCTION_CALL useContext -> ambiguous_unknown -
Auth.jsx:19:18 JSX_ATTRIBUTE onClick={logout} -> callback_registered Auth.jsx:7:18 <arrow>
Auth.jsx:23:30 FUNCTION_CALL useContext -> ambiguous_unknown -
Auth.jsx:27:10 JSX_ELEMENT <AuthCtx.Consumer>{(auth) => <a onClick={auth.logout}>out</a -> ambiguous_unknown -
Auth.jsx:27:42 JSX_ATTRIBUTE onClick={auth.logout} -> callback_registered Auth.jsx:7:18 <arrow>
Auth.jsx:32:30 FUNCTION_CALL useContext -> ambiguous_unknown -
Auth.jsx:33:18 JSX_ATTRIBUTE onClick={toggle} -> callback_registered Auth.jsx:12:30 <arrow>
Auth.jsx:33:18 JSX_ATTRIBUTE onClick={toggle} -> callback_registered Auth.jsx:4:49 <arrow>
Auth.jsx:3:24 FUNCTION_CALL createContext -> ambiguous_unknown -
Auth.jsx:4:25 FUNCTION_CALL createContext -> ambiguous_unknown -
Auth.jsx:4:25 FUNCTION_CALL createContext -> callback_registered Auth.jsx:4:49 <arrow>
Auth.jsx:8:10 JSX_ELEMENT <AuthCtx.Provider value={{ logout }}>{children}</AuthCtx.Pro -> ambiguous_unknown -
Menu.jsx:5:16 FUNCTION_CALL useAuth -> known_edge Auth.jsx:23:24 <arrow>
Menu.jsx:6:14 JSX_ATTRIBUTE onClick={auth.logout} -> callback_registered Auth.jsx:7:18 <arrow>
Plain.jsx:9:21 FUNCTION_CALL connect -> known_edge db.js:1:1 connect
Profile.jsx:10:18 JSX_ATTRIBUTE onSubmit={onSave} -> callback_registered Profile.jsx:4:1 saveProfile
Profile.jsx:10:44 JSX_ATTRIBUTE onClick={this.props.onSave} -> callback_registered Profile.jsx:4:1 saveProfile
Profile.jsx:14:16 FUNCTION_CALL connect -> ambiguous_unknown -
Profile.jsx:14:16 FUNCTION_CALL connect -> callback_registered Profile.jsx:4:1 saveProfile
Profile.jsx:14:16 FUNCTION_CALL connect(null, { onSave: saveProfile }) -> ambiguous_unknown -
Profile.jsx:21:15 JSX_ATTRIBUTE onClick={this.props.onOpen} -> callback_registered Profile.jsx:16:1 openFavorites
Profile.jsx:25:35 FUNCTION_CALL connect -> ambiguous_unknown -
Profile.jsx:25:35 FUNCTION_CALL connect -> callback_registered Profile.jsx:16:1 openFavorites
Profile.jsx:25:35 FUNCTION_CALL connect(null, { onOpen: openFavorites }) -> ambiguous_unknown -
Settings.jsx:12:16 FUNCTION_CALL connect -> ambiguous_unknown -
Settings.jsx:12:16 FUNCTION_CALL connect -> callback_registered Settings.jsx:7:25 <arrow>
Settings.jsx:12:16 FUNCTION_CALL connect -> callback_registered Settings.jsx:8:28 <arrow>
Settings.jsx:12:16 FUNCTION_CALL connect(mapStateToProps, mapDispatchToProps) -> ambiguous_unknown -
Settings.jsx:12:16 FUNCTION_CALL connect(mapStateToProps, mapDispatchToProps) -> callback_registered Settings.jsx:3:1 Settings
Settings.jsx:4:18 JSX_ATTRIBUTE onClick={props.onClickLogout} -> callback_registered Settings.jsx:9:18 <arrow>
Settings.jsx:9:24 FUNCTION_CALL dispatch -> ambiguous_unknown -
main.jsx:10:7 JSX_ELEMENT <ThemeProvider>\n <LogoutButton /><LogoutLink /><Them -> known_edge Auth.jsx:11:1 ThemeProvider
main.jsx:11:25 JSX_ELEMENT <LogoutLink /> -> known_edge Auth.jsx:26:1 LogoutLink
main.jsx:11:39 JSX_ELEMENT <ThemeButton /> -> known_edge Auth.jsx:31:1 ThemeButton
main.jsx:11:54 JSX_ELEMENT <Menu /> -> known_edge Menu.jsx:4:1 Menu
main.jsx:11:9 JSX_ELEMENT <LogoutButton /> -> known_edge Auth.jsx:17:1 LogoutButton
main.jsx:12:30 JSX_ELEMENT <ConnectedProfile /> -> known_edge Profile.jsx:8:3 render
main.jsx:12:50 JSX_ELEMENT <Plain /> -> known_edge Plain.jsx:4:1 Plain
main.jsx:12:9 JSX_ELEMENT <ConnectedSettings /> -> known_edge Settings.jsx:3:1 Settings
main.jsx:9:5 JSX_ELEMENT <AuthProvider>\n <ThemeProvider>\n <LogoutButton -> known_edge Auth.jsx:6:1 AuthProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Menu.jsx:5:16 FUNCTION_CALL useAuth EXACT Auth.jsx:23:24
Plain.jsx:9:21 FUNCTION_CALL connect EXACT db.js:1:1
# defects: 0
Loading