Skip to content

Commit 180e45d

Browse files
Merge pull request #18599 from joefarebrother/python-qual-not-named-self-cls
Python: Modernize py/not-named-self and py/not-named-cls queries
2 parents e02577d + f46a2a1 commit 180e45d

File tree

14 files changed

+189
-101
lines changed

14 files changed

+189
-101
lines changed
+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/** Definitions for reasoning about the expected first argument names for methods. */
2+
3+
import python
4+
import semmle.python.ApiGraphs
5+
import semmle.python.dataflow.new.internal.DataFlowDispatch
6+
import DataFlow
7+
8+
/** Holds if `f` is a method of the class `c`. */
9+
private predicate methodOfClass(Function f, Class c) {
10+
exists(FunctionDef d | d.getDefinedFunction() = f and d.getScope() = c)
11+
}
12+
13+
/** Holds if `c` is a metaclass. */
14+
private predicate isMetaclass(Class c) {
15+
c = API::builtin("type").getASubclass*().asSource().asExpr().(ClassExpr).getInnerScope()
16+
}
17+
18+
/** Holds if `c` is a Zope interface. */
19+
private predicate isZopeInterface(Class c) {
20+
c =
21+
API::moduleImport("zope")
22+
.getMember("interface")
23+
.getMember("Interface")
24+
.getASubclass*()
25+
.asSource()
26+
.asExpr()
27+
.(ClassExpr)
28+
.getInnerScope()
29+
}
30+
31+
/**
32+
* Holds if `f` is used in the initialisation of `c`.
33+
* This means `f` isn't being used as a normal method.
34+
* Ideally it should be a `@staticmethod`; however this wasn't possible prior to Python 3.10.
35+
* We exclude this case from the `not-named-self` query.
36+
* However there is potential for a new query that specifically covers and alerts for this case.
37+
*/
38+
private predicate usedInInit(Function f, Class c) {
39+
methodOfClass(f, c) and
40+
exists(Call call |
41+
call.getScope() = c and
42+
DataFlow::localFlow(DataFlow::exprNode(f.getDefinition()), DataFlow::exprNode(call.getFunc()))
43+
)
44+
}
45+
46+
/**
47+
* Holds if `f` has no arguments, and also has a decorator.
48+
* We assume that the decorator affect the method in such a way that a `self` parameter is unneeded.
49+
*/
50+
private predicate noArgsWithDecorator(Function f) {
51+
not exists(f.getAnArg()) and
52+
exists(f.getADecorator())
53+
}
54+
55+
/** Holds if the first parameter of `f` should be named `self`. */
56+
predicate shouldBeSelf(Function f, Class c) {
57+
methodOfClass(f, c) and
58+
not isStaticmethod(f) and
59+
not isClassmethod(f) and
60+
not isMetaclass(c) and
61+
not isZopeInterface(c) and
62+
not usedInInit(f, c) and
63+
not noArgsWithDecorator(f)
64+
}
65+
66+
/** Holds if the first parameter of `f` should be named `cls`. */
67+
predicate shouldBeCls(Function f, Class c) {
68+
methodOfClass(f, c) and
69+
not isStaticmethod(f) and
70+
(
71+
isClassmethod(f) and not isMetaclass(c)
72+
or
73+
isMetaclass(c) and not isClassmethod(f)
74+
)
75+
}
76+
77+
/** Holds if the first parameter of `f` is named `self`. */
78+
predicate firstArgNamedSelf(Function f) { f.getArgName(0) = "self" }
79+
80+
/** 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. */
81+
predicate firstArgRefersToCls(Function f, Class c) {
82+
methodOfClass(f, c) and
83+
exists(string argname | argname = f.getArgName(0) |
84+
argname = "cls"
85+
or
86+
/* Not PEP8, but relatively common */
87+
argname = "mcls"
88+
or
89+
/* If c is a metaclass, allow arguments named `self`. */
90+
argname = "self" and
91+
isMetaclass(c)
92+
)
93+
}
94+
95+
/** Holds if the first parameter of `f` should be named `self`, but isn't. */
96+
predicate firstArgShouldBeNamedSelfAndIsnt(Function f) {
97+
shouldBeSelf(f, _) and
98+
not firstArgNamedSelf(f)
99+
}
100+
101+
/** Holds if the first parameter of `f` should be named `cls`, but isn't. */
102+
predicate firstArgShouldReferToClsAndDoesnt(Function f) {
103+
exists(Class c |
104+
methodOfClass(f, c) and
105+
shouldBeCls(f, c) and
106+
not firstArgRefersToCls(f, c)
107+
)
108+
}

python/ql/src/Functions/NonCls.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
class Entry(object):
22
@classmethod
3-
def make(klass):
3+
def make(self):
44
return Entry()

python/ql/src/Functions/NonCls.qhelp

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,19 @@
55

66

77
<overview>
8-
<p> The first parameter of a class method, a new method or any metaclass method
9-
should be called <code>cls</code>. This makes the purpose of the parameter clear to other developers.
8+
<p> The first parameter of a class method (including certain special methods such as <code>__new__</code>), or a method of a metaclass,
9+
should be named <code>cls</code>.
1010
</p>
1111

1212
</overview>
1313
<recommendation>
1414

15-
<p>Change the name of the first parameter to <code>cls</code> as recommended by the style guidelines
15+
<p>Ensure that the first parameter of class methods is named <code>cls</code>, as recommended by the style guidelines
1616
in PEP 8.</p>
1717

1818
</recommendation>
1919
<example>
20-
<p>In the example, the first parameter to <code>make()</code> is <code>klass</code> which should be changed to <code>cls</code>
21-
for ease of comprehension.
20+
<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>.
2221
</p>
2322

2423
<sample src="NonCls.py" />
@@ -29,6 +28,7 @@ for ease of comprehension.
2928

3029
<li>Python PEP 8: <a href="http://www.python.org/dev/peps/pep-0008/#function-and-method-arguments">Function and method arguments</a>.</li>
3130
<li>Python Tutorial: <a href="http://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
31+
<li>Python Docs: <a href="https://docs.python.org/3/library/functions.html#classmethod">classmethod</a>.</li>
3232

3333

3434
</references>

python/ql/src/Functions/NonCls.ql

+3-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/**
22
* @name First parameter of a class method is not named 'cls'
3-
* @description Using an alternative name for the first parameter of a class method makes code more
4-
* difficult to read; PEP8 states that the first parameter to class methods should be 'cls'.
3+
* @description By the PEP8 style guide, the first parameter of a class method should be named `cls`.
54
* @kind problem
65
* @tags maintainability
76
* readability
@@ -13,30 +12,11 @@
1312
*/
1413

1514
import python
16-
17-
predicate first_arg_cls(Function f) {
18-
exists(string argname | argname = f.getArgName(0) |
19-
argname = "cls"
20-
or
21-
/* Not PEP8, but relatively common */
22-
argname = "mcls"
23-
)
24-
}
25-
26-
predicate is_type_method(Function f) {
27-
exists(ClassValue c | c.getScope() = f.getScope() and c.getASuperType() = ClassValue::type())
28-
}
29-
30-
predicate classmethod_decorators_only(Function f) {
31-
forall(Expr decorator | decorator = f.getADecorator() | decorator.(Name).getId() = "classmethod")
32-
}
15+
import MethodArgNames
3316

3417
from Function f, string message
3518
where
36-
(f.getADecorator().(Name).getId() = "classmethod" or is_type_method(f)) and
37-
not first_arg_cls(f) and
38-
classmethod_decorators_only(f) and
39-
not f.getName() = "__new__" and
19+
firstArgShouldReferToClsAndDoesnt(f) and
4020
(
4121
if exists(f.getArgName(0))
4222
then

python/ql/src/Functions/NonSelf.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
class Point:
2-
def __init__(val, x, y): # first parameter is mis-named 'val'
2+
def __init__(val, x, y): # BAD: first parameter is mis-named 'val'
33
val._x = x
44
val._y = y
55

66
class Point2:
7-
def __init__(self, x, y): # first parameter is correctly named 'self'
7+
def __init__(self, x, y): # GOOD: first parameter is correctly named 'self'
88
self._x = x
99
self._y = y

python/ql/src/Functions/NonSelf.qhelp

+4-8
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,18 @@
66

77
<overview>
88
<p> Normal methods should have at least one parameter and the first parameter should be called <code>self</code>.
9-
This makes the purpose of the parameter clear to other developers.
109
</p>
1110
</overview>
1211

1312
<recommendation>
14-
<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
13+
<p>Ensure that the first parameter of a normal method is named <code>self</code>, as recommended by the style guidelines
1514
in PEP 8.</p>
16-
<p>If there are no parameters, then it cannot be a normal method. It may need to be marked as a <code>staticmethod</code>
17-
or it could be moved out of the class as a normal function.
15+
<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.
1816
</p>
1917
</recommendation>
2018

2119
<example>
22-
<p>The following methods can both be used to assign values to variables in a <code>point</code>
23-
object. The second method makes the association clearer because the <code>self</code> parameter is
24-
used.</p>
20+
<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>
2521
<sample src="NonSelf.py" />
2622

2723

@@ -31,7 +27,7 @@ used.</p>
3127
<li>Python PEP 8: <a href="http://www.python.org/dev/peps/pep-0008/#function-and-method-arguments">Function and
3228
method arguments</a>.</li>
3329
<li>Python Tutorial: <a href="http://docs.python.org/2/tutorial/classes.html">Classes</a>.</li>
34-
30+
<li>Python Docs: <a href="https://docs.python.org/3/library/functions.html#staticmethod">staticmethod</a>.</li>
3531

3632

3733
</references>

python/ql/src/Functions/NonSelf.ql

+13-41
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/**
22
* @name First parameter of a method is not named 'self'
3-
* @description Using an alternative name for the first parameter of an instance method makes
4-
* code more difficult to read; PEP8 states that the first parameter to instance
5-
* methods should be 'self'.
3+
* @description By the PEP8 style guide, the first parameter of a normal method should be named `self`.
64
* @kind problem
75
* @tags maintainability
86
* readability
@@ -14,45 +12,19 @@
1412
*/
1513

1614
import python
17-
import semmle.python.libraries.Zope
15+
import MethodArgNames
1816

19-
predicate is_type_method(FunctionValue fv) {
20-
exists(ClassValue c | c.declaredAttribute(_) = fv and c.getASuperType() = ClassValue::type())
21-
}
22-
23-
predicate used_in_defining_scope(FunctionValue fv) {
24-
exists(Call c | c.getScope() = fv.getScope().getScope() and c.getFunc().pointsTo(fv))
25-
}
26-
27-
from Function f, FunctionValue fv, string message
17+
from Function f, string message
2818
where
29-
exists(ClassValue cls, string name |
30-
cls.declaredAttribute(name) = fv and
31-
cls.isNewStyle() and
32-
not name = "__new__" and
33-
not name = "__metaclass__" and
34-
not name = "__init_subclass__" and
35-
not name = "__class_getitem__" and
36-
/* declared in scope */
37-
f.getScope() = cls.getScope()
38-
) and
39-
not f.getArgName(0) = "self" and
40-
not is_type_method(fv) and
41-
fv.getScope() = f and
42-
not f.getName() = "lambda" and
43-
not used_in_defining_scope(fv) and
19+
firstArgShouldBeNamedSelfAndIsnt(f) and
4420
(
45-
(
46-
if exists(f.getArgName(0))
47-
then
48-
message =
49-
"Normal methods should have 'self', rather than '" + f.getArgName(0) +
50-
"', as their first parameter."
51-
else
52-
message =
53-
"Normal methods should have at least one parameter (the first of which should be 'self')."
54-
) and
55-
not f.hasVarArg()
56-
) and
57-
not fv instanceof ZopeInterfaceMethodValue
21+
if exists(f.getArgName(0))
22+
then
23+
message =
24+
"Normal methods should have 'self', rather than '" + f.getArgName(0) +
25+
"', as their first parameter."
26+
else
27+
message =
28+
"Normal methods should have at least one parameter (the first of which should be 'self')."
29+
)
5830
select f, message

python/ql/test/query-tests/Functions/general/NonCls.expected

-3
This file was deleted.

python/ql/test/query-tests/Functions/general/NonCls.qlref

-1
This file was deleted.

python/ql/test/query-tests/Functions/general/NonSelf.expected

-4
This file was deleted.

python/ql/test/query-tests/Functions/general/NonSelf.qlref

-1
This file was deleted.

0 commit comments

Comments
 (0)