JS(ql): support optional chaining

This commit is contained in:
Esben Sparre Andreasen
2018-11-20 14:08:33 +01:00
parent 00587ba7b4
commit 41b45352aa
21 changed files with 213 additions and 3 deletions

View File

@@ -16,5 +16,6 @@ private import semmle.javascript.dataflow.InferredTypes
from InvokeExpr invk, DataFlow::AnalyzedNode callee
where callee.asExpr() = invk.getCallee() and
forex (InferredType tp | tp = callee.getAType() | tp != TTFunction() and tp != TTClass()) and
not invk.isAmbient()
not invk.isAmbient() and
not invk instanceof OptionalUse
select invk, "Callee is not a function: it has type " + callee.ppTypes() + "."

View File

@@ -32,5 +32,6 @@ from PropAccess pacc, DataFlow::AnalyzedNode base
where base.asExpr() = pacc.getBase() and
forex (InferredType tp | tp = base.getAType() | tp = TTNull() or tp = TTUndefined()) and
not namespaceOrConstEnumAccess(pacc.getBase()) and
not pacc.isAmbient()
not pacc.isAmbient() and
not pacc instanceof OptionalUse
select pacc, "The base expression of this property access is always " + base.ppTypes() + "."

View File

@@ -1901,4 +1901,36 @@ private class LiteralDynamicImportPath extends PathExprInModule, ConstantString
}
override string getValue() { result = this.(ConstantString).getStringValue() }
}
}
/**
* A call or member access that evaluates to `undefined` if its base operand evaluates to `undefined` or `null`.
*/
class OptionalUse extends Expr, @optionalchainable { OptionalUse() { isOptionalChaining(this) } }
private class ChainElem extends Expr, @optionalchainable {
/**
* Gets the base operand of this chainable element.
*/
ChainElem getChainBase() {
result = this.(CallExpr).getCallee() or
result = this.(PropAccess).getBase()
}
}
/**
* The root in a chain of calls or property accesses, where at least one call or property access is optional.
*/
class OptionalChainRoot extends ChainElem {
OptionalUse optionalUse;
OptionalChainRoot() {
getChainBase*() = optionalUse and
not exists(ChainElem other | this = other.getChainBase())
}
/**
* Gets an optional call or property access in the chain of this root.
*/
OptionalUse getAnOptionalUse() { result = optionalUse }
}

View File

@@ -413,3 +413,15 @@ private class AnalyzedAssignAddExpr extends AnalyzedCompoundAssignExpr {
isAddition(astNode) and result = abstractValueOfType(TTNumber())
}
}
/**
* Flow analysis for optional chaining expressions.
*/
private class AnalyzedOptionalChainExpr extends DataFlow::AnalyzedValueNode {
override OptionalChainRoot astNode;
override AbstractValue getALocalValue() {
result = super.getALocalValue() or
result = TAbstractUndefined()
}
}

View File

@@ -0,0 +1,18 @@
| short-circuiting.js:3:5:3:18 | x?.(o1 = null) | short-circuiting.js:3:5:3:18 | x?.(o1 = null) |
| short-circuiting.js:7:5:7:18 | x?.[o2 = null] | short-circuiting.js:7:5:7:18 | x?.[o2 = null] |
| short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) | short-circuiting.js:12:5:12:18 | x?.[o3 = null] |
| short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) | short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) |
| tst.js:2:1:2:6 | a?.b.c | tst.js:2:1:2:4 | a?.b |
| tst.js:3:1:3:6 | a.b?.c | tst.js:3:1:3:6 | a.b?.c |
| tst.js:4:1:4:7 | a?.b?.c | tst.js:4:1:4:4 | a?.b |
| tst.js:4:1:4:7 | a?.b?.c | tst.js:4:1:4:7 | a?.b?.c |
| tst.js:7:1:7:7 | f?.()() | tst.js:7:1:7:5 | f?.() |
| tst.js:8:1:8:7 | f()?.() | tst.js:8:1:8:7 | f()?.() |
| tst.js:9:1:9:9 | f?.()?.() | tst.js:9:1:9:5 | f?.() |
| tst.js:9:1:9:9 | f?.()?.() | tst.js:9:1:9:9 | f?.()?.() |
| tst.js:12:1:12:8 | a?.m().b | tst.js:12:1:12:4 | a?.m |
| tst.js:13:1:13:9 | a.m?.().b | tst.js:13:1:13:7 | a.m?.() |
| tst.js:14:1:14:8 | a.m()?.b | tst.js:14:1:14:8 | a.m()?.b |
| tst.js:15:1:15:11 | a?.m?.()?.b | tst.js:15:1:15:4 | a?.m |
| tst.js:15:1:15:11 | a?.m?.()?.b | tst.js:15:1:15:8 | a?.m?.() |
| tst.js:15:1:15:11 | a?.m?.()?.b | tst.js:15:1:15:11 | a?.m?.()?.b |

View File

@@ -0,0 +1,4 @@
import javascript
from OptionalChainRoot root
select root, root.getAnOptionalUse()

View File

@@ -0,0 +1,18 @@
| short-circuiting.js:3:5:3:18 | x?.(o1 = null) |
| short-circuiting.js:7:5:7:18 | x?.[o2 = null] |
| short-circuiting.js:12:5:12:18 | x?.[o3 = null] |
| short-circuiting.js:12:5:12:31 | x?.[o3 ... = null) |
| tst.js:2:1:2:4 | a?.b |
| tst.js:3:1:3:6 | a.b?.c |
| tst.js:4:1:4:4 | a?.b |
| tst.js:4:1:4:7 | a?.b?.c |
| tst.js:7:1:7:5 | f?.() |
| tst.js:8:1:8:7 | f()?.() |
| tst.js:9:1:9:5 | f?.() |
| tst.js:9:1:9:9 | f?.()?.() |
| tst.js:12:1:12:4 | a?.m |
| tst.js:13:1:13:7 | a.m?.() |
| tst.js:14:1:14:8 | a.m()?.b |
| tst.js:15:1:15:4 | a?.m |
| tst.js:15:1:15:8 | a?.m?.() |
| tst.js:15:1:15:11 | a?.m?.()?.b |

View File

@@ -0,0 +1,3 @@
import javascript
select any(OptionalUse u)

View File

@@ -0,0 +1,8 @@
| short-circuiting.js:4:10:4:11 | o1 | file://:0:0:0:0 | null |
| short-circuiting.js:4:10:4:11 | o1 | short-circuiting.js:2:14:2:15 | object literal |
| short-circuiting.js:8:10:8:11 | o2 | file://:0:0:0:0 | null |
| short-circuiting.js:8:10:8:11 | o2 | short-circuiting.js:6:14:6:15 | object literal |
| short-circuiting.js:13:10:13:11 | o3 | file://:0:0:0:0 | null |
| short-circuiting.js:13:10:13:11 | o3 | short-circuiting.js:10:14:10:15 | object literal |
| short-circuiting.js:14:10:14:11 | o4 | file://:0:0:0:0 | null |
| short-circuiting.js:14:10:14:11 | o4 | short-circuiting.js:11:14:11:15 | object literal |

View File

@@ -0,0 +1,7 @@
import javascript
from CallExpr c, Expr arg
where
c.getCalleeName() = "DUMP" and
arg = c.getArgument(0)
select arg, arg.analyze().getAValue()

View File

@@ -0,0 +1,8 @@
| short-circuiting.js:4:10:4:11 | o1 | file://:0:0:0:0 | null |
| short-circuiting.js:4:10:4:11 | o1 | short-circuiting.js:2:14:2:15 | object literal |
| short-circuiting.js:8:10:8:11 | o2 | file://:0:0:0:0 | null |
| short-circuiting.js:8:10:8:11 | o2 | short-circuiting.js:6:14:6:15 | object literal |
| short-circuiting.js:13:10:13:11 | o3 | file://:0:0:0:0 | null |
| short-circuiting.js:13:10:13:11 | o3 | short-circuiting.js:10:14:10:15 | object literal |
| short-circuiting.js:14:10:14:11 | o4 | file://:0:0:0:0 | null |
| short-circuiting.js:14:10:14:11 | o4 | short-circuiting.js:11:14:11:15 | object literal |

View File

@@ -0,0 +1,7 @@
import javascript
from CallExpr c, Expr arg
where
c.getCalleeName() = "DUMP" and
arg = c.getArgument(0)
select arg, arg.analyze().getAValue()

View File

@@ -0,0 +1,16 @@
(function() {
var o1 = {};
x?.(o1 = null);
DUMP(o1);
var o2 = {};
x?.[o2 = null];
DUMP(o2);
var o3 = {},
o4 = {};
x?.[o3 = null]?.(o4 = null);
DUMP(o3);
DUMP(o4);
});
// semmle-extractor-options: --experimental

View File

@@ -0,0 +1,17 @@
a.b.c;
a?.b.c;
a.b?.c;
a?.b?.c;
f()();
f?.()();
f()?.();
f?.()?.();
a.m().b;
a?.m().b;
a.m?.().b;
a.m()?.b;
a?.m?.()?.b;
// semmle-extractor-options: --experimental

View File

@@ -0,0 +1,14 @@
| tst.js:2:14:2:21 | (null)() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:3:14:3:23 | (null)?.() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:3:14:3:23 | (null)?.() | file://:0:0:0:0 | undefined |
| tst.js:4:14:4:26 | (undefined)() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:5:14:5:28 | (undefined)?.() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:5:14:5:28 | (undefined)?.() | file://:0:0:0:0 | undefined |
| tst.js:6:14:6:24 | (unknown)() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:7:14:7:26 | (unknown)?.() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:7:14:7:26 | (unknown)?.() | file://:0:0:0:0 | undefined |
| tst.js:9:13:11:5 | unknown ... ;\\n } | file://:0:0:0:0 | undefined |
| tst.js:9:13:11:5 | unknown ... ;\\n } | tst.js:9:33:11:5 | anonymous function |
| tst.js:12:14:12:16 | f() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:13:14:13:18 | f?.() | file://:0:0:0:0 | indefinite value (call) |
| tst.js:13:14:13:18 | f?.() | file://:0:0:0:0 | undefined |

View File

@@ -0,0 +1,5 @@
import javascript
from Variable v, Expr e
where e = v.getAnAssignedExpr()
select e, e.analyze().getAValue()

View File

@@ -0,0 +1,15 @@
(function() {
var v1 = (null)();
var v2 = (null)?.();
var v3 = (undefined)();
var v4 = (undefined)?.();
var v5 = (unknown)();
var v6 = (unknown)?.();
var f = unknown? undefined: function(){
return 42;
}
var v7 = f();
var v8 = f?.();
});
// semmle-extractor-options: --experimental

View File

@@ -1,3 +1,5 @@
| SuspiciousInvocation.js:11:5:11:58 | error(" ... status) | Callee is not a function: it has type undefined. |
| namespace.ts:23:1:23:3 | g() | Callee is not a function: it has type object. |
| optional-chaining.js:3:5:3:7 | a() | Callee is not a function: it has type null. |
| optional-chaining.js:7:5:7:7 | b() | Callee is not a function: it has type undefined. |
| super.js:11:5:11:11 | super() | Callee is not a function: it has type number. |

View File

@@ -0,0 +1,10 @@
(function(){
var a = null;
a();
a?.();
var b = undefined;
b();
b?.();
});
// semmle-extractor-options: --experimental

View File

@@ -1,4 +1,6 @@
| SuspiciousPropAccess.js:4:10:4:21 | result.value | The base expression of this property access is always undefined. |
| optional-chaining.js:3:5:3:7 | a.p | The base expression of this property access is always null. |
| optional-chaining.js:7:5:7:7 | b.p | The base expression of this property access is always undefined. |
| tst.js:32:32:32:38 | a(1)[0] | The base expression of this property access is always null. |
| tst.ts:19:3:19:5 | x.p | The base expression of this property access is always undefined. |
| typeassertion.ts:14:3:14:9 | z.field | The base expression of this property access is always null. |

View File

@@ -0,0 +1,10 @@
(function(){
var a = null;
a.p;
a?.p;
var b = undefined;
b.p;
b?.p;
});
// semmle-extractor-options: --experimental