Skip to content

Commit 36f99c1

Browse files
authored
Merge pull request #1840 from markshannon/python-better-hasattribute-handling
Python: Add 'hasAttribute' predicate to ObjectInternal and Value.
2 parents cac7758 + 5892ce2 commit 36f99c1

File tree

13 files changed

+103
-7
lines changed

13 files changed

+103
-7
lines changed

python/ql/src/semmle/python/objects/Classes.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ abstract class ClassObjectInternal extends ObjectInternal {
9797
/* Classes aren't usually iterable, but can e.g. Enums */
9898
override ObjectInternal getIterNext() { result = ObjectInternal::unknown() }
9999

100+
override predicate hasAttribute(string name) {
101+
this.getClassDeclaration().declaresAttribute(name)
102+
or
103+
Types::getBase(this, _).hasAttribute(name)
104+
}
105+
100106
}
101107

102108
/** Class representing Python source classes */

python/ql/src/semmle/python/objects/Modules.qll

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ abstract class ModuleObjectInternal extends ObjectInternal {
5959
/* Modules aren't iterable */
6060
override ObjectInternal getIterNext() { none() }
6161

62+
/** Holds if this module "exports" name.
63+
* That is, does it define `name` in `__all__` or is
64+
* `__all__` not defined and `name` a global variable that does not start with "_"
65+
* This is the set of names imported by `from ... import *`.
66+
*/
67+
predicate exports(string name) {
68+
not this.(ModuleObjectInternal).attribute("__all__", _, _) and this.hasAttribute(name)
69+
and not name.charAt(0) = "_"
70+
or
71+
py_exports(this.getSourceModule(), name)
72+
}
73+
6274
}
6375

6476
/** A class representing built-in modules */
@@ -209,6 +221,13 @@ class PackageObjectInternal extends ModuleObjectInternal, TPackageObject {
209221
)
210222
}
211223

224+
/** Holds if this value has the attribute `name` */
225+
override predicate hasAttribute(string name) {
226+
this.getInitModule().hasAttribute(name)
227+
or
228+
exists(this.submodule(name))
229+
}
230+
212231
}
213232

214233
/** A class representing Python modules */
@@ -261,6 +280,24 @@ class PythonModuleObjectInternal extends ModuleObjectInternal, TPythonModule {
261280
result = this.getSourceModule().getEntryNode()
262281
}
263282

283+
/** Holds if this value has the attribute `name` */
284+
override predicate hasAttribute(string name) {
285+
name = "__name__"
286+
or
287+
this.getSourceModule().(ImportTimeScope).definesName(name)
288+
or
289+
exists(ModuleObjectInternal mod, ImportStarNode imp |
290+
PointsToInternal::pointsTo(imp, _, mod, _) and
291+
imp.getScope() = this.getSourceModule() and
292+
mod.exports(name)
293+
)
294+
or
295+
exists(ObjectInternal defined |
296+
this.attribute(name, defined, _) and
297+
not defined instanceof UndefinedInternal
298+
)
299+
}
300+
264301
}
265302

266303
/** A class representing a module that is missing from the DB, but inferred to exists from imports. */

python/ql/src/semmle/python/objects/ObjectAPI.qll

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ class Value extends TObject {
9494
result = this.(ObjectInternal).getName()
9595
}
9696

97+
/** Holds if this value has the attribute `name` */
98+
predicate hasAttribute(string name) {
99+
this.(ObjectInternal).hasAttribute(name)
100+
}
101+
97102
}
98103

99104
/** Class representing modules in the Python program
@@ -111,10 +116,7 @@ class ModuleValue extends Value {
111116
* This is the set of names imported by `from ... import *`.
112117
*/
113118
predicate exports(string name) {
114-
not this.(ModuleObjectInternal).attribute("__all__", _, _) and exists(this.attr(name))
115-
and not name.charAt(0) = "_"
116-
or
117-
py_exports(this.getScope(), name)
119+
PointsTo::moduleExports(this, name)
118120
}
119121

120122
/** Gets the scope for this module, provided that it is a Python module. */

python/ql/src/semmle/python/objects/ObjectInternal.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ class ObjectInternal extends TObject {
182182
*/
183183
abstract ObjectInternal getIterNext();
184184

185+
/** Holds if this value has the attribute `name` */
186+
predicate hasAttribute(string name) {
187+
this.(ObjectInternal).attribute(name, _, _)
188+
}
189+
185190
}
186191

187192

python/ql/src/semmle/python/pointsto/PointsTo.qll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ module PointsTo {
166166
)
167167
}
168168

169+
cached predicate moduleExports(ModuleObjectInternal mod, string name) {
170+
InterModulePointsTo::moduleExportsBoolean(mod, name) = true
171+
}
172+
169173
}
170174

171175
cached module PointsToInternal {

python/ql/src/semmle/python/types/ClassObject.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class ClassObject extends Object {
151151

152152
/** Whether this class has a attribute named `name`, either declared or inherited.*/
153153
predicate hasAttribute(string name) {
154-
Types::getMro(theClass()).getAnItem().getClassDeclaration().declaresAttribute(name)
154+
theClass().hasAttribute(name)
155155
}
156156

157157
/** Whether it is impossible to know all the attributes of this class. Usually because it is

python/ql/src/semmle/python/types/ModuleObject.qll

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,8 @@ abstract class ModuleObject extends Object {
5151
result = this.getAttribute(name)
5252
}
5353

54-
5554
predicate hasAttribute(string name) {
56-
exists(theModule().attr(name))
55+
theModule().hasAttribute(name)
5756
}
5857

5958
predicate attributeRefersTo(string name, Object obj, ControlFlowNode origin) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| decorated_exports.py:3:33:3:45 | Str | The name 'not_defined' is exported by __all__ but is not defined. |
2+
| exports.py:1:57:1:64 | Str | The name 'nosuch' is exported by __all__ but is not defined. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Variables/UndefinedExport.ql

python/ql/test/query-tests/Variables/undefined/UndefinedGlobal.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
| UndefinedGlobal.py:123:5:123:7 | ug3 | This use of global variable 'ug3' may be undefined. |
55
| UninitializedLocal.py:5:13:5:15 | ug1 | This use of global variable 'ug1' may be undefined. |
66
| UninitializedLocal.py:22:9:22:11 | ug1 | This use of global variable 'ug1' may be undefined. |
7+
| decorated_exports.py:10:2:10:19 | undotted_decorator | This use of global variable 'undotted_decorator' may be undefined. |
8+
| decorated_exports.py:14:2:14:13 | not_imported | This use of global variable 'not_imported' may be undefined. |

0 commit comments

Comments
 (0)