From 7e665384456ff80ea61e1600492ec33c3ba9aa6c Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 20 Feb 2026 14:52:14 +0000 Subject: [PATCH 1/4] Python: Port ContainsNonContainer.ql Uses the new `DuckTyping` module to handle recognising whether a class is a container or not. Only trivial test changes (one version uses "class", the other "Class"). Note that the ported query has no understanding of built-in classes. At some point we'll likely want to replace `hasUnresolvedBase` (which will hold for any class that extends a built-in) with something that's aware of the built-in classes. --- .../src/Expressions/ContainsNonContainer.ql | 24 ++++++++----------- .../general/ContainsNonContainer.expected | 4 ++-- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/python/ql/src/Expressions/ContainsNonContainer.ql b/python/ql/src/Expressions/ContainsNonContainer.ql index de8c38795675..a1b989020586 100644 --- a/python/ql/src/Expressions/ContainsNonContainer.ql +++ b/python/ql/src/Expressions/ContainsNonContainer.ql @@ -12,25 +12,21 @@ */ import python -private import LegacyPointsTo +import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.internal.DataFlowDispatch -predicate rhs_in_expr(ControlFlowNode rhs, Compare cmp) { - exists(Cmpop op, int i | cmp.getOp(i) = op and cmp.getComparator(i) = rhs.getNode() | +predicate rhs_in_expr(Expr rhs, Compare cmp) { + exists(Cmpop op, int i | cmp.getOp(i) = op and cmp.getComparator(i) = rhs | op instanceof In or op instanceof NotIn ) } -from - ControlFlowNodeWithPointsTo non_seq, Compare cmp, Value v, ClassValue cls, ControlFlowNode origin +from Compare cmp, DataFlow::LocalSourceNode origin, DataFlow::Node rhs, Class cls where - rhs_in_expr(non_seq, cmp) and - non_seq.pointsTo(_, v, origin) and - v.getClass() = cls and - not Types::failedInference(cls, _) and - not cls.hasAttribute("__contains__") and - not cls.hasAttribute("__iter__") and - not cls.hasAttribute("__getitem__") and - not cls = ClassValue::nonetype() and - not cls = Value::named("types.MappingProxyType") + origin = classInstanceTracker(cls) and + origin.flowsTo(rhs) and + not DuckTyping::isContainer(cls) and + not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and + rhs_in_expr(rhs.asExpr(), cmp) select cmp, "This test may raise an Exception as the $@ may be of non-container class $@.", origin, "target", cls, cls.getName() diff --git a/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected b/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected index cf6d78d0d36b..132852c73f1c 100644 --- a/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected +++ b/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected @@ -1,2 +1,2 @@ -| expressions_test.py:89:8:89:15 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | class XIter | XIter | -| expressions_test.py:91:8:91:19 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | class XIter | XIter | +| expressions_test.py:89:8:89:15 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | Class XIter | XIter | +| expressions_test.py:91:8:91:19 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | Class XIter | XIter | From 73ef50c4d8c2304b235e6a2cb51704ef20dc5d97 Mon Sep 17 00:00:00 2001 From: Taus Date: Mon, 16 Mar 2026 14:36:29 +0000 Subject: [PATCH 2/4] Python: Remove some FPs for ContainsNonContainer.ql First fix handles the case where there's interference from a class-based decorator on a function. In this case, _technically_ we have an instance of the decorator class, but in practice this decorator will (hopefully) forward all accesses to the thing it wraps. The second fix has to do with methods that are added dynamically using `setattr`. In this case, we cannot be sure that the relevant methods are actually missing. --- .../src/Expressions/ContainsNonContainer.ql | 23 +++++++++++ .../Expressions/general/expressions_test.py | 38 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/python/ql/src/Expressions/ContainsNonContainer.ql b/python/ql/src/Expressions/ContainsNonContainer.ql index a1b989020586..ad0a80bb1dd6 100644 --- a/python/ql/src/Expressions/ContainsNonContainer.ql +++ b/python/ql/src/Expressions/ContainsNonContainer.ql @@ -21,12 +21,35 @@ predicate rhs_in_expr(Expr rhs, Compare cmp) { ) } +/** + * Holds if `origin` is the result of applying a class as a decorator to a function. + * Such decorator classes act as proxies, and the runtime value of the decorated + * attribute may be of a different type than the decorator class itself. + */ +predicate isDecoratorApplication(DataFlow::LocalSourceNode origin) { + exists(FunctionExpr fe | origin.asExpr() = fe.getADecoratorCall()) +} + +/** + * Holds if `cls` has methods dynamically added via `setattr`, so we cannot + * statically determine its full interface. + */ +predicate hasDynamicMethods(Class cls) { + exists(CallNode setattr_call | + setattr_call.getFunction().(NameNode).getId() = "setattr" and + setattr_call.getArg(0).(NameNode).getId() = cls.getName() and + setattr_call.getScope() = cls.getScope() + ) +} + from Compare cmp, DataFlow::LocalSourceNode origin, DataFlow::Node rhs, Class cls where origin = classInstanceTracker(cls) and origin.flowsTo(rhs) and not DuckTyping::isContainer(cls) and not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and + not isDecoratorApplication(origin) and + not hasDynamicMethods(cls) and rhs_in_expr(rhs.asExpr(), cmp) select cmp, "This test may raise an Exception as the $@ may be of non-container class $@.", origin, "target", cls, cls.getName() diff --git a/python/ql/test/query-tests/Expressions/general/expressions_test.py b/python/ql/test/query-tests/Expressions/general/expressions_test.py index 5e07b58e2041..cf4c04e0a34e 100644 --- a/python/ql/test/query-tests/Expressions/general/expressions_test.py +++ b/python/ql/test/query-tests/Expressions/general/expressions_test.py @@ -279,3 +279,41 @@ def local(): def apply(f): pass apply(foo)([1]) + +# Class used as a decorator: the runtime value at attribute access is the +# function's return value, not the decorator class instance. +class cached_property(object): + def __init__(self, func): + self.func = func + def __get__(self, obj, cls): + val = self.func(obj) + setattr(obj, self.func.__name__, val) + return val + +class MyForm(object): + @cached_property + def changed_data(self): + return [1, 2, 3] + +def test_decorator_class(form): + f = MyForm() + # OK: cached_property is a descriptor; the actual runtime value is a list. + if "name" in f.changed_data: + pass + +# Class with dynamically added methods via setattr: we cannot statically +# determine its full interface, so we should not flag it. +class DynamicProxy(object): + def __init__(self, args): + self._args = args + +for method_name in ["__contains__", "__iter__", "__len__"]: + def wrapper(self, *args, __method_name=method_name): + pass + setattr(DynamicProxy, method_name, wrapper) + +def test_dynamic_methods(): + proxy = DynamicProxy(()) + # OK: __contains__ is added dynamically via setattr. + if "name" in proxy: + pass From a2ec5b3bc1667a9efe4e5fac19d29ca796f56b76 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 9 Apr 2026 20:22:57 +0000 Subject: [PATCH 3/4] Python: Get rid of `isinstance` FPs Eliminates cases where we explicitly check whether the object in question is an instance of (a subclass of) a built-in container type. --- .../src/Expressions/ContainsNonContainer.ql | 31 +++++++++++++++++++ .../Expressions/general/expressions_test.py | 21 +++++++++++++ 2 files changed, 52 insertions(+) diff --git a/python/ql/src/Expressions/ContainsNonContainer.ql b/python/ql/src/Expressions/ContainsNonContainer.ql index ad0a80bb1dd6..cba1a945ee23 100644 --- a/python/ql/src/Expressions/ContainsNonContainer.ql +++ b/python/ql/src/Expressions/ContainsNonContainer.ql @@ -14,6 +14,7 @@ import python import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.DataFlowDispatch +private import semmle.python.ApiGraphs predicate rhs_in_expr(Expr rhs, Compare cmp) { exists(Cmpop op, int i | cmp.getOp(i) = op and cmp.getComparator(i) = rhs | @@ -42,6 +43,35 @@ predicate hasDynamicMethods(Class cls) { ) } +/** + * Holds if `cls_arg` references a known container builtin type, either directly + * or as an element of a tuple. + */ +private predicate isContainerTypeArg(DataFlow::Node cls_arg) { + cls_arg = + API::builtin(["list", "tuple", "set", "frozenset", "dict", "str", "bytes", "bytearray"]) + .getAValueReachableFromSource() + or + isContainerTypeArg(DataFlow::exprNode(cls_arg.asExpr().(Tuple).getAnElt())) +} + +/** + * Holds if `rhs` is guarded by an `isinstance` check that tests for + * a container type. + */ +predicate guardedByIsinstanceContainer(DataFlow::Node rhs) { + exists( + DataFlow::GuardNode guard, DataFlow::CallCfgNode isinstance_call, DataFlow::LocalSourceNode src + | + isinstance_call = API::builtin("isinstance").getACall() and + src.flowsTo(isinstance_call.getArg(0)) and + src.flowsTo(rhs) and + isContainerTypeArg(isinstance_call.getArg(1)) and + guard = isinstance_call.asCfgNode() and + guard.controlsBlock(rhs.asCfgNode().getBasicBlock(), true) + ) +} + from Compare cmp, DataFlow::LocalSourceNode origin, DataFlow::Node rhs, Class cls where origin = classInstanceTracker(cls) and @@ -50,6 +80,7 @@ where not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and not isDecoratorApplication(origin) and not hasDynamicMethods(cls) and + not guardedByIsinstanceContainer(rhs) and rhs_in_expr(rhs.asExpr(), cmp) select cmp, "This test may raise an Exception as the $@ may be of non-container class $@.", origin, "target", cls, cls.getName() diff --git a/python/ql/test/query-tests/Expressions/general/expressions_test.py b/python/ql/test/query-tests/Expressions/general/expressions_test.py index cf4c04e0a34e..f9d522d328b3 100644 --- a/python/ql/test/query-tests/Expressions/general/expressions_test.py +++ b/python/ql/test/query-tests/Expressions/general/expressions_test.py @@ -317,3 +317,24 @@ def test_dynamic_methods(): # OK: __contains__ is added dynamically via setattr. if "name" in proxy: pass + +# isinstance guard should suppress non-container warning +def guarded_contains(x): + obj = XIter() + if isinstance(obj, dict): + if x in obj: # OK: guarded by isinstance + pass + +def guarded_contains_tuple(x): + obj = XIter() + if isinstance(obj, (list, dict, set)): + if x in obj: # OK: guarded by isinstance with tuple of types + pass + +# Negated isinstance guard: early return when NOT a container +def guarded_contains_negated(x): + obj = XIter() + if not isinstance(obj, dict): + return + if x in obj: # OK: guarded by negated isinstance + early return + pass From c2c96b97a46c7dd75f303820fa2f9dfeaf96eaba Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 10 Apr 2026 21:16:09 +0000 Subject: [PATCH 4/4] Python: Use global data-flow for ContainsNonContainer.ql Replaces the `classTracker`-based approach with one based on global data-flow. To make it easy to share across queries, this is implemented as a parameterised module. The data-flow configuration itself keeps track of two flow states: whether we're tracking a reference to a class or a reference to an instance. --- .../new/internal/ClassInstanceFlow.qll | 118 ++++++++++++++++++ .../src/Expressions/ContainsNonContainer.ql | 86 +++++-------- .../general/ContainsNonContainer.expected | 24 +++- 3 files changed, 169 insertions(+), 59 deletions(-) create mode 100644 python/ql/lib/semmle/python/dataflow/new/internal/ClassInstanceFlow.qll diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/ClassInstanceFlow.qll b/python/ql/lib/semmle/python/dataflow/new/internal/ClassInstanceFlow.qll new file mode 100644 index 000000000000..5558175d04ba --- /dev/null +++ b/python/ql/lib/semmle/python/dataflow/new/internal/ClassInstanceFlow.qll @@ -0,0 +1,118 @@ +/** + * Provides a reusable data-flow configuration for tracking class instances + * through global data-flow with full path support. + * + * This module is designed for quality queries that check whether instances + * of certain classes reach operations that require a specific interface + * (e.g., `__contains__`, `__iter__`, `__hash__`). + * + * The configuration uses two flow states: + * - `TrackingClass`: tracking a reference to the class itself + * - `TrackingInstance`: tracking an instance of the class + * + * At instantiation points (e.g., `cls()`), the state transitions from + * `TrackingClass` to `TrackingInstance`. Sinks are only matched in the + * `TrackingInstance` state. + */ + +private import python +import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.internal.DataFlowDispatch +private import semmle.python.ApiGraphs + +/** A flow state for tracking class references and their instances. */ +abstract class ClassInstanceFlowState extends string { + bindingset[this] + ClassInstanceFlowState() { any() } +} + +/** A state signifying that the tracked value is a reference to the class itself. */ +class TrackingClass extends ClassInstanceFlowState { + TrackingClass() { this = "TrackingClass" } +} + +/** A state signifying that the tracked value is an instance of the class. */ +class TrackingInstance extends ClassInstanceFlowState { + TrackingInstance() { this = "TrackingInstance" } +} + +/** + * Signature module for parameterizing `ClassInstanceFlow` per query. + */ +signature module ClassInstanceFlowSig { + /** Holds if `cls` is a class whose instances should be tracked to sinks. */ + predicate isRelevantClass(Class cls); + + /** Holds if `sink` is a location where reaching instances indicate a violation. */ + predicate isInstanceSink(DataFlow::Node sink); + + /** + * Holds if an `isinstance` check against `checkedType` should act as a barrier, + * suppressing alerts when the instance has been verified to have the expected interface. + */ + predicate isGuardType(DataFlow::Node checkedType); +} + +/** + * Constructs a global data-flow configuration for tracking instances of + * relevant classes from their definition to violation sinks. + */ +module ClassInstanceFlow { + /** + * Holds if `guard` is an `isinstance` call checking `node` against a type + * that should suppress the alert. + */ + private predicate isinstanceGuard(DataFlow::GuardNode guard, ControlFlowNode node, boolean branch) { + exists(DataFlow::CallCfgNode isinstance_call | + isinstance_call = API::builtin("isinstance").getACall() and + isinstance_call.getArg(0).asCfgNode() = node and + ( + Sig::isGuardType(isinstance_call.getArg(1)) + or + // Also handle tuples of types: isinstance(x, (T1, T2)) + Sig::isGuardType(DataFlow::exprNode(isinstance_call.getArg(1).asExpr().(Tuple).getAnElt())) + ) and + guard = isinstance_call.asCfgNode() and + branch = true + ) + } + + private module Config implements DataFlow::StateConfigSig { + class FlowState = ClassInstanceFlowState; + + predicate isSource(DataFlow::Node source, FlowState state) { + exists(ClassExpr ce | + Sig::isRelevantClass(ce.getInnerScope()) and + source.asExpr() = ce and + state instanceof TrackingClass + ) + } + + predicate isSink(DataFlow::Node sink, FlowState state) { + Sig::isInstanceSink(sink) and + state instanceof TrackingInstance + } + + predicate isBarrier(DataFlow::Node node) { + node = DataFlow::BarrierGuard::getABarrierNode() + } + + predicate isAdditionalFlowStep( + DataFlow::Node nodeFrom, FlowState stateFrom, DataFlow::Node nodeTo, FlowState stateTo + ) { + // Instantiation: class reference at the call function position + // flows to the call result as an instance. + stateFrom instanceof TrackingClass and + stateTo instanceof TrackingInstance and + exists(CallNode call | + nodeFrom.asCfgNode() = call.getFunction() and + nodeTo.asCfgNode() = call and + // Exclude decorator applications, where the result is a proxy + // rather than a typical instance. + not call.getNode() = any(FunctionExpr fe).getADecoratorCall() + ) + } + } + + module Flow = DataFlow::GlobalWithState; +} diff --git a/python/ql/src/Expressions/ContainsNonContainer.ql b/python/ql/src/Expressions/ContainsNonContainer.ql index cba1a945ee23..ad2e10de017b 100644 --- a/python/ql/src/Expressions/ContainsNonContainer.ql +++ b/python/ql/src/Expressions/ContainsNonContainer.ql @@ -1,7 +1,7 @@ /** * @name Membership test with a non-container * @description A membership test, such as 'item in sequence', with a non-container on the right hand side will raise a 'TypeError'. - * @kind problem + * @kind path-problem * @tags quality * reliability * correctness @@ -14,6 +14,7 @@ import python import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.DataFlowDispatch +private import semmle.python.dataflow.new.internal.ClassInstanceFlow private import semmle.python.ApiGraphs predicate rhs_in_expr(Expr rhs, Compare cmp) { @@ -22,65 +23,36 @@ predicate rhs_in_expr(Expr rhs, Compare cmp) { ) } -/** - * Holds if `origin` is the result of applying a class as a decorator to a function. - * Such decorator classes act as proxies, and the runtime value of the decorated - * attribute may be of a different type than the decorator class itself. - */ -predicate isDecoratorApplication(DataFlow::LocalSourceNode origin) { - exists(FunctionExpr fe | origin.asExpr() = fe.getADecoratorCall()) -} +module ContainsNonContainerSig implements ClassInstanceFlowSig { + predicate isRelevantClass(Class cls) { + not DuckTyping::isContainer(cls) and + not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and + not exists(CallNode setattr_call | + setattr_call.getFunction().(NameNode).getId() = "setattr" and + setattr_call.getArg(0).(NameNode).getId() = cls.getName() and + setattr_call.getScope() = cls.getScope() + ) + } -/** - * Holds if `cls` has methods dynamically added via `setattr`, so we cannot - * statically determine its full interface. - */ -predicate hasDynamicMethods(Class cls) { - exists(CallNode setattr_call | - setattr_call.getFunction().(NameNode).getId() = "setattr" and - setattr_call.getArg(0).(NameNode).getId() = cls.getName() and - setattr_call.getScope() = cls.getScope() - ) -} + predicate isInstanceSink(DataFlow::Node sink) { rhs_in_expr(sink.asExpr(), _) } -/** - * Holds if `cls_arg` references a known container builtin type, either directly - * or as an element of a tuple. - */ -private predicate isContainerTypeArg(DataFlow::Node cls_arg) { - cls_arg = - API::builtin(["list", "tuple", "set", "frozenset", "dict", "str", "bytes", "bytearray"]) - .getAValueReachableFromSource() - or - isContainerTypeArg(DataFlow::exprNode(cls_arg.asExpr().(Tuple).getAnElt())) + predicate isGuardType(DataFlow::Node checkedType) { + checkedType = + API::builtin(["list", "tuple", "set", "frozenset", "dict", "str", "bytes", "bytearray"]) + .getAValueReachableFromSource() + } } -/** - * Holds if `rhs` is guarded by an `isinstance` check that tests for - * a container type. - */ -predicate guardedByIsinstanceContainer(DataFlow::Node rhs) { - exists( - DataFlow::GuardNode guard, DataFlow::CallCfgNode isinstance_call, DataFlow::LocalSourceNode src - | - isinstance_call = API::builtin("isinstance").getACall() and - src.flowsTo(isinstance_call.getArg(0)) and - src.flowsTo(rhs) and - isContainerTypeArg(isinstance_call.getArg(1)) and - guard = isinstance_call.asCfgNode() and - guard.controlsBlock(rhs.asCfgNode().getBasicBlock(), true) - ) -} +module ContainsNonContainerFlow = ClassInstanceFlow; + +import ContainsNonContainerFlow::Flow::PathGraph -from Compare cmp, DataFlow::LocalSourceNode origin, DataFlow::Node rhs, Class cls +from + ContainsNonContainerFlow::Flow::PathNode source, ContainsNonContainerFlow::Flow::PathNode sink, + ClassExpr ce where - origin = classInstanceTracker(cls) and - origin.flowsTo(rhs) and - not DuckTyping::isContainer(cls) and - not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and - not isDecoratorApplication(origin) and - not hasDynamicMethods(cls) and - not guardedByIsinstanceContainer(rhs) and - rhs_in_expr(rhs.asExpr(), cmp) -select cmp, "This test may raise an Exception as the $@ may be of non-container class $@.", origin, - "target", cls, cls.getName() + ContainsNonContainerFlow::Flow::flowPath(source, sink) and + source.getNode().asExpr() = ce +select sink.getNode(), source, sink, + "This test may raise an Exception as the $@ may be of non-container class $@.", source.getNode(), + "target", ce.getInnerScope(), ce.getInnerScope().getName() diff --git a/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected b/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected index 132852c73f1c..eca57f44333a 100644 --- a/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected +++ b/python/ql/test/query-tests/Expressions/general/ContainsNonContainer.expected @@ -1,2 +1,22 @@ -| expressions_test.py:89:8:89:15 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | Class XIter | XIter | -| expressions_test.py:91:8:91:19 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | Class XIter | XIter | +edges +| expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | expressions_test.py:77:7:77:11 | ControlFlowNode for XIter | provenance | | +| expressions_test.py:77:7:77:11 | ControlFlowNode for XIter | expressions_test.py:88:11:88:15 | ControlFlowNode for XIter | provenance | | +| expressions_test.py:88:5:88:7 | ControlFlowNode for seq | expressions_test.py:89:13:89:15 | ControlFlowNode for seq | provenance | | +| expressions_test.py:88:5:88:7 | ControlFlowNode for seq | expressions_test.py:91:17:91:19 | ControlFlowNode for seq | provenance | | +| expressions_test.py:88:5:88:7 | ControlFlowNode for seq | expressions_test.py:91:17:91:19 | ControlFlowNode for seq | provenance | | +| expressions_test.py:88:11:88:15 | ControlFlowNode for XIter | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | provenance | Config | +| expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | expressions_test.py:88:5:88:7 | ControlFlowNode for seq | provenance | | +nodes +| expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | semmle.label | ControlFlowNode for ClassExpr | +| expressions_test.py:77:7:77:11 | ControlFlowNode for XIter | semmle.label | ControlFlowNode for XIter | +| expressions_test.py:88:5:88:7 | ControlFlowNode for seq | semmle.label | ControlFlowNode for seq | +| expressions_test.py:88:11:88:15 | ControlFlowNode for XIter | semmle.label | ControlFlowNode for XIter | +| expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | semmle.label | ControlFlowNode for XIter() | +| expressions_test.py:89:13:89:15 | ControlFlowNode for seq | semmle.label | ControlFlowNode for seq | +| expressions_test.py:91:17:91:19 | ControlFlowNode for seq | semmle.label | ControlFlowNode for seq | +| expressions_test.py:91:17:91:19 | ControlFlowNode for seq | semmle.label | ControlFlowNode for seq | +subpaths +#select +| expressions_test.py:89:13:89:15 | ControlFlowNode for seq | expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | expressions_test.py:89:13:89:15 | ControlFlowNode for seq | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | target | expressions_test.py:77:1:77:20 | Class XIter | XIter | +| expressions_test.py:91:17:91:19 | ControlFlowNode for seq | expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | expressions_test.py:91:17:91:19 | ControlFlowNode for seq | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | target | expressions_test.py:77:1:77:20 | Class XIter | XIter | +| expressions_test.py:91:17:91:19 | ControlFlowNode for seq | expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | expressions_test.py:91:17:91:19 | ControlFlowNode for seq | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:77:1:77:20 | ControlFlowNode for ClassExpr | target | expressions_test.py:77:1:77:20 | Class XIter | XIter |