diff --git a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected index 960972c508c8..bb44ee105b58 100644 --- a/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected +++ b/python/ql/integration-tests/query-suite/python-code-quality-extended.qls.expected @@ -6,7 +6,7 @@ ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql ql/python/ql/src/Classes/MissingCallToDel.ql ql/python/ql/src/Classes/MissingCallToInit.ql ql/python/ql/src/Classes/MutatingDescriptor.ql -ql/python/ql/src/Classes/SubclassShadowing.ql +ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql diff --git a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected index 960972c508c8..bb44ee105b58 100644 --- a/python/ql/integration-tests/query-suite/python-code-quality.qls.expected +++ b/python/ql/integration-tests/query-suite/python-code-quality.qls.expected @@ -6,7 +6,7 @@ ql/python/ql/src/Classes/InitCallsSubclass/InitCallsSubclassMethod.ql ql/python/ql/src/Classes/MissingCallToDel.ql ql/python/ql/src/Classes/MissingCallToInit.ql ql/python/ql/src/Classes/MutatingDescriptor.ql -ql/python/ql/src/Classes/SubclassShadowing.ql +ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql ql/python/ql/src/Classes/WrongNameForArgumentInClassInstantiation.ql diff --git a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected index 170d9f442f92..8799990b86e1 100644 --- a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected +++ b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected @@ -11,7 +11,7 @@ ql/python/ql/src/Classes/MutatingDescriptor.ql ql/python/ql/src/Classes/OverwritingAttributeInSuperClass.ql ql/python/ql/src/Classes/PropertyInOldStyleClass.ql ql/python/ql/src/Classes/SlotsInOldStyleClass.ql -ql/python/ql/src/Classes/SubclassShadowing.ql +ql/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql ql/python/ql/src/Classes/SuperInOldStyleClass.ql ql/python/ql/src/Classes/SuperclassDelCalledMultipleTimes.ql ql/python/ql/src/Classes/SuperclassInitCalledMultipleTimes.ql diff --git a/python/ql/src/Classes/SubclassShadowing.py b/python/ql/src/Classes/SubclassShadowing.py deleted file mode 100644 index 617db3c58e0b..000000000000 --- a/python/ql/src/Classes/SubclassShadowing.py +++ /dev/null @@ -1,17 +0,0 @@ -class Mammal(object): - - def __init__(self, milk = 0): - self.milk = milk - - -class Cow(Mammal): - - def __init__(self): - Mammal.__init__(self) - - def milk(self): - return "Milk" - -#Cow().milk() will raise an error as Cow().milk is the 'milk' attribute -#set in Mammal.__init__, not the 'milk' method defined on Cow. - diff --git a/python/ql/src/Classes/SubclassShadowing.qhelp b/python/ql/src/Classes/SubclassShadowing.qhelp deleted file mode 100644 index 90daa9a992ab..000000000000 --- a/python/ql/src/Classes/SubclassShadowing.qhelp +++ /dev/null @@ -1,27 +0,0 @@ - - - -

Subclass shadowing occurs when an instance attribute of a superclass has the -the same name as a method of a subclass, or vice-versa. -The semantics of Python attribute look-up mean that the instance attribute of -the superclass hides the method in the subclass. -

- -
- - -

Rename the method in the subclass or rename the attribute in the superclass.

- -
- -

The following code includes an example of subclass shadowing. When you call Cow().milk() -an error is raised because Cow().milk is interpreted as the 'milk' attribute set in -Mammal.__init__, not the 'milk' method defined within Cow. This can be fixed -by changing the name of either the 'milk' attribute or the 'milk' method.

- - - -
-
diff --git a/python/ql/src/Classes/SubclassShadowing.ql b/python/ql/src/Classes/SubclassShadowing.ql deleted file mode 100644 index 542cf31c76aa..000000000000 --- a/python/ql/src/Classes/SubclassShadowing.ql +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @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 - */ - -/* - * Determine if a class defines a method that is shadowed by an attribute - * defined in a super-class - */ - -/* Need to find attributes defined in superclass (only in __init__?) */ -import python - -predicate shadowed_by_super_class( - ClassObject c, ClassObject supercls, Assign assign, FunctionObject f -) { - c.getASuperType() = supercls and - c.declaredAttribute(_) = f and - exists(FunctionObject init, Attribute attr | - supercls.declaredAttribute("__init__") = init and - attr = assign.getATarget() and - attr.getObject().(Name).getId() = "self" and - attr.getName() = f.getName() and - assign.getScope() = init.getOrigin().(FunctionExpr).getInnerScope() - ) and - /* - * It's OK if the super class defines the method as well. - * We assume that the original method must have been defined for a reason. - */ - - not supercls.hasAttribute(f.getName()) -} - -from ClassObject c, ClassObject supercls, Assign assign, FunctionObject shadowed -where shadowed_by_super_class(c, supercls, assign, shadowed) -select shadowed.getOrigin(), - "Method " + shadowed.getName() + " is shadowed by an $@ in super class '" + supercls.getName() + - "'.", assign, "attribute" diff --git a/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp new file mode 100644 index 000000000000..5345d2c91780 --- /dev/null +++ b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.qhelp @@ -0,0 +1,39 @@ + + + +

+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. +

+ +
+ + +

+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. +

+ +
+ +

+In the following example, the _foo attribute of class A shadows the method _foo of class B. +Calls to B()._foo() will result in a TypeError, as 3 will be called instead. +

+ + + +

+In the following example, the behavior of the default attribute being shadowed to allow for customization during initialization is +intended in within the superclass A. Overriding default in the subclass B is then OK. +

+ + + +
+
diff --git a/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql new file mode 100644 index 000000000000..39a320f75ac6 --- /dev/null +++ b/python/ql/src/Classes/SubclassShadowing/SubclassShadowing.ql @@ -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() diff --git a/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py new file mode 100644 index 000000000000..00a221760b4c --- /dev/null +++ b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingBad.py @@ -0,0 +1,9 @@ +class A: + def __init__(self): + self._foo = 3 + +class B(A): + # BAD: _foo is shadowed by attribute A._foo + def _foo(self): + return 2 + diff --git a/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py new file mode 100644 index 000000000000..8fca041176ca --- /dev/null +++ b/python/ql/src/Classes/SubclassShadowing/examples/SubclassShadowingGood.py @@ -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 {} \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected index caad71a9a31f..5f5513ae9906 100644 --- a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected +++ b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.expected @@ -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 in the superclass.) | subclass_shadowing.py:35:9:35:16 | ControlFlowNode for Attribute | attribute foo | subclass_shadowing.py:33:1:33:12 | Class Base3 | Base3 | diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref index 5fed3f9f8fc6..5205014a3d55 100644 --- a/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref +++ b/python/ql/test/query-tests/Classes/subclass-shadowing/SubclassShadowing.qlref @@ -1 +1,2 @@ -Classes/SubclassShadowing.ql +query: Classes/SubclassShadowing/SubclassShadowing.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py b/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py index 98e7f992e84e..b9fcd975eb33 100644 --- a/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py +++ b/python/ql/test/query-tests/Classes/subclass-shadowing/subclass_shadowing.py @@ -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 \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy