Skip to content

Commit 308da07

Browse files
authored
Merge pull request #2525 from asger-semmle/promise-missing-await
JS: New query: missing await
2 parents de15ecf + ef79023 commit 308da07

File tree

10 files changed

+259
-0
lines changed

10 files changed

+259
-0
lines changed

javascript/config/suites/javascript/correctness-core

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
+ semmlecode-javascript-queries/Expressions/DuplicateSwitchCase.ql: /Correctness/Expressions
1717
+ semmlecode-javascript-queries/Expressions/HeterogeneousComparison.ql: /Correctness/Expressions
1818
+ semmlecode-javascript-queries/Expressions/MisspelledVariableName.ql: /Correctness/Expressions
19+
+ semmlecode-javascript-queries/Expressions/MissingAwait.ql: /Correctness/Expressions
1920
+ semmlecode-javascript-queries/Expressions/MissingDotLengthInComparison.ql: /Correctness/Expressions
2021
+ semmlecode-javascript-queries/Expressions/ShiftOutOfRange.ql: /Correctness/Expressions
2122
+ semmlecode-javascript-queries/Expressions/RedundantExpression.ql: /Correctness/Expressions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
In JavaScript, <code>async</code> functions always return a promise object.
8+
To obtain the underlying value of the promise, use the <code>await</code> operator or call the <code>then</code> method.
9+
Attempting to use a promise object instead of its underlying value can lead to unexpected behavior.
10+
</p>
11+
12+
</overview>
13+
<recommendation>
14+
15+
<p>
16+
Use the <code>await</code> operator to get the value contained in the promise.
17+
Alternatively, call <code>then</code> on the promise and use the value passed to the callback.
18+
</p>
19+
20+
</recommendation>
21+
<example>
22+
23+
<p>
24+
In the following example, the <code>getData</code> function returns a promise,
25+
and the caller checks if the returned promise is <code>null</code>:
26+
</p>
27+
28+
<sample src="examples/MissingAwait.js" />
29+
30+
<p>
31+
However, the null check does not work as expected. The <code>return null</code> statement
32+
on line 2 actually returns a <em>promise</em> containing the <code>null</code> value.
33+
Since the promise object itself is not equal to <code>null</code>, the error check is bypassed.
34+
</p>
35+
36+
<p>
37+
The issue can be corrected by inserting <code>await</code> before the promise:
38+
</p>
39+
40+
<sample src="examples/MissingAwaitGood.js" />
41+
42+
</example>
43+
<references>
44+
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises">Using promises</a></li>
45+
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function">Async functions</a></li>
46+
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await">Await operator</a></li>
47+
</references>
48+
</qhelp>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @name Missing await
3+
* @description Using a promise without awaiting its result can lead to unexpected behavior.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id js/missing-await
7+
* @tags correctness
8+
* @precision high
9+
*/
10+
11+
import javascript
12+
13+
/**
14+
* Holds if `call` is a call to an `async` function.
15+
*/
16+
predicate isAsyncCall(DataFlow::CallNode call) {
17+
// If a callee is known, and all known callees are async, assume all
18+
// possible callees are async.
19+
forex(Function callee | call.getACallee() = callee | callee.isAsync())
20+
}
21+
22+
/**
23+
* Holds if `node` is always a promise.
24+
*/
25+
predicate isPromise(DataFlow::SourceNode node, boolean nullable) {
26+
isAsyncCall(node) and
27+
nullable = false
28+
or
29+
not isAsyncCall(node) and
30+
node.asExpr().getType() instanceof PromiseType and
31+
nullable = true
32+
}
33+
34+
/**
35+
* Holds the result of `e` is used in a way that doesn't make sense for Promise objects.
36+
*/
37+
predicate isBadPromiseContext(Expr expr) {
38+
exists(BinaryExpr binary |
39+
expr = binary.getAnOperand() and
40+
not binary instanceof LogicalExpr and
41+
not binary instanceof InstanceofExpr
42+
)
43+
or
44+
expr = any(LogicalBinaryExpr e).getLeftOperand()
45+
or
46+
expr = any(UnaryExpr e).getOperand()
47+
or
48+
expr = any(UpdateExpr e).getOperand()
49+
or
50+
expr = any(ConditionalExpr e).getCondition()
51+
or
52+
expr = any(IfStmt stmt).getCondition()
53+
or
54+
expr = any(ForInStmt stmt).getIterationDomain()
55+
or
56+
expr = any(IndexExpr e).getIndex()
57+
}
58+
59+
string tryGetPromiseExplanation(Expr e) {
60+
result = "The value '" + e.(VarAccess).getName() + "' is always a promise."
61+
or
62+
result = "The call to '" + e.(CallExpr).getCalleeName() + "' always returns a promise."
63+
}
64+
65+
string getPromiseExplanation(Expr e) {
66+
result = tryGetPromiseExplanation(e)
67+
or
68+
not exists(tryGetPromiseExplanation(e)) and
69+
result = "This value is always a promise."
70+
}
71+
72+
from Expr expr, boolean nullable
73+
where
74+
isBadPromiseContext(expr) and
75+
isPromise(expr.flow().getImmediatePredecessor*(), nullable) and
76+
(
77+
nullable = false
78+
or
79+
expr.inNullSensitiveContext()
80+
)
81+
select expr, "Missing await. " + getPromiseExplanation(expr)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
async function getData(id) {
2+
let req = await fetch(`https://example.com/data?id=${id}`);
3+
if (!req.ok) return null;
4+
return req.json();
5+
}
6+
7+
async function showData(id) {
8+
let data = getData(id);
9+
if (data == null) {
10+
console.warn("No data for: " + id);
11+
return;
12+
}
13+
// ...
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
async function getData(id) {
2+
let req = await fetch(`https://example.com/data?id=${id}`);
3+
if (!req.ok) return null;
4+
return req.json();
5+
}
6+
7+
async function showData(id) {
8+
let data = await getData(id);
9+
if (data == null) {
10+
console.warn("No data for: " + id);
11+
return;
12+
}
13+
// ...
14+
}

javascript/ql/src/semmle/javascript/SSA.qll

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,29 @@ class SsaPhiNode extends SsaPseudoDefinition, TPhi {
670670
endcolumn = startcolumn and
671671
getBasicBlock().getLocation().hasLocationInfo(filepath, startline, startcolumn, _, _)
672672
}
673+
674+
/**
675+
* If all inputs to this phi node are (transitive) refinements of the same variable,
676+
* gets that variable.
677+
*/
678+
SsaVariable getRephinedVariable() {
679+
forex(SsaVariable input | input = getAnInput() |
680+
result = getRefinedVariable(input)
681+
)
682+
}
683+
}
684+
685+
/**
686+
* Gets the input to the given refinement node or rephinement node.
687+
*/
688+
private SsaVariable getRefinedVariable(SsaVariable v) {
689+
result = getRefinedVariable(v.(SsaRefinementNode).getAnInput())
690+
or
691+
result = getRefinedVariable(v.(SsaPhiNode).getRephinedVariable())
692+
or
693+
not v instanceof SsaRefinementNode and
694+
not v instanceof SsaPhiNode and
695+
result = v
673696
}
674697

675698
/**

javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ module DataFlow {
208208
result = TSsaDefNode(refinement.getAnInput())
209209
)
210210
or
211+
exists(SsaPhiNode phi |
212+
this = TSsaDefNode(phi) and
213+
result = TSsaDefNode(phi.getRephinedVariable())
214+
)
215+
or
211216
// IIFE call -> return value of IIFE
212217
exists(Function fun |
213218
localCall(this.asExpr(), fun) and
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
| tst.js:8:9:8:13 | thing | Missing await. The value 'thing' is always a promise. |
2+
| tst.js:10:9:10:13 | thing | Missing await. The value 'thing' is always a promise. |
3+
| tst.js:12:15:12:19 | thing | Missing await. The value 'thing' is always a promise. |
4+
| tst.js:14:19:14:23 | thing | Missing await. The value 'thing' is always a promise. |
5+
| tst.js:19:19:19:23 | thing | Missing await. The value 'thing' is always a promise. |
6+
| tst.js:20:9:20:13 | thing | Missing await. The value 'thing' is always a promise. |
7+
| tst.js:22:15:22:19 | thing | Missing await. The value 'thing' is always a promise. |
8+
| tst.js:25:13:25:17 | thing | Missing await. The value 'thing' is always a promise. |
9+
| tst.js:48:12:48:16 | thing | Missing await. The value 'thing' is always a promise. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Expressions/MissingAwait.ql
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
async function getThing() {
2+
return something();
3+
}
4+
5+
function useThing() {
6+
let thing = getThing();
7+
8+
if (thing === undefined) {} // NOT OK
9+
10+
if (thing == null) {} // NOT OK
11+
12+
something(thing ? 1 : 2); // NOT OK
13+
14+
for (let x in thing) { // NOT OK
15+
something(x);
16+
}
17+
18+
let obj = something();
19+
something(obj[thing]); // NOT OK
20+
obj[thing] = 5; // NOT OK
21+
22+
something(thing + "bar"); // NOT OK
23+
24+
if (something()) {
25+
if (thing) { // NOT OK
26+
something(3);
27+
}
28+
}
29+
}
30+
31+
async function useThingCorrectly() {
32+
let thing = await getThing();
33+
34+
if (thing === undefined) {} // OK
35+
36+
if (thing == null) {} // OK
37+
38+
return thing + "bar"; // OK
39+
}
40+
41+
async function useThingCorrectly2() {
42+
let thing = getThing();
43+
44+
if (await thing === undefined) {} // OK
45+
46+
if (await thing == null) {} // OK
47+
48+
return thing + "bar"; // NOT OK
49+
}
50+
51+
function getThingSync() {
52+
return something();
53+
}
54+
55+
function useThingPossiblySync(b) {
56+
let thing = b ? getThing() : getThingSync();
57+
58+
if (thing === undefined) {} // OK
59+
60+
if (thing == null) {} // OK
61+
62+
return thing + "bar"; // NOT OK - but we don't flag it
63+
}

0 commit comments

Comments
 (0)