From d1cf7f0624819bd653f335f58c7a90c9110c609a Mon Sep 17 00:00:00 2001 From: Taus Date: Wed, 4 Jun 2025 14:40:07 +0000 Subject: [PATCH 1/3] Python: Support type annotations in call graph Adds support for tracking instances via type annotations. Also adds a convenience method to the newly added `Annotation` class, `getAnnotatedExpression`, that returns the expression that is annotated with the given type. For return annotations this is any value returned from the annotated function in question. Co-authored-by: Napalys Klicius --- python/ql/lib/semmle/python/Exprs.qll | 11 +++++++++++ .../python/dataflow/new/internal/DataFlowDispatch.qll | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/python/ql/lib/semmle/python/Exprs.qll b/python/ql/lib/semmle/python/Exprs.qll index accc370481aa..9e00e4f794b3 100644 --- a/python/ql/lib/semmle/python/Exprs.qll +++ b/python/ql/lib/semmle/python/Exprs.qll @@ -762,6 +762,17 @@ class Annotation extends Expr { or this = any(FunctionExpr f).getReturns() } + + /** Gets the expression that this annotation annotates. */ + Expr getAnnotatedExpression() { + result = any(AnnAssign a | a.getAnnotation() = this).getTarget() + or + result = any(Parameter p | p.getAnnotation() = this) + or + exists(FunctionExpr f | + this = f.getReturns() and result = f.getInnerScope().getReturnNode().getNode() + ) + } } /* Expression Contexts */ diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll index 1a38593bce48..781023a9658b 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll @@ -580,6 +580,11 @@ private module TrackClassInstanceInput implements CallGraphConstruction::Simple: class State = Class; predicate start(Node start, Class cls) { + exists(Annotation ann | + ann = classTracker(cls).asExpr() and + start.asExpr() = ann.getAnnotatedExpression() + ) + or resolveClassCall(start.(CallCfgNode).asCfgNode(), cls) or // result of `super().__new__` as used in a `__new__` method implementation From 2c45550a9f5d1b775fa2d17e74fd6bb3a2699380 Mon Sep 17 00:00:00 2001 From: Taus Date: Wed, 4 Jun 2025 14:44:32 +0000 Subject: [PATCH 2/3] Python: Add change note Co-authored-by: Napalys Klicius --- .../change-notes/2025-06-04-call-graph-type-annotations.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 python/ql/lib/change-notes/2025-06-04-call-graph-type-annotations.md diff --git a/python/ql/lib/change-notes/2025-06-04-call-graph-type-annotations.md b/python/ql/lib/change-notes/2025-06-04-call-graph-type-annotations.md new file mode 100644 index 000000000000..2aa17e576326 --- /dev/null +++ b/python/ql/lib/change-notes/2025-06-04-call-graph-type-annotations.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Type annotations such as `foo : Bar` are now treated by the call graph as an indication that `foo` may be an instance of `Bar`. From c6c6a857df244e5e83742bf2e020f2ca17f06eae Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 11 Jul 2025 12:03:00 +0000 Subject: [PATCH 3/3] Python: Add tests Also fixes an issue with the return type annotations that caused these to not work properly. Currently, annotated assignments don't work properly, due to the fact that our flow relation doesn't consider flow going to the "type" part of an annotated assignment. This means that in `x : Foo`, we do correctly note that `x` is annotated with `Foo`, but we have no idea what `Foo` is, since it has no incoming flow. To fix this we should probably just extend the flow relation, but this may need to be done with some care, so I have left it as future work. --- python/ql/lib/semmle/python/Exprs.qll | 4 +-- .../InlineCallGraphTest.expected | 6 ++++ .../InlineCallGraphTest.qlref | 1 + .../type_annotations.py | 33 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.expected create mode 100644 python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.qlref create mode 100644 python/ql/test/experimental/library-tests/CallGraph-type-annotations/type_annotations.py diff --git a/python/ql/lib/semmle/python/Exprs.qll b/python/ql/lib/semmle/python/Exprs.qll index 9e00e4f794b3..a7f67b0b80ea 100644 --- a/python/ql/lib/semmle/python/Exprs.qll +++ b/python/ql/lib/semmle/python/Exprs.qll @@ -769,8 +769,8 @@ class Annotation extends Expr { or result = any(Parameter p | p.getAnnotation() = this) or - exists(FunctionExpr f | - this = f.getReturns() and result = f.getInnerScope().getReturnNode().getNode() + exists(FunctionExpr f, Return r | + this = f.getReturns() and r.getScope() = f.getInnerScope() and result = r.getValue() ) } } diff --git a/python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.expected b/python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.expected new file mode 100644 index 000000000000..a08ad78be2e1 --- /dev/null +++ b/python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.expected @@ -0,0 +1,6 @@ +testFailures +debug_callableNotUnique +pointsTo_found_typeTracker_notFound +typeTracker_found_pointsTo_notFound +| type_annotations.py:6:5:6:14 | ControlFlowNode for Attribute() | Foo.method | +| type_annotations.py:16:5:16:14 | ControlFlowNode for Attribute() | Foo.method | diff --git a/python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.qlref b/python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.qlref new file mode 100644 index 000000000000..25117a4582bd --- /dev/null +++ b/python/ql/test/experimental/library-tests/CallGraph-type-annotations/InlineCallGraphTest.qlref @@ -0,0 +1 @@ +../CallGraph/InlineCallGraphTest.ql diff --git a/python/ql/test/experimental/library-tests/CallGraph-type-annotations/type_annotations.py b/python/ql/test/experimental/library-tests/CallGraph-type-annotations/type_annotations.py new file mode 100644 index 000000000000..51c08d1675e2 --- /dev/null +++ b/python/ql/test/experimental/library-tests/CallGraph-type-annotations/type_annotations.py @@ -0,0 +1,33 @@ +class Foo: + def method(self): + pass + +def test_parameter_annotation(x: Foo): + x.method() #$ tt=Foo.method + +def test_no_parameter_annotation(x): + x.method() + +def function_with_return_annotation() -> Foo: + return eval("Foo()") + +def test_return_annotation(): + x = function_with_return_annotation() #$ pt,tt=function_with_return_annotation + x.method() #$ tt=Foo.method + +def function_without_return_annotation(): + return eval("Foo()") + +def test_no_return_annotation(): + x = function_without_return_annotation() #$ pt,tt=function_without_return_annotation + x.method() + +def test_variable_annotation(): + x = eval("Foo()") + x : Foo + # Currently fails because there is no flow from the class definition to the type annotation. + x.method() #$ MISSING: tt=Foo.method + +def test_no_variable_annotation(): + x = eval("Foo()") + x.method()