The Wayback Machine - https://web.archive.org/web/20260302172822/https://github.com/github/codeql/pull/5137
Skip to content

JS: Model Redux and related libraries#5137

Merged
codeql-ci merged 20 commits intogithub:mainfrom
asgerf:js/redux-less
Apr 9, 2021
Merged

JS: Model Redux and related libraries#5137
codeql-ci merged 20 commits intogithub:mainfrom
asgerf:js/redux-less

Conversation

@asgerf
Copy link
Contributor

@asgerf asgerf commented Feb 10, 2021

Adds a model of the Redux library and related libraries in the Redux ecosystem, including React-Redux.

Quick overview of Redux

A Redux store consists of an initial state, a list of actions and a reducer function. The current state can always be computed by doing actions.reduce(reducer, initialState). The only valid way to alter the current state is to append more actions to the action list. Appending such an action is called dispatching the action. The idea is that the state of the UI as a whole is derived this current state object provided by Redux.

In the Redux model we are mainly concerned with two data flow steps:

  • Flow from a dispatched action into the reducer function,
  • Flow from the reducer function's return value to an access of the current state.

Some of the complications here are:

  • There are many ways to create and dispatch an action.
  • There are many ways to construct and compose reducer functions.
  • There are many ways to access the current state.

The bulk of the model is thus to model all the ways you can do the above, in particular, handling the two primary higher-order reducers we care about: the state-delegator, and the action-delegator.

The state-delegator, commonly called combineReducers, delegates a part of the state object to another function:

let reducer = combineReducers({ foo: fooReducer, bar: barReducer })

// equivalent to:
let reducer = (state, action) => {
  return {
    foo: fooReducer(state.foo, action),
    bar: barReducer(state.bar, action),
  }
};

The action-delegator delegates to another function based on the action.type property:

let reducer = handleAction('foo', fooReducer);

// equivalent to:
let reducer = (state, action) => {
  if (action.type === 'foo') {
    return fooReducer(state, action);
  } else {
    return state;
  }
};

Much of the abstraction comes down to handling those two cases.

Program slicing

Redux doesn't have any global state, but I decided to model it globally to avoid having to model every step of the wiring between the Redux store and an action dispatch or state access. In react-redux that would require reasoning about the nesting of JSX components all the way up to the root component, and the chance of missing a step would be too high.

To avoid false flow in monorepos containing multiple web apps, we use the enclosing package.json files to indicate which app a given file belongs to, and thereby which Redux store it can access.

React-redux

React-redux is responsible for the connect(mapStateToProps, mapDispatchToProps) calls that tend to wrap every component in a react-redux app. mapStateToProps provides a way to access the current state, and mapDispatchToProps provides a way to dispatch actions, and in principle the model just needs to contribute these state accesses and actions dispatch calls to the abstractions set up by the main model.

However, there's one place where the core Redux model has a direct dependency on the react-redux model (ActionCreator.ref), which was to avoid a circular dependency with type tracking.

Evaluations

Evaluations: (internal links)

@asgerf asgerf added the JS label Feb 10, 2021
@asgerf asgerf requested a review from a team as a code owner February 10, 2021 12:34
Copy link
Contributor

@erik-krogh erik-krogh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good, but takes a while to chew through.

The order of the classes/predicates is sometimes a bit chaotic.
It's mostly RootStateSource/rootStateAccessPath, where there is a huge distance (~700 lines) between their definitions and the first use.

I have some minor comments.

* wrapWithLogging(g); // step: g -> wrapWithLogging(g)
* ```
*/
predicate functionForwardingStep(DataFlow::Node pred, DataFlow::Node succ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be a PreCallgraphStep, instead of only being used by redux?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe as a type tracking step it would make sense, e.g. for tracking class constructors through some wrapper function. For an AdditionalFlowStep it seems a little dubious to me.

I'll run an experiment to see if anything interesting happens.

@asgerf
Copy link
Contributor Author

asgerf commented Feb 15, 2021

Thanks @erik-krogh!

The order of the classes/predicates is sometimes a bit chaotic.

Yes, sorry about that. I've shuffled things around a lot, but couldn't find a solution that didn't have this problem.

@erik-krogh
Copy link
Contributor

LGTM.
But I think a change-note is needed.

erik-krogh
erik-krogh previously approved these changes Feb 18, 2021
@asgerf
Copy link
Contributor Author

asgerf commented Feb 19, 2021

It looks like we hit the BDD node limit after the review changes 🤦

@asgerf asgerf force-pushed the js/redux-less branch 2 times, most recently from 299b8b0 to bccecd4 Compare March 19, 2021 15:56
@asgerf
Copy link
Contributor Author

asgerf commented Apr 8, 2021

Updated evaluations:

erik-krogh
erik-krogh previously approved these changes Apr 8, 2021
@codeql-ci codeql-ci merged commit ad26740 into github:main Apr 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

HTTPS · web.archive.org
← Home