Merge pull request #18599 from joefarebrother/python-qual-not-named-self-cls

Python: Modernize py/not-named-self and py/not-named-cls queries
This commit is contained in:
Joe Farebrother
2025-02-17 08:58:34 +00:00
committed by GitHub
14 changed files with 189 additions and 101 deletions

View File

@@ -0,0 +1,108 @@
/** Definitions for reasoning about the expected first argument names for methods. */
import python
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowDispatch
import DataFlow
/** Holds if `f` is a method of the class `c`. */
private predicate methodOfClass(Function f, Class c) {
exists(FunctionDef d | d.getDefinedFunction() = f and d.getScope() = c)
}
/** Holds if `c` is a metaclass. */
private predicate isMetaclass(Class c) {
c = API::builtin("type").getASubclass*().asSource().asExpr().(ClassExpr).getInnerScope()
}
/** Holds if `c` is a Zope interface. */
private predicate isZopeInterface(Class c) {
c =
API::moduleImport("zope")
.getMember("interface")
.getMember("Interface")
.getASubclass*()
.asSource()
.asExpr()
.(ClassExpr)
.getInnerScope()
}
/**
* Holds if `f` is used in the initialisation of `c`.
* This means `f` isn't being used as a normal method.
* Ideally it should be a `@staticmethod`; however this wasn't possible prior to Python 3.10.
* We exclude this case from the `not-named-self` query.
* However there is potential for a new query that specifically covers and alerts for this case.
*/
private predicate usedInInit(Function f, Class c) {
methodOfClass(f, c) and
exists(Call call |
call.getScope() = c and
DataFlow::localFlow(DataFlow::exprNode(f.getDefinition()), DataFlow::exprNode(call.getFunc()))
)
}
/**
* Holds if `f` has no arguments, and also has a decorator.
* We assume that the decorator affect the method in such a way that a `self` parameter is unneeded.
*/
private predicate noArgsWithDecorator(Function f) {
not exists(f.getAnArg()) and
exists(f.getADecorator())
}
/** Holds if the first parameter of `f` should be named `self`. */
predicate shouldBeSelf(Function f, Class c) {
methodOfClass(f, c) and
not isStaticmethod(f) and
not isClassmethod(f) and
not isMetaclass(c) and
not isZopeInterface(c) and
not usedInInit(f, c) and
not noArgsWithDecorator(f)
}
/** Holds if the first parameter of `f` should be named `cls`. */
predicate shouldBeCls(Function f, Class c) {
methodOfClass(f, c) and
not isStaticmethod(f) and
(
isClassmethod(f) and not isMetaclass(c)
or
isMetaclass(c) and not isClassmethod(f)
)
}
/** Holds if the first parameter of `f` is named `self`. */
predicate firstArgNamedSelf(Function f) { f.getArgName(0) = "self" }
/** Holds if the first parameter of `f` refers to the class - it is either named `cls`, or it is named `self` and is a method of a metaclass. */
predicate firstArgRefersToCls(Function f, Class c) {
methodOfClass(f, c) and
exists(string argname | argname = f.getArgName(0) |
argname = "cls"
or
/* Not PEP8, but relatively common */
argname = "mcls"
or
/* If c is a metaclass, allow arguments named `self`. */
argname = "self" and
isMetaclass(c)
)
}
/** Holds if the first parameter of `f` should be named `self`, but isn't. */
predicate firstArgShouldBeNamedSelfAndIsnt(Function f) {
shouldBeSelf(f, _) and
not firstArgNamedSelf(f)
}
/** Holds if the first parameter of `f` should be named `cls`, but isn't. */
predicate firstArgShouldReferToClsAndDoesnt(Function f) {
exists(Class c |
methodOfClass(f, c) and
shouldBeCls(f, c) and
not firstArgRefersToCls(f, c)
)
}

View File

@@ -1,4 +1,4 @@
class Entry(object):
@classmethod
def make(klass):
def make(self):
return Entry()

View File

@@ -5,20 +5,19 @@
<overview>
<p> The first parameter of a class method, a new method or any metaclass method
should be called <code>cls</code>. This makes the purpose of the parameter clear to other developers.
<p> The first parameter of a class method (including certain special methods such as <code>__new__</code>), or a method of a metaclass,
should be named <code>cls</code>.
</p>
</overview>
<recommendation>
<p>Change the name of the first parameter to <code>cls</code> as recommended by the style guidelines
<p>Ensure that the first parameter of class methods is named <code>cls</code>, as recommended by the style guidelines
in PEP 8.</p>
</recommendation>
<example>
<p>In the example, the first parameter to <code>make()</code> is <code>klass</code> which should be changed to <code>cls</code>
for ease of comprehension.
<p>In the following example, the first parameter of the class method <code>make</code> is named <code>self</code> instead of <code>cls</code>.
</p>
<sample src="NonCls.py" />
@@ -29,6 +28,7 @@ for ease of comprehension.
<li>Python PEP 8: <a href="http://www.python.org/dev/peps/pep-0008/#function-and-method-arguments">Function and method arguments</a>.</li>
<li>Python Tutorial: <a href="http://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
<li>Python Docs: <a href="https://docs.python.org/3/library/functions.html#classmethod">classmethod</a>.</li>
</references>

View File

@@ -1,7 +1,6 @@
/**
* @name First parameter of a class method is not named 'cls'
* @description Using an alternative name for the first parameter of a class method makes code more
* difficult to read; PEP8 states that the first parameter to class methods should be 'cls'.
* @description By the PEP8 style guide, the first parameter of a class method should be named `cls`.
* @kind problem
* @tags maintainability
* readability
@@ -13,30 +12,11 @@
*/
import python
predicate first_arg_cls(Function f) {
exists(string argname | argname = f.getArgName(0) |
argname = "cls"
or
/* Not PEP8, but relatively common */
argname = "mcls"
)
}
predicate is_type_method(Function f) {
exists(ClassValue c | c.getScope() = f.getScope() and c.getASuperType() = ClassValue::type())
}
predicate classmethod_decorators_only(Function f) {
forall(Expr decorator | decorator = f.getADecorator() | decorator.(Name).getId() = "classmethod")
}
import MethodArgNames
from Function f, string message
where
(f.getADecorator().(Name).getId() = "classmethod" or is_type_method(f)) and
not first_arg_cls(f) and
classmethod_decorators_only(f) and
not f.getName() = "__new__" and
firstArgShouldReferToClsAndDoesnt(f) and
(
if exists(f.getArgName(0))
then

View File

@@ -1,9 +1,9 @@
class Point:
def __init__(val, x, y): # first parameter is mis-named 'val'
def __init__(val, x, y): # BAD: first parameter is mis-named 'val'
val._x = x
val._y = y
class Point2:
def __init__(self, x, y): # first parameter is correctly named 'self'
def __init__(self, x, y): # GOOD: first parameter is correctly named 'self'
self._x = x
self._y = y

View File

@@ -6,22 +6,18 @@
<overview>
<p> Normal methods should have at least one parameter and the first parameter should be called <code>self</code>.
This makes the purpose of the parameter clear to other developers.
</p>
</overview>
<recommendation>
<p>If there is at least one parameter, then change the name of the first parameter to <code>self</code> as recommended by the style guidelines
<p>Ensure that the first parameter of a normal method is named <code>self</code>, as recommended by the style guidelines
in PEP 8.</p>
<p>If there are no parameters, then it cannot be a normal method. It may need to be marked as a <code>staticmethod</code>
or it could be moved out of the class as a normal function.
<p>If a <code>self</code> parameter is unneeded, the method should be decorated with <code>staticmethod</code>, or moved out of the class as a regular function.
</p>
</recommendation>
<example>
<p>The following methods can both be used to assign values to variables in a <code>point</code>
object. The second method makes the association clearer because the <code>self</code> parameter is
used.</p>
<p>In the following cases, the first argument of <code>Point.__init__</code> is named <code>val</code> instead; whereas in <code>Point2.__init__</code> it is correctly named <code>self</code>.</p>
<sample src="NonSelf.py" />
@@ -31,7 +27,7 @@ used.</p>
<li>Python PEP 8: <a href="http://www.python.org/dev/peps/pep-0008/#function-and-method-arguments">Function and
method arguments</a>.</li>
<li>Python Tutorial: <a href="http://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
<li>Python Docs: <a href="https://docs.python.org/3/library/functions.html#staticmethod">staticmethod</a>.</li>
</references>

View File

@@ -1,8 +1,6 @@
/**
* @name First parameter of a method is not named 'self'
* @description Using an alternative name for the first parameter of an instance method makes
* code more difficult to read; PEP8 states that the first parameter to instance
* methods should be 'self'.
* @description By the PEP8 style guide, the first parameter of a normal method should be named `self`.
* @kind problem
* @tags maintainability
* readability
@@ -14,45 +12,19 @@
*/
import python
import semmle.python.libraries.Zope
import MethodArgNames
predicate is_type_method(FunctionValue fv) {
exists(ClassValue c | c.declaredAttribute(_) = fv and c.getASuperType() = ClassValue::type())
}
predicate used_in_defining_scope(FunctionValue fv) {
exists(Call c | c.getScope() = fv.getScope().getScope() and c.getFunc().pointsTo(fv))
}
from Function f, FunctionValue fv, string message
from Function f, string message
where
exists(ClassValue cls, string name |
cls.declaredAttribute(name) = fv and
cls.isNewStyle() and
not name = "__new__" and
not name = "__metaclass__" and
not name = "__init_subclass__" and
not name = "__class_getitem__" and
/* declared in scope */
f.getScope() = cls.getScope()
) and
not f.getArgName(0) = "self" and
not is_type_method(fv) and
fv.getScope() = f and
not f.getName() = "lambda" and
not used_in_defining_scope(fv) and
firstArgShouldBeNamedSelfAndIsnt(f) and
(
(
if exists(f.getArgName(0))
then
message =
"Normal methods should have 'self', rather than '" + f.getArgName(0) +
"', as their first parameter."
else
message =
"Normal methods should have at least one parameter (the first of which should be 'self')."
) and
not f.hasVarArg()
) and
not fv instanceof ZopeInterfaceMethodValue
if exists(f.getArgName(0))
then
message =
"Normal methods should have 'self', rather than '" + f.getArgName(0) +
"', as their first parameter."
else
message =
"Normal methods should have at least one parameter (the first of which should be 'self')."
)
select f, message