Skip to content

Commit 9182383

Browse files
sonar-nigel[bot]Sonar Vibe Botguillaume-dequenne
authored andcommitted
SONARPY-3900 Rule S8490 Enum classes should not be decorated with "@DataClass" (#955)
Co-authored-by: Sonar Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: Guillaume Dequenne <guillaume.dequenne@sonarsource.com> GitOrigin-RevId: 2ea7d69e608246c14839314a1b539e1e06db7457
1 parent 90364f0 commit 9182383

File tree

7 files changed

+370
-0
lines changed

7 files changed

+370
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.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.ClassDef;
24+
import org.sonar.plugins.python.api.tree.Decorator;
25+
import org.sonar.plugins.python.api.tree.Expression;
26+
import org.sonar.plugins.python.api.tree.Tree;
27+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
28+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
29+
30+
@Rule(key = "S8490")
31+
public class DataClassOnEnumCheck extends PythonSubscriptionCheck {
32+
33+
private static final String MESSAGE = "Remove this \"@dataclass\" decorator; it is incompatible with Enum classes.";
34+
35+
private static final TypeMatcher IS_ENUM_MATCHER = TypeMatchers.any(
36+
TypeMatchers.isOrExtendsType("enum.Enum"),
37+
TypeMatchers.isOrExtendsType("enum.IntEnum"),
38+
TypeMatchers.isOrExtendsType("enum.IntFlag"));
39+
private static final TypeMatcher IS_DATACLASS_MATCHER = TypeMatchers.isType("dataclasses.dataclass");
40+
41+
@Override
42+
public void initialize(Context context) {
43+
context.registerSyntaxNodeConsumer(Tree.Kind.CLASSDEF, DataClassOnEnumCheck::checkClassDef);
44+
}
45+
46+
private static void checkClassDef(SubscriptionContext ctx) {
47+
ClassDef classDef = (ClassDef) ctx.syntaxNode();
48+
49+
if (classDef.decorators().isEmpty()) {
50+
return;
51+
}
52+
53+
if (!IS_ENUM_MATCHER.isTrueFor(classDef.name(), ctx)) {
54+
return;
55+
}
56+
57+
for (Decorator decorator : classDef.decorators()) {
58+
Expression decoratorExpr = getDecoratorFunctionExpression(decorator);
59+
if (IS_DATACLASS_MATCHER.isTrueFor(decoratorExpr, ctx)) {
60+
ctx.addIssue(decorator, MESSAGE);
61+
}
62+
}
63+
}
64+
65+
private static Expression getDecoratorFunctionExpression(Decorator decorator) {
66+
Expression expr = decorator.expression();
67+
if (expr instanceof CallExpression callExpr) {
68+
return callExpr.callee();
69+
}
70+
return expr;
71+
}
72+
}

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
@@ -181,6 +181,7 @@ public Stream<Class<?>> getChecks() {
181181
CorsCheck.class,
182182
CorsMiddlewareOrderingCheck.class,
183183
CsrfDisabledCheck.class,
184+
DataClassOnEnumCheck.class,
184185
DataEncryptionCheck.class,
185186
DbNoPasswordCheck.class,
186187
DeadStoreCheck.class,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<p>This is an issue when a class inherits from <code>Enum</code> and is also decorated with <code>@dataclass</code>.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>The <code>@dataclass</code> decorator and <code>Enum</code> classes are incompatible and should not be used together.</p>
4+
<p>The <code>@dataclass</code> decorator automatically generates special methods like <code>__init__</code>, <code>__repr__</code>, and
5+
<code>__eq__</code> for regular classes. It modifies the class definition to add these methods based on the class attributes.</p>
6+
<p>However, <code>Enum</code> classes use a special metaclass called <code>EnumMeta</code> that controls how enum members are created and behave. Enum
7+
members are not regular instances - they are singleton constants that are created when the class is defined.</p>
8+
<p>When you try to combine these two features, several problems occur:</p>
9+
<ul>
10+
<li>The <code>@dataclass</code> decorator tries to modify the class in ways that conflict with the Enum metaclass</li>
11+
<li>The automatic <code>__init__</code> generation from <code>@dataclass</code> interferes with how Enum members are instantiated</li>
12+
<li>At runtime, this typically results in a <code>TypeError</code> because the modifications are incompatible</li>
13+
</ul>
14+
<p>Enums are designed to represent a fixed set of named constants, while dataclasses are meant to hold data with potentially varying values. These are
15+
fundamentally different purposes that should not be mixed.</p>
16+
<h3>What is the potential impact?</h3>
17+
<p>This code pattern will cause a <code>TypeError</code> at runtime when Python attempts to create the class. The application will fail to start or
18+
the module will fail to import, preventing the code from running at all.</p>
19+
<p>This is a critical reliability issue that blocks normal program execution.</p>
20+
<h2>How to fix it</h2>
21+
<p>Remove the <code>@dataclass</code> decorator from the Enum class. Enums work perfectly fine without it and have their own mechanisms for defining
22+
members.</p>
23+
<h3>Code examples</h3>
24+
<h4>Noncompliant code example</h4>
25+
<pre data-diff-id="1" data-diff-type="noncompliant">
26+
from dataclasses import dataclass
27+
from enum import Enum
28+
29+
@dataclass # Noncompliant
30+
class Status(Enum):
31+
PENDING = 1
32+
APPROVED = 2
33+
REJECTED = 3
34+
</pre>
35+
<h4>Compliant solution</h4>
36+
<pre data-diff-id="1" data-diff-type="compliant">
37+
from enum import Enum
38+
39+
class Status(Enum):
40+
PENDING = 1
41+
APPROVED = 2
42+
REJECTED = 3
43+
</pre>
44+
<h2>Resources</h2>
45+
<h3>Documentation</h3>
46+
<ul>
47+
<li>Python Documentation - <a href="https://docs.python.org/3/library/enum.html">Enum</a></li>
48+
<li>Python Documentation - <a href="https://docs.python.org/3/library/dataclasses.html">dataclasses</a></li>
49+
<li>PEP 557 - <a href="https://peps.python.org/pep-0557/">Data Classes</a></li>
50+
</ul>
51+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "Enum classes should not be decorated with \"@dataclass\"",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"pitfall",
11+
"enum"
12+
],
13+
"defaultSeverity": "Blocker",
14+
"ruleSpecification": "RSPEC-8490",
15+
"sqKey": "S8490",
16+
"scope": "All",
17+
"quickfix": "unknown",
18+
"code": {
19+
"impacts": {
20+
"RELIABILITY": "BLOCKER"
21+
},
22+
"attribute": "LOGICAL"
23+
}
24+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@
328328
"S8413",
329329
"S8414",
330330
"S8415",
331+
"S8490",
331332
"S8494",
332333
"S8495",
333334
"S8504",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 DataClassOnEnumCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/dataClassOnEnum.py", new DataClassOnEnumCheck());
27+
}
28+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
from dataclasses import dataclass
2+
from enum import Enum, IntEnum, Flag, IntFlag, StrEnum
3+
import dataclasses
4+
5+
6+
# @dataclass on Enum - Noncompliant cases
7+
8+
@dataclass # Noncompliant {{Remove this "@dataclass" decorator; it is incompatible with Enum classes.}}
9+
class Status(Enum):
10+
PENDING = 1
11+
APPROVED = 2
12+
REJECTED = 3
13+
14+
15+
# @dataclass with arguments
16+
17+
@dataclass(frozen=True) # Noncompliant
18+
class Color(Enum):
19+
RED = 1
20+
GREEN = 2
21+
BLUE = 3
22+
23+
24+
# Using dataclasses.dataclass qualified name
25+
26+
@dataclasses.dataclass # Noncompliant
27+
class Direction(Enum):
28+
NORTH = "N"
29+
SOUTH = "S"
30+
EAST = "E"
31+
WEST = "W"
32+
33+
34+
@dataclasses.dataclass(eq=True, order=True) # Noncompliant
35+
class Priority(Enum):
36+
LOW = 1
37+
MEDIUM = 2
38+
HIGH = 3
39+
40+
41+
# @dataclass on IntEnum and Flag (transitively extend Enum)
42+
43+
@dataclass # Noncompliant
44+
class ErrorCode(IntEnum):
45+
NOT_FOUND = 404
46+
SERVER_ERROR = 500
47+
48+
49+
@dataclass # Noncompliant
50+
class Permission(Flag):
51+
READ = 1
52+
WRITE = 2
53+
EXECUTE = 4
54+
55+
56+
@dataclass # Noncompliant
57+
class FileMode(IntFlag):
58+
READ = 4
59+
WRITE = 2
60+
EXECUTE = 1
61+
62+
63+
# @dataclass on StrEnum (Python 3.11+)
64+
65+
@dataclass # Noncompliant
66+
class HttpMethod(StrEnum):
67+
GET = "GET"
68+
POST = "POST"
69+
70+
71+
# @dataclass on an indirect Enum subclass
72+
73+
class BaseStatus(Enum):
74+
pass
75+
76+
@dataclass # Noncompliant
77+
class ExtendedStatus(BaseStatus):
78+
PENDING = 1
79+
ACTIVE = 2
80+
81+
82+
# @dataclass among multiple decorators
83+
84+
def my_decorator(cls):
85+
return cls
86+
87+
@my_decorator
88+
@dataclass # Noncompliant
89+
class State(Enum):
90+
OPEN = "open"
91+
CLOSED = "closed"
92+
93+
94+
@dataclass # Noncompliant
95+
@my_decorator
96+
class Stage(Enum):
97+
START = 1
98+
END = 2
99+
100+
101+
# @dataclass with multiple keyword arguments
102+
103+
@dataclass(repr=False, eq=False) # Noncompliant
104+
class Suit(Enum):
105+
HEARTS = 1
106+
DIAMONDS = 2
107+
CLUBS = 3
108+
SPADES = 4
109+
110+
111+
# @dataclass on class with multiple bases including Enum
112+
113+
class Mixin:
114+
pass
115+
116+
@dataclass # Noncompliant
117+
class Mixed(Mixin, Enum):
118+
FIRST = 1
119+
SECOND = 2
120+
121+
122+
# Compliant: Enum without @dataclass
123+
124+
class CompliantStatus(Enum):
125+
PENDING = 1
126+
APPROVED = 2
127+
REJECTED = 3
128+
129+
130+
class CompliantErrorCode(IntEnum):
131+
NOT_FOUND = 404
132+
SERVER_ERROR = 500
133+
134+
135+
class CompliantPermission(Flag):
136+
READ = 1
137+
WRITE = 2
138+
EXECUTE = 4
139+
140+
141+
class CompliantHttpMethod(StrEnum):
142+
GET = "GET"
143+
POST = "POST"
144+
145+
146+
# Compliant: @dataclass on a plain class (not Enum)
147+
148+
@dataclass
149+
class Point:
150+
x: float
151+
y: float
152+
153+
154+
@dataclass(frozen=True)
155+
class Config:
156+
host: str
157+
port: int
158+
159+
160+
@dataclasses.dataclass
161+
class Rectangle:
162+
width: float
163+
height: float
164+
165+
166+
# Compliant: custom (non-dataclass) decorator on an Enum
167+
168+
@my_decorator
169+
class DecoratedStatus(Enum):
170+
ACTIVE = 1
171+
INACTIVE = 2
172+
173+
174+
# Compliant: Enum subclass without @dataclass
175+
176+
class CompliantExtended(BaseStatus):
177+
ACTIVE = 1
178+
INACTIVE = 2
179+
180+
181+
# Compliant: unknown base class — cannot confirm Enum inheritance
182+
183+
@dataclass
184+
class UnknownBase(UnknownParent):
185+
value: int = 0
186+
187+
188+
# Compliant: unknown decorator on an Enum — not a dataclass
189+
190+
@unknown_decorator
191+
class StatusWithUnknownDecorator(Enum):
192+
ACTIVE = 1
193+
INACTIVE = 2

0 commit comments

Comments
 (0)