Skip to content

JS: Merge ES6Class to FunctionStyleClass #19356

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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Improved analysis for `ES6 classes` mixed with `function prototypes`, leading to more accurate call graph resolution.
2 changes: 1 addition & 1 deletion javascript/ql/lib/semmle/javascript/ApiGraphs.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ module API {
exists(DataFlow::ClassNode cls | nd = MkClassInstance(cls) |
ref = cls.getAReceiverNode()
or
ref = cls.(DataFlow::ClassNode::FunctionStyleClass).getAPrototypeReference()
ref = cls.(DataFlow::ClassNode::StandardClassNode).getAPrototypeReference()
)
or
nd = MkUse(ref)
Expand Down
322 changes: 246 additions & 76 deletions javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1214,81 +1214,6 @@ module ClassNode {
DataFlow::Node getADecorator() { none() }
}

/**
* An ES6 class as a `ClassNode` instance.
*/
private class ES6Class extends Range, DataFlow::ValueNode {
override ClassDefinition astNode;

override string getName() { result = astNode.getName() }

override string describe() { result = astNode.describe() }

override FunctionNode getConstructor() { result = astNode.getConstructor().getBody().flow() }

override FunctionNode getInstanceMember(string name, MemberKind kind) {
exists(MethodDeclaration method |
method = astNode.getMethod(name) and
not method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
kind = MemberKind::method() and
result = this.getConstructor().getReceiver().getAPropertySource(name)
}

override FunctionNode getAnInstanceMember(MemberKind kind) {
exists(MethodDeclaration method |
method = astNode.getAMethod() and
not method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
kind = MemberKind::method() and
result = this.getConstructor().getReceiver().getAPropertySource()
}

override FunctionNode getStaticMember(string name, MemberKind kind) {
exists(MethodDeclaration method |
method = astNode.getMethod(name) and
method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
kind.isMethod() and
result = this.getAPropertySource(name)
}

override FunctionNode getAStaticMember(MemberKind kind) {
exists(MethodDeclaration method |
method = astNode.getAMethod() and
method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
kind.isMethod() and
result = this.getAPropertySource()
}

override DataFlow::Node getASuperClassNode() { result = astNode.getSuperClass().flow() }

override TypeAnnotation getFieldTypeAnnotation(string fieldName) {
exists(FieldDeclaration field |
field.getDeclaringClass() = astNode and
fieldName = field.getName() and
result = field.getTypeAnnotation()
)
}

override DataFlow::Node getADecorator() {
result = astNode.getADecorator().getExpression().flow()
}
}

private DataFlow::PropRef getAPrototypeReferenceInFile(string name, File f) {
result.getBase() = AccessPath::getAReferenceOrAssignmentTo(name) and
result.getPropertyName() = "prototype" and
Expand All @@ -1314,7 +1239,7 @@ module ClassNode {
/**
* A function definition, targeted by a `new`-call or with prototype manipulation, seen as a `ClassNode` instance.
*/
class FunctionStyleClass extends Range, DataFlow::ValueNode {
deprecated class FunctionStyleClass extends Range, DataFlow::ValueNode {
override Function astNode;
AbstractFunction function;

Expand Down Expand Up @@ -1433,6 +1358,251 @@ module ClassNode {
)
}
}

/**
* A function definition, targeted by a `new`-call or with prototype manipulation, seen as a `ClassNode` instance.
* Or An ES6 class as a `ClassNode` instance.
*/
class StandardClassNode extends Range, DataFlow::ValueNode {
override AST::ValueNode astNode;
AbstractCallable function;

StandardClassNode() {
// ES6 class case
astNode instanceof ClassDefinition and
function.(AbstractClass).getClass() = astNode
or
// Function-style class case
astNode instanceof Function and
function.getFunction() = astNode and
(
exists(getAFunctionValueWithPrototype(function))
or
function = any(NewNode new).getCalleeNode().analyze().getAValue()
or
exists(string name | this = AccessPath::getAnAssignmentTo(name) |
exists(getAPrototypeReferenceInFile(name, this.getFile()))
or
exists(getAnInstantiationInFile(name, this.getFile()))
)
)
}

override string getName() {
astNode instanceof ClassDefinition and result = astNode.(ClassDefinition).getName()
or
astNode instanceof Function and result = astNode.(Function).getName()
}

override string describe() {
astNode instanceof ClassDefinition and result = astNode.(ClassDefinition).describe()
or
astNode instanceof Function and result = astNode.(Function).describe()
}

override FunctionNode getConstructor() {
// For ES6 classes
astNode instanceof ClassDefinition and
result = astNode.(ClassDefinition).getConstructor().getBody().flow()
or
// For function-style classes
astNode instanceof Function and result = this
}

private PropertyAccessor getAnAccessor(MemberKind kind) {
// Only applies to function-style classes
astNode instanceof Function and
result.getObjectExpr() = this.getAPrototypeReference().asExpr() and
(
kind = MemberKind::getter() and
result instanceof PropertyGetter
or
kind = MemberKind::setter() and
result instanceof PropertySetter
)
}

override FunctionNode getInstanceMember(string name, MemberKind kind) {
// ES6 class methods
exists(MethodDeclaration method |
astNode instanceof ClassDefinition and
method = astNode.(ClassDefinition).getMethod(name) and
not method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
// ES6 class property in constructor
astNode instanceof ClassDefinition and
kind = MemberKind::method() and
exists(ThisNode receiver |
receiver = this.getConstructor().getReceiver() and
receiver.hasPropertyWrite(name, result)
)
or
// Function-style class methods via prototype
kind = MemberKind::method() and
exists(DataFlow::SourceNode proto |
proto = this.getAPrototypeReference() and
proto.hasPropertyWrite(name, result)
)
or
// Function-style class methods via constructor
astNode instanceof Function and
kind = MemberKind::method() and
exists(ThisNode receiver |
receiver = this.getConstructor().getReceiver() and
receiver.hasPropertyWrite(name, result)
)
or
// Function-style class accessors
astNode instanceof Function and
exists(PropertyAccessor accessor |
accessor = this.getAnAccessor(kind) and
accessor.getName() = name and
result = accessor.getInit().flow()
)
}

override FunctionNode getAnInstanceMember(MemberKind kind) {
// ES6 class methods
exists(MethodDeclaration method |
astNode instanceof ClassDefinition and
method = astNode.(ClassDefinition).getAMethod() and
not method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
// ES6 class property in constructor
astNode instanceof ClassDefinition and
kind = MemberKind::method() and
exists(ThisNode receiver |
receiver = this.getConstructor().getReceiver() and
result = receiver.getAPropertySource()
)
or
// Function-style class methods via prototype
kind = MemberKind::method() and
exists(DataFlow::SourceNode proto |
proto = this.getAPrototypeReference() and
result = proto.getAPropertySource()
)
or
// Function-style class methods via constructor
astNode instanceof Function and
kind = MemberKind::method() and
exists(ThisNode receiver |
receiver = this.getConstructor().getReceiver() and
result = receiver.getAPropertySource()
)
or
// Function-style class accessors
astNode instanceof Function and
exists(PropertyAccessor accessor |
accessor = this.getAnAccessor(kind) and
result = accessor.getInit().flow()
)
}

override FunctionNode getStaticMember(string name, MemberKind kind) {
exists(MethodDeclaration method |
astNode instanceof ClassDefinition and
method = astNode.(ClassDefinition).getMethod(name) and
method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
kind.isMethod() and
result = this.getAPropertySource(name)
}

override FunctionNode getAStaticMember(MemberKind kind) {
exists(MethodDeclaration method |
astNode instanceof ClassDefinition and
method = astNode.(ClassDefinition).getAMethod() and
method.isStatic() and
kind = MemberKind::of(method) and
result = method.getBody().flow()
)
or
kind.isMethod() and
result = this.getAPropertySource()
}

/**
* Gets a reference to the prototype of this class.
* Only applies to function-style classes.
*/
DataFlow::SourceNode getAPrototypeReference() {
exists(DataFlow::SourceNode base | base = getAFunctionValueWithPrototype(function) |
result = base.getAPropertyRead("prototype")
or
result = base.getAPropertySource("prototype")
)
or
exists(string name |
this = AccessPath::getAnAssignmentTo(name) and
result = getAPrototypeReferenceInFile(name, this.getFile())
)
or
exists(ExtendCall call |
call.getDestinationOperand() = this.getAPrototypeReference() and
result = call.getASourceOperand()
)
or
exists(DataFlow::PropRead protoRead |
protoRead.getPropertyName() = "prototype" and
protoRead.getBase().analyze().getAValue() = this.analyze().getAValue() and
result = protoRead
)
}

override DataFlow::Node getASuperClassNode() {
// ES6 class superclass
astNode instanceof ClassDefinition and
result = astNode.(ClassDefinition).getSuperClass().flow()
or
(
// C.prototype = Object.create(D.prototype)
exists(DataFlow::InvokeNode objectCreate, DataFlow::PropRead superProto |
this.getAPropertySource("prototype") = objectCreate and
objectCreate = DataFlow::globalVarRef("Object").getAMemberCall("create") and
superProto.flowsTo(objectCreate.getArgument(0)) and
superProto.getPropertyName() = "prototype" and
result = superProto.getBase()
)
or
// C.prototype = new D()
exists(DataFlow::NewNode newCall |
this.getAPropertySource("prototype") = newCall and
result = newCall.getCalleeNode()
)
or
// util.inherits(C, D);
exists(DataFlow::CallNode inheritsCall |
inheritsCall = DataFlow::moduleMember("util", "inherits").getACall()
|
this = inheritsCall.getArgument(0).getALocalSource() and
result = inheritsCall.getArgument(1)
)
)
}

override TypeAnnotation getFieldTypeAnnotation(string fieldName) {
exists(FieldDeclaration field |
field.getDeclaringClass() = astNode and
fieldName = field.getName() and
result = field.getTypeAnnotation()
)
}

override DataFlow::Node getADecorator() {
astNode instanceof ClassDefinition and
result = astNode.(ClassDefinition).getADecorator().getExpression().flow()
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ module CallGraph {
not exists(DataFlow::ClassNode cls |
node = cls.getConstructor().getReceiver()
or
node = cls.(DataFlow::ClassNode::FunctionStyleClass).getAPrototypeReference()
node = cls.(DataFlow::ClassNode::StandardClassNode).getAPrototypeReference()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class AnnotatedCall extends DataFlow::Node {

AnnotatedCall() {
this instanceof DataFlow::InvokeNode and
calls = getAnnotation(this.asExpr(), kind) and
calls = getAnnotation(this.getEnclosingExpr(), kind) and
kind = "calls"
or
this instanceof DataFlow::PropRef and
Expand Down Expand Up @@ -79,12 +79,14 @@ query predicate spuriousCallee(AnnotatedCall call, Function target, int boundArg
}

query predicate missingCallee(
AnnotatedCall call, AnnotatedFunction target, int boundArgs, string kind
InvokeExpr invoke, AnnotatedFunction target, int boundArgs, string kind
) {
not callEdge(call, target, boundArgs) and
kind = call.getKind() and
target = call.getAnExpectedCallee(kind) and
boundArgs = call.getBoundArgsOrMinusOne()
forex(AnnotatedCall call | call.getEnclosingExpr() = invoke |
not callEdge(call, target, boundArgs) and
kind = call.getKind() and
target = call.getAnExpectedCallee(kind) and
boundArgs = call.getBoundArgsOrMinusOne()
)
}

query predicate badAnnotation(string name) {
Expand Down
Loading