Skip to content

Commit a9043e4

Browse files
sonar-nigel[bot]sonartech
authored andcommitted
SONARPY-3980 Rule S8515 TypeVars should not be both covariant and contravariant (#1014)
GitOrigin-RevId: e1596360a39433be9b8b6783cb293b438cab9962
1 parent 8401c22 commit a9043e4

File tree

6 files changed

+259
-0
lines changed

6 files changed

+259
-0
lines changed

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
@@ -440,6 +440,7 @@ public Stream<Class<?>> getChecks() {
440440
TrailingCommentCheck.class,
441441
TrailingWhitespaceCheck.class,
442442
TypeAliasAnnotationCheck.class,
443+
TypeVarCovariantAndContravariantCheck.class,
443444
TfFunctionDependOnOutsideVariableCheck.class,
444445
TfFunctionRecursivityCheck.class,
445446
TfInputShapeOnModelSubclassCheck.class,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
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.sonar.check.Rule;
20+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
21+
import org.sonar.plugins.python.api.SubscriptionContext;
22+
import org.sonar.plugins.python.api.tree.CallExpression;
23+
import org.sonar.plugins.python.api.tree.RegularArgument;
24+
import org.sonar.plugins.python.api.tree.Tree;
25+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
26+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
27+
import org.sonar.python.checks.utils.Expressions;
28+
import org.sonar.python.tree.TreeUtils;
29+
30+
@Rule(key = "S8515")
31+
public class TypeVarCovariantAndContravariantCheck extends PythonSubscriptionCheck {
32+
33+
private static final String MESSAGE = "Remove either \"covariant=True\" or \"contravariant=True\"; a TypeVar cannot be both covariant and contravariant.";
34+
private static final TypeMatcher TYPEVAR_MATCHER = TypeMatchers.any(
35+
TypeMatchers.isType("typing.TypeVar"),
36+
TypeMatchers.isType("typing_extensions.TypeVar")
37+
);
38+
39+
@Override
40+
public void initialize(Context context) {
41+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, TypeVarCovariantAndContravariantCheck::checkTypeVarCall);
42+
}
43+
44+
private static void checkTypeVarCall(SubscriptionContext ctx) {
45+
CallExpression call = (CallExpression) ctx.syntaxNode();
46+
if (!TYPEVAR_MATCHER.isTrueFor(call.callee(), ctx)) {
47+
return;
48+
}
49+
RegularArgument covariantArg = TreeUtils.argumentByKeyword("covariant", call.arguments());
50+
if (covariantArg == null) {
51+
return;
52+
}
53+
RegularArgument contravariantArg = TreeUtils.argumentByKeyword("contravariant", call.arguments());
54+
if (contravariantArg == null) {
55+
return;
56+
}
57+
if (Expressions.isTruthy(covariantArg.expression()) && Expressions.isTruthy(contravariantArg.expression())) {
58+
ctx.addIssue(call.callee(), MESSAGE);
59+
}
60+
}
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<p>This rule raises an issue when a <code>TypeVar</code> is declared with both <code>covariant=True</code> and <code>contravariant=True</code>.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>A <code>TypeVar</code> in Python represents a type variable used in generic programming. It can have one of three variance modes:</p>
4+
<ul>
5+
<li><strong>invariant</strong>, the default: the type must match exactly</li>
6+
<li><strong>covariant</strong> with <code>covariant=True</code>: the type can be replaced with a subtype</li>
7+
<li><strong>contravariant</strong> with <code>contravariant=True</code>: the type can be replaced with a supertype</li>
8+
</ul>
9+
<p>These variance modes are mutually exclusive. A type variable cannot be both covariant and contravariant simultaneously because these represent opposite relationships in the type hierarchy. Python's <code>typing</code> module will raise a <code>ValueError</code> at runtime when it encounters such a declaration.</p>
10+
<h3>What is the potential impact?</h3>
11+
<ul>
12+
<li>Python raises a <code>ValueError</code> when the module is imported, causing the application to crash immediately.</li>
13+
<li>Static type checkers cannot analyze the code properly, reducing confidence in the type safety of the codebase.</li>
14+
</ul>
15+
<h2>How to fix it</h2>
16+
<p>Choose the appropriate variance mode for your type variable:</p>
17+
<ul>
18+
<li>if you need covariance to allow subtypes, use only <code>covariant=True</code></li>
19+
<li>if you need contravariance to allow supertypes, use only <code>contravariant=True</code></li>
20+
<li>if you need exact type matching, omit both parameters for the default invariant behavior</li>
21+
</ul>
22+
<h3>Code examples</h3>
23+
<h4>Noncompliant code example</h4>
24+
<pre data-diff-id="1" data-diff-type="noncompliant">
25+
from typing import TypeVar
26+
27+
T = TypeVar('T', covariant=True, contravariant=True) # Noncompliant
28+
</pre>
29+
<h4>Compliant solution</h4>
30+
<pre data-diff-id="1" data-diff-type="compliant">
31+
from typing import TypeVar
32+
33+
T_co = TypeVar('T_co', covariant=True)
34+
</pre>
35+
<h2>Resources</h2>
36+
<h3>Documentation</h3>
37+
<ul>
38+
<li>Python Documentation - <a href="https://docs.python.org/3/library/typing.html#typing.TypeVar"><code>TypeVar</code></a></li>
39+
<li>PEP 484 - <a href="https://peps.python.org/pep-0484/#covariance-and-contravariance">Covariance and Contravariance</a></li>
40+
<li>Mypy - <a href="https://mypy.readthedocs.io/en/stable/generics.html#variance-of-generic-types">Variance of generic types</a></li>
41+
</ul>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"title": "TypeVars should not be both covariant and contravariant",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Blocker",
11+
"ruleSpecification": "RSPEC-8515",
12+
"sqKey": "S8515",
13+
"scope": "All",
14+
"quickfix": "unknown"
15+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
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 TypeVarCovariantAndContravariantCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify(
27+
"src/test/resources/checks/typeVarCovariantAndContravariant.py",
28+
new TypeVarCovariantAndContravariantCheck());
29+
}
30+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from typing import TypeVar
2+
from typing import TypeVar as TV
3+
from typing_extensions import TypeVar as ExtTV
4+
import typing
5+
import typing_extensions
6+
7+
T1 = TypeVar('T1', covariant=True, contravariant=True) # Noncompliant
8+
# ^^^^^^^
9+
T2 = TypeVar('T2', contravariant=True, covariant=True) # Noncompliant
10+
# ^^^^^^^
11+
T3 = TypeVar('T3', int, str, covariant=True, contravariant=True) # Noncompliant
12+
# ^^^^^^^
13+
T4 = TypeVar('T4', bound=int, covariant=True, contravariant=True) # Noncompliant
14+
# ^^^^^^^
15+
T5 = typing.TypeVar('T5', covariant=True, contravariant=True) # Noncompliant
16+
# ^^^^^^^^^^^^^^
17+
T6 = typing_extensions.TypeVar('T6', covariant=True, contravariant=True) # Noncompliant
18+
# ^^^^^^^^^^^^^^^^^^^^^^^^^
19+
20+
cov = True
21+
T7 = TypeVar('T7', covariant=cov, contravariant=True) # Noncompliant
22+
# ^^^^^^^
23+
24+
contra = True
25+
T8 = TypeVar('T8', covariant=True, contravariant=contra) # Noncompliant
26+
# ^^^^^^^
27+
28+
cov2 = True
29+
contra2 = True
30+
T9 = TypeVar('T9', covariant=cov2, contravariant=contra2) # Noncompliant
31+
# ^^^^^^^
32+
33+
34+
T_co = TypeVar('T_co', covariant=True)
35+
T_contra = TypeVar('T_contra', contravariant=True)
36+
T_inv = TypeVar('T_inv')
37+
T_inv2 = TypeVar('T_inv2', covariant=False, contravariant=False)
38+
T_inv3 = TypeVar('T_inv3', covariant=True, contravariant=False)
39+
T_inv4 = TypeVar('T_inv4', covariant=False, contravariant=True)
40+
T_ext_co = typing_extensions.TypeVar('T_ext_co', covariant=True)
41+
T_ext_contra = typing_extensions.TypeVar('T_ext_contra', contravariant=True)
42+
43+
44+
def get_flag():
45+
return True
46+
47+
48+
T_unk = TypeVar('T_unk', covariant=get_flag(), contravariant=True)
49+
T_unk2 = TypeVar('T_unk2', covariant=True, contravariant=get_flag())
50+
51+
52+
def my_func(name, covariant=False, contravariant=False):
53+
pass
54+
55+
56+
my_func('X', covariant=True, contravariant=True)
57+
58+
59+
cov3 = False
60+
cov3 = True
61+
T_fn = TypeVar('T_fn', covariant=cov3, contravariant=True)
62+
63+
cov_false = False
64+
T_false_name = TypeVar('T_false_name', covariant=cov_false, contravariant=True)
65+
66+
T_num_noncompliant = TypeVar('T_num_noncompliant', covariant=1, contravariant=1) # Noncompliant
67+
T_num_mixed = TypeVar('T_num_mixed', covariant=1, contravariant=True) # Noncompliant
68+
69+
T_num_zero_cov = TypeVar('T_num_zero_cov', covariant=0, contravariant=True)
70+
T_num_zero_contra = TypeVar('T_num_zero_contra', covariant=True, contravariant=0)
71+
T_num_zero_both = TypeVar('T_num_zero_both', covariant=0, contravariant=0)
72+
73+
T_float_zero_cov = TypeVar('T_float_zero_cov', covariant=0.0, contravariant=True)
74+
T_float_zero_contra = TypeVar('T_float_zero_contra', covariant=True, contravariant=0.0)
75+
76+
T_complex_zero_cov = TypeVar('T_complex_zero_cov', covariant=0j, contravariant=True)
77+
T_complex_zero_contra = TypeVar('T_complex_zero_contra', covariant=True, contravariant=0j)
78+
79+
T_none_cov = TypeVar('T_none_cov', covariant=None, contravariant=True)
80+
T_none_contra = TypeVar('T_none_contra', covariant=True, contravariant=None)
81+
82+
T_str_noncompliant = TypeVar('T_str_noncompliant', covariant="yes", contravariant=True) # Noncompliant
83+
T_str_empty_cov = TypeVar('T_str_empty_cov', covariant="", contravariant=True)
84+
85+
T_list_empty_cov = TypeVar('T_list_empty_cov', covariant=[], contravariant=True)
86+
T_list_empty_contra = TypeVar('T_list_empty_contra', covariant=True, contravariant=[])
87+
T_list_nonempty = TypeVar('T_list_nonempty', covariant=[1], contravariant=True) # Noncompliant
88+
89+
T_tuple_empty_cov = TypeVar('T_tuple_empty_cov', covariant=(), contravariant=True)
90+
T_tuple_empty_contra = TypeVar('T_tuple_empty_contra', covariant=True, contravariant=())
91+
T_tuple_nonempty = TypeVar('T_tuple_nonempty', covariant=(1,), contravariant=True) # Noncompliant
92+
93+
T_dict_empty_cov = TypeVar('T_dict_empty_cov', covariant={}, contravariant=True)
94+
T_dict_empty_contra = TypeVar('T_dict_empty_contra', covariant=True, contravariant={})
95+
T_dict_nonempty = TypeVar('T_dict_nonempty', covariant={1: 2}, contravariant=True) # Noncompliant
96+
97+
T_set_nonempty = TypeVar('T_set_nonempty', covariant={1, 2}, contravariant=True) # Noncompliant
98+
99+
T_ext_inv = typing_extensions.TypeVar('T_ext_inv', covariant=False, contravariant=False)
100+
101+
T_alias = TV('T_alias', covariant=True, contravariant=True) # Noncompliant
102+
# ^^
103+
T_ext_alias = ExtTV('T_ext_alias', covariant=True, contravariant=True) # Noncompliant
104+
# ^^^^^
105+
106+
T_str_contra_truthy = TypeVar('T_str_contra_truthy', covariant=True, contravariant="yes") # Noncompliant
107+
# ^^^^^^^
108+
T_str_empty_contra = TypeVar('T_str_empty_contra', covariant=True, contravariant="")
109+
110+
kwargs_with_covariant = {'covariant': True, 'contravariant': True}
111+
T_kwargs_unpacked = TypeVar('T_kwargs_unpacked', **kwargs_with_covariant)

0 commit comments

Comments
 (0)