@@ -1977,3 +1977,185 @@ private module OutNodes {
19771977 * `kind`.
19781978 */
19791979OutNode getAnOutNode ( DataFlowCall call , ReturnKind kind ) { call = result .getCall ( kind ) }
1980+
1981+ /**
1982+ * Provides predicates for approximating type properties of user-defined classes
1983+ * based on their structure (method declarations, base classes).
1984+ *
1985+ * This module should _not_ be used in the call graph computation itself, as parts of it may depend
1986+ * on layers that themselves build upon the call graph (e.g. API graphs).
1987+ */
1988+ module DuckTyping {
1989+ private import semmle.python.ApiGraphs
1990+
1991+ /**
1992+ * Holds if `cls` or any of its resolved superclasses declares a method with the given `name`.
1993+ */
1994+ predicate hasMethod ( Class cls , string name ) {
1995+ cls .getAMethod ( ) .getName ( ) = name
1996+ or
1997+ hasMethod ( getADirectSuperclass ( cls ) , name )
1998+ }
1999+
2000+ /**
2001+ * Holds if `cls` has a base class that cannot be resolved to a user-defined class
2002+ * and is not just `object`, meaning it may inherit methods from an unknown class.
2003+ */
2004+ predicate hasUnresolvedBase ( Class cls ) {
2005+ exists ( Expr base | base = cls .getABase ( ) |
2006+ not base = classTracker ( _) .asExpr ( ) and
2007+ not base = API:: builtin ( "object" ) .getAValueReachableFromSource ( ) .asExpr ( )
2008+ )
2009+ }
2010+
2011+ /**
2012+ * Holds if `cls` supports the container protocol, i.e. it declares
2013+ * `__contains__`, `__iter__`, or `__getitem__`.
2014+ */
2015+ predicate isContainer ( Class cls ) {
2016+ hasMethod ( cls , "__contains__" ) or
2017+ hasMethod ( cls , "__iter__" ) or
2018+ hasMethod ( cls , "__getitem__" )
2019+ }
2020+
2021+ /**
2022+ * Holds if `cls` supports the iterable protocol, i.e. it declares
2023+ * `__iter__` or `__getitem__`.
2024+ */
2025+ predicate isIterable ( Class cls ) {
2026+ hasMethod ( cls , "__iter__" ) or
2027+ hasMethod ( cls , "__getitem__" )
2028+ }
2029+
2030+ /**
2031+ * Holds if `cls` supports the iterator protocol, i.e. it declares
2032+ * both `__iter__` and `__next__`.
2033+ */
2034+ predicate isIterator ( Class cls ) {
2035+ hasMethod ( cls , "__iter__" ) and
2036+ hasMethod ( cls , "__next__" )
2037+ }
2038+
2039+ /**
2040+ * Holds if `cls` supports the context manager protocol, i.e. it declares
2041+ * both `__enter__` and `__exit__`.
2042+ */
2043+ predicate isContextManager ( Class cls ) {
2044+ hasMethod ( cls , "__enter__" ) and
2045+ hasMethod ( cls , "__exit__" )
2046+ }
2047+
2048+ /**
2049+ * Holds if `cls` supports the descriptor protocol, i.e. it declares
2050+ * `__get__`, `__set__`, or `__delete__`.
2051+ */
2052+ predicate isDescriptor ( Class cls ) {
2053+ hasMethod ( cls , "__get__" ) or
2054+ hasMethod ( cls , "__set__" ) or
2055+ hasMethod ( cls , "__delete__" )
2056+ }
2057+
2058+ /**
2059+ * Holds if `cls` directly assigns to an attribute named `name` in its class body.
2060+ * This covers attribute assignments like `x = value`, but not method definitions.
2061+ */
2062+ predicate declaresAttribute ( Class cls , string name ) { exists ( getAnAttributeValue ( cls , name ) ) }
2063+
2064+ /**
2065+ * Gets the value expression assigned to attribute `name` directly in the class body of `cls`.
2066+ */
2067+ Expr getAnAttributeValue ( Class cls , string name ) {
2068+ exists ( Assign a |
2069+ a .getScope ( ) = cls and
2070+ a .getATarget ( ) .( Name ) .getId ( ) = name and
2071+ result = a .getValue ( )
2072+ )
2073+ }
2074+
2075+ /**
2076+ * Holds if `cls` is callable, i.e. it declares `__call__`.
2077+ */
2078+ predicate isCallable ( Class cls ) { hasMethod ( cls , "__call__" ) }
2079+
2080+ /**
2081+ * Holds if `cls` supports the mapping protocol, i.e. it declares
2082+ * `__getitem__` and `keys`, or `__getitem__` and `__iter__`.
2083+ */
2084+ predicate isMapping ( Class cls ) {
2085+ hasMethod ( cls , "__getitem__" ) and
2086+ ( hasMethod ( cls , "keys" ) or hasMethod ( cls , "__iter__" ) )
2087+ }
2088+
2089+ /**
2090+ * Holds if `cls` is a new-style class. In Python 3, all classes are new-style.
2091+ * In Python 2, a class is new-style if it (transitively) inherits from `object`,
2092+ * or has a declared `__metaclass__`, or is in a module with a module-level
2093+ * `__metaclass__` declaration, or has an unresolved base class.
2094+ */
2095+ predicate isNewStyle ( Class cls ) {
2096+ major_version ( ) = 3
2097+ or
2098+ major_version ( ) = 2 and
2099+ (
2100+ cls .getABase ( ) = API:: builtin ( "object" ) .getAValueReachableFromSource ( ) .asExpr ( )
2101+ or
2102+ isNewStyle ( getADirectSuperclass ( cls ) )
2103+ or
2104+ hasUnresolvedBase ( cls )
2105+ or
2106+ exists ( cls .getMetaClass ( ) )
2107+ or
2108+ // Module-level __metaclass__ = type makes all classes in the module new-style
2109+ exists ( Assign a |
2110+ a .getScope ( ) = cls .getEnclosingModule ( ) and
2111+ a .getATarget ( ) .( Name ) .getId ( ) = "__metaclass__" and
2112+ a .getValue ( ) = API:: builtin ( "type" ) .getAValueReachableFromSource ( ) .asExpr ( )
2113+ )
2114+ )
2115+ }
2116+
2117+ /**
2118+ * Gets the `__init__` function that will be invoked when `cls` is constructed,
2119+ * resolved according to the MRO.
2120+ */
2121+ Function getInit ( Class cls ) { result = invokedFunctionFromClassConstruction ( cls , "__init__" ) }
2122+
2123+ /**
2124+ * Holds if `cls` or any of its superclasses uses multiple inheritance, or
2125+ * has an unresolved base class. In these cases, our MRO approximation may
2126+ * resolve to the wrong `__init__`, so we should not flag argument mismatches.
2127+ */
2128+ predicate hasUnreliableMro ( Class cls ) {
2129+ exists ( Class sup | sup = getADirectSuperclass * ( cls ) |
2130+ exists ( sup .getBase ( 1 ) )
2131+ or
2132+ hasUnresolvedBase ( sup )
2133+ )
2134+ }
2135+
2136+ /**
2137+ * Holds if `f` overrides a method in a superclass with the same name.
2138+ */
2139+ predicate overridesMethod ( Function f ) { overridesMethod ( f , _, _) }
2140+
2141+ /**
2142+ * Holds if `f` overrides `overridden` declared in `superclass`.
2143+ */
2144+ predicate overridesMethod ( Function f , Class superclass , Function overridden ) {
2145+ exists ( Class cls |
2146+ f .getScope ( ) = cls and
2147+ superclass = getADirectSuperclass + ( cls ) and
2148+ overridden = superclass .getMethod ( f .getName ( ) )
2149+ )
2150+ }
2151+
2152+ /**
2153+ * Holds if `f` is a property accessor (decorated with `@property`, `@name.setter`,
2154+ * or `@name.deleter`).
2155+ */
2156+ predicate isPropertyAccessor ( Function f ) {
2157+ exists ( Attribute a | a = f .getADecorator ( ) | a .getName ( ) = "setter" or a .getName ( ) = "deleter" )
2158+ or
2159+ f .getADecorator ( ) .( Name ) .getId ( ) = "property"
2160+ }
2161+ }
0 commit comments