diff --git a/javascript/ql/lib/change-notes/2025-04-29-combined-es6-func.md b/javascript/ql/lib/change-notes/2025-04-29-combined-es6-func.md new file mode 100644 index 000000000000..2303d3d8c629 --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-04-29-combined-es6-func.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Improved analysis for `ES6 classes` mixed with `function prototypes`, leading to more accurate call graph resolution. diff --git a/javascript/ql/lib/semmle/javascript/ApiGraphs.qll b/javascript/ql/lib/semmle/javascript/ApiGraphs.qll index 974fdd7c0cbf..af1676086628 100644 --- a/javascript/ql/lib/semmle/javascript/ApiGraphs.qll +++ b/javascript/ql/lib/semmle/javascript/ApiGraphs.qll @@ -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) diff --git a/javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll b/javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll index 2e2313835227..f0d31df2a8f3 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll @@ -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; } /** diff --git a/javascript/ql/lib/semmle/javascript/dataflow/internal/CallGraphs.qll b/javascript/ql/lib/semmle/javascript/dataflow/internal/CallGraphs.qll index 541e3a6f3e90..cc4c883381ea 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/internal/CallGraphs.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/internal/CallGraphs.qll @@ -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() ) } diff --git a/javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/Test.ql b/javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/Test.ql index 4388a2d388d1..82533ba74c4a 100644 --- a/javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/Test.ql +++ b/javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/Test.ql @@ -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) { diff --git a/javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/prototypes.js b/javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/prototypes.js new file mode 100644 index 000000000000..815a9e7f1e77 --- /dev/null +++ b/javascript/ql/test/library-tests/CallGraphs/AnnotatedTest/prototypes.js @@ -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(); +}