Files
codeql/javascript/ql/lib/semmle/javascript/frameworks/Redux.qll
2023-05-03 15:31:00 +02:00

1246 lines
45 KiB
Plaintext

/**
* Provides classes and predicates for reasoning about data flow through the redux package.
*/
import javascript
private import semmle.javascript.dataflow.internal.PreCallGraphStep
private import semmle.javascript.Unit
/**
* Provides classes and predicates for reasoning about data flow through the redux package.
*/
module Redux {
/**
* To avoid mixing up the state between independent Redux apps that live in a monorepo,
* we do a heuristic program slicing based on `package.json` files. For most projects this has no effect.
*/
private module ProgramSlicing {
/** Gets the innermost `package.json` file in a directory containing the given file. */
private PackageJson getPackageJson(Container f) {
f = result.getFile().getParentContainer()
or
not exists(f.getFile("package.json")) and
result = getPackageJson(f.getParentContainer())
}
private predicate packageDependsOn(PackageJson importer, PackageJson dependency) {
importer.getADependenciesObject("").getADependency(dependency.getPackageName(), _)
}
/** Gets a package that can be considered an entry point for a Redux app. */
private PackageJson entryPointPackage() {
result = getPackageJson(any(StoreCreation c).getFile())
or
// Any package that imports a store-creating package is considered a potential entry point.
packageDependsOn(result, entryPointPackage())
}
pragma[nomagic]
private predicate arePackagesInSameReduxApp(PackageJson a, PackageJson b) {
exists(PackageJson entry |
entry = entryPointPackage() and
packageDependsOn*(entry, a) and
packageDependsOn*(entry, b)
)
}
/** Holds if the two files are considered to be part of the same Redux app. */
pragma[inline]
predicate areFilesInSameReduxApp(File a, File b) {
not exists(PackageJson pkg)
or
arePackagesInSameReduxApp(getPackageJson(a), getPackageJson(b))
}
}
/**
* A creation of a redux store, usually via a call to `createStore`.
*/
class StoreCreation extends DataFlow::SourceNode instanceof StoreCreation::Range {
/** Gets a reference to the store. */
DataFlow::SourceNode ref() { result = this.asApiNode().getAValueReachableFromSource() }
/** Gets an API node that refers to this store creation. */
API::Node asApiNode() { result.asSource() = this }
/** Gets the data flow node holding the root reducer for this store. */
DataFlow::Node getReducerArg() { result = super.getReducerArg() }
/** Gets a data flow node referring to the root reducer. */
DataFlow::SourceNode getAReducerSource() {
result = this.getReducerArg().(ReducerArg).getASource()
}
}
/** Companion module to the `StoreCreation` class. */
module StoreCreation {
/**
* The creation of a redux store. Additional `StoreCreation` instances can be generated by subclassing this class.
*/
abstract class Range extends DataFlow::SourceNode {
/** Gets the data flow node holding the root reducer for this store. */
abstract DataFlow::Node getReducerArg();
}
private class CreateStore extends DataFlow::CallNode, Range {
CreateStore() {
this = API::moduleImport(["redux", "@reduxjs/toolkit"]).getMember("createStore").getACall()
}
override DataFlow::Node getReducerArg() { result = this.getArgument(0) }
}
private class ToolkitStore extends API::CallNode, Range {
ToolkitStore() {
this = API::moduleImport("@reduxjs/toolkit").getMember("configureStore").getACall()
}
override DataFlow::Node getReducerArg() {
result = this.getParameter(0).getMember("reducer").asSink()
}
}
}
/** An API node that is a source of the Redux root state. */
abstract private class RootStateSource extends API::Node { }
/** Gets an API node referring to the Redux root state. */
private API::Node rootState() {
result instanceof RootStateSource
or
stateStep(rootState().getAValueReachableFromSource(), result.asSource())
}
/**
* Gets an API node referring to the given (non-empty) access path within the Redux state.
*/
private API::Node rootStateAccessPath(string accessPath) {
result = rootState().getMember(accessPath)
or
exists(string base, string prop |
result = rootStateAccessPath(base).getMember(prop) and
accessPath = joinAccessPaths(base, prop)
)
or
stateStep(rootStateAccessPath(accessPath).getAValueReachableFromSource(), result.asSource())
}
/**
* Combines two state access paths, while disallowing unbounded growth of access paths.
*/
bindingset[base, prop]
private string joinAccessPaths(string base, string prop) {
result = base + "." + prop and
// Allow at most two occurrences of a given property name in the path
// (one in the base, plus the one we're appending now).
count(base.indexOf("." + prop + ".")) <= 1
}
/**
* The creation of a reducer function that delegates to one or more other reducer functions.
*
* Delegating reducers can delegate specific parts of the state object (`getStateHandlerArg`),
* actions of a specific type (`getActionHandlerArg`), or everything (`getAPlainHandlerArg`).
*/
abstract class DelegatingReducer extends DataFlow::SourceNode {
/**
* Gets a data flow node holding a reducer to which handling of `state.prop` is delegated.
*
* For example, gets the `fn` in `combineReducers({foo: fn})` with `prop` bound to `foo`.
*
* The delegating reducer should behave as a function of this form:
* ```js
* function outer(state, action) {
* return {
* prop: inner(state.prop, action),
* ...
* }
* }
* ```
*/
DataFlow::Node getStateHandlerArg(string prop) { none() }
/**
* Gets a data flow node holding a reducer to which actions of the given type are delegated.
*
* For example, gets the `fn` in `handleAction(a, fn)` with `actionType` bound to `a`.
*
* The `actionType` node may refer an action creator or a string value corresponding to `action.type`.
*/
DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) { none() }
/**
* Gets a data flow node holding a reducer to which every request is forwarded (for the
* purpose of this model).
*
* For example, gets the `fn` in `persistReducer(config, fn)`.
*/
DataFlow::Node getAPlainHandlerArg() { none() }
/** Gets the use site of this reducer. */
final ReducerArg getUseSite() { result.getASource() = this }
}
private module DelegatingReducer {
private API::Node combineReducers() {
result =
API::moduleImport(["redux", "redux-immutable", "@reduxjs/toolkit"])
.getMember("combineReducers")
}
/**
* A call to `combineReducers`, which delegates properties of `state` to individual sub-reducers.
*/
private class CombineReducers extends API::CallNode, DelegatingReducer {
CombineReducers() { this = combineReducers().getACall() }
override DataFlow::Node getStateHandlerArg(string prop) {
result = this.getParameter(0).getMember(prop).asSink()
}
}
/**
* An object literal flowing into a nested property in a `combineReducers` object, such as the `{ bar }` object in:
* ```js
* combineReducers({ foo: { bar } })
* ```
*
* Although the object itself is clearly not a function, we use the object to model the corresponding reducer function created by `combineReducers`.
*/
private class NestedCombineReducers extends DelegatingReducer, DataFlow::ObjectLiteralNode {
NestedCombineReducers() {
this = combineReducers().getParameter(0).getAMember+().getAValueReachingSink()
}
override DataFlow::Node getStateHandlerArg(string prop) {
result = this.getAPropertyWrite(prop).getRhs()
}
}
/**
* A call to `handleActions`, creating a reducer function that dispatched based on the action type:
*
* ```js
* let reducer = handleActions({
* actionType1: (state, action) => { ... },
* actionType2: (state, action) => { ... },
* })
* ```
*/
private class HandleActions extends API::CallNode, DelegatingReducer {
HandleActions() {
this =
API::moduleImport(["redux-actions", "redux-ts-utils"])
.getMember("handleActions")
.getACall()
}
override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
exists(DataFlow::PropWrite write |
result = this.getParameter(0).getAMember().asSink() and
write.getRhs() = result and
actionType = write.getPropertyNameExpr().flow()
)
}
}
/**
* A call to `handleAction`, creating a reducer function that only handles a given action type:
*
* ```js
* let reducer = handleAction('actionType', (state, action) => { ... });
* ```
*/
private class HandleAction extends API::CallNode, DelegatingReducer {
HandleAction() {
this =
API::moduleImport(["redux-actions", "redux-ts-utils"])
.getMember("handleAction")
.getACall()
}
override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
actionType = this.getArgument(0) and
result = this.getArgument(1)
}
}
/**
* A call to `persistReducer`, which we model as a plain wrapper around another reducer.
*/
private class PersistReducer extends DataFlow::CallNode, DelegatingReducer {
PersistReducer() {
this = API::moduleImport("redux-persist").getMember("persistReducer").getACall()
}
override DataFlow::Node getAPlainHandlerArg() { result = this.getArgument(1) }
}
/**
* A call to `immer` or `immer.produce`, which we model as a plain wrapper around another reducer.
*/
private class ImmerProduce extends DataFlow::CallNode, DelegatingReducer {
ImmerProduce() {
this = API::moduleImport("immer").getACall()
or
this = API::moduleImport("immer").getMember("produce").getACall()
}
override DataFlow::Node getAPlainHandlerArg() { result = this.getArgument(0) }
}
/**
* A call to `reduce-reducers`, modeled as a reducer that dispatches to an arbitrary subreducer.
*
* In reality, this function chains together all of the reducers, but in practice it is only used
* when the reducers handle a disjoint set of action types, which makes it behave as if it
* dispatched to just one of them.
*
* For example:
* ```js
* let reducer = reduceReducers([
* handleAction('action1', (state, action) => { ... }),
* handleAction('action2', (state, action) => { ... }),
* ]);
* ```
*/
private class ReduceReducers extends DataFlow::CallNode, DelegatingReducer {
ReduceReducers() {
this = API::moduleImport("reduce-reducers").getACall() or
this =
API::moduleImport(["redux-actions", "redux-ts-utils"])
.getMember("reduceReducers")
.getACall()
}
override DataFlow::Node getAPlainHandlerArg() {
result = this.getAnArgument()
or
result = this.getArgument(0).getALocalSource().(DataFlow::ArrayCreationNode).getAnElement()
}
}
/**
* A call to `createReducer`, for example:
*
* ```js
* let reducer = createReducer(initialState, (builder) => {
* builder
* .addCase(actionType1, (state, action) => { ... })
* .addCase(actionType2, (state, action) => { ... });
* });
* ```
*/
private class CreateReducer extends API::CallNode, DelegatingReducer {
CreateReducer() {
this = API::moduleImport("@reduxjs/toolkit").getMember("createReducer").getACall()
}
private API::Node getABuilderRef() {
result = this.getParameter(1).getParameter(0)
or
result = this.getABuilderRef().getAMember().getReturn()
}
override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
exists(API::CallNode addCase |
addCase = this.getABuilderRef().getMember("addCase").getACall() and
actionType = addCase.getArgument(0) and
result = addCase.getArgument(1)
)
}
}
/**
* A reducer created by a call to `createSlice`. Note that `createSlice` creates both
* reducers and actions; this class models the reducers only.
*
* For example:
* ```js
* let slice = createSlice({
* name: 'mySlice',
* reducers: {
* actionType1: (state, action) => { ... },
* actionType2: (state, action) => { ... },
* },
* extraReducers: (builder) => {
* builder.addCase('actionType3', (state, action) => { ... })
* }
* });
* export default slice.reducer;
* export const { actionType1, actionType2 } = slice.actions;
* ```
*/
private class CreateSliceReducer extends DelegatingReducer {
API::CallNode call;
CreateSliceReducer() {
call = API::moduleImport("@reduxjs/toolkit").getMember("createSlice").getACall() and
this = call.getReturn().getMember("reducer").asSource()
}
private API::Node getABuilderRef() {
result = call.getParameter(0).getMember("extraReducers").getParameter(0)
or
result = this.getABuilderRef().getAMember().getReturn()
}
override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) {
exists(string name |
result = call.getParameter(0).getMember("reducers").getMember(name).asSink() and
actionType = call.getReturn().getMember("actions").getMember(name).asSource()
)
or
// Properties of 'extraReducers':
// { extraReducers: { [action]: reducer }}
exists(DataFlow::PropWrite write |
result = call.getParameter(0).getMember("extraReducers").getAMember().asSink() and
write.getRhs() = result and
actionType = write.getPropertyNameExpr().flow()
)
or
// Builder callback to 'extraReducers':
// extraReducers: builder => builder.addCase(action, reducer)
exists(API::CallNode addCase |
addCase = this.getABuilderRef().getMember("addCase").getACall() and
actionType = addCase.getArgument(0) and
result = addCase.getArgument(1)
)
}
}
}
/**
* A function for creating and dispatching action objects of shape `{type, payload}`.
*
* In the simplest case, an action creator is a function, which, for some string `T` behaves as the function `x => {type: T, payload: x}`.
*
* An action creator may have a middleware function `f`, which makes it behave as the function `x => {type: T, payload: f(x)}` (that is,
* the function `f` converts the argument into the actual payload).
*
* Some action creators dispatch the action to a store, while for others, the value is returned and it is simply assumed to be dispatched
* at some point. We model all action creators as if they dispatch the action they create.
*/
class ActionCreator extends DataFlow::SourceNode instanceof ActionCreator::Range {
/** Gets the `type` property of actions created by this action creator, if it is known. */
string getTypeTag() { result = super.getTypeTag() }
/**
* Gets the middleware function that transforms arguments passed to this function into the
* action payload.
*
* Not every action creator has a middleware function; in such cases the first argument is
* treated as the action payload.
*
* If `async` is true, the middlware function returns a promise whose value eventually becomes
* the action payload. Otherwise, the return value is the payload itself.
*/
DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
result = super.getMiddlewareFunction(async)
}
/** Gets a data flow node referring to this action creator. */
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
t.start() and
result = this
or
// x -> bindActionCreators({ x, ... })
exists(BindActionCreatorsCall bind, string prop |
this.ref(t.continue()).flowsTo(bind.getParameter(0).getMember(prop).asSink()) and
result = bind.getReturn().getMember(prop).asSource()
)
or
// x -> combineActions(x, ...)
exists(API::CallNode combiner |
combiner =
API::moduleImport(["redux-actions", "redux-ts-utils"])
.getMember("combineActions")
.getACall() and
this.ref(t.continue()).flowsTo(combiner.getAnArgument()) and
result = combiner
)
or
// x -> x.fulfilled, for async action creators
result = this.ref(t.continue()).getAPropertyRead("fulfilled")
or
// follow flow through mapDispatchToProps
ReactRedux::dispatchToPropsStep(this.ref(t.continue()).getALocalUse(), result)
or
exists(DataFlow::TypeTracker t2 | result = this.ref(t2).track(t2, t))
}
/** Gets a data flow node referring to this action creator. */
DataFlow::SourceNode ref() { result = this.ref(DataFlow::TypeTracker::end()) }
/**
* Gets a block that is executed when a check has determined that `action` originated from this action creator.
*/
private ReachableBasicBlock getASuccessfulTypeCheckBlock(DataFlow::SourceNode action) {
action = getAnUntypedActionInReducer() and
result = getASuccessfulTypeCheckBlock(action, this.getTypeTag())
or
// some action creators implement a .match method for this purpose
exists(ConditionGuardNode guard, DataFlow::CallNode call |
call = this.ref().getAMethodCall("match") and
guard.getTest() = call.asExpr() and
action.flowsTo(call.getArgument(0)) and
guard.getOutcome() = true and
result = guard.getBasicBlock()
)
}
/**
* Gets a reducer that handles the type of action created by this action creator, for example:
* ```js
* handleAction(TYPE, (state, action) => { ... action.payload ... })
* ```
*
* Does not include reducers that perform their own action type checking.
*/
DataFlow::FunctionNode getAReducerFunction() {
exists(ReducerArg reducer |
reducer.isTypeTagHandler(this.getTypeTag())
or
reducer.isActionTypeHandler(this.ref().getALocalUse())
|
result = reducer.getASource()
)
}
/** Gets a data flow node referring a payload of this action (usually in the reducer function). */
DataFlow::SourceNode getAPayloadReference() {
// `if (action.type === TYPE) { ... action.payload ... }`
exists(DataFlow::SourceNode actionSrc |
actionSrc = getAnUntypedActionInReducer() and
result = actionSrc.getAPropertyRead("payload") and
this.getASuccessfulTypeCheckBlock(actionSrc).dominates(result.getBasicBlock())
)
or
result = this.getAReducerFunction().getParameter(1).getAPropertyRead("payload")
}
/** Gets a data flow node referring to the first argument of the action creator invocation. */
DataFlow::SourceNode getAMetaArgReference() {
exists(ReducerArg reducer |
reducer
.isActionTypeHandler(this.ref().getAPropertyRead(["fulfilled", "rejected", "pending"])) and
result =
reducer
.getASource()
.(DataFlow::FunctionNode)
.getParameter(1)
.getAPropertyRead("meta")
.getAPropertyRead("arg")
)
}
}
/** Companion module to the `ActionCreator` class. */
module ActionCreator {
/** A function for creating and dispatching action objects of shape `{type, payload}`. */
abstract class Range extends DataFlow::SourceNode {
/** Gets the `type` property of actions created by this action creator */
abstract string getTypeTag();
/** Gets the function transforming arguments into the action payload. */
DataFlow::FunctionNode getMiddlewareFunction(boolean async) { none() }
}
/**
* An action creator made using `createAction`:
* ```js
* let action1 = createAction('action1');
* let action2 = createAction('action2', (x,y) => { x, y });
* ```
*/
private class SingleAction extends Range, API::CallNode {
SingleAction() {
this =
API::moduleImport(["@reduxjs/toolkit", "redux-actions", "redux-ts-utils"])
.getMember("createAction")
.getACall()
}
override string getTypeTag() { this.getArgument(0).mayHaveStringValue(result) }
override DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
result = this.getCallback(1) and async = false
}
}
/**
* An action creator made by a call to `createActions`:
* ```js
* let { actionOne, actionTwo } = createActions({
* ACTION_ONE: (x, y) => { x, y },
* ACTION_TWO: (x, y) => { x, y },
* })
* ```
*/
class MultiAction extends Range {
API::CallNode createActions;
string name;
MultiAction() {
createActions = API::moduleImport("redux-actions").getMember("createActions").getACall() and
this = createActions.getReturn().getMember(name).asSource()
}
override DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
result.flowsTo(createActions.getParameter(0).getMember(this.getTypeTag()).asSink()) and
async = false
}
override string getTypeTag() {
result = name.regexpReplaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase()
}
}
/**
* An action creator made by a call to `createSlice`. Note that `createSlice` creates both
* reducers and actions; this class models the action creators.
*
* ```js
* let slice = createSlice({
* name: 'mySlice',
* reducers: {
* actionType1: (state, action) => { ... },
* actionType2: (state, action) => { ... },
* },
* });
* export const { actionType1, actionType2 } = slice.actions;
* ```
*/
private class CreateSliceAction extends Range {
API::CallNode call;
string actionName;
CreateSliceAction() {
call = API::moduleImport("@reduxjs/toolkit").getMember("createSlice").getACall() and
this = call.getReturn().getMember("actions").getMember(actionName).asSource()
}
override string getTypeTag() {
exists(string prefix |
call.getParameter(0).getMember("name").asSink().mayHaveStringValue(prefix) and
result = prefix + "/" + actionName
)
}
}
/**
* An action creator made by a call to `createAsyncThunk`:
* ```js
* const fetchUserId = createAsyncThunk('fetchUserId', async (id) => {
* return (await fetchUserId(id)).data;
* });
* ```
*/
private class CreateAsyncThunk extends Range, API::CallNode {
CreateAsyncThunk() {
this = API::moduleImport("@reduxjs/toolkit").getMember("createAsyncThunk").getACall()
}
override DataFlow::FunctionNode getMiddlewareFunction(boolean async) {
async = true and
result = this.getParameter(1).getAValueReachingSink()
}
override string getTypeTag() { this.getArgument(0).mayHaveStringValue(result) }
}
}
/**
* Gets the type tag of an action creator reaching `node`.
*/
private string getAnActionTypeTag(DataFlow::SourceNode node) {
exists(ActionCreator action |
node = action.ref() and
result = action.getTypeTag()
)
}
/** Gets the type tag of an action reaching `node`, or the string value of `node`. */
// Inlined to avoid duplicating `mayHaveStringValue`
pragma[inline]
private string getATypeTagFromNode(DataFlow::Node node) {
node.mayHaveStringValue(result)
or
node.asExpr().(Label).getName() = result
or
result = getAnActionTypeTag(node.getALocalSource())
}
/** A data flow node that is used as a reducer. */
class ReducerArg extends DataFlow::Node {
ReducerArg() {
this = any(StoreCreation c).getReducerArg()
or
this = any(DelegatingReducer r).getStateHandlerArg(_)
or
this = any(DelegatingReducer r).getActionHandlerArg(_)
}
/** Gets a data flow node that flows to this reducer argument. */
DataFlow::SourceNode getASource(DataFlow::TypeBackTracker t) {
t.start() and
result = this.getALocalSource()
or
// Step through forwarding functions
DataFlow::functionForwardingStep(result.getALocalUse(), this.getASource(t.continue()))
or
// Step through library functions like `redux-persist`
result.getALocalUse() =
this.getASource(t.continue()).(DelegatingReducer).getAPlainHandlerArg()
or
// Step through function composition (usually composed with various state "enhancer" functions)
exists(FunctionCompositionCall compose, DataFlow::CallNode call |
this.getASource(t.continue()) = call and
call = compose.getACall() and
result.getALocalUse() = [compose.getAnOperandNode(), call.getAnArgument()]
)
or
exists(DataFlow::TypeBackTracker t2 | result = this.getASource(t2).backtrack(t2, t))
}
/** Gets a data flow node that flows to this reducer argument. */
DataFlow::SourceNode getASource() { result = this.getASource(DataFlow::TypeBackTracker::end()) }
/**
* Holds if the actions dispatched to this reducer have the given type, that is,
* it is created by an action creator that flows to `actionType`, or has `action.type` set to
* the string value of `actionType`.
*/
predicate isActionTypeHandler(DataFlow::Node actionType) {
exists(DelegatingReducer r |
this = r.getActionHandlerArg(actionType)
or
this = r.getStateHandlerArg(_) and
r.getUseSite().isActionTypeHandler(actionType)
)
}
/**
* Holds if the actions dispatched to this reducer have the given `action.type` value.
*/
predicate isTypeTagHandler(string actionType) {
exists(DataFlow::Node node |
this.isActionTypeHandler(node) and
actionType = getATypeTagFromNode(node)
)
}
/**
* Holds if this reducer operates on the root state, as opposed to some access path within the state.
*/
predicate isRootStateHandler() {
this = any(StoreCreation c).getReducerArg()
or
exists(DelegatingReducer r |
this = r.getActionHandlerArg(_) and
r.getUseSite().isRootStateHandler()
)
}
}
/**
* A source of the `dispatch` function, used as starting point for `getADispatchFunctionNode`.
*/
abstract private class DispatchFunctionSource extends API::Node { }
/**
* A value that is dispatched, that is, flows to the first argument of `dispatch`
* (but where the call to `dispatch` is not necessarily explicit in the code).
*
* Used as starting point for `getADispatchedValueNode`.
*/
abstract private class DispatchedValueSink extends API::Node { }
/** Gets an API node referring to the Redux `dispatch` function. */
API::Node getADispatchFunctionNode() {
result instanceof DispatchFunctionSource
or
result = getADispatchedValueNode().getParameter(0)
}
/** Gets an API node corresponding to a value being passed to the `dispatch` function. */
API::Node getADispatchedValueNode() {
result instanceof DispatchedValueSink
or
result = getADispatchFunctionNode().getParameter(0)
}
private class StoreDispatchSource extends DispatchFunctionSource {
StoreDispatchSource() { this = any(StoreCreation c).asApiNode().getMember("dispatch") }
}
/** Gets the `action` parameter of a reducer that isn't behind an implied type guard. */
DataFlow::SourceNode getAnUntypedActionInReducer() {
exists(ReducerArg reducer |
not reducer.isTypeTagHandler(_) and
result = reducer.getASource().(DataFlow::FunctionNode).getParameter(1)
)
}
/** A call to `bindActionCreators` */
private class BindActionCreatorsCall extends API::CallNode {
BindActionCreatorsCall() {
this =
API::moduleImport(["redux", "@reduxjs/toolkit"]).getMember("bindActionCreators").getACall()
}
}
/** The return value of a function flowing into `bindActionCreators`, seen as a value that is dispatched. */
private class BindActionDispatchSink extends DispatchedValueSink {
BindActionDispatchSink() {
this = any(BindActionCreatorsCall c).getParameter(0).getAMember().getReturn()
}
}
/**
* Holds if `pred -> succ` is step from an action creation to its use in a reducer function.
*/
predicate actionToReducerStep(DataFlow::Node pred, DataFlow::Node succ) {
// Actions created by an action creator library
exists(ActionCreator action |
exists(DataFlow::CallNode call | call = action.ref().getACall() |
exists(int i |
pred = call.getArgument(i) and
succ = action.getMiddlewareFunction(_).getParameter(i)
)
or
not exists(action.getMiddlewareFunction(_)) and
pred = call.getArgument(0) and
succ = action.getAPayloadReference()
or
pred = call.getArgument(0) and
succ = action.getAMetaArgReference()
)
or
pred = action.getMiddlewareFunction(false).getReturnNode() and
succ = action.getAPayloadReference()
)
or
// Manually created and dispatched actions
exists(string actionType, string prop, DataFlow::SourceNode actionSrc |
actionSrc = getAnUntypedActionInReducer() and
pred = getAManuallyDispatchedValue(actionType).getAPropertyWrite(prop).getRhs() and
succ = actionSrc.getAPropertyRead(prop)
|
getASuccessfulTypeCheckBlock(actionSrc, actionType).dominates(succ.getBasicBlock())
or
exists(ReducerArg reducer |
reducer.isTypeTagHandler(actionType) and
actionSrc = reducer.getASource().(DataFlow::FunctionNode).getParameter(1)
)
)
}
/** Holds if `pred -> succ` is a step from the promise of an action payload to its use in a reducer function. */
predicate actionToReducerPromiseStep(DataFlow::Node pred, DataFlow::SourceNode succ) {
exists(ActionCreator action |
pred = action.getMiddlewareFunction(true).getReturnNode() and
succ = action.getAPayloadReference()
)
}
private class ActionToReducerStep extends DataFlow::SharedFlowStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
actionToReducerStep(pred, succ)
}
override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
actionToReducerPromiseStep(pred, succ) and prop = Promises::valueProp()
}
}
/** Gets the access path which `reducer` operates on. */
string getAffectedStateAccessPath(ReducerArg reducer) {
exists(DelegatingReducer r |
exists(string prop | reducer = r.getStateHandlerArg(prop) |
result = joinAccessPaths(getAffectedStateAccessPath(r.getUseSite()), prop)
or
r.getUseSite().isRootStateHandler() and
result = prop
)
or
reducer = r.getActionHandlerArg(_) and
result = getAffectedStateAccessPath(r.getUseSite())
)
}
/**
* Holds if `pred -> succ` should be a step from a reducer to a state access affected by the reducer.
*/
predicate reducerToStateStep(DataFlow::Node pred, DataFlow::Node succ) {
reducerToStateStepAux(pred, succ) and
ProgramSlicing::areFilesInSameReduxApp(pred.getFile(), succ.getFile())
}
/**
* Holds if `pred -> succ` should be a step from a reducer to a state access affected by the reducer.
*
* This is a helper predicate for `reducerToStateStep` without the program-slicing check.
*/
pragma[nomagic]
private predicate reducerToStateStepAux(DataFlow::Node pred, DataFlow::SourceNode succ) {
exists(ReducerArg reducer, DataFlow::FunctionNode function, string accessPath |
function = reducer.getASource() and
accessPath = getAffectedStateAccessPath(reducer)
|
pred = function.getReturnNode() and
succ = rootStateAccessPath(accessPath).asSource()
or
exists(string suffix, DataFlow::SourceNode base |
base = [function.getParameter(0), function.getReturnNode().getALocalSource()] and
pred = AccessPath::getAnAssignmentTo(base, suffix) and
succ = rootStateAccessPath(accessPath + "." + suffix).asSource()
)
)
or
exists(
ReducerArg reducer, DataFlow::FunctionNode function, string suffix, DataFlow::SourceNode base
|
function = reducer.getASource() and
reducer.isRootStateHandler() and
base = [function.getParameter(0), function.getReturnNode().getALocalSource()] and
pred = AccessPath::getAnAssignmentTo(base, suffix) and
succ = rootStateAccessPath(suffix).asSource()
)
}
private class ReducerToStateStep extends DataFlow::SharedFlowStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
reducerToStateStep(pred, succ)
}
}
/**
* Gets a dispatched object literal with a property `type: actionType`.
*/
private DataFlow::ObjectLiteralNode getAManuallyDispatchedValue(string actionType) {
result.getAPropertyWrite("type").getRhs().mayHaveStringValue(actionType) and
result = getADispatchedValueNode().getAValueReachingSink()
}
/**
* Gets the block to be executed after a check has determined that `action.type` is `actionType`,
* or the entry block of a closure dominated by such a check.
*/
private ReachableBasicBlock getASuccessfulTypeCheckBlock(
DataFlow::SourceNode action, string actionType
) {
action = getAnUntypedActionInReducer() and
(
exists(MembershipCandidate candidate, ConditionGuardNode guard |
action.getAPropertyRead("type").flowsTo(candidate) and
candidate.getAMemberString() = actionType and
guard.getTest() = candidate.getTest().asExpr() and
guard.getOutcome() = candidate.getTestPolarity() and
result = guard.getBasicBlock()
)
or
exists(SwitchStmt switch, SwitchCase case |
action.getAPropertyRead("type").flowsTo(switch.getExpr().flow()) and
case = switch.getACase() and
case.getExpr().mayHaveStringValue(actionType) and
result = getCaseBlock(case)
)
)
or
exists(Function f |
getASuccessfulTypeCheckBlock(action, actionType)
.dominates(f.(ControlFlowNode).getBasicBlock()) and
result = f.getEntryBB()
)
}
/** Gets the block to execute when `case` matches successfully. */
private BasicBlock getCaseBlock(SwitchCase case) {
result = case.getBodyStmt(0).getBasicBlock()
or
not exists(case.getABodyStmt()) and
exists(SwitchStmt stmt, int i |
stmt.getCase(i) = case and
result = getCaseBlock(stmt.getCase(i + 1))
)
}
/**
* Defines a flow step to be used for propagating tracking access to `state`.
*
* A `SharedFlowStep` is generated for these steps as well.
* It is distinct from `SharedFlowStep` to avoid recursion between that and the propagation of `state`.
*/
private class StateStep extends Unit {
abstract predicate step(DataFlow::Node pred, DataFlow::Node succ);
}
private predicate stateStep(DataFlow::Node pred, DataFlow::Node succ) {
any(StateStep s).step(pred, succ)
}
private class StateStepAsFlowStep extends DataFlow::SharedFlowStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) { stateStep(pred, succ) }
}
/**
* Model of the `react-redux` package.
*/
private module ReactRedux {
/** Gets an API node referring to the `useSelector` function. */
API::Node useSelector() {
result = API::moduleImport("react-redux").getMember("useSelector").getForwardingFunction*()
}
/**
* A step out of a `useSelector` call, such as from `state.x` to the result of `useSelector(state => state.x)`.
*/
class UseSelectorStep extends StateStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(API::CallNode call |
call = useSelector().getACall() and
pred = call.getCallback(0).getReturnNode() and
succ = call
)
}
}
/** The argument to a `useSelector` callback, seen as a root state reference. */
class UseSelectorStateSource extends RootStateSource {
UseSelectorStateSource() { this = useSelector().getParameter(0).getParameter(0) }
}
/** A call to `useDispatch`, as a source of the `dispatch` function. */
private class UseDispatchFunctionSource extends DispatchFunctionSource {
UseDispatchFunctionSource() {
this = API::moduleImport("react-redux").getMember("useDispatch").getReturn()
}
}
/**
* A call to `connect()`, typically as part of a code pattern like the following:
* ```js
* let withConnect = connect(mapStateToProps, mapDispatchToProps);
* let MyAwesomeComponent = compose(withConnect, otherStuff)(MyComponent);
* ```
*/
abstract private class ConnectCall extends API::CallNode {
/** Gets the API node corresponding to the `mapStateToProps` argument. */
abstract API::Node getMapStateToProps();
/** Gets the API node corresponding to the `mapDispatchToProps` argument. */
abstract API::Node getMapDispatchToProps();
/**
* Gets a function whose first argument becomes the React component to connect.
*/
DataFlow::SourceNode getAComponentTransformer() {
result = this
or
exists(FunctionCompositionCall compose |
this.getAComponentTransformer().flowsTo(compose.getAnOperandNode()) and
result = compose
)
}
/**
* Gets a data-flow node that should flow to `props.name` via the `mapDispatchToProps` function.
*/
DataFlow::Node getDispatchPropNode(string name) {
// Implicitly bound by bindActionCreators:
//
// const mapDispatchToProps = { foo }
//
result = this.getMapDispatchToProps().getMember(name).asSink()
or
//
// const mapDispatchToProps = dispatch => ( { foo } )
//
result = this.getMapDispatchToProps().getReturn().getMember(name).asSink()
or
// Explicitly bound by bindActionCreators:
//
// const mapDispatchToProps = dispatch => bindActionCreators({ foo }, dispatch);
//
exists(BindActionCreatorsCall bind |
bind.flowsTo(this.getMapDispatchToProps().getReturn().asSink()) and
result = bind.getOptionArgument(0, name)
)
}
/**
* Gets the React component decorated by this call, if one can be determined.
*/
ReactComponent getReactComponent() {
exists(DataFlow::SourceNode component | component = result.getAComponentCreatorReference() |
component.flowsTo(this.getAComponentTransformer().getACall().getArgument(0))
or
component.(DataFlow::ClassNode).getADecorator() = this.getAComponentTransformer()
)
}
}
/** A call to `connect`. */
private class RealConnectFunction extends ConnectCall {
RealConnectFunction() {
this = API::moduleImport("react-redux").getMember("connect").getACall()
}
override API::Node getMapStateToProps() { result = this.getParameter(0) }
override API::Node getMapDispatchToProps() { result = this.getParameter(1) }
}
/**
* An API entry point corresponding to a `connect` function which we couldn't recognize exactly.
*
* The `connect` call is recognized based on an argument being named either `mapStateToProps` or `mapDispatchToProps`.
* Used to catch cases where the `connect` function was not recognized by API graphs (usually because of it being
* wrapped in another function, which API graphs won't look through).
*/
private class HeuristicConnectEntryPoint extends API::EntryPoint {
HeuristicConnectEntryPoint() { this = "react-redux-connect" }
override DataFlow::SourceNode getASource() {
exists(DataFlow::CallNode call |
call.getAnArgument().asExpr().(Identifier).getName() =
["mapStateToProps", "mapDispatchToProps"] and
// exclude genuine calls to avoid duplication
not call = DataFlow::moduleMember("react-redux", "connect").getACall() and
result = call.getCalleeNode().getALocalSource()
)
}
}
/** A heuristic call to `connect`, recognized by it taking arguments named `mapStateToProps` and `mapDispatchToProps`. */
private class HeuristicConnectFunction extends ConnectCall {
HeuristicConnectFunction() { this = any(HeuristicConnectEntryPoint e).getANode().getACall() }
override API::Node getMapStateToProps() {
result = this.getAParameter() and
result.asSink().asExpr().(Identifier).getName() = "mapStateToProps"
}
override API::Node getMapDispatchToProps() {
result = this.getAParameter() and
result.asSink().asExpr().(Identifier).getName() = "mapDispatchToProps"
}
}
/**
* A step from the return value of `mapStateToProps` to a `props` access.
*/
private class StateToPropsStep extends StateStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(ConnectCall call |
pred = call.getMapStateToProps().getReturn().asSink() and
succ = call.getReactComponent().getADirectPropsAccess()
)
}
}
/**
* Holds if `pred -> succ` is a step from `mapDispatchToProps` to a `props` property access.
*/
predicate dispatchToPropsStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(ConnectCall call, string member |
pred = call.getDispatchPropNode(member) and
succ = call.getReactComponent().getAPropRead(member)
)
}
/** The first argument to `mapDispatchToProps` as a source of the `dispatch` function */
private class MapDispatchToPropsArg extends DispatchFunctionSource {
MapDispatchToPropsArg() { this = any(ConnectCall c).getMapDispatchToProps().getParameter(0) }
}
/** If `mapDispatchToProps` is an object, each method's return value is dispatched. */
private class MapDispatchToPropsMember extends DispatchedValueSink {
MapDispatchToPropsMember() {
this = any(ConnectCall c).getMapDispatchToProps().getAMember().getReturn()
}
}
/** The first argument to `mapStateToProps` as an access to the root state. */
private class MapStateToPropsStateSource extends RootStateSource {
MapStateToPropsStateSource() {
this = any(ConnectCall c).getMapStateToProps().getParameter(0)
}
}
}
private module Reselect {
/**
* A call to `createSelector`.
*
* Such calls have two forms. The single-argument version is simply a memoized function wrapper:
*
* ```js
* createSelector(state => state.foo)
* ```
*
* If multiple arguments are used, each callback independently maps over the state, and last
* callback collects all the intermediate results into the final result:
*
* ```js
* createSelector(
* state => state.foo,
* state => state.bar,
* ([foo, bar]) => {...}
* )
* ```
*
* Although selectors can work on any data, not just the Redux state, they are in practice only used
* with the state.
*/
class CreateSelectorCall extends API::CallNode {
CreateSelectorCall() {
this =
API::moduleImport(["reselect", "@reduxjs/toolkit"]).getMember("createSelector").getACall()
}
/** Gets the `i`th selector callback, that is, a callback other than the result function. */
API::Node getSelectorFunction(int i) {
// When there are multiple callbacks, exclude the last one
result = this.getParameter(i) and
(i = 0 or i < this.getNumArgument() - 1)
or
// Selector functions may be given as an array
exists(DataFlow::ArrayCreationNode array |
array.flowsTo(this.getArgument(0)) and
result.getAValueReachableFromSource() = array.getElement(i)
)
}
}
/** The state argument to a selector */
private class SelectorStateArg extends RootStateSource {
SelectorStateArg() { this = any(CreateSelectorCall c).getSelectorFunction(_).getParameter(0) }
}
/** A flow step between the callbacks of `createSelector` or out of its final selector. */
private class CreateSelectorStep extends StateStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
// Return value of `i`th callback flows to the `i`th parameter of the last callback.
exists(CreateSelectorCall call, int index |
call.getNumArgument() > 1 and
pred = call.getSelectorFunction(index).getReturn().asSink() and
succ = call.getLastParameter().getParameter(index).asSource()
)
or
// The result of the last callback is the final result
exists(CreateSelectorCall call |
pred = call.getLastParameter().getReturn().asSink() and
succ = call
)
}
}
}
/** For testing only. */
module Internal {
predicate getRootStateAccessPath = rootStateAccessPath/1;
}
}