-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
a61d42e
JS: Make inline CG tests report call target if NONE was given
asgerf 4043765
JS: Avoid ambiguity in an inline CG annotation
asgerf 9321d69
JS: Add CG test showing lack of calls down to subclasses
asgerf aff458d
JS: Also add tests for upward calls and overriding
asgerf b8b2b9a
JS: Resolve calls downward in the class hierarchy
asgerf d3c4b5d
JS: Add test with spurious flow due to up-down calls
asgerf ff7bc7c
JS: Track types of classes in data flow
asgerf ab5fc9f
JS: Implement viableImplInCallContext
asgerf 97eb09f
JS: Accept updated test output
asgerf b8f48aa
JS: Change note
asgerf 24e7aad
JS: Overriden -> Overridden
asgerf e40ee82
JS: Update a qldoc comment
asgerf ad4522c
JS: Make 'typeStrongerThan' transitive
asgerf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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" | ||||||
} | ||||||
|
||||||
|
@@ -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) { | ||||||
|
@@ -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()) | ||||||
|
@@ -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() | ||||||
} | ||||||
|
||||||
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() } | ||||||
|
@@ -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) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
? |
||||||
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] | ||||||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,5 +50,6 @@ obj.f(); | |
/** calls:NONE */ | ||
C.f(); | ||
|
||
const d = new D(); | ||
/** calls:NONE */ | ||
new D().f(); | ||
d.f(); |
60 changes: 60 additions & 0 deletions
60
javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/subclasses.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 withSubclass1
andSubclass2
that doesn't meanSubclass1
andSubclass2
are compatible.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, right.
LGTM 👍