Skip to content

Commit b819f55

Browse files
authored
Merge pull request github#12792 from asgerf/js/redux-model-perf
JS: add getForwardingFunction and use to sharpen useSelector model
2 parents 8cb54b7 + aef0fa3 commit b819f55

File tree

6 files changed

+107
-2
lines changed

6 files changed

+107
-2
lines changed

javascript/ql/lib/semmle/javascript/ApiGraphs.qll

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,46 @@ module API {
347347
result = this.getASuccessor(Label::promisedError())
348348
}
349349

350+
/**
351+
* Gets a node representing a function that is a wrapper around the function represented by this node.
352+
*
353+
* Concretely, a function that forwards all its parameters to a call to `f` and returns the result of that call
354+
* is considered a wrapper around `f`.
355+
*
356+
* Examples:
357+
* ```js
358+
* function f(x) {
359+
* return g(x); // f = g.getForwardingFunction()
360+
* }
361+
*
362+
* function doExec(x) {
363+
* console.log(x);
364+
* return exec(x); // doExec = exec.getForwardingFunction()
365+
* }
366+
*
367+
* function doEither(x, y) {
368+
* if (x > y) {
369+
* return foo(x, y); // doEither = foo.getForwardingFunction()
370+
* } else {
371+
* return bar(x, y); // doEither = bar.getForwardingFunction()
372+
* }
373+
* }
374+
*
375+
* function wrapWithLogging(f) {
376+
* return (x) => {
377+
* console.log(x);
378+
* return f(x); // f.getForwardingFunction() = anonymous arrow function
379+
* }
380+
* }
381+
* wrapWithLogging(g); // g.getForwardingFunction() = wrapWithLogging(g)
382+
* ```
383+
*/
384+
cached
385+
Node getForwardingFunction() {
386+
Stages::ApiStage::ref() and
387+
result = this.getASuccessor(Label::forwardingFunction())
388+
}
389+
350390
/**
351391
* Gets any class that has this value as a decorator.
352392
*
@@ -901,6 +941,9 @@ module API {
901941
or
902942
lbl = Label::return() and
903943
ref = pred.getAnInvocation()
944+
or
945+
lbl = Label::forwardingFunction() and
946+
DataFlow::functionForwardingStep(pred.getALocalUse(), ref)
904947
)
905948
or
906949
exists(DataFlow::Node def, DataFlow::FunctionNode fn |
@@ -1431,6 +1474,9 @@ module API {
14311474
/** Gets the `return` edge label. */
14321475
LabelReturn return() { any() }
14331476

1477+
/** Gets the label representing a function wrapper that forwards to an underlying function. */
1478+
LabelForwardingFunction forwardingFunction() { any() }
1479+
14341480
/** Gets the `promised` edge label connecting a promise to its contained value. */
14351481
LabelPromised promised() { any() }
14361482

@@ -1483,6 +1529,7 @@ module API {
14831529
MkLabelDecoratedClass() or
14841530
MkLabelDecoratedMember() or
14851531
MkLabelDecoratedParameter() or
1532+
MkLabelForwardingFunction() or
14861533
MkLabelEntryPoint(API::EntryPoint e)
14871534

14881535
/** A label for an entry-point. */
@@ -1566,6 +1613,11 @@ module API {
15661613
override string toString() { result = "getReceiver()" }
15671614
}
15681615

1616+
/** A label for a function that is a wrapper around another function. */
1617+
class LabelForwardingFunction extends ApiLabel, MkLabelForwardingFunction {
1618+
override string toString() { result = "getForwardingFunction()" }
1619+
}
1620+
15691621
/** A label for a class decorated by the current value. */
15701622
class LabelDecoratedClass extends ApiLabel, MkLabelDecoratedClass {
15711623
override string toString() { result = "getADecoratedClass()" }

javascript/ql/lib/semmle/javascript/frameworks/Redux.qll

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,9 @@ module Redux {
985985
*/
986986
private module ReactRedux {
987987
/** Gets an API node referring to the `useSelector` function. */
988-
API::Node useSelector() { result = API::moduleImport("react-redux").getMember("useSelector") }
988+
API::Node useSelector() {
989+
result = API::moduleImport("react-redux").getMember("useSelector").getForwardingFunction*()
990+
}
989991

990992
/**
991993
* A step out of a `useSelector` call, such as from `state.x` to the result of `useSelector(state => state.x)`.
@@ -994,7 +996,7 @@ module Redux {
994996
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
995997
exists(API::CallNode call |
996998
call = useSelector().getACall() and
997-
pred = call.getParameter(0).getReturn().asSink() and
999+
pred = call.getCallback(0).getReturnNode() and
9981000
succ = call
9991001
)
10001002
}
@@ -1231,4 +1233,9 @@ module Redux {
12311233
}
12321234
}
12331235
}
1236+
1237+
/** For testing only. */
1238+
module Internal {
1239+
predicate getRootStateAccessPath = rootStateAccessPath/1;
1240+
}
12341241
}

javascript/ql/lib/semmle/javascript/internal/CachedStages.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ module Stages {
277277
.getUnknownMember()
278278
.getInstance()
279279
.getReceiver()
280+
.getForwardingFunction()
280281
.getPromisedError()
281282
.getADecoratedClass()
282283
.getADecoratedMember()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useSelector } from 'react-redux';
2+
3+
function useSelectorWrapped(fn) {
4+
return useSelector(fn);
5+
}
6+
7+
function MyComponent(props) {
8+
const x1 = useSelectorWrapped(state => state.x1);
9+
const x2 = useSelectorWrapped(state => state.x2);
10+
const x3 = useSelectorWrapped(state => state.x3);
11+
const x4 = useSelectorWrapped(state => state.x4);
12+
const x5 = useSelectorWrapped(state => state.x5);
13+
14+
return <span>X</span>;
15+
}

javascript/ql/test/library-tests/frameworks/Redux/test.expected

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,11 @@ taintFlow
113113
| react-redux.jsx:69:31:69:38 | source() | react-redux.jsx:76:10:76:36 | props.p ... Action3 |
114114
| react-redux.jsx:70:30:70:37 | source() | react-redux.jsx:77:10:77:28 | props.propFromAsync |
115115
reactComponentRef
116+
| accessPaths.js:7:1:15:1 | functio ... pan>;\\n} | accessPaths.js:7:1:15:1 | functio ... pan>;\\n} |
116117
| react-redux.jsx:64:1:80:1 | functio ... r}}/>\\n} | react-redux.jsx:64:1:80:1 | functio ... r}}/>\\n} |
117118
| react-redux.jsx:64:1:80:1 | functio ... r}}/>\\n} | react-redux.jsx:94:28:94:84 | connect ... ponent) |
118119
| react-redux.jsx:64:1:80:1 | functio ... r}}/>\\n} | react-redux.jsx:97:12:97:12 | c |
120+
ambiguousAccessPath
119121
getAffectedStateAccessPath
120122
| react-redux.jsx:12:33:17:9 | (state, ... } | toolkit |
121123
| react-redux.jsx:18:41:23:9 | (state, ... } | toolkit |
@@ -163,3 +165,24 @@ reducerToStateStep
163165
| react-redux.jsx:35:45:35:58 | action.payload | react-redux.jsx:86:31:86:54 | state.m ... alValue |
164166
| react-redux.jsx:39:42:39:55 | action.payload | react-redux.jsx:87:32:87:56 | state.m ... lValue2 |
165167
| react-redux.jsx:44:27:46:14 | [1, 2, ... }) | react-redux.jsx:88:32:88:56 | state.m ... lValue3 |
168+
getRootStateAccessPath
169+
| manual | react-redux.jsx:86:31:86:42 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("manual") |
170+
| manual | react-redux.jsx:87:32:87:43 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("manual") |
171+
| manual | react-redux.jsx:88:32:88:43 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("manual") |
172+
| manual.manualValue | react-redux.jsx:86:31:86:54 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("manual").getMember("manualValue") |
173+
| manual.manualValue2 | react-redux.jsx:87:32:87:56 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("manual").getMember("manualValue2") |
174+
| manual.manualValue3 | react-redux.jsx:88:32:88:56 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("manual").getMember("manualValue3") |
175+
| toolkit | react-redux.jsx:84:32:84:44 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("toolkit") |
176+
| toolkit | react-redux.jsx:85:24:85:36 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("toolkit") |
177+
| toolkit.asyncValue | react-redux.jsx:85:24:85:47 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("toolkit").getMember("asyncValue") |
178+
| toolkit.value | react-redux.jsx:84:32:84:50 | use entryPoint("react-redux-connect").getParameter(0).getParameter(0).getMember("toolkit").getMember("value") |
179+
| x1 | accessPaths.js:8:16:8:52 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getForwardingFunction().getReturn() |
180+
| x1 | accessPaths.js:8:44:8:51 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getParameter(0).getParameter(0).getMember("x1") |
181+
| x2 | accessPaths.js:9:16:9:52 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getForwardingFunction().getReturn() |
182+
| x2 | accessPaths.js:9:44:9:51 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getParameter(0).getParameter(0).getMember("x2") |
183+
| x3 | accessPaths.js:10:16:10:52 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getForwardingFunction().getReturn() |
184+
| x3 | accessPaths.js:10:44:10:51 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getParameter(0).getParameter(0).getMember("x3") |
185+
| x4 | accessPaths.js:11:16:11:52 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getForwardingFunction().getReturn() |
186+
| x4 | accessPaths.js:11:44:11:51 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getParameter(0).getParameter(0).getMember("x4") |
187+
| x5 | accessPaths.js:12:16:12:52 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getForwardingFunction().getReturn() |
188+
| x5 | accessPaths.js:12:44:12:51 | use moduleImport("react-redux").getMember("exports").getMember("useSelector").getParameter(0).getParameter(0).getMember("x5") |

javascript/ql/test/library-tests/frameworks/Redux/test.ql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,10 @@ query predicate taintFlow(DataFlow::Node source, DataFlow::Node sink) {
6363
query DataFlow::SourceNode reactComponentRef(ReactComponent component) {
6464
result = component.getAComponentCreatorReference()
6565
}
66+
67+
query predicate ambiguousAccessPath(API::Node node, string path) {
68+
count(string accessPath | Redux::Internal::getRootStateAccessPath(accessPath) = node) > 1 and
69+
Redux::Internal::getRootStateAccessPath(path) = node
70+
}
71+
72+
query predicate getRootStateAccessPath = Redux::Internal::getRootStateAccessPath/1;

0 commit comments

Comments
 (0)