Skip to content

Commit 794d5bd

Browse files
authored
Merge pull request #2116 from erik-krogh/arrayCBRet
Approved by max-schaefer
2 parents 8c16b36 + 6cac961 commit 794d5bd

5 files changed

Lines changed: 221 additions & 127 deletions

File tree

javascript/ql/src/Expressions/ExprHasNoEffect.ql

Lines changed: 2 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -13,122 +13,10 @@
1313
*/
1414

1515
import javascript
16-
import DOMProperties
17-
import semmle.javascript.frameworks.xUnit
18-
import semmle.javascript.RestrictedLocations
1916
import ExprHasNoEffect
17+
import semmle.javascript.RestrictedLocations
2018

21-
/**
22-
* Holds if `e` is of the form `x;` or `e.p;` and has a JSDoc comment containing a tag.
23-
* In that case, it is probably meant as a declaration and shouldn't be flagged by this query.
24-
*
25-
* This will still flag cases where the JSDoc comment contains no tag at all (and hence carries
26-
* no semantic information), and expression statements with an ordinary (non-JSDoc) comment
27-
* attached to them.
28-
*/
29-
predicate isDeclaration(Expr e) {
30-
(e instanceof VarAccess or e instanceof PropAccess) and
31-
exists(e.getParent().(ExprStmt).getDocumentation().getATag())
32-
}
33-
34-
/**
35-
* Holds if there exists a getter for a property called `name` anywhere in the program.
36-
*/
37-
predicate isGetterProperty(string name) {
38-
// there is a call of the form `Object.defineProperty(..., name, descriptor)` ...
39-
exists(CallToObjectDefineProperty defProp | name = defProp.getPropertyName() |
40-
// ... where `descriptor` defines a getter
41-
defProp.hasPropertyAttributeWrite("get", _)
42-
or
43-
// ... where `descriptor` may define a getter
44-
exists(DataFlow::SourceNode descriptor | descriptor.flowsTo(defProp.getPropertyDescriptor()) |
45-
descriptor.isIncomplete(_)
46-
or
47-
// minimal escape analysis for the descriptor
48-
exists(DataFlow::InvokeNode invk |
49-
not invk = defProp and
50-
descriptor.flowsTo(invk.getAnArgument())
51-
)
52-
)
53-
)
54-
or
55-
// there is an object expression with a getter property `name`
56-
exists(ObjectExpr obj | obj.getPropertyByName(name) instanceof PropertyGetter)
57-
}
58-
59-
/**
60-
* A property access that may invoke a getter.
61-
*/
62-
class GetterPropertyAccess extends PropAccess {
63-
override predicate isImpure() { isGetterProperty(getPropertyName()) }
64-
}
65-
66-
/**
67-
* Holds if `c` is an indirect eval call of the form `(dummy, eval)(...)`, where
68-
* `dummy` is some expression whose value is discarded, and which simply
69-
* exists to prevent the call from being interpreted as a direct eval.
70-
*/
71-
predicate isIndirectEval(CallExpr c, Expr dummy) {
72-
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
73-
dummy = seq.getOperand(0) and
74-
seq.getOperand(1).(GlobalVarAccess).getName() = "eval" and
75-
seq.getNumOperands() = 2
76-
)
77-
}
78-
79-
/**
80-
* Holds if `c` is a call of the form `(dummy, e[p])(...)`, where `dummy` is
81-
* some expression whose value is discarded, and which simply exists
82-
* to prevent the call from being interpreted as a method call.
83-
*/
84-
predicate isReceiverSuppressingCall(CallExpr c, Expr dummy, PropAccess callee) {
85-
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
86-
dummy = seq.getOperand(0) and
87-
seq.getOperand(1) = callee and
88-
seq.getNumOperands() = 2
89-
)
90-
}
91-
92-
/**
93-
* Holds if evaluating `e` has no side effects (except potentially allocating
94-
* and initializing a new object).
95-
*
96-
* For calls, we do not check whether their arguments have any side effects:
97-
* even if they do, the call itself is useless and should be flagged by this
98-
* query.
99-
*/
100-
predicate noSideEffects(Expr e) {
101-
e.isPure()
102-
or
103-
// `new Error(...)`, `new SyntaxError(...)`, etc.
104-
forex(Function f | f = e.flow().(DataFlow::NewNode).getACallee() |
105-
f.(ExternalType).getASupertype*().getName() = "Error"
106-
)
107-
}
10819

10920
from Expr e
110-
where
111-
noSideEffects(e) and
112-
inVoidContext(e) and
113-
// disregard pure expressions wrapped in a void(...)
114-
not e instanceof VoidExpr and
115-
// filter out directives (unknown directives are handled by UnknownDirective.ql)
116-
not exists(Directive d | e = d.getExpr()) and
117-
// or about externs
118-
not e.inExternsFile() and
119-
// don't complain about declarations
120-
not isDeclaration(e) and
121-
// exclude DOM properties, which sometimes have magical auto-update properties
122-
not isDOMProperty(e.(PropAccess).getPropertyName()) and
123-
// exclude xUnit.js annotations
124-
not e instanceof XUnitAnnotation and
125-
// exclude common patterns that are most likely intentional
126-
not isIndirectEval(_, e) and
127-
not isReceiverSuppressingCall(_, e, _) and
128-
// exclude anonymous function expressions as statements; these can only arise
129-
// from a syntax error we already flag
130-
not exists(FunctionExpr fe, ExprStmt es | fe = e |
131-
fe = es.getExpr() and
132-
not exists(fe.getName())
133-
)
21+
where hasNoEffect(e)
13422
select e.(FirstLineOf), "This expression has no effect."

javascript/ql/src/Expressions/ExprHasNoEffect.qll

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44

55
import javascript
6+
import DOMProperties
7+
import semmle.javascript.frameworks.xUnit
68

79
/**
810
* Holds if `e` appears in a syntactic context where its value is discarded.
@@ -37,3 +39,121 @@ predicate inVoidContext(Expr e) {
3739
or
3840
exists(LogicalBinaryExpr logical | e = logical.getRightOperand() and inVoidContext(logical))
3941
}
42+
43+
44+
/**
45+
* Holds if `e` is of the form `x;` or `e.p;` and has a JSDoc comment containing a tag.
46+
* In that case, it is probably meant as a declaration and shouldn't be flagged by this query.
47+
*
48+
* This will still flag cases where the JSDoc comment contains no tag at all (and hence carries
49+
* no semantic information), and expression statements with an ordinary (non-JSDoc) comment
50+
* attached to them.
51+
*/
52+
predicate isDeclaration(Expr e) {
53+
(e instanceof VarAccess or e instanceof PropAccess) and
54+
exists(e.getParent().(ExprStmt).getDocumentation().getATag())
55+
}
56+
57+
/**
58+
* Holds if there exists a getter for a property called `name` anywhere in the program.
59+
*/
60+
predicate isGetterProperty(string name) {
61+
// there is a call of the form `Object.defineProperty(..., name, descriptor)` ...
62+
exists(CallToObjectDefineProperty defProp | name = defProp.getPropertyName() |
63+
// ... where `descriptor` defines a getter
64+
defProp.hasPropertyAttributeWrite("get", _)
65+
or
66+
// ... where `descriptor` may define a getter
67+
exists(DataFlow::SourceNode descriptor | descriptor.flowsTo(defProp.getPropertyDescriptor()) |
68+
descriptor.isIncomplete(_)
69+
or
70+
// minimal escape analysis for the descriptor
71+
exists(DataFlow::InvokeNode invk |
72+
not invk = defProp and
73+
descriptor.flowsTo(invk.getAnArgument())
74+
)
75+
)
76+
)
77+
or
78+
// there is an object expression with a getter property `name`
79+
exists(ObjectExpr obj | obj.getPropertyByName(name) instanceof PropertyGetter)
80+
}
81+
82+
/**
83+
* A property access that may invoke a getter.
84+
*/
85+
class GetterPropertyAccess extends PropAccess {
86+
override predicate isImpure() { isGetterProperty(getPropertyName()) }
87+
}
88+
89+
/**
90+
* Holds if `c` is an indirect eval call of the form `(dummy, eval)(...)`, where
91+
* `dummy` is some expression whose value is discarded, and which simply
92+
* exists to prevent the call from being interpreted as a direct eval.
93+
*/
94+
predicate isIndirectEval(CallExpr c, Expr dummy) {
95+
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
96+
dummy = seq.getOperand(0) and
97+
seq.getOperand(1).(GlobalVarAccess).getName() = "eval" and
98+
seq.getNumOperands() = 2
99+
)
100+
}
101+
102+
/**
103+
* Holds if `c` is a call of the form `(dummy, e[p])(...)`, where `dummy` is
104+
* some expression whose value is discarded, and which simply exists
105+
* to prevent the call from being interpreted as a method call.
106+
*/
107+
predicate isReceiverSuppressingCall(CallExpr c, Expr dummy, PropAccess callee) {
108+
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
109+
dummy = seq.getOperand(0) and
110+
seq.getOperand(1) = callee and
111+
seq.getNumOperands() = 2
112+
)
113+
}
114+
115+
/**
116+
* Holds if evaluating `e` has no side effects (except potentially allocating
117+
* and initializing a new object).
118+
*
119+
* For calls, we do not check whether their arguments have any side effects:
120+
* even if they do, the call itself is useless and should be flagged by this
121+
* query.
122+
*/
123+
predicate noSideEffects(Expr e) {
124+
e.isPure()
125+
or
126+
// `new Error(...)`, `new SyntaxError(...)`, etc.
127+
forex(Function f | f = e.flow().(DataFlow::NewNode).getACallee() |
128+
f.(ExternalType).getASupertype*().getName() = "Error"
129+
)
130+
}
131+
132+
/**
133+
* Holds if the expression `e` should be reported as having no effect.
134+
*/
135+
predicate hasNoEffect(Expr e) {
136+
noSideEffects(e) and
137+
inVoidContext(e) and
138+
// disregard pure expressions wrapped in a void(...)
139+
not e instanceof VoidExpr and
140+
// filter out directives (unknown directives are handled by UnknownDirective.ql)
141+
not exists(Directive d | e = d.getExpr()) and
142+
// or about externs
143+
not e.inExternsFile() and
144+
// don't complain about declarations
145+
not isDeclaration(e) and
146+
// exclude DOM properties, which sometimes have magical auto-update properties
147+
not isDOMProperty(e.(PropAccess).getPropertyName()) and
148+
// exclude xUnit.js annotations
149+
not e instanceof XUnitAnnotation and
150+
// exclude common patterns that are most likely intentional
151+
not isIndirectEval(_, e) and
152+
not isReceiverSuppressingCall(_, e, _) and
153+
// exclude anonymous function expressions as statements; these can only arise
154+
// from a syntax error we already flag
155+
not exists(FunctionExpr fe, ExprStmt es | fe = e |
156+
fe = es.getExpr() and
157+
not exists(fe.getName())
158+
)
159+
}

javascript/ql/src/Statements/UseOfReturnlessFunction.ql

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,87 @@ predicate alwaysThrows(Function f) {
8282
)
8383
}
8484

85-
from DataFlow::CallNode call
86-
where
87-
not call.isIndefinite(_) and
88-
forex(Function f | f = call.getACallee() |
85+
/**
86+
* Holds if the last statement in the function is flagged by the js/useless-expression query.
87+
*/
88+
predicate lastStatementHasNoEffect(Function f) {
89+
hasNoEffect(f.getExit().getAPredecessor())
90+
}
91+
92+
/**
93+
* Holds if `func` is a callee of `call`, and all possible callees of `call` never return a value.
94+
*/
95+
predicate callToVoidFunction(DataFlow::CallNode call, Function func) {
96+
not call.isIncomplete() and
97+
func = call.getACallee() and
98+
forall(Function f | f = call.getACallee() |
8999
returnsVoid(f) and not isStub(f) and not alwaysThrows(f)
100+
)
101+
}
102+
103+
/**
104+
* Holds if `name` is the name of a method from `Array.prototype` or Lodash,
105+
* where that method takes a callback as parameter,
106+
* and the callback is expected to return a value.
107+
*/
108+
predicate hasNonVoidCallbackMethod(string name) {
109+
name = "every" or
110+
name = "filter" or
111+
name = "find" or
112+
name = "findIndex" or
113+
name = "flatMap" or
114+
name = "map" or
115+
name = "reduce" or
116+
name = "reduceRight" or
117+
name = "some" or
118+
name = "sort"
119+
}
120+
121+
DataFlow::SourceNode array(DataFlow::TypeTracker t) {
122+
t.start() and result instanceof DataFlow::ArrayCreationNode
123+
or
124+
exists (DataFlow::TypeTracker t2 |
125+
result = array(t2).track(t2, t)
126+
)
127+
}
128+
129+
DataFlow::SourceNode array() { result = array(DataFlow::TypeTracker::end()) }
130+
131+
/**
132+
* Holds if `call` is an Array or Lodash method accepting a callback `func`,
133+
* where the `call` expects a callback that returns an expression,
134+
* but `func` does not return a value.
135+
*/
136+
predicate voidArrayCallback(DataFlow::CallNode call, Function func) {
137+
hasNonVoidCallbackMethod(call.getCalleeName()) and
138+
exists(int index |
139+
index = min(int i | exists(call.getCallback(i))) and
140+
func = call.getCallback(index).getFunction()
141+
) and
142+
returnsVoid(func) and
143+
not isStub(func) and
144+
not alwaysThrows(func) and
145+
(
146+
call.getReceiver().getALocalSource() = array()
147+
or
148+
call.getCalleeNode().getALocalSource() instanceof LodashUnderscore::Member
149+
)
150+
}
151+
152+
from DataFlow::CallNode call, Function func, string name, string msg
153+
where
154+
(
155+
callToVoidFunction(call, func) and
156+
msg = "the $@ does not return anything, yet the return value is used." and
157+
name = func.describe()
158+
or
159+
voidArrayCallback(call, func) and
160+
msg = "the $@ does not return anything, yet the return value from the call to " + call.getCalleeName() + " is used." and
161+
name = "callback function"
90162
) and
91-
92163
not benignContext(call.asExpr()) and
93-
164+
not lastStatementHasNoEffect(func) and
94165
// anonymous one-shot closure. Those are used in weird ways and we ignore them.
95166
not oneshotClosure(call.asExpr())
96167
select
97-
call, "the function $@ does not return anything, yet the return value is used.", call.getACallee(), call.getCalleeName()
98-
168+
call, msg, func, name
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
| tst.js:20:17:20:33 | onlySideEffects() | the function $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | onlySideEffects |
2-
| tst.js:24:13:24:29 | onlySideEffects() | the function $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | onlySideEffects |
3-
| tst.js:30:20:30:36 | onlySideEffects() | the function $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | onlySideEffects |
4-
| tst.js:53:10:53:34 | bothOnl ... fects() | the function $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | bothOnlyHaveSideEffects |
5-
| tst.js:53:10:53:34 | bothOnl ... fects() | the function $@ does not return anything, yet the return value is used. | tst.js:48:2:50:5 | functio ... )\\n } | bothOnlyHaveSideEffects |
1+
| tst.js:20:17:20:33 | onlySideEffects() | the $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | function onlySideEffects |
2+
| tst.js:24:13:24:29 | onlySideEffects() | the $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | function onlySideEffects |
3+
| tst.js:30:20:30:36 | onlySideEffects() | the $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | function onlySideEffects |
4+
| tst.js:53:10:53:34 | bothOnl ... fects() | the $@ does not return anything, yet the return value is used. | tst.js:11:5:13:5 | functio ... )\\n } | function onlySideEffects |
5+
| tst.js:53:10:53:34 | bothOnl ... fects() | the $@ does not return anything, yet the return value is used. | tst.js:48:2:50:5 | functio ... )\\n } | function onlySideEffects2 |
6+
| tst.js:76:12:76:46 | [1,2,3] ... n, 3)}) | the $@ does not return anything, yet the return value from the call to filter is used. | tst.js:76:27:76:45 | n => {equals(n, 3)} | callback function |
7+
| tst.js:80:12:80:50 | filter( ... 3) } ) | the $@ does not return anything, yet the return value from the call to filter is used. | tst.js:80:28:80:48 | x => { ... x, 3) } | callback function |

javascript/ql/test/query-tests/Statements/UseOfReturnlessFunction/tst.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,18 @@
6868

6969
var h = returnsValue() || alwaysThrows(); // OK!
7070
console.log(h);
71+
72+
function equals(x, y) {
73+
return x === y;
74+
}
75+
76+
var foo = [1,2,3].filter(n => {equals(n, 3)}) // NOT OK!
77+
console.log(foo);
78+
79+
import { filter } from 'lodash'
80+
var bar = filter([1,2,4], x => { equals(x, 3) } ) // NOT OK!
81+
console.log(bar);
82+
83+
var baz = [1,2,3].filter(n => {n === 3}) // OK
84+
console.log(baz);
7185
})();

0 commit comments

Comments
 (0)