Skip to content

Commit af94ebe

Browse files
Modernize attribute shadows subclass, Add cases for properties
1 parent f432cf9 commit af94ebe

File tree

3 files changed

+79
-35
lines changed

3 files changed

+79
-35
lines changed

python/ql/src/Classes/SubclassShadowing.ql

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,53 @@
1717
* defined in a super-class
1818
*/
1919

20-
/* Need to find attributes defined in superclass (only in __init__?) */
2120
import python
21+
import semmle.python.ApiGraphs
22+
import semmle.python.dataflow.new.internal.DataFlowDispatch
2223

23-
predicate shadowed_by_super_class(
24-
ClassObject c, ClassObject supercls, Assign assign, FunctionObject f
24+
predicate isSettableProperty(Function prop) {
25+
isProperty(prop) and
26+
exists(Function setter, DataFlow::AttrRead setterRead, FunctionExpr propExpr |
27+
setterRead.asExpr() = setter.getADecorator() and
28+
setterRead.getAttributeName() = "setter" and
29+
propExpr.getInnerScope() = prop and
30+
DataFlow::exprNode(propExpr).(DataFlow::LocalSourceNode).flowsTo(setterRead.getObject())
31+
)
32+
}
33+
34+
predicate isProperty(Function prop) {
35+
prop.getADecorator() = API::builtin("property").asSource().asExpr()
36+
}
37+
38+
predicate shadowedBySuperclass(
39+
Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed
2540
) {
26-
c.getASuperType() = supercls and
27-
c.declaredAttribute(_) = f and
28-
exists(FunctionObject init, Attribute attr |
29-
supercls.declaredAttribute("__init__") = init and
30-
attr = assign.getATarget() and
31-
attr.getObject().(Name).getId() = "self" and
32-
attr.getName() = f.getName() and
33-
assign.getScope() = init.getOrigin().(FunctionExpr).getInnerScope()
41+
getADirectSuperclass+(cls) = superclass and
42+
shadowed = cls.getAMethod() and
43+
exists(Function init |
44+
init = superclass.getInitMethod() and
45+
DataFlow::parameterNode(init.getArg(0)).(DataFlow::LocalSourceNode).flowsTo(write.getObject()) and
46+
write.getAttributeName() = shadowed.getName()
3447
) and
35-
/*
36-
* It's OK if the super class defines the method as well.
37-
* We assume that the original method must have been defined for a reason.
38-
*/
39-
40-
not supercls.hasAttribute(f.getName())
48+
// Allow cases in which the super class defines the method as well.
49+
// We assume that the original method must have been defined for a reason.
50+
not exists(Function superShadowed |
51+
superShadowed = superclass.getAMethod() and
52+
superShadowed.getName() = shadowed.getName()
53+
) and
54+
// Allow properties if they have setters, as the write in the superclass will call the setter.
55+
not isSettableProperty(shadowed)
4156
}
4257

43-
from ClassObject c, ClassObject supercls, Assign assign, FunctionObject shadowed
44-
where shadowed_by_super_class(c, supercls, assign, shadowed)
45-
select shadowed.getOrigin(),
46-
"Method " + shadowed.getName() + " is shadowed by an $@ in super class '" + supercls.getName() +
47-
"'.", assign, "attribute"
58+
from Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed, string extra
59+
where
60+
shadowedBySuperclass(cls, superclass, write, shadowed) and
61+
(
62+
if isProperty(shadowed)
63+
then
64+
not isSettableProperty(shadowed) and
65+
extra = " (read-only property may cause an error if written to.)"
66+
else extra = ""
67+
)
68+
select shadowed, "This method is shadowed by $@ in superclass $@." + extra, write,
69+
"attribute " + write.getAttributeName(), superclass, superclass.getName()
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Classes/SubclassShadowing.ql
1+
query: Classes/SubclassShadowing.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql
Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,51 @@
11
#Subclass shadowing
22

3-
class Base(object):
3+
# BAD: `shadow` method shadows attribute
4+
class Base:
45

56
def __init__(self):
67
self.shadow = 4
78

89
class Derived(Base):
910

10-
def shadow(self):
11+
def shadow(self): # $ Alert
1112
pass
1213

1314

14-
#OK if the super class defines the method as well.
15-
#Since the original method must exist for some reason.
16-
#See JSONEncoder.default for real example
15+
# OK: Allow if superclass also shadows its own method, as this is likely intended.
16+
# Example: stdlib JSONEncoder.default uses this pattern.
17+
class Base2:
1718

18-
class Base2(object):
19+
def __init__(self, default=None):
20+
if default:
21+
self.default = default
1922

20-
def __init__(self, shadowy=None):
21-
if shadowy:
22-
self.shadow = shadowy
23-
24-
def shadow(self):
23+
def default(self):
2524
pass
2625

2726
class Derived2(Base2):
2827

29-
def shadow(self):
28+
def default(self): # No alert
3029
return 0
30+
31+
# Properties
32+
33+
class Base3:
34+
def __init__(self):
35+
self.foo = 1
36+
self.bar = 2
37+
38+
class Derived3(Base3):
39+
# BAD: Write to foo in superclass init raises an error.
40+
@property
41+
def foo(self): # $ Alert
42+
return 2
43+
44+
# OK: This property has a setter, so the write is OK.
45+
@property
46+
def bar(self): # No alert
47+
return self._bar
48+
49+
@bar.setter
50+
def bar(self, val):
51+
self._bar = val

0 commit comments

Comments
 (0)