Skip to content

Commit 41b4535

Browse files
author
Esben Sparre Andreasen
committed
JS(ql): support optional chaining
1 parent 00587ba commit 41b4535

File tree

21 files changed

+213
-3
lines changed

21 files changed

+213
-3
lines changed

javascript/ql/src/Expressions/SuspiciousInvocation.ql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ private import semmle.javascript.dataflow.InferredTypes
1616
from InvokeExpr invk, DataFlow::AnalyzedNode callee
1717
where callee.asExpr() = invk.getCallee() and
1818
forex (InferredType tp | tp = callee.getAType() | tp != TTFunction() and tp != TTClass()) and
19-
not invk.isAmbient()
19+
not invk.isAmbient() and
20+
not invk instanceof OptionalUse
2021
select invk, "Callee is not a function: it has type " + callee.ppTypes() + "."

javascript/ql/src/Expressions/SuspiciousPropAccess.ql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ from PropAccess pacc, DataFlow::AnalyzedNode base
3232
where base.asExpr() = pacc.getBase() and
3333
forex (InferredType tp | tp = base.getAType() | tp = TTNull() or tp = TTUndefined()) and
3434
not namespaceOrConstEnumAccess(pacc.getBase()) and
35-
not pacc.isAmbient()
35+
not pacc.isAmbient() and
36+
not pacc instanceof OptionalUse
3637
select pacc, "The base expression of this property access is always " + base.ppTypes() + "."

javascript/ql/src/semmle/javascript/Expr.qll

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1901,4 +1901,36 @@ private class LiteralDynamicImportPath extends PathExprInModule, ConstantString
19011901
}
19021902

19031903
override string getValue() { result = this.(ConstantString).getStringValue() }
1904-
}
1904+
}
1905+
1906+
/**
1907+
* A call or member access that evaluates to `undefined` if its base operand evaluates to `undefined` or `null`.
1908+
*/
1909+
class OptionalUse extends Expr, @optionalchainable { OptionalUse() { isOptionalChaining(this) } }
1910+
1911+
private class ChainElem extends Expr, @optionalchainable {
1912+
/**
1913+
* Gets the base operand of this chainable element.
1914+
*/
1915+
ChainElem getChainBase() {
1916+
result = this.(CallExpr).getCallee() or
1917+
result = this.(PropAccess).getBase()
1918+
}
1919+
}
1920+
1921+
/**
1922+
* The root in a chain of calls or property accesses, where at least one call or property access is optional.
1923+
*/
1924+
class OptionalChainRoot extends ChainElem {
1925+
OptionalUse optionalUse;
1926+
1927+
OptionalChainRoot() {
1928+
getChainBase*() = optionalUse and
1929+
not exists(ChainElem other | this = other.getChainBase())
1930+
}
1931+
1932+
/**
1933+
* Gets an optional call or property access in the chain of this root.
1934+
*/
1935+
OptionalUse getAnOptionalUse() { result = optionalUse }
1936+
}

javascript/ql/src/semmle/javascript/dataflow/internal/BasicExprTypeInference.qll

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,15 @@ private class AnalyzedAssignAddExpr extends AnalyzedCompoundAssignExpr {
413413
isAddition(astNode) and result = abstractValueOfType(TTNumber())
414414
}
415415
}
416+
417+
/**
418+
* Flow analysis for optional chaining expressions.
419+
*/
420+
private class AnalyzedOptionalChainExpr extends DataFlow::AnalyzedValueNode {
421+
override OptionalChainRoot astNode;
422+
423+
override AbstractValue getALocalValue() {
424+
result = super.getALocalValue() or
425+
result = TAbstractUndefined()
426+
}
427+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
| short-circuiting.js:3:5:3:18 | x?.(o1 = null) | short-circuiting.js:3:5:3:18 | x?.(o1 = null) |
2+
| short-circuiting.js:7:5:7:18 | x?.[o2 = null] | short-circuiting.js:7:5:7:18 | x?.[o2 = null] |
3+
| short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) | short-circuiting.js:12:5:12:18 | x?.[o3 = null] |
4+
| short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) | short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) |
5+
| tst.js:2:1:2:6 | a?.b.c | tst.js:2:1:2:4 | a?.b |
6+
| tst.js:3:1:3:6 | a.b?.c | tst.js:3:1:3:6 | a.b?.c |
7+
| tst.js:4:1:4:7 | a?.b?.c | tst.js:4:1:4:4 | a?.b |
8+
| tst.js:4:1:4:7 | a?.b?.c | tst.js:4:1:4:7 | a?.b?.c |
9+
| tst.js:7:1:7:7 | f?.()() | tst.js:7:1:7:5 | f?.() |
10+
| tst.js:8:1:8:7 | f()?.() | tst.js:8:1:8:7 | f()?.() |
11+
| tst.js:9:1:9:9 | f?.()?.() | tst.js:9:1:9:5 | f?.() |
12+
| tst.js:9:1:9:9 | f?.()?.() | tst.js:9:1:9:9 | f?.()?.() |
13+
| tst.js:12:1:12:8 | a?.m().b | tst.js:12:1:12:4 | a?.m |
14+
| tst.js:13:1:13:9 | a.m?.().b | tst.js:13:1:13:7 | a.m?.() |
15+
| tst.js:14:1:14:8 | a.m()?.b | tst.js:14:1:14:8 | a.m()?.b |
16+
| tst.js:15:1:15:11 | a?.m?.()?.b | tst.js:15:1:15:4 | a?.m |
17+
| tst.js:15:1:15:11 | a?.m?.()?.b | tst.js:15:1:15:8 | a?.m?.() |
18+
| tst.js:15:1:15:11 | a?.m?.()?.b | tst.js:15:1:15:11 | a?.m?.()?.b |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import javascript
2+
3+
from OptionalChainRoot root
4+
select root, root.getAnOptionalUse()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
| short-circuiting.js:3:5:3:18 | x?.(o1 = null) |
2+
| short-circuiting.js:7:5:7:18 | x?.[o2 = null] |
3+
| short-circuiting.js:12:5:12:18 | x?.[o3 = null] |
4+
| short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) |
5+
| tst.js:2:1:2:4 | a?.b |
6+
| tst.js:3:1:3:6 | a.b?.c |
7+
| tst.js:4:1:4:4 | a?.b |
8+
| tst.js:4:1:4:7 | a?.b?.c |
9+
| tst.js:7:1:7:5 | f?.() |
10+
| tst.js:8:1:8:7 | f()?.() |
11+
| tst.js:9:1:9:5 | f?.() |
12+
| tst.js:9:1:9:9 | f?.()?.() |
13+
| tst.js:12:1:12:4 | a?.m |
14+
| tst.js:13:1:13:7 | a.m?.() |
15+
| tst.js:14:1:14:8 | a.m()?.b |
16+
| tst.js:15:1:15:4 | a?.m |
17+
| tst.js:15:1:15:8 | a?.m?.() |
18+
| tst.js:15:1:15:11 | a?.m?.()?.b |
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import javascript
2+
3+
select any(OptionalUse u)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
| short-circuiting.js:4:10:4:11 | o1 | file://:0:0:0:0 | null |
2+
| short-circuiting.js:4:10:4:11 | o1 | short-circuiting.js:2:14:2:15 | object literal |
3+
| short-circuiting.js:8:10:8:11 | o2 | file://:0:0:0:0 | null |
4+
| short-circuiting.js:8:10:8:11 | o2 | short-circuiting.js:6:14:6:15 | object literal |
5+
| short-circuiting.js:13:10:13:11 | o3 | file://:0:0:0:0 | null |
6+
| short-circuiting.js:13:10:13:11 | o3 | short-circuiting.js:10:14:10:15 | object literal |
7+
| short-circuiting.js:14:10:14:11 | o4 | file://:0:0:0:0 | null |
8+
| short-circuiting.js:14:10:14:11 | o4 | short-circuiting.js:11:14:11:15 | object literal |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import javascript
2+
3+
from CallExpr c, Expr arg
4+
where
5+
c.getCalleeName() = "DUMP" and
6+
arg = c.getArgument(0)
7+
select arg, arg.analyze().getAValue()

0 commit comments

Comments
 (0)