Skip to content

Commit 6317546

Browse files
authored
Merge pull request #493 from markshannon/python-queries
Initial commit of Python queries and QL libraries.
2 parents 165bb8b + a135e46 commit 6317546

File tree

728 files changed

+63546
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

728 files changed

+63546
-0
lines changed

.lgtm.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ path_classifiers:
1010
- javascript/extractor/tests
1111
- javascript/ql/src
1212
- javascript/ql/test
13+
- python/ql/src
14+
- python/ql/test
1315

1416
queries:
1517
- include: "*"

python/ql/src/.project

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<projectDescription>
3+
<name>semmlecode-python-queries</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.python.pydev.PyDevBuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
</buildSpec>
14+
<natures>
15+
<nature>org.python.pydev.pythonNature</nature>
16+
<nature>com.semmle.plugin.qdt.core.qlnature</nature>
17+
</natures>
18+
</projectDescription>

python/ql/src/.qlpath

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<ns:qlpath xmlns:ns="https://semmle.com/schemas/qlpath">
3+
<librarypath/>
4+
<dbscheme>/semmlecode-python-queries/semmlecode.python.dbscheme</dbscheme>
5+
<defaultImports><defaultImport>python</defaultImport></defaultImports>
6+
</ns:qlpath>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
class TCPServer(object):
3+
4+
def process_request(self, request, client_address):
5+
self.do_work(request, client_address)
6+
self.shutdown_request(request)
7+
8+
9+
class ThreadingMixIn:
10+
"""Mix-in class to handle each request in a new thread."""
11+
12+
def process_request(self, request, client_address):
13+
"""Start a new thread to process the request."""
14+
t = threading.Thread(target = self.do_work, args = (request, client_address))
15+
t.daemon = self.daemon_threads
16+
t.start()
17+
18+
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
When a class subclasses multiple base classes, attribute lookup is performed from left to right amongst the base classes.
9+
This form of attribute lookup is called "method resolution order" and is a solution to the
10+
<a href="http://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem">diamond inheritance problem</a> where several base classes
11+
override a method in a shared superclass.
12+
</p>
13+
<p>
14+
Unfortunately, this means that if more than one base class defines the same attribute, the leftmost base class will effectively override
15+
the attribute of the rightmost base class, even though the leftmost base class is not a subclass of the rightmost base class.
16+
Unless the methods in question are designed for inheritance, using <code>super</code>, then this implicit overriding may not be the desired behavior.
17+
Even if it is the desired behavior it makes the code hard to understand and maintain.
18+
</p>
19+
20+
</overview>
21+
<recommendation>
22+
<p>There are a number of ways that might be used to address this issue:
23+
</p><ul>
24+
<li>Override the attribute in the subclass to implement the correct behavior.</li>
25+
<li>Modify the class hierarchy and move equivalent or redundant methods to a common super class.</li>
26+
<li>Modify the method hierarchy, breaking up complex methods into constituent parts.</li>
27+
<li>Use delegation rather than inheritance.</li>
28+
</ul>
29+
30+
</recommendation>
31+
<example>
32+
33+
<p>
34+
In this example the class <code>ThreadingTCPServer</code> inherits from <code>ThreadingMixIn</code> and from <code>TCPServer</code>.
35+
However, both these classes implement <code>process_request</code> which means that <code>ThreadingTCPServer</code> will inherit
36+
<code>process_request</code> from <code>ThreadingMixIn</code>. Consequently, the implementation of <code>process_request</code> in <code>TCPServer</code>
37+
will be ignored, which may not be the correct behavior.
38+
</p>
39+
<sample src="ConflictingAttributesInBaseClasses.py" />
40+
41+
<p>
42+
This can be fixed either by overriding the method, as shown in class <code>ThreadingTCPServerOverriding</code>
43+
or by ensuring that the
44+
functionality provided by the two base classes does not overlap, as shown in class <code>ThreadingTCPServerChangedHierarchy</code>.
45+
</p>
46+
<sample src="ConflictingAttributesInBaseClasses_Fixed.py" />
47+
48+
49+
</example>
50+
<references>
51+
52+
<li>Python Language Reference: <a href="https://docs.python.org/2/reference/datamodel.html">Data model</a>.</li>
53+
<li>Python releases: <a href="https://www.python.org/download/releases/2.3/mro/">The Python 2.3 Method Resolution Order</a>.</li>
54+
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/C3_linearization">C3 linearization</a>.</li>
55+
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Composition_over_inheritance">Composition over inheritance</a>.</li>
56+
57+
58+
</references>
59+
</qhelp>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @name Conflicting attributes in base classes
3+
* @description When a class subclasses multiple base classes and more than one base class defines the same attribute, attribute overriding may result in unexpected behavior by instances of this class.
4+
* @kind problem
5+
* @tags reliability
6+
* maintainability
7+
* modularity
8+
* @problem.severity warning
9+
* @sub-severity low
10+
* @precision high
11+
* @id py/conflicting-attributes
12+
*/
13+
14+
import python
15+
16+
predicate does_nothing(PyFunctionObject f) {
17+
not exists(Stmt s | s.getScope() = f.getFunction() |
18+
not s instanceof Pass and not ((ExprStmt)s).getValue() = f.getFunction().getDocString()
19+
)
20+
}
21+
22+
/* If a method performs a super() call then it is OK as the 'overridden' method will get called */
23+
predicate calls_super(FunctionObject f) {
24+
exists(Call sup, Call meth, Attribute attr, GlobalVariable v |
25+
meth.getScope() = f.getFunction() and
26+
meth.getFunc() = attr and
27+
attr.getObject() = sup and
28+
attr.getName() = f.getName() and
29+
sup.getFunc() = v.getAnAccess() and
30+
v.getId() = "super"
31+
)
32+
}
33+
34+
/** Holds if the given name is white-listed for some reason */
35+
predicate whitelisted(string name) {
36+
/* The standard library specifically recommends this :(
37+
* See https://docs.python.org/3/library/socketserver.html#asynchronous-mixins */
38+
name = "process_request"
39+
}
40+
41+
from ClassObject c, ClassObject b1, ClassObject b2, string name,
42+
int i1, int i2, Object o1, Object o2
43+
where c.getBaseType(i1) = b1 and
44+
c.getBaseType(i2) = b2 and
45+
i1 < i2 and o1 != o2 and
46+
o1 = b1.lookupAttribute(name) and
47+
o2 = b2.lookupAttribute(name) and
48+
not name.matches("\\_\\_%\\_\\_") and
49+
not calls_super(o1) and
50+
not does_nothing(o2) and
51+
not whitelisted(name) and
52+
not o1.overrides(o2) and
53+
not o2.overrides(o1) and
54+
not c.declaresAttribute(name)
55+
56+
select c, "Base classes have conflicting values for attribute '" + name + "': $@ and $@.", o1, o1.toString(), o2, o2.toString()
57+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
#Fixed by overriding. This does not change behavior, but makes it explicit and comprehensible.
3+
class ThreadingTCPServerOverriding(ThreadingMixIn, TCPServer):
4+
5+
def process_request(self, request, client_address):
6+
#process_request forwards to do_work, so it is OK to call ThreadingMixIn.process_request directly
7+
ThreadingMixIn.process_request(self, request, client_address)
8+
9+
10+
#Fixed by separating threading functionality from request handling.
11+
class ThreadingMixIn:
12+
"""Mix-in class to help with threads."""
13+
14+
def do_job_in_thread(self, job, args):
15+
"""Start a new thread to do the job"""
16+
t = threading.Thread(target = job, args = args)
17+
t.start()
18+
19+
class ThreadingTCPServerChangedHierarchy(ThreadingMixIn, TCPServer):
20+
21+
def process_request(self, request, client_address):
22+
"""Start a new thread to process the request."""
23+
self.do_job_in_thread(self.do_work, (request, client_address))
24+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
class Point(object):
2+
3+
def __init__(self, x, y):
4+
self._x = x
5+
self._y = y
6+
7+
def __repr__(self):
8+
return 'Point(%r, %r)' % (self._x, self._y)
9+
10+
def __eq__(self, other):
11+
if not isinstance(other, Point):
12+
return False
13+
return self._x == other._x and self._y == other._y
14+
15+
class ColorPoint(Point):
16+
17+
def __init__(self, x, y, color):
18+
Point.__init__(self, x, y)
19+
self._color = color
20+
21+
def __repr__(self):
22+
return 'ColorPoint(%r, %r)' % (self._x, self._y, self._color)
23+
24+
#ColorPoint(0, 0, Red) == ColorPoint(0, 0, Green) should be False, but is True.
25+
26+
#Fixed version
27+
class ColorPoint(Point):
28+
29+
def __init__(self, x, y, color):
30+
Point.__init__(self, x, y)
31+
self._color = color
32+
33+
def __repr__(self):
34+
return 'ColorPoint(%r, %r)' % (self._x, self._y, self._color)
35+
36+
def __eq__(self, other):
37+
if not isinstance(other, ColorPoint):
38+
return False
39+
return Point.__eq__(self, other) and self._color = other._color
40+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>A class that defines attributes that are not present in its superclasses
8+
may need to override the <code>__eq__()</code> method (<code>__ne__()</code>
9+
should also be defined).</p>
10+
11+
<p>Adding additional attributes without overriding <code>__eq__()</code> means
12+
that the additional attributes will not be accounted for in equality tests.</p>
13+
14+
15+
</overview>
16+
<recommendation>
17+
18+
<p>Override the <code>__eq__</code> method.</p>
19+
20+
21+
</recommendation>
22+
<example>
23+
<p>In the following example the <code>ColorPoint</code>
24+
class subclasses the <code>Point</code> class and adds a new attribute,
25+
but does not override the <code>__eq__</code> method.
26+
</p>
27+
28+
<sample src="DefineEqualsWhenAddingAttributes.py" />
29+
30+
31+
</example>
32+
<references>
33+
34+
<li>Peter Grogono, Philip Santas: <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.48.5109&amp;rep=rep1&amp;type=pdf">Equality in Object Oriented Languages</a></li>
35+
36+
</references>
37+
</qhelp>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @name __eq__ not overridden when adding attributes
3+
* @description When adding new attributes to instances of a class, equality for that class needs to be defined.
4+
* @kind problem
5+
* @tags reliability
6+
* correctness
7+
* @problem.severity warning
8+
* @sub-severity high
9+
* @precision high
10+
* @id py/missing-equals
11+
*/
12+
13+
import python
14+
import semmle.python.SelfAttribute
15+
import Equality
16+
17+
predicate class_stores_to_attribute(ClassObject cls, SelfAttributeStore store, string name) {
18+
exists(FunctionObject f | f = cls.declaredAttribute(_) and store.getScope() = f.getFunction() and store.getName() = name) and
19+
/* Exclude classes used as metaclasses */
20+
not cls.getASuperType() = theTypeType()
21+
}
22+
23+
predicate should_override_eq(ClassObject cls, Object base_eq) {
24+
not cls.declaresAttribute("__eq__") and
25+
exists(ClassObject sup | sup = cls.getABaseType() and sup.declaredAttribute("__eq__") = base_eq |
26+
not exists(GenericEqMethod eq | eq.getScope() = sup.getPyClass()) and
27+
not exists(IdentityEqMethod eq | eq.getScope() = sup.getPyClass()) and
28+
not base_eq.(FunctionObject).getFunction() instanceof IdentityEqMethod and
29+
not base_eq = theObjectType().declaredAttribute("__eq__")
30+
)
31+
}
32+
33+
/** Does the non-overridden __eq__ method access the attribute,
34+
* which implies that the __eq__ method does not need to be overridden.
35+
*/
36+
predicate superclassEqExpectsAttribute(ClassObject cls, PyFunctionObject base_eq, string attrname) {
37+
not cls.declaresAttribute("__eq__") and
38+
exists(ClassObject sup | sup = cls.getABaseType() and sup.declaredAttribute("__eq__") = base_eq |
39+
exists(SelfAttributeRead store |
40+
store.getName() = attrname |
41+
store.getScope() = base_eq.getFunction()
42+
)
43+
)
44+
}
45+
46+
from ClassObject cls, SelfAttributeStore store, Object base_eq
47+
where class_stores_to_attribute(cls, store, _) and should_override_eq(cls, base_eq) and
48+
/* Don't report overridden unittest.TestCase. -- TestCase overrides __eq__, but subclasses do not really need to. */
49+
not cls.getASuperType().getName() = "TestCase" and
50+
not superclassEqExpectsAttribute(cls, base_eq, store.getName())
51+
52+
select cls, "The class '" + cls.getName() + "' does not override $@, but adds the new attribute $@.", base_eq, "'__eq__'", store, store.getName()

0 commit comments

Comments
 (0)