-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Python: Modernise Superclass attribute shadows subclass method query #20142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
joefarebrother
wants to merge
10
commits into
github:main
Choose a base branch
from
joefarebrother:python-qual-subclass-shadow
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
af94ebe
Modernize attribute shadows subclass, Add cases for properties
joefarebrother 796a606
Exclude setters and update tests
joefarebrother 34317d2
Update documentation
joefarebrother 2516f94
Move to subfolder
joefarebrother 63577f0
Add extra example
joefarebrother 1efc09b
Update integration tests
joefarebrother 71a6b22
Update python/ql/src/Classes/SubclassShadowing/examples/SubclassShado…
joefarebrother 79d1deb
Update python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
joefarebrother bc60914
Update test output
joefarebrother 5e09c1d
Merge remote-tracking branch 'origin/python-qual-subclass-shadow' int…
joefarebrother File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
39 changes: 39 additions & 0 deletions
39
python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
<p> | ||
When an object has an attribute that shares the same name a method on the object's class (or another class attribute), the instance attribute is | ||
prioritized during attribute lookup, shadowing the method. | ||
|
||
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 | ||
shadowing behavior is nonlocal and may be unintended. | ||
</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p> | ||
Ensure method names on subclasses don't conflict with attribute names on superclasses, and rename one. | ||
If the shadowing behavior is intended, ensure this is explicit in the superclass. | ||
</p> | ||
|
||
</recommendation> | ||
<example> | ||
<p> | ||
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>. | ||
Calls to <code>B()._foo()</code> will result in a <code>TypeError</code>, as <code>3</code> will be called instead. | ||
</p> | ||
|
||
<sample src="examples/SubclassShadowingBad.py" /> | ||
|
||
<p> | ||
In the following example, the behavior of the <code>default</code> attribute being shadowed to allow for customization during initialization is | ||
intended in within the superclass <code>A</code>. Overriding <code>default</code> in the subclass <code>B</code> is then OK. | ||
</p> | ||
|
||
<sample src="examples/SubclassShadowingGood.py" /> | ||
|
||
</example> | ||
</qhelp> |
71 changes: 71 additions & 0 deletions
71
python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* @name Superclass attribute shadows subclass method | ||
* @description Defining an attribute in a superclass method with a name that matches a subclass | ||
* method, hides the method in the subclass. | ||
* @kind problem | ||
* @problem.severity error | ||
* @tags quality | ||
* reliability | ||
* correctness | ||
* @sub-severity low | ||
* @precision high | ||
* @id py/attribute-shadows-method | ||
*/ | ||
|
||
import python | ||
import semmle.python.ApiGraphs | ||
import semmle.python.dataflow.new.internal.DataFlowDispatch | ||
|
||
predicate isSettableProperty(Function prop) { | ||
isProperty(prop) and | ||
exists(Function setter | | ||
setter.getScope() = prop.getScope() and | ||
setter.getName() = prop.getName() and | ||
isSetter(setter) | ||
) | ||
} | ||
|
||
predicate isSetter(Function f) { | ||
exists(DataFlow::AttrRead attr | | ||
f.getADecorator() = attr.asExpr() and | ||
attr.getAttributeName() = "setter" | ||
) | ||
} | ||
|
||
predicate isProperty(Function prop) { | ||
prop.getADecorator() = API::builtin("property").asSource().asExpr() | ||
} | ||
|
||
predicate shadowedBySuperclass( | ||
Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed | ||
) { | ||
getADirectSuperclass+(cls) = superclass and | ||
shadowed = cls.getAMethod() and | ||
exists(Function init | | ||
init = superclass.getInitMethod() and | ||
DataFlow::parameterNode(init.getArg(0)).(DataFlow::LocalSourceNode).flowsTo(write.getObject()) and | ||
write.getAttributeName() = shadowed.getName() | ||
) and | ||
// Allow cases in which the super class defines the method as well. | ||
// We assume that the original method must have been defined for a reason. | ||
not exists(Function superShadowed | | ||
superShadowed = superclass.getAMethod() and | ||
superShadowed.getName() = shadowed.getName() | ||
) and | ||
// Allow properties if they have setters, as the write in the superclass will call the setter. | ||
not isSettableProperty(shadowed) and | ||
not isSetter(shadowed) | ||
} | ||
|
||
from Class cls, Class superclass, DataFlow::AttrWrite write, Function shadowed, string extra | ||
where | ||
shadowedBySuperclass(cls, superclass, write, shadowed) and | ||
( | ||
if isProperty(shadowed) | ||
then | ||
// it's not a setter, so it's a read-only property | ||
extra = " (read-only property may cause an error if written to in the superclass.)" | ||
else extra = "" | ||
) | ||
select shadowed, "This method is shadowed by $@ in superclass $@." + extra, write, | ||
"attribute " + write.getAttributeName(), superclass, superclass.getName() |
9 changes: 9 additions & 0 deletions
9
python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
class A: | ||
def __init__(self): | ||
self._foo = 3 | ||
|
||
class B: | ||
joefarebrother marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# BAD: _foo is shadowed by attribute A._foo | ||
def _foo(self): | ||
return 2 | ||
|
15 changes: 15 additions & 0 deletions
15
python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
class A: | ||
def __init__(self, default_func=None): | ||
if default_func is not None: | ||
self.default = default_func | ||
|
||
# GOOD: The shadowing behavior is explicitly intended in the superclass. | ||
def default(self): | ||
return [] | ||
|
||
class B(A): | ||
|
||
# Subclasses may override the method `default`, which will still be shadowed by the attribute `default` if it is set. | ||
# As this is part of the expected behavior of the superclass, this is fine. | ||
def default(self): | ||
return {} |
3 changes: 2 additions & 1 deletion
3
python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
| subclass_shadowing.py:10:5:10:21 | FunctionExpr | Method shadow is shadowed by an $@ in super class 'Base'. | subclass_shadowing.py:6:9:6:23 | AssignStmt | attribute | | ||
| subclass_shadowing.py:11:5:11:21 | Function shadow | This method is shadowed by $@ in superclass $@. | subclass_shadowing.py:7:9:7:19 | ControlFlowNode for Attribute | attribute shadow | subclass_shadowing.py:4:1:4:11 | Class Base | Base | | ||
| subclass_shadowing.py:41:5:41:18 | Function foo | This method is shadowed by $@ in superclass $@. (read-only property may cause an error if written to.) | subclass_shadowing.py:35:9:35:16 | ControlFlowNode for Attribute | attribute foo | subclass_shadowing.py:33:1:33:12 | Class Base3 | Base3 | |
3 changes: 2 additions & 1 deletion
3
python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
Classes/SubclassShadowing.ql | ||
query: Classes/SubclassShadowing/SubclassShadowing.ql | ||
postprocess: utils/test/InlineExpectationsTestQuery.ql |
45 changes: 33 additions & 12 deletions
45
python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,51 @@ | ||
#Subclass shadowing | ||
|
||
class Base(object): | ||
# BAD: `shadow` method shadows attribute | ||
class Base: | ||
|
||
def __init__(self): | ||
self.shadow = 4 | ||
|
||
class Derived(Base): | ||
|
||
def shadow(self): | ||
def shadow(self): # $ Alert | ||
pass | ||
|
||
|
||
#OK if the super class defines the method as well. | ||
#Since the original method must exist for some reason. | ||
#See JSONEncoder.default for real example | ||
# OK: Allow if superclass also shadows its own method, as this is likely intended. | ||
# Example: stdlib JSONEncoder.default uses this pattern. | ||
class Base2: | ||
|
||
class Base2(object): | ||
def __init__(self, default=None): | ||
if default: | ||
self.default = default | ||
|
||
def __init__(self, shadowy=None): | ||
if shadowy: | ||
self.shadow = shadowy | ||
|
||
def shadow(self): | ||
def default(self): | ||
pass | ||
|
||
class Derived2(Base2): | ||
|
||
def shadow(self): | ||
def default(self): # No alert | ||
return 0 | ||
|
||
# Properties | ||
|
||
class Base3: | ||
def __init__(self): | ||
self.foo = 1 | ||
self.bar = 2 | ||
|
||
class Derived3(Base3): | ||
# BAD: Write to foo in superclass init raises an error. | ||
@property | ||
def foo(self): # $ Alert | ||
return 2 | ||
|
||
# OK: This property has a setter, so the write is OK. | ||
@property | ||
def bar(self): # No alert | ||
return self._bar | ||
|
||
@bar.setter | ||
def bar(self, val): | ||
self._bar = val |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.