mirror of
https://github.com/github/codeql.git
synced 2026-01-08 12:10:22 +01:00
1246 lines
45 KiB
Plaintext
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;
|
|
}
|
|
}
|