Merge pull request #19356 from Napalys/js/merge_classes

JS: Merge `ES6Class` to `FunctionStyleClass`
This commit is contained in:
Napalys Klicius
2025-05-16 10:31:33 +02:00
committed by GitHub
6 changed files with 379 additions and 241 deletions

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Improved analysis for `ES6 classes` mixed with `function prototypes`, leading to more accurate call graph resolution.

View File

@@ -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).getAPrototypeReference()
)
or
nd = MkUse(ref)

View File

@@ -861,21 +861,61 @@ module MemberKind {
*
* Additional patterns can be recognized as class nodes, by extending `DataFlow::ClassNode::Range`.
*/
class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
class ClassNode extends DataFlow::ValueNode, DataFlow::SourceNode {
override AST::ValueNode astNode;
AbstractCallable function;
ClassNode() {
// ES6 class case
astNode instanceof ClassDefinition and
function.(AbstractClass).getClass() = astNode
or
// Function-style class case
astNode instanceof Function and
not astNode = any(ClassDefinition cls).getConstructor().getBody() 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()))
)
)
}
/**
* Gets the unqualified name of the class, if it has one or one can be determined from the context.
*/
string getName() { result = super.getName() }
string getName() {
astNode instanceof ClassDefinition and result = astNode.(ClassDefinition).getName()
or
astNode instanceof Function and result = astNode.(Function).getName()
}
/**
* Gets a description of the class.
*/
string describe() { result = super.describe() }
string describe() {
astNode instanceof ClassDefinition and result = astNode.(ClassDefinition).describe()
or
astNode instanceof Function and result = astNode.(Function).describe()
}
/**
* Gets the constructor function of this class.
*/
FunctionNode getConstructor() { result = super.getConstructor() }
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
}
/**
* Gets an instance method declared in this class, with the given name, if any.
@@ -883,7 +923,7 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
* Does not include methods from superclasses.
*/
FunctionNode getInstanceMethod(string name) {
result = super.getInstanceMember(name, MemberKind::method())
result = this.getInstanceMember(name, MemberKind::method())
}
/**
@@ -893,7 +933,7 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
*
* Does not include methods from superclasses.
*/
FunctionNode getAnInstanceMethod() { result = super.getAnInstanceMember(MemberKind::method()) }
FunctionNode getAnInstanceMethod() { result = this.getAnInstanceMember(MemberKind::method()) }
/**
* Gets the instance method, getter, or setter with the given name and kind.
@@ -901,7 +941,29 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
* Does not include members from superclasses.
*/
FunctionNode getInstanceMember(string name, MemberKind kind) {
result = super.getInstanceMember(name, 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
// Function-style class accessors
astNode instanceof Function and
exists(PropertyAccessor accessor |
accessor = this.getAnAccessor(kind) and
accessor.getName() = name and
result = accessor.getInit().flow()
)
or
kind = MemberKind::method() and
result =
[
this.getConstructor().getReceiver().getAPropertySource(name),
this.getAPrototypeReference().getAPropertySource(name)
]
}
/**
@@ -909,20 +971,52 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
*
* Does not include members from superclasses.
*/
FunctionNode getAnInstanceMember(MemberKind kind) { result = super.getAnInstanceMember(kind) }
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
// Function-style class accessors
astNode instanceof Function and
exists(PropertyAccessor accessor |
accessor = this.getAnAccessor(kind) and
result = accessor.getInit().flow()
)
or
kind = MemberKind::method() and
result =
[
this.getConstructor().getReceiver().getAPropertySource(),
this.getAPrototypeReference().getAPropertySource()
]
}
/**
* Gets an instance method, getter, or setter declared in this class.
*
* Does not include members from superclasses.
*/
FunctionNode getAnInstanceMember() { result = super.getAnInstanceMember(_) }
FunctionNode getAnInstanceMember() { result = this.getAnInstanceMember(_) }
/**
* Gets the static method, getter, or setter declared in this class with the given name and kind.
*/
FunctionNode getStaticMember(string name, MemberKind kind) {
result = super.getStaticMember(name, 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)
}
/**
@@ -935,7 +1029,18 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
/**
* Gets a static method, getter, or setter declared in this class with the given kind.
*/
FunctionNode getAStaticMember(MemberKind kind) { result = super.getAStaticMember(kind) }
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 static method declared in this class.
@@ -944,10 +1049,79 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
*/
FunctionNode getAStaticMethod() { result = this.getAStaticMember(MemberKind::method()) }
/**
* 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(string name, DataFlow::SourceNode root |
result = AccessPath::getAReferenceOrAssignmentTo(root, name + ".prototype").getALocalSource() and
this = AccessPath::getAnAssignmentTo(root, name)
)
or
exists(ExtendCall call |
call.getDestinationOperand() = this.getAPrototypeReference() and
result = call.getASourceOperand()
)
}
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
)
}
/**
* Gets a dataflow node that refers to the superclass of this class.
*/
DataFlow::Node getASuperClassNode() { result = super.getASuperClassNode() }
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)
)
)
}
/**
* Gets a direct super class of this class.
@@ -1136,13 +1310,47 @@ class ClassNode extends DataFlow::SourceNode instanceof ClassNode::Range {
* Gets the type annotation for the field `fieldName`, if any.
*/
TypeAnnotation getFieldTypeAnnotation(string fieldName) {
result = super.getFieldTypeAnnotation(fieldName)
exists(FieldDeclaration field |
field.getDeclaringClass() = astNode and
fieldName = field.getName() and
result = field.getTypeAnnotation()
)
}
/**
* Gets a decorator applied to this class.
*/
DataFlow::Node getADecorator() { result = super.getADecorator() }
DataFlow::Node getADecorator() {
astNode instanceof ClassDefinition and
result = astNode.(ClassDefinition).getADecorator().getExpression().flow()
}
}
/**
* Helper predicate to get a prototype reference in a file.
*/
private DataFlow::PropRef getAPrototypeReferenceInFile(string name, File f) {
result.getBase() = AccessPath::getAReferenceOrAssignmentTo(name) and
result.getPropertyName() = "prototype" and
result.getFile() = f
}
/**
* Helper predicate to get an instantiation in a file.
*/
private DataFlow::NewNode getAnInstantiationInFile(string name, File f) {
result = AccessPath::getAReferenceTo(name).(DataFlow::LocalSourceNode).getAnInstantiation() and
result.getFile() = f
}
/**
* Gets a reference to the function `func`, where there exists a read/write of the "prototype" property on that reference.
*/
pragma[noinline]
private DataFlow::SourceNode getAFunctionValueWithPrototype(AbstractValue func) {
exists(result.getAPropertyReference("prototype")) and
result.analyze().getAValue() = pragma[only_bind_into](func) and
func instanceof AbstractCallable // the join-order goes bad if `func` has type `AbstractFunction`.
}
module ClassNode {
@@ -1214,225 +1422,7 @@ 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
result.getFile() = f
}
pragma[nomagic]
private DataFlow::NewNode getAnInstantiationInFile(string name, File f) {
result = AccessPath::getAReferenceTo(name).(DataFlow::LocalSourceNode).getAnInstantiation() and
result.getFile() = f
}
/**
* Gets a reference to the function `func`, where there exists a read/write of the "prototype" property on that reference.
*/
pragma[noinline]
private DataFlow::SourceNode getAFunctionValueWithPrototype(AbstractValue func) {
exists(result.getAPropertyReference("prototype")) and
result.analyze().getAValue() = pragma[only_bind_into](func) and
func instanceof AbstractFunction // the join-order goes bad if `func` has type `AbstractFunction`.
}
/**
* A function definition, targeted by a `new`-call or with prototype manipulation, seen as a `ClassNode` instance.
*/
class FunctionStyleClass extends Range, DataFlow::ValueNode {
override Function astNode;
AbstractFunction function;
FunctionStyleClass() {
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() { result = astNode.getName() }
override string describe() { result = astNode.describe() }
override FunctionNode getConstructor() { result = this }
private PropertyAccessor getAnAccessor(MemberKind kind) {
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) {
kind = MemberKind::method() and
result = this.getAPrototypeReference().getAPropertySource(name)
or
kind = MemberKind::method() and
result = this.getConstructor().getReceiver().getAPropertySource(name)
or
exists(PropertyAccessor accessor |
accessor = this.getAnAccessor(kind) and
accessor.getName() = name and
result = accessor.getInit().flow()
)
}
override FunctionNode getAnInstanceMember(MemberKind kind) {
kind = MemberKind::method() and
result = this.getAPrototypeReference().getAPropertySource()
or
kind = MemberKind::method() and
result = this.getConstructor().getReceiver().getAPropertySource()
or
exists(PropertyAccessor accessor |
accessor = this.getAnAccessor(kind) and
result = accessor.getInit().flow()
)
}
override FunctionNode getStaticMember(string name, MemberKind kind) {
kind.isMethod() and
result = this.getAPropertySource(name)
}
override FunctionNode getAStaticMember(MemberKind kind) {
kind.isMethod() and
result = this.getAPropertySource()
}
/**
* Gets a reference to the prototype of this class.
*/
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()
)
}
override DataFlow::Node getASuperClassNode() {
// 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)
)
}
}
deprecated class FunctionStyleClass = ClassNode;
}
/**

View File

@@ -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).getAPrototypeReference()
)
}

View File

@@ -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
@@ -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) {

View File

@@ -0,0 +1,142 @@
import 'dummy'
class Baz {
baz() {
console.log("Baz baz");
/** calls:Baz.greet calls:Derived.greet1 calls:BazExtented.greet2 */
this.greet();
}
/** name:Baz.greet */
greet() { console.log("Baz greet"); }
}
/** name:Baz.shout */
Baz.prototype.shout = function() { console.log("Baz shout"); };
/** name:Baz.staticShout */
Baz.staticShout = function() { console.log("Baz staticShout"); };
function foo(baz){
/** calls:Baz.greet */
baz.greet();
/** calls:Baz.shout */
baz.shout();
/** calls:Baz.staticShout */
Baz.staticShout();
}
const baz = new Baz();
foo(baz);
class Derived extends Baz {
/** name:Derived.greet1 */
greet() {
console.log("Derived greet");
super.greet();
}
/** name:Derived.shout1 */
shout() {
console.log("Derived shout");
super.shout();
}
}
function bar(derived){
/** calls:Derived.greet1 */
derived.greet();
/** calls:Derived.shout1 */
derived.shout();
}
bar(new Derived());
class BazExtented {
constructor() {
console.log("BazExtented construct");
}
/** name:BazExtented.greet2 */
greet() {
console.log("BazExtented greet");
/** calls:Baz.greet */
Baz.prototype.greet.call(this);
};
}
BazExtented.prototype = Object.create(Baz.prototype);
BazExtented.prototype.constructor = BazExtented;
BazExtented.staticShout = Baz.staticShout;
/** name:BazExtented.talk */
BazExtented.prototype.talk = function() { console.log("BazExtented talk"); };
/** name:BazExtented.shout2 */
BazExtented.prototype.shout = function() {
console.log("BazExtented shout");
/** calls:Baz.shout */
Baz.prototype.shout.call(this);
};
function barbar(bazExtented){
/** calls:BazExtented.talk */
bazExtented.talk();
/** calls:BazExtented.shout2 */
bazExtented.shout();
/** calls:BazExtented.greet2 */
bazExtented.greet();
/** calls:Baz.staticShout */
BazExtented.staticShout();
}
barbar(new BazExtented());
class Base {
constructor() {
/** calls:Base.read calls:Derived1.read calls:Derived2.read */
this.read();
}
/** name:Base.read */
read() { }
}
class Derived1 extends Base {}
/** name:Derived1.read */
Derived1.prototype.read = function() {};
class Derived2 {}
Derived2.prototype = Object.create(Base.prototype);
/** name:Derived2.read */
Derived2.prototype.read = function() {};
/** name:BanClass.tmpClass */
function tmpClass() {}
function callerClass() {
/** calls:BanClass.tmpClass */
this.tmpClass();
}
class BanClass {
constructor() {
this.tmpClass = tmpClass;
this.callerClass = callerClass;
}
}
/** name:BanProtytpe.tmpPrototype */
function tmpPrototype() {}
function callerPrototype() {
/** calls:BanProtytpe.tmpPrototype */
this.tmpPrototype();
}
function BanProtytpe() {
this.tmpPrototype = tmpPrototype;
this.callerPrototype = callerPrototype;
}
function banInstantiation(){
const instance = new BanProtytpe();
instance.callerPrototype();
}