Skip to content

Commit 7ef2b01

Browse files
Merge pull request #20142 from joefarebrother/python-qual-subclass-shadow
Python: Modernise Superclass attribute shadows subclass method query
2 parents 919ed3c + 45910b9 commit 7ef2b01

13 files changed

+174
-108
lines changed

python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
88
ql/python/ql/src/Classes/MissingCallToDel.ql
99
ql/python/ql/src/Classes/MissingCallToInit.ql
1010
ql/python/ql/src/Classes/MutatingDescriptor.ql
11-
ql/python/ql/src/Classes/SubclassShadowing.ql
11+
ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
1212
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
1313
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
1414
ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql

python/ql/integration-tests/query-suite/python-code-quality.qls.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql
88
ql/python/ql/src/Classes/MissingCallToDel.ql
99
ql/python/ql/src/Classes/MissingCallToInit.ql
1010
ql/python/ql/src/Classes/MutatingDescriptor.ql
11-
ql/python/ql/src/Classes/SubclassShadowing.ql
11+
ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
1212
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
1313
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql
1414
ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql

python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ql/python/ql/src/Classes/MutatingDescriptor.ql
1111
ql/python/ql/src/Classes/OverwritingAttributeInSuperClass.ql
1212
ql/python/ql/src/Classes/PropertyInOldStyleClass.ql
1313
ql/python/ql/src/Classes/SlotsInOldStyleClass.ql
14-
ql/python/ql/src/Classes/SubclassShadowing.ql
14+
ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
1515
ql/python/ql/src/Classes/SuperInOldStyleClass.ql
1616
ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql
1717
ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql

python/ql/src/Classes/SubclassShadowing.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

python/ql/src/Classes/SubclassShadowing.qhelp

Lines changed: 0 additions & 27 deletions
This file was deleted.

python/ql/src/Classes/SubclassShadowing.ql

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
When an object has an attribute that shares its name with a method on the object's class (or another class attribute), the instance attribute is
8+
prioritized during attribute lookup, shadowing the method.
9+
10+
If a method on a subclass is shadowed by an attribute on a superclass in this way, this may lead to unexpected results or errors, as this
11+
shadowing behavior is nonlocal and may be unintended.
12+
</p>
13+
14+
</overview>
15+
<recommendation>
16+
17+
<p>
18+
Ensure method names on subclasses don't conflict with attribute names on superclasses, and rename one.
19+
If the shadowing behavior is intended, ensure this is explicit in the superclass.
20+
</p>
21+
22+
</recommendation>
23+
<example>
24+
<p>
25+
In the following example, the <code>_foo</code> attribute of class <code>A</code> shadows the method <code>_foo</code> of class <code>B</code>.
26+
Calls to <code>B()._foo()</code> will result in a <code>TypeError</code>, as <code>3</code> will be called instead.
27+
</p>
28+
29+
<sample src="examples/SubclassShadowingBad.py" />
30+
31+
<p>
32+
In the following example, the behavior of the <code>default</code> attribute being shadowed to allow for customization during initialization is
33+
intended in within the superclass <code>A</code>. Overriding <code>default</code> in the subclass <code>B</code> is then OK.
34+
</p>
35+
36+
<sample src="examples/SubclassShadowingGood.py" />
37+
38+
</example>
39+
</qhelp>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @name Superclass attribute shadows subclass method
3+
* @description Defining an attribute in a superclass method with a name that matches a subclass
4+
* method, hides the method in the subclass.
5+
* @kind problem
6+
* @problem.severity error
7+
* @tags quality
8+
* reliability
9+
* correctness
10+
* @sub-severity low
11+
* @precision high
12+
* @id py/attribute-shadows-method
13+
*/
14+
15+
import python
16+
import semmle.python.ApiGraphs
17+
import semmle.python.dataflow.new.internal.DataFlowDispatch
18+
19+
predicate isSettableProperty(Function prop) {
20+
isProperty(prop) and
21+
exists(Function setter |
22+
setter.getScope() = prop.getScope() and
23+
setter.getName() = prop.getName() and
24+
isSetter(setter)
25+
)
26+
}
27+
28+
predicate isSetter(Function f) {
29+
exists(DataFlow::AttrRead attr |
30+
f.getADecorator() = attr.asExpr() and
31+
attr.getAttributeName() = "setter"
32+
)
33+
}
34+
35+
predicate isProperty(Function prop) {
36+
prop.getADecorator() = API::builtin("property").asSource().asExpr()
37+
}
38+
39+
predicate shadowedBySuperclass(
40+
Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed
41+
) {
42+
getADirectSuperclass+(cls) = superclass and
43+
shadowed = cls.getAMethod() and
44+
exists(Function init |
45+
init = superclass.getInitMethod() and
46+
DataFlow::parameterNode(init.getArg(0)).(DataFlow::LocalSourceNode).flowsTo(write.getObject()) and
47+
write.getAttributeName() = shadowed.getName()
48+
) and
49+
// Allow cases in which the super class defines the method as well.
50+
// We assume that the original method must have been defined for a reason.
51+
not exists(Function superShadowed |
52+
superShadowed = superclass.getAMethod() and
53+
superShadowed.getName() = shadowed.getName()
54+
) and
55+
// Allow properties if they have setters, as the write in the superclass will call the setter.
56+
not isSettableProperty(shadowed) and
57+
not isSetter(shadowed)
58+
}
59+
60+
from Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed, string extra
61+
where
62+
shadowedBySuperclass(cls, superclass, write, shadowed) and
63+
(
64+
if isProperty(shadowed)
65+
then
66+
// it's not a setter, so it's a read-only property
67+
extra = " (read-only property may cause an error if written to in the superclass)"
68+
else extra = ""
69+
)
70+
select shadowed, "This method is shadowed by $@ in superclass $@." + extra, write,
71+
"attribute " + write.getAttributeName(), superclass, superclass.getName()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class A:
2+
def __init__(self):
3+
self._foo = 3
4+
5+
class B(A):
6+
# BAD: _foo is shadowed by attribute A._foo
7+
def _foo(self):
8+
return 2
9+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class A:
2+
def __init__(self, default_func=None):
3+
if default_func is not None:
4+
self.default = default_func
5+
6+
# GOOD: The shadowing behavior is explicitly intended in the superclass.
7+
def default(self):
8+
return []
9+
10+
class B(A):
11+
12+
# Subclasses may override the method `default`, which will still be shadowed by the attribute `default` if it is set.
13+
# As this is part of the expected behavior of the superclass, this is fine.
14+
def default(self):
15+
return {}

0 commit comments

Comments
 (0)