Skip to content

Commit 90364f0

Browse files
joke1196marc-jasper-sonarsource
authored andcommitted
SONARPY-4016 Rule S8511 Multiple inheritance should not create Method Resolution Order (MRO) conflicts (#1046)
Co-authored-by: Marc Jasper <marc.jasper@sonarsource.com> GitOrigin-RevId: 162f6b70593e6b0898303e9f1909388427d190f6
1 parent 3873563 commit 90364f0

File tree

9 files changed

+698
-0
lines changed

9 files changed

+698
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import java.util.ArrayDeque;
20+
import java.util.ArrayList;
21+
import java.util.Deque;
22+
import java.util.HashSet;
23+
import java.util.List;
24+
import java.util.Set;
25+
import javax.annotation.CheckForNull;
26+
import org.sonar.check.Rule;
27+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
28+
import org.sonar.plugins.python.api.SubscriptionContext;
29+
import org.sonar.plugins.python.api.tree.ArgList;
30+
import org.sonar.plugins.python.api.tree.Argument;
31+
import org.sonar.plugins.python.api.tree.ClassDef;
32+
import org.sonar.plugins.python.api.tree.Expression;
33+
import org.sonar.plugins.python.api.tree.RegularArgument;
34+
import org.sonar.plugins.python.api.tree.Tree;
35+
import org.sonar.plugins.python.api.types.v2.ClassType;
36+
import org.sonar.plugins.python.api.types.v2.PythonType;
37+
import org.sonar.plugins.python.api.types.v2.TypeWrapper;
38+
39+
@Rule(key = "S8511")
40+
public class MultipleInheritanceMROConflictCheck extends PythonSubscriptionCheck {
41+
42+
private static final String MESSAGE = "Reorder or remove base classes to fix this MRO conflict.";
43+
private static final String SECONDARY_MESSAGE = "This base class is an ancestor of another listed base class appearing after it.";
44+
45+
@Override
46+
public void initialize(Context context) {
47+
context.registerSyntaxNodeConsumer(Tree.Kind.CLASSDEF, MultipleInheritanceMROConflictCheck::checkClassDef);
48+
}
49+
50+
private static void checkClassDef(SubscriptionContext ctx) {
51+
ClassDef classDef = (ClassDef) ctx.syntaxNode();
52+
ArgList argList = classDef.args();
53+
if (argList == null) {
54+
return;
55+
}
56+
57+
List<Expression> bases = collectPositionalBases(argList);
58+
if (bases.size() < 2) {
59+
return;
60+
}
61+
62+
List<ClassType> types = new ArrayList<>();
63+
for (Expression base : bases) {
64+
types.add(resolveClassType(base));
65+
}
66+
67+
int conflictIndex = findAncestorConflictIndex(types);
68+
if (hasMroConflict(types, conflictIndex)) {
69+
PreciseIssue issue = ctx.addIssue(classDef.name(), MESSAGE);
70+
if (conflictIndex >= 0) {
71+
issue.secondary(bases.get(conflictIndex), SECONDARY_MESSAGE);
72+
}
73+
}
74+
}
75+
76+
private static List<Expression> collectPositionalBases(ArgList argList) {
77+
List<Expression> bases = new ArrayList<>();
78+
for (Argument argument : argList.arguments()) {
79+
if (argument instanceof RegularArgument regularArgument && regularArgument.keywordArgument() == null) {
80+
bases.add(regularArgument.expression());
81+
}
82+
}
83+
return bases;
84+
}
85+
86+
/**
87+
* Detects an MRO conflict: either via C3 when every base is fully resolved, or otherwise only
88+
* when {@link #findAncestorConflictIndex} finds an earlier base that is a strict superclass of a
89+
* later base in our type graph (same structural issue as {@code class C(A, B)} with {@code B}
90+
* subclassing {@code A}).
91+
*/
92+
private static boolean hasMroConflict(List<ClassType> types, int conflictIndex) {
93+
return isFullyResolved(types)
94+
? !ClassType.wouldHaveValidMro(types)
95+
: (conflictIndex >= 0);
96+
}
97+
98+
/**
99+
* Returns {@code true} if every base class is fully resolved (non-null, with a fully known type
100+
* hierarchy). Only when this is true can we run the complete C3 algorithm safely.
101+
*/
102+
private static boolean isFullyResolved(List<ClassType> types) {
103+
for (ClassType type : types) {
104+
if (type == null || type.hasUnresolvedHierarchy()) {
105+
return false;
106+
}
107+
}
108+
return true;
109+
}
110+
111+
/**
112+
* Returns the index {@code i} of the first base class that is an ancestor of some later base
113+
* class at index {@code j > i}, or {@code -1} if no such pair exists.
114+
*/
115+
private static int findAncestorConflictIndex(List<ClassType> types) {
116+
for (int i = 0; i < types.size() - 1; i++) {
117+
if (laterBaseExtendsEarlier(types, i)) {
118+
return i;
119+
}
120+
}
121+
return -1;
122+
}
123+
124+
private static boolean laterBaseExtendsEarlier(List<ClassType> types, int i) {
125+
ClassType typeI = types.get(i);
126+
if (typeI == null) {
127+
return false;
128+
}
129+
for (int j = i + 1; j < types.size(); j++) {
130+
ClassType typeJ = types.get(j);
131+
if (typeJ != null && isOrExtendsClass(typeJ, typeI)) {
132+
return true;
133+
}
134+
}
135+
return false;
136+
}
137+
138+
/**
139+
* Returns {@code true} if {@code candidate} is or extends {@code ancestor} (i.e., {@code ancestor}
140+
* appears anywhere in {@code candidate}'s type hierarchy). Uses reference equality since
141+
* {@link ClassType} instances are canonical singletons within a single analysis.
142+
*/
143+
private static boolean isOrExtendsClass(ClassType candidate, ClassType ancestor) {
144+
Set<PythonType> visited = new HashSet<>();
145+
Deque<PythonType> queue = new ArrayDeque<>();
146+
queue.add(candidate);
147+
while (!queue.isEmpty()) {
148+
PythonType current = queue.poll();
149+
if (!visited.add(current)) {
150+
continue;
151+
}
152+
if (current == ancestor) {
153+
return true;
154+
}
155+
if (current instanceof ClassType ct) {
156+
enqueueDirectSuperclasses(ct, queue);
157+
}
158+
}
159+
return false;
160+
}
161+
162+
private static void enqueueDirectSuperclasses(ClassType ct, Deque<PythonType> queue) {
163+
for (TypeWrapper sw : ct.superClasses()) {
164+
queue.add(sw.type());
165+
}
166+
}
167+
168+
@CheckForNull
169+
private static ClassType resolveClassType(Expression expression) {
170+
PythonType type = expression.typeV2();
171+
return (type instanceof ClassType classType) ? classType : null;
172+
}
173+
}

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ public Stream<Class<?>> getChecks() {
319319
MissingNewlineAtEndOfFileCheck.class,
320320
ModifiedParameterValueCheck.class,
321321
ModuleNameCheck.class,
322+
MultipleInheritanceMROConflictCheck.class,
322323
MultipleWhitespaceCheck.class,
323324
MutableDefaultValueCheck.class,
324325
NeedlessPassCheck.class,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<p>This rule raises an issue when a class definition with multiple inheritance results in an unresolvable Method Resolution Order (MRO).</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>Python uses the C3 linearization algorithm to determine the Method Resolution Order (MRO) — the order in which base classes are searched when
4+
looking up methods and attributes. When a class is created with multiple inheritance, Python must be able to construct a valid MRO. If the inheritance
5+
structure violates the C3 linearization rules, Python raises a <code>TypeError</code> at class definition time, preventing the class from being
6+
created at all.</p>
7+
<p>The C3 algorithm enforces these key constraints:</p>
8+
<ul>
9+
<li><strong>Local precedence ordering</strong>: If class <code>C</code> inherits from <code>A</code> then <code>B</code>, then <code>A</code> must
10+
appear before <code>B</code> in the MRO.</li>
11+
<li><strong>Monotonicity</strong>: If class <code>A</code> appears before class <code>B</code> in the MRO of a parent class, then <code>A</code>
12+
must also appear before <code>B</code> in the MRO of any child class.</li>
13+
</ul>
14+
<p>MRO inconsistencies cause immediate runtime failures. The class cannot be instantiated or used, breaking the application at import time.</p>
15+
<h2>How to fix it</h2>
16+
<p>Restructure the inheritance hierarchy to respect the C3 linearization rules. Remove redundant base classes from the inheritance list, as a class
17+
does not need to directly inherit from a grandparent if it is already inherited through a parent.</p>
18+
<h3>Code examples</h3>
19+
<h4>Noncompliant code example</h4>
20+
<pre data-diff-id="1" data-diff-type="noncompliant">
21+
class A:
22+
pass
23+
24+
class B(A):
25+
pass
26+
27+
class C(A):
28+
pass
29+
30+
class D(B, A, C): # Noncompliant: explicitly inheriting from A violates local precedence
31+
pass
32+
</pre>
33+
<h4>Compliant solution</h4>
34+
<pre data-diff-id="1" data-diff-type="compliant">
35+
class A:
36+
pass
37+
38+
class B(A):
39+
pass
40+
41+
class C(A):
42+
pass
43+
44+
class D(B, C): # Compliant: A is inherited through B and C
45+
pass
46+
</pre>
47+
<h2>Resources</h2>
48+
<h3>Documentation</h3>
49+
<ul>
50+
<li>Python Documentation - <a href="https://docs.python.org/3/tutorial/classes.html#multiple-inheritance">Multiple Inheritance</a></li>
51+
<li>Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#class.__mro__"><code>__mro__</code> attribute</a></li>
52+
<li>Python - <a href="https://www.python.org/download/releases/2.3/mro/">The Python 2.3 Method Resolution Order</a></li>
53+
</ul>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Multiple inheritance should not create Method Resolution Order (MRO) conflicts",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "15 min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Critical",
11+
"ruleSpecification": "RSPEC-8511",
12+
"sqKey": "S8511",
13+
"scope": "Main",
14+
"quickfix": "unknown",
15+
"code": {
16+
"impacts": {
17+
"RELIABILITY": "HIGH"
18+
},
19+
"attribute": "LOGICAL"
20+
}
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class MultipleInheritanceMROConflictCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/multipleInheritanceMROConflict.py", new MultipleInheritanceMROConflictCheck());
27+
}
28+
29+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
class Base:
2+
pass
3+
4+
class Mid(Base):
5+
pass
6+
7+
class Leaf(Mid):
8+
pass
9+
10+
11+
class Broken(Base, Leaf): # Noncompliant {{Reorder or remove base classes to fix this MRO conflict.}}
12+
# ^^^^^^ ^^^^< {{This base class is an ancestor of another listed base class appearing after it.}}
13+
pass
14+
15+
16+
class CommonRoot:
17+
pass
18+
19+
class LeftChild(CommonRoot):
20+
pass
21+
22+
class RightChild(CommonRoot):
23+
pass
24+
25+
class LeftFirst(LeftChild, RightChild):
26+
pass
27+
28+
class RightFirst(RightChild, LeftChild):
29+
pass
30+
31+
class C3Conflict(LeftFirst, RightFirst): # Noncompliant {{Reorder or remove base classes to fix this MRO conflict.}}
32+
pass
33+
34+
35+
def not_a_class():
36+
pass
37+
38+
39+
class CompliantPartialResolution(not_a_class, Base):
40+
pass
41+
42+
43+
class OnlyMetaclass(metaclass=type):
44+
pass
45+
46+
47+
class CompliantC3Order(LeftFirst, RightChild):
48+
pass
49+
50+
51+
# --- Super types not fully resolved (heuristic path; full C3 is skipped) ---
52+
from unknown_module import ExternalMixin, ExternalOther
53+
54+
55+
# Compliant: later base not fully resolved for C3 (unknown import / incomplete hierarchy), and the
56+
# heuristic does not show it extending Base.
57+
class MidWithUnresolvedMixin(Base, ExternalMixin):
58+
pass
59+
60+
61+
# Compliant: two unrelated imported bases (incomplete hierarchies); no "earlier ancestor of later" pair in the model
62+
class CompliantTwoUnresolvedPeers(ExternalMixin, ExternalOther):
63+
pass
64+
65+
66+
# Bases not fully resolved for C3 (second base has incomplete hierarchy), yet the heuristic still
67+
# finds that the second base subclasses Base — flagged without running full C3.
68+
class ConflictMid(Base, MidWithUnresolvedMixin): # Noncompliant {{Reorder or remove base classes to fix this MRO conflict.}}
69+
# ^^^^^^^^^^^ ^^^^< {{This base class is an ancestor of another listed base class appearing after it.}}
70+
pass

0 commit comments

Comments
 (0)