threadcheck 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- threadcheck/__init__.py +13 -0
- threadcheck/__main__.py +3 -0
- threadcheck/_version.py +1 -0
- threadcheck/cli.py +89 -0
- threadcheck/dynamic/__init__.py +0 -0
- threadcheck/dynamic/__main__.py +38 -0
- threadcheck/dynamic/clock.py +31 -0
- threadcheck/dynamic/hook.py +97 -0
- threadcheck/dynamic/tracker.py +191 -0
- threadcheck/dynamic/transform.py +192 -0
- threadcheck/pytest_plugin.py +60 -0
- threadcheck/reporting/__init__.py +0 -0
- threadcheck/reporting/formatter.py +33 -0
- threadcheck/reporting/sarif.py +100 -0
- threadcheck/reporting/types.py +3 -0
- threadcheck/static/__init__.py +0 -0
- threadcheck/static/analyzer.py +104 -0
- threadcheck/static/lock_tracker.py +42 -0
- threadcheck/static/models.py +48 -0
- threadcheck/static/visitors.py +324 -0
- threadcheck-0.0.1.dist-info/METADATA +248 -0
- threadcheck-0.0.1.dist-info/RECORD +25 -0
- threadcheck-0.0.1.dist-info/WHEEL +4 -0
- threadcheck-0.0.1.dist-info/entry_points.txt +5 -0
- threadcheck-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .models import RaceWarning, Severity, WarningCategory, Confidence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _calc_confidence(context, func_name: str | None) -> Confidence:
|
|
8
|
+
if func_name and context.is_thread_target(func_name):
|
|
9
|
+
return Confidence.HIGH
|
|
10
|
+
if context.has_any_thread():
|
|
11
|
+
return Confidence.MEDIUM
|
|
12
|
+
return Confidence.LOW
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GlobalVisitor(ast.NodeVisitor):
|
|
16
|
+
def __init__(self, filepath: Path, context):
|
|
17
|
+
self.filepath = filepath
|
|
18
|
+
self.context = context
|
|
19
|
+
self.warnings: list[RaceWarning] = []
|
|
20
|
+
self._globals_in_function: set[str] = set()
|
|
21
|
+
self._current_func: str | None = None
|
|
22
|
+
|
|
23
|
+
def visit_FunctionDef(self, node):
|
|
24
|
+
old = self._globals_in_function, self._current_func
|
|
25
|
+
self._globals_in_function = set()
|
|
26
|
+
self._current_func = node.name
|
|
27
|
+
self.generic_visit(node)
|
|
28
|
+
self._globals_in_function, self._current_func = old
|
|
29
|
+
|
|
30
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
31
|
+
|
|
32
|
+
def visit_Global(self, node):
|
|
33
|
+
for name in node.names:
|
|
34
|
+
self._globals_in_function.add(name)
|
|
35
|
+
|
|
36
|
+
def _check_name(self, node):
|
|
37
|
+
if isinstance(node, ast.Name) and node.id in self._globals_in_function:
|
|
38
|
+
if self.context.is_protected(node.lineno):
|
|
39
|
+
return
|
|
40
|
+
confidence = _calc_confidence(self.context, self._current_func)
|
|
41
|
+
self.warnings.append(
|
|
42
|
+
RaceWarning(
|
|
43
|
+
file=self.filepath,
|
|
44
|
+
line=node.lineno,
|
|
45
|
+
col=node.col_offset,
|
|
46
|
+
severity=Severity.WARNING,
|
|
47
|
+
category=WarningCategory.UNSAFE_GLOBAL,
|
|
48
|
+
message=f"Global variable `{node.id}` modified without lock",
|
|
49
|
+
suggestion=f"Use `threading.Lock()` to protect `{node.id}`",
|
|
50
|
+
confidence=confidence,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def visit_Assign(self, node):
|
|
55
|
+
for target in node.targets:
|
|
56
|
+
if isinstance(target, ast.Name):
|
|
57
|
+
self._check_name(target)
|
|
58
|
+
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
59
|
+
for elt in target.elts:
|
|
60
|
+
if isinstance(elt, ast.Name):
|
|
61
|
+
self._check_name(elt)
|
|
62
|
+
self.generic_visit(node)
|
|
63
|
+
|
|
64
|
+
def visit_AugAssign(self, node):
|
|
65
|
+
self._check_name(node.target)
|
|
66
|
+
self.generic_visit(node)
|
|
67
|
+
|
|
68
|
+
def visit_Delete(self, node):
|
|
69
|
+
for target in node.targets:
|
|
70
|
+
if isinstance(target, ast.Name):
|
|
71
|
+
self._check_name(target)
|
|
72
|
+
self.generic_visit(node)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class NonlocalVisitor(ast.NodeVisitor):
|
|
76
|
+
def __init__(self, filepath: Path, context):
|
|
77
|
+
self.filepath = filepath
|
|
78
|
+
self.context = context
|
|
79
|
+
self.warnings: list[RaceWarning] = []
|
|
80
|
+
self._nonlocals_in_function: set[str] = set()
|
|
81
|
+
self._current_func: str | None = None
|
|
82
|
+
|
|
83
|
+
def visit_FunctionDef(self, node):
|
|
84
|
+
old = self._nonlocals_in_function, self._current_func
|
|
85
|
+
self._nonlocals_in_function = set()
|
|
86
|
+
self._current_func = node.name
|
|
87
|
+
self.generic_visit(node)
|
|
88
|
+
self._nonlocals_in_function, self._current_func = old
|
|
89
|
+
|
|
90
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
91
|
+
|
|
92
|
+
def visit_Nonlocal(self, node):
|
|
93
|
+
for name in node.names:
|
|
94
|
+
self._nonlocals_in_function.add(name)
|
|
95
|
+
|
|
96
|
+
def _check_name(self, node):
|
|
97
|
+
if isinstance(node, ast.Name) and node.id in self._nonlocals_in_function:
|
|
98
|
+
if self.context.is_protected(node.lineno):
|
|
99
|
+
return
|
|
100
|
+
confidence = _calc_confidence(self.context, self._current_func)
|
|
101
|
+
self.warnings.append(
|
|
102
|
+
RaceWarning(
|
|
103
|
+
file=self.filepath,
|
|
104
|
+
line=node.lineno,
|
|
105
|
+
col=node.col_offset,
|
|
106
|
+
severity=Severity.WARNING,
|
|
107
|
+
category=WarningCategory.UNSAFE_NONLOCAL,
|
|
108
|
+
message=f"Nonlocal variable `{node.id}` modified without lock",
|
|
109
|
+
suggestion=f"Use `threading.Lock()` to protect `{node.id}`",
|
|
110
|
+
confidence=confidence,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def visit_Assign(self, node):
|
|
115
|
+
for target in node.targets:
|
|
116
|
+
if isinstance(target, ast.Name):
|
|
117
|
+
self._check_name(target)
|
|
118
|
+
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
119
|
+
for elt in target.elts:
|
|
120
|
+
if isinstance(elt, ast.Name):
|
|
121
|
+
self._check_name(elt)
|
|
122
|
+
self.generic_visit(node)
|
|
123
|
+
|
|
124
|
+
def visit_AugAssign(self, node):
|
|
125
|
+
self._check_name(node.target)
|
|
126
|
+
self.generic_visit(node)
|
|
127
|
+
|
|
128
|
+
def visit_Delete(self, node):
|
|
129
|
+
for target in node.targets:
|
|
130
|
+
if isinstance(target, ast.Name):
|
|
131
|
+
self._check_name(target)
|
|
132
|
+
self.generic_visit(node)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ThreadVisitor(ast.NodeVisitor):
|
|
136
|
+
def __init__(self, filepath: Path, context):
|
|
137
|
+
self.filepath = filepath
|
|
138
|
+
self.context = context
|
|
139
|
+
self.warnings: list[RaceWarning] = []
|
|
140
|
+
|
|
141
|
+
def visit_Call(self, node):
|
|
142
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr == "Thread":
|
|
143
|
+
if isinstance(node.func.value, ast.Name) and node.func.value.id == "threading":
|
|
144
|
+
target = None
|
|
145
|
+
for kw in node.keywords:
|
|
146
|
+
if kw.arg == "target":
|
|
147
|
+
if isinstance(kw.value, ast.Name):
|
|
148
|
+
target = kw.value.id
|
|
149
|
+
elif isinstance(kw.value, (ast.Lambda, ast.FunctionDef)):
|
|
150
|
+
target = "<lambda>"
|
|
151
|
+
elif isinstance(kw.value, ast.Attribute):
|
|
152
|
+
target = ast.unparse(kw.value)
|
|
153
|
+
label = f" (target={target})" if target else ""
|
|
154
|
+
self.warnings.append(
|
|
155
|
+
RaceWarning(
|
|
156
|
+
file=self.filepath,
|
|
157
|
+
line=node.lineno,
|
|
158
|
+
col=node.col_offset,
|
|
159
|
+
severity=Severity.INFO,
|
|
160
|
+
category=WarningCategory.THREAD_USAGE,
|
|
161
|
+
message=f"Thread creation detected{label}",
|
|
162
|
+
suggestion="Ensure shared variable access in thread functions is lock-protected",
|
|
163
|
+
confidence=Confidence.LOW,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
self.generic_visit(node)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SharedMutableVisitor(ast.NodeVisitor):
|
|
170
|
+
def __init__(self, filepath: Path, context):
|
|
171
|
+
self.filepath = filepath
|
|
172
|
+
self.context = context
|
|
173
|
+
self.warnings: list[RaceWarning] = []
|
|
174
|
+
self._module_level_assigns: set[str] = set()
|
|
175
|
+
self._in_function = False
|
|
176
|
+
self._current_func: str | None = None
|
|
177
|
+
|
|
178
|
+
def visit_Module(self, node):
|
|
179
|
+
for item in node.body:
|
|
180
|
+
if isinstance(item, ast.Assign):
|
|
181
|
+
for target in item.targets:
|
|
182
|
+
if isinstance(target, ast.Name) and _is_mutable_literal(item.value):
|
|
183
|
+
self._module_level_assigns.add(target.id)
|
|
184
|
+
self.generic_visit(node)
|
|
185
|
+
|
|
186
|
+
def visit_FunctionDef(self, node):
|
|
187
|
+
self._in_function = True
|
|
188
|
+
self._current_func = node.name
|
|
189
|
+
self.generic_visit(node)
|
|
190
|
+
self._current_func = None
|
|
191
|
+
self._in_function = False
|
|
192
|
+
|
|
193
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
194
|
+
|
|
195
|
+
def _add_warning(self, node, var_name: str, detail: str = ""):
|
|
196
|
+
if self.context.is_protected(node.lineno):
|
|
197
|
+
return
|
|
198
|
+
confidence = _calc_confidence(self.context, self._current_func)
|
|
199
|
+
msg = f"Module-level mutable object `{var_name}` modified inside function"
|
|
200
|
+
if detail:
|
|
201
|
+
msg = f"Module-level mutable object `{detail}` called from multiple threads"
|
|
202
|
+
self.warnings.append(
|
|
203
|
+
RaceWarning(
|
|
204
|
+
file=self.filepath,
|
|
205
|
+
line=node.lineno,
|
|
206
|
+
col=node.col_offset,
|
|
207
|
+
severity=Severity.WARNING,
|
|
208
|
+
category=WarningCategory.SHARED_MUTABLE,
|
|
209
|
+
message=msg,
|
|
210
|
+
suggestion="Consider using thread-safe data structures or add lock protection",
|
|
211
|
+
confidence=confidence,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def visit_Assign(self, node):
|
|
216
|
+
if self._in_function:
|
|
217
|
+
for target in node.targets:
|
|
218
|
+
if isinstance(target, ast.Name) and target.id in self._module_level_assigns:
|
|
219
|
+
self._add_warning(node, target.id)
|
|
220
|
+
self.generic_visit(node)
|
|
221
|
+
|
|
222
|
+
def visit_AugAssign(self, node):
|
|
223
|
+
if self._in_function and isinstance(node.target, ast.Name):
|
|
224
|
+
if node.target.id in self._module_level_assigns:
|
|
225
|
+
self._add_warning(node, node.target.id)
|
|
226
|
+
self.generic_visit(node)
|
|
227
|
+
|
|
228
|
+
def visit_Call(self, node):
|
|
229
|
+
if self._in_function:
|
|
230
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr in (
|
|
231
|
+
"append", "extend", "pop", "remove", "clear",
|
|
232
|
+
"insert", "sort", "reverse", "update", "add", "discard",
|
|
233
|
+
):
|
|
234
|
+
if isinstance(node.func.value, ast.Name) and node.func.value.id in self._module_level_assigns:
|
|
235
|
+
detail = f"{node.func.value.id}.{node.func.attr}()"
|
|
236
|
+
self._add_warning(node, node.func.value.id, detail)
|
|
237
|
+
self.generic_visit(node)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ClassAttributeVisitor(ast.NodeVisitor):
|
|
241
|
+
def __init__(self, filepath: Path, context):
|
|
242
|
+
self.filepath = filepath
|
|
243
|
+
self.context = context
|
|
244
|
+
self.warnings: list[RaceWarning] = []
|
|
245
|
+
self._current_class: str | None = None
|
|
246
|
+
self._current_method: str | None = None
|
|
247
|
+
self._class_is_thread = False
|
|
248
|
+
|
|
249
|
+
def visit_ClassDef(self, node):
|
|
250
|
+
old = self._current_class, self._class_is_thread
|
|
251
|
+
self._current_class = node.name
|
|
252
|
+
self._class_is_thread = any(
|
|
253
|
+
_is_name_or_attr(base, "Thread") for base in node.bases
|
|
254
|
+
)
|
|
255
|
+
self.generic_visit(node)
|
|
256
|
+
self._current_class, self._class_is_thread = old
|
|
257
|
+
|
|
258
|
+
def visit_FunctionDef(self, node):
|
|
259
|
+
old = self._current_method
|
|
260
|
+
if node.name in ("__init__", "__new__", "__class_getitem__"):
|
|
261
|
+
self._current_method = None
|
|
262
|
+
self.generic_visit(node)
|
|
263
|
+
self._current_method = old
|
|
264
|
+
return
|
|
265
|
+
self._current_method = node.name
|
|
266
|
+
self.generic_visit(node)
|
|
267
|
+
self._current_method = old
|
|
268
|
+
|
|
269
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
270
|
+
|
|
271
|
+
def _check_attr(self, node, attr_name: str):
|
|
272
|
+
if self.context.is_protected(node.lineno):
|
|
273
|
+
return
|
|
274
|
+
in_thread_target = self._current_method and self.context.is_thread_target(
|
|
275
|
+
self._current_method
|
|
276
|
+
)
|
|
277
|
+
if in_thread_target or self._class_is_thread:
|
|
278
|
+
confidence = Confidence.HIGH
|
|
279
|
+
elif self.context.has_any_thread():
|
|
280
|
+
confidence = Confidence.MEDIUM
|
|
281
|
+
else:
|
|
282
|
+
confidence = Confidence.LOW
|
|
283
|
+
self.warnings.append(
|
|
284
|
+
RaceWarning(
|
|
285
|
+
file=self.filepath,
|
|
286
|
+
line=node.lineno,
|
|
287
|
+
col=node.col_offset,
|
|
288
|
+
severity=Severity.WARNING,
|
|
289
|
+
category=WarningCategory.CLASS_ATTRIBUTE,
|
|
290
|
+
message=(
|
|
291
|
+
f"Attribute `{attr_name}` of class `{self._current_class}` "
|
|
292
|
+
f"modified without lock in method `{self._current_method}`"
|
|
293
|
+
),
|
|
294
|
+
suggestion="Use `threading.Lock()` to protect class attribute access",
|
|
295
|
+
confidence=confidence,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def visit_Assign(self, node):
|
|
300
|
+
if self._current_class and self._current_method:
|
|
301
|
+
for target in node.targets:
|
|
302
|
+
if isinstance(target, ast.Attribute):
|
|
303
|
+
if isinstance(target.value, ast.Name) and target.value.id == "self":
|
|
304
|
+
self._check_attr(node, target.attr)
|
|
305
|
+
self.generic_visit(node)
|
|
306
|
+
|
|
307
|
+
def visit_AugAssign(self, node):
|
|
308
|
+
if self._current_class and self._current_method:
|
|
309
|
+
if isinstance(node.target, ast.Attribute):
|
|
310
|
+
if isinstance(node.target.value, ast.Name) and node.target.value.id == "self":
|
|
311
|
+
self._check_attr(node, node.target.attr)
|
|
312
|
+
self.generic_visit(node)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _is_mutable_literal(node):
|
|
316
|
+
return isinstance(node, (ast.List, ast.Dict, ast.Set, ast.ListComp, ast.SetComp, ast.DictComp))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _is_name_or_attr(node, name: str) -> bool:
|
|
320
|
+
if isinstance(node, ast.Name):
|
|
321
|
+
return node.id == name
|
|
322
|
+
if isinstance(node, ast.Attribute):
|
|
323
|
+
return node.attr == name
|
|
324
|
+
return False
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: threadcheck
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Data Race Detector for Free-Threading Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/ChidcGithub/Threadcheck
|
|
6
|
+
Project-URL: Source, https://github.com/ChidcGithub/Threadcheck
|
|
7
|
+
Project-URL: BugTracker, https://github.com/ChidcGithub/Threadcheck/issues
|
|
8
|
+
Author: threadcheck contributors
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest; extra == 'test'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# threadcheck
|
|
25
|
+
|
|
26
|
+
[](https://www.python.org)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
[](https://pypi.org/project/threadcheck/)
|
|
29
|
+
[]()
|
|
30
|
+
|
|
31
|
+
[**中文文档**](README_CN.md)
|
|
32
|
+
|
|
33
|
+
Python data race detector for the free-threading (no-GIL) era. Detects concurrent access to shared mutable state in multi-threaded Python programs through static analysis and runtime instrumentation.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Problem
|
|
38
|
+
|
|
39
|
+
Python 3.14 (2026) introduces free-threading, removing the Global Interpreter Lock (GIL). This enables true parallel execution of multi-threaded code, but the ecosystem lacks debugging tools for concurrency bugs. Go has `-race`, C++ has ThreadSanitizer, Java has SpotBugs. Python has nothing comparable without recompiling the interpreter with Clang and TSan.
|
|
40
|
+
|
|
41
|
+
threadcheck is a pure-Python race detector that installs with `pip` and works out of the box.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Static analysis** -- scans AST for shared mutable state (global, nonlocal, class attributes) and missing lock protection
|
|
48
|
+
- **Runtime detection** -- instruments code via AST transformation at import time; tracks memory accesses with vector clocks and detects happens-before violations
|
|
49
|
+
- **Lock-aware suppression** -- understands `threading.Lock`, `threading.RLock`, and `with`-based synchronization; raises confidence when locks are missing and suppresses warnings when they are present
|
|
50
|
+
- **Confidence scoring** -- each warning tagged HIGH / MEDIUM / LOW based on thread context and lock coverage
|
|
51
|
+
- **CLI tool** -- single-command static scan or instrumented execution
|
|
52
|
+
- **JSON and SARIF output** -- suitable for CI/CD pipeline integration
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install threadcheck
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python 3.12+. Python 3.14+ is recommended for free-threading features.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
### Static Analysis
|
|
69
|
+
|
|
70
|
+
Scan a file or directory for potential race conditions without running any code:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
threadcheck scan my_project/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Output:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
[WARNING] [HIGH] [unsafe_global] my_project/counter.py:8:8
|
|
80
|
+
Global variable `counter` modified without lock in thread
|
|
81
|
+
Suggestion: use `threading.Lock()` to protect access
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
JSON output:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
threadcheck scan my_project/ --json -o report.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Runtime Detection
|
|
91
|
+
|
|
92
|
+
Execute a script with instrumentation to detect actual data races:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
threadcheck run my_script.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Output for a racing script:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Data races detected:
|
|
102
|
+
[!] `counter`
|
|
103
|
+
Thread-28928 (write) at my_script.py:8
|
|
104
|
+
Thread-9888 (write) at my_script.py:8
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A script protected with locks reports:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
No data races detected
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### CI Integration
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pip install threadcheck
|
|
117
|
+
threadcheck scan src/ --json -o threadcheck_report.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Commands
|
|
123
|
+
|
|
124
|
+
| Command | Description | Status |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `scan <path>` | Static race analysis of file or directory | Stable |
|
|
127
|
+
| `run <script>` | Execute script with runtime race detection | Beta |
|
|
128
|
+
| `check-compat <path>` | Free-threading compatibility check | Planned |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Library Usage
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from threadcheck import analyze_file, analyze_path
|
|
136
|
+
|
|
137
|
+
warnings = analyze_file("my_module.py")
|
|
138
|
+
warnings = analyze_path("src/")
|
|
139
|
+
|
|
140
|
+
for w in warnings:
|
|
141
|
+
print(f"{w.file}:{w.line} [{w.confidence.value}] {w.message}")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from threadcheck.dynamic.tracker import ThreadCheckTracker
|
|
146
|
+
from threadcheck.dynamic.transform import transform_and_compile
|
|
147
|
+
|
|
148
|
+
code = transform_and_compile(source, "script.py")
|
|
149
|
+
ThreadCheckTracker.start()
|
|
150
|
+
exec(code, {"_threadcheck_tracker": ThreadCheckTracker})
|
|
151
|
+
ThreadCheckTracker.stop()
|
|
152
|
+
|
|
153
|
+
print(ThreadCheckTracker.format_races())
|
|
154
|
+
ThreadCheckTracker.reset()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Architecture
|
|
160
|
+
|
|
161
|
+
### Static Analysis Pipeline
|
|
162
|
+
|
|
163
|
+
1. Parse source into AST
|
|
164
|
+
2. Identify shared mutable state: globals, nonlocals, class attributes (`self.x`), module-level mutable objects
|
|
165
|
+
3. Detect thread creation sites (`threading.Thread`)
|
|
166
|
+
4. Cross-reference with lock usage (`with lock:`, `lock.acquire()`)
|
|
167
|
+
5. Assign confidence: HIGH (thread target, no lock), MEDIUM (thread present, no lock), LOW (suspicious pattern, no thread context)
|
|
168
|
+
6. Report findings with repair suggestions
|
|
169
|
+
|
|
170
|
+
### Runtime Detection Pipeline
|
|
171
|
+
|
|
172
|
+
1. Parse source into AST
|
|
173
|
+
2. Identify shared variables per function scope
|
|
174
|
+
3. Transform AST: inject `write_before()`, `lock_acquire()`, `lock_release()` calls around shared variable accesses
|
|
175
|
+
4. Compile and execute transformed code under a tracker that maintains per-thread vector clocks
|
|
176
|
+
5. On lock acquire, synchronize clocks (happens-before merge)
|
|
177
|
+
6. After execution, scan access log for conflicting operations (concurrent writes or write-read pairs with no happens-before relationship)
|
|
178
|
+
7. Report detected races with thread IDs and source locations
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Project Structure
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
threadcheck/
|
|
186
|
+
├── pyproject.toml
|
|
187
|
+
├── src/
|
|
188
|
+
│ └── threadcheck/
|
|
189
|
+
│ ├── __init__.py
|
|
190
|
+
│ ├── __main__.py
|
|
191
|
+
│ ├── _version.py # single version source
|
|
192
|
+
│ ├── cli.py # argument parsing + dispatch
|
|
193
|
+
│ ├── static/
|
|
194
|
+
│ │ ├── analyzer.py # static analysis entry point
|
|
195
|
+
│ │ ├── visitors.py # AST visitors (global, nonlocal, class attr, shared mutable)
|
|
196
|
+
│ │ ├── lock_tracker.py # lock usage analysis
|
|
197
|
+
│ │ └── models.py # RaceWarning, Severity, Confidence models
|
|
198
|
+
│ ├── dynamic/
|
|
199
|
+
│ │ ├── __main__.py # run_script entry point
|
|
200
|
+
│ │ ├── transform.py # AST transformation engine
|
|
201
|
+
│ │ ├── tracker.py # runtime tracker with vector clocks
|
|
202
|
+
│ │ ├── clock.py # vector clock implementation
|
|
203
|
+
│ │ └── hook.py # sys.meta_path import hook
|
|
204
|
+
│ ├── reporting/
|
|
205
|
+
│ │ ├── formatter.py # terminal output formatting
|
|
206
|
+
│ │ └── types.py # type re-exports
|
|
207
|
+
│ └── pytest_plugin.py # pytest integration (planned)
|
|
208
|
+
├── tests/
|
|
209
|
+
│ ├── fixtures/ # sample code with known races
|
|
210
|
+
│ ├── test_static_analyzer.py
|
|
211
|
+
│ └── test_dynamic_detector.py
|
|
212
|
+
└── README.md / README_CN.md
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Roadmap
|
|
218
|
+
|
|
219
|
+
| Phase | Feature | Status |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| 1 | CLI, static analysis (globals/nonlocals) | Done |
|
|
222
|
+
| 2 | Class attributes, lock suppression, confidence scoring | Done |
|
|
223
|
+
| 3 | AST import hook, runtime instrumentation, vector clocks | Done |
|
|
224
|
+
| 4 | Race report deduplication, enhanced happens-before analysis | Planned |
|
|
225
|
+
| 5 | SARIF output, JSON reporting | Planned |
|
|
226
|
+
| 6 | pytest plugin | Planned |
|
|
227
|
+
| 7 | Free-threading compatibility checker | Planned |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Limitations
|
|
232
|
+
|
|
233
|
+
- Static analysis may produce false positives (reports race that cannot occur at runtime) and false negatives (misses races that involve indirect sharing through aliases or containers)
|
|
234
|
+
- Runtime detection modifies the AST before execution; code that introspects its own source or frame objects may behave differently
|
|
235
|
+
- Runtime instrumentation incurs overhead (approximately 2-5x slowdown for typical code)
|
|
236
|
+
- Lock tracking supports `threading.Lock`, `threading.RLock`, and standard `with`-based patterns; other synchronization primitives (`threading.Event`, `threading.Condition`, third-party libraries) are not tracked
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
MIT
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Contributing
|
|
247
|
+
|
|
248
|
+
Contributions are welcome. Please open an issue or submit a pull request on GitHub.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
threadcheck/__init__.py,sha256=ySY4Q9AN1WXp1sDMrOd0SotrC1PwTNqBeDTt55wa8Z4,374
|
|
2
|
+
threadcheck/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
|
|
3
|
+
threadcheck/_version.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
|
|
4
|
+
threadcheck/cli.py,sha256=RzbJWXtvjGBfZkdlxGIxYYA9DqajCEWnpQHFwGP6S8g,2834
|
|
5
|
+
threadcheck/pytest_plugin.py,sha256=5AvZWl_CR7sZAmvcG8QqG7za50nn87GSKkCALnpvyjc,1918
|
|
6
|
+
threadcheck/dynamic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
threadcheck/dynamic/__main__.py,sha256=9_Q8impnDtwP2SfvYCPCgTS0kmw1UQZhPCsDU42gI3I,962
|
|
8
|
+
threadcheck/dynamic/clock.py,sha256=1zbHTHNFgwxCvP9yYA25UwFgCUnydkNOzcH1ogEisfY,907
|
|
9
|
+
threadcheck/dynamic/hook.py,sha256=fg_OR4p9rlCs5ES09VPteq9riss-avd-PR1uMZEsxZk,3032
|
|
10
|
+
threadcheck/dynamic/tracker.py,sha256=Ty8Fm2zWQxU8PP2MLb8tzhyExy40aNQdjgX29Vu5oEM,6215
|
|
11
|
+
threadcheck/dynamic/transform.py,sha256=_ldNaih-q4U0OJcxBH11LwIKcT-AVKB3zI9JcxBr-zs,6556
|
|
12
|
+
threadcheck/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
threadcheck/reporting/formatter.py,sha256=OZUdeOfcMq54J4b9jFQBhXgD_KSjVYsLCY9EGDu1Q_o,865
|
|
14
|
+
threadcheck/reporting/sarif.py,sha256=cXih48-HhUXfIFcfG5GVa7xBiPVvidvLfnpMESCVR1o,3572
|
|
15
|
+
threadcheck/reporting/types.py,sha256=KG8lwBtpGIEfi5YYb2xp-uHWxxXR71SwDWKWxnIeqq0,97
|
|
16
|
+
threadcheck/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
threadcheck/static/analyzer.py,sha256=MvQDdGwAV_c8Gc4ms1R7sCrjA38jPcvU2Rf5SvJgaiA,3087
|
|
18
|
+
threadcheck/static/lock_tracker.py,sha256=NbuR5zWKZ7jTdT2KRV06Sq1xPNCqeuvjnCnorQAIPUE,1476
|
|
19
|
+
threadcheck/static/models.py,sha256=iUaQjvrfoocqBHf7nTAa493EZFr0RDxGcqb4g5xeOeo,1113
|
|
20
|
+
threadcheck/static/visitors.py,sha256=yJCxZ_7K8c9DzDnQL4H1U85ViUAPhfZIujlkU1P3zf4,12538
|
|
21
|
+
threadcheck-0.0.1.dist-info/METADATA,sha256=IrMM0wHwuvMjwBdOt7fHDLfIlhWtGM_LtZ7Vrs_bULM,8504
|
|
22
|
+
threadcheck-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
23
|
+
threadcheck-0.0.1.dist-info/entry_points.txt,sha256=ZmGa6T6j2vSEq5-vjMGtunc55yiQDRGuyZG0W9PJDSU,105
|
|
24
|
+
threadcheck-0.0.1.dist-info/licenses/LICENSE,sha256=wqDapPVwjdlXZPEEEoRLLST4Pw4vKcoFCjTPXW-VeXE,1062
|
|
25
|
+
threadcheck-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chidc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|