Skip to content

JS: Resolve calls downward in class hierarchy #18783

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1066,20 +1066,46 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
result = this.getAnInstanceReference(DataFlow::TypeTracker::end())
}

pragma[nomagic]
private DataFlow::PropRead getAnOwnInstanceMemberAccess(string name, DataFlow::TypeTracker t) {
result = this.getAnInstanceReference(t.continue()).getAPropertyRead(name)
}

pragma[nomagic]
private DataFlow::PropRead getAnInstanceMemberAccessOnSubClass(
string name, DataFlow::TypeTracker t
) {
exists(DataFlow::ClassNode subclass |
subclass = this.getADirectSubClass() and
not exists(subclass.getInstanceMember(name, _))
|
result = subclass.getAnOwnInstanceMemberAccess(name, t)
or
result = subclass.getAnInstanceMemberAccessOnSubClass(name, t)
)
}

pragma[nomagic]
private DataFlow::PropRead getAnInstanceMemberAccessOnSuperClass(string name) {
result = this.getADirectSuperClass().getAReceiverNode().getAPropertyRead(name)
or
result = this.getADirectSuperClass().getAnInstanceMemberAccessOnSuperClass(name)
}

/**
* Gets a property read that accesses the property `name` on an instance of this class.
*
* Concretely, this holds when the base is an instance of this class or a subclass thereof.
* This includes accesses on subclasses (if the member is not overridden) and accesses in a base class
* (only if accessed on `this`).
*/
pragma[nomagic]
DataFlow::PropRead getAnInstanceMemberAccess(string name, DataFlow::TypeTracker t) {
result = this.getAnInstanceReference(t.continue()).getAPropertyRead(name)
result = this.getAnOwnInstanceMemberAccess(name, t)
or
exists(DataFlow::ClassNode subclass |
result = subclass.getAnInstanceMemberAccess(name, t) and
not exists(subclass.getInstanceMember(name, _)) and
this = subclass.getADirectSuperClass()
)
result = this.getAnInstanceMemberAccessOnSubClass(name, t)
or
t.start() and
result = this.getAnInstanceMemberAccessOnSuperClass(name)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,15 +558,19 @@ DataFlowCallable nodeGetEnclosingCallable(Node node) {

newtype TDataFlowType =
TFunctionType(Function f) or
TInstanceType(DataFlow::ClassNode cls) or
TAnyType()

class DataFlowType extends TDataFlowType {
string toDebugString() {
this instanceof TFunctionType and
result =
"TFunctionType(" + this.asFunction().toString() + ") at line " +
this.asFunction().getLocation().getStartLine()
or
result =
"TInstanceType(" + this.asInstanceOfClass().toString() + ") at line " +
this.asInstanceOfClass().getLocation().getStartLine()
or
this instanceof TAnyType and result = "TAnyType"
}

Expand All @@ -575,13 +579,20 @@ class DataFlowType extends TDataFlowType {
}

Function asFunction() { this = TFunctionType(result) }

DataFlow::ClassNode asInstanceOfClass() { this = TInstanceType(result) }
}

/**
* Holds if `t1` is strictly stronger than `t2`.
*/
predicate typeStrongerThan(DataFlowType t1, DataFlowType t2) {
t1 instanceof TFunctionType and t2 = TAnyType()
// 't1' is a subclass of 't2'
t1.asInstanceOfClass() = t2.asInstanceOfClass().getADirectSubClass+()
or
// Ensure all types are stronger than 'any'
not t1 = TAnyType() and
t2 = TAnyType()
}

private DataFlowType getPreciseType(Node node) {
Expand All @@ -590,6 +601,9 @@ private DataFlowType getPreciseType(Node node) {
result = TFunctionType(f)
)
or
result.asInstanceOfClass() =
unique(DataFlow::ClassNode cls | cls.getAnInstanceReference().getALocalUse() = node)
or
result = getPreciseType(node.getImmediatePredecessor())
or
result = getPreciseType(node.(PostUpdateNode).getPreUpdateNode())
Expand Down Expand Up @@ -683,18 +697,27 @@ predicate neverSkipInPathGraph(Node node) {
string ppReprType(DataFlowType t) { none() }

pragma[inline]
private predicate compatibleTypesNonSymRefl(DataFlowType t1, DataFlowType t2) {
private predicate compatibleTypesWithAny(DataFlowType t1, DataFlowType t2) {
t1 != TAnyType() and
t2 = TAnyType()
}

pragma[nomagic]
private predicate compatibleTypes1(DataFlowType t1, DataFlowType t2) {
t1.asInstanceOfClass().getADirectSubClass+() = t2.asInstanceOfClass()
}

Comment on lines +705 to +709
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transitive closure is not automatically handled by the shared dataflow library for the compatibleTypes predicate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type-compatibility is not transitive. If Base is compatible with Subclass1 and Subclass2 that doesn't mean Subclass1 and Subclass2 are compatible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right.

LGTM 👍

pragma[inline]
predicate compatibleTypes(DataFlowType t1, DataFlowType t2) {
t1 = t2
or
compatibleTypesNonSymRefl(t1, t2)
compatibleTypesWithAny(t1, t2)
or
compatibleTypesWithAny(t2, t1)
or
compatibleTypesNonSymRefl(t2, t1)
compatibleTypes1(t1, t2)
or
compatibleTypes1(t2, t1)
}

predicate forceHighPrecision(Content c) { none() }
Expand Down Expand Up @@ -1061,17 +1084,54 @@ DataFlowCallable viableCallable(DataFlowCall node) {
result.asSourceCallableNotExterns() = node.asImpliedLambdaCall()
}

private DataFlowCall getACallOnThis(DataFlow::ClassNode cls) {
result.asOrdinaryCall() = cls.getAReceiverNode().getAPropertyRead().getACall()
or
result.asAccessorCall() = cls.getAReceiverNode().getAPropertyRead()
or
result.asPartialCall().getACallbackNode() = cls.getAReceiverNode().getAPropertyRead()
}

private predicate downwardCall(DataFlowCall call) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private predicate downwardCall(DataFlowCall call) {
private predicate mayCallDownwards(DataFlowCall call) {

?

exists(DataFlow::ClassNode cls |
call = getACallOnThis(cls) and
viableCallable(call).asSourceCallable() =
cls.getADirectSubClass+().getAnInstanceMember().getFunction()
)
}

/**
* Holds if the set of viable implementations that can be called by `call`
* might be improved by knowing the call context.
*/
predicate mayBenefitFromCallContext(DataFlowCall call) { none() }
predicate mayBenefitFromCallContext(DataFlowCall call) { downwardCall(call) }

/** Gets the type of the receiver of `call`. */
private DataFlowType getThisArgumentType(DataFlowCall call) {
exists(DataFlow::Node node |
isArgumentNodeImpl(node, call, MkThisParameter()) and
result = getNodeType(node)
)
}

/** Gets the type of the 'this' parameter of `call`. */
private DataFlowType getThisParameterType(DataFlowCallable callable) {
exists(DataFlow::Node node |
isParameterNodeImpl(node, callable, MkThisParameter()) and
result = getNodeType(node)
)
}

/**
* Gets a viable dispatch target of `call` in the context `ctx`. This is
* restricted to those `call`s for which a context might make a difference.
*/
DataFlowCallable viableImplInCallContext(DataFlowCall call, DataFlowCall ctx) { none() }
DataFlowCallable viableImplInCallContext(DataFlowCall call, DataFlowCall ctx) {
mayBenefitFromCallContext(call) and
result = viableCallable(call) and
viableCallable(ctx) = call.getEnclosingCallable() and
compatibleTypes(getThisArgumentType(ctx), getThisParameterType(result))
}

bindingset[node, fun]
pragma[inline_late]
Expand Down
6 changes: 6 additions & 0 deletions javascript/ql/src/change-notes/2025-02-17-downward-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: majorAnalysis
---
* Improved call resolution logic to better handle calls resolving "downwards", targeting
a method declared in a subclass of the enclosing class. Data flow analysis
has also improved to avoid spurious flow between unrelated classes in the class hierarchy.
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ accessorCall
| accessors.js:44:1:44:9 | new D().f | accessors.js:37:8:37:13 | (x) {} |
| accessors.js:48:1:48:5 | obj.f | accessors.js:5:8:5:12 | () {} |
| accessors.js:51:1:51:3 | C.f | accessors.js:19:15:19:19 | () {} |
| accessors.js:54:1:54:9 | new D().f | accessors.js:34:8:34:12 | () {} |
| accessors.js:55:1:55:3 | d.f | accessors.js:34:8:34:12 | () {} |
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,23 @@ class AnnotatedCall extends DataFlow::Node {
string getKind() { result = kind }
}

predicate callEdge(AnnotatedCall call, AnnotatedFunction target, int boundArgs) {
predicate callEdge(AnnotatedCall call, Function target, int boundArgs) {
FlowSteps::calls(call, target) and boundArgs = -1
or
FlowSteps::callsBound(call, target, boundArgs)
}

query predicate spuriousCallee(
AnnotatedCall call, AnnotatedFunction target, int boundArgs, string kind
) {
query predicate spuriousCallee(AnnotatedCall call, Function target, int boundArgs, string kind) {
callEdge(call, target, boundArgs) and
kind = call.getKind() and
not (
target = call.getAnExpectedCallee(kind) and
boundArgs = call.getBoundArgsOrMinusOne()
) and
(
target instanceof AnnotatedFunction
or
call.getCallTargetName() = "NONE"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ obj.f();
/** calls:NONE */
C.f();

const d = new D();
/** calls:NONE */
new D().f();
d.f();
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'dummy';

class Base {
workInBase() {
/** calls:methodInBase */
this.methodInBase();

/** calls:methodInSub1 calls:methodInSub2 */
this.methodInSub();

/** calls:overriddenInSub0 calls:overriddenInSub1 calls:overriddenInSub2 */
this.overriddenInSub();
}

/** name:methodInBase */
methodInBase() {
/** calls:methodInSub1 calls:methodInSub2 */
this.methodInSub();
}

/** name:overriddenInSub0 */
overriddenInSub() {
}
}

class Subclass1 extends Base {
workInSub() {
/** calls:methodInBase */
this.methodInBase();

/** calls:overriddenInSub1 */
this.overriddenInSub();
}

/** name:methodInSub1 */
methodInSub() {
}

/** name:overriddenInSub1 */
overriddenInSub() {
}
}

class Subclass2 extends Base {
workInSub() {
/** calls:methodInBase */
this.methodInBase();

/** calls:overriddenInSub2 */
this.overriddenInSub();
}

/** name:methodInSub2 */
methodInSub() {
}

/** name:overriddenInSub2 */
overriddenInSub() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ test_getAFunctionValue
| classes.js:3:10:5:5 | () {\\n ... ;\\n } | classes.js:3:10:5:5 | () {\\n ... ;\\n } |
| classes.js:7:6:9:5 | () {\\n ... ;\\n } | classes.js:7:6:9:5 | () {\\n ... ;\\n } |
| classes.js:8:7:8:16 | this.hello | classes.js:3:10:5:5 | () {\\n ... ;\\n } |
| classes.js:8:7:8:16 | this.hello | classes.js:13:10:15:5 | () {\\n ... ;\\n } |
| classes.js:12:3:16:3 | B | classes.js:12:21:12:20 | (...arg ... rgs); } |
| classes.js:12:3:16:3 | class B ... }\\n } | classes.js:12:21:12:20 | (...arg ... rgs); } |
| classes.js:12:19:12:19 | A | classes.js:2:11:2:10 | () {} |
Expand Down Expand Up @@ -447,6 +448,7 @@ test_getACallee
| a.js:3:1:3:5 | bar() | b.js:2:8:2:24 | function bar() {} |
| a.js:4:1:4:5 | qux() | c.js:2:8:2:24 | function bar() {} |
| classes.js:8:7:8:18 | this.hello() | classes.js:3:10:5:5 | () {\\n ... ;\\n } |
| classes.js:8:7:8:18 | this.hello() | classes.js:13:10:15:5 | () {\\n ... ;\\n } |
| classes.js:12:21:12:20 | super(...args) | classes.js:2:11:2:10 | () {} |
| classes.js:18:3:18:9 | new B() | classes.js:12:21:12:20 | (...arg ... rgs); } |
| classes.js:18:3:18:17 | new B().hello() | classes.js:13:10:15:5 | () {\\n ... ;\\n } |
Expand Down
34 changes: 34 additions & 0 deletions javascript/ql/test/library-tests/TripleDot/subclass.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'dummy';

class Base {
baseMethod(x) {
this.subclassMethod(x);
}
}

class Subclass1 extends Base {
work() {
this.baseMethod(source("sub1"));
}
subclassMethod(x) {
sink(x); // $ hasValueFlow=sub1
}
}

class Subclass2 extends Base {
work() {
this.baseMethod(source("sub2"));
}
subclassMethod(x) {
sink(x); // $ hasValueFlow=sub2
}
}

class Subclass3 extends Base {
work() {
this.baseMethod("safe");
}
subclassMethod(x) {
sink(x);
}
}