diffsense 2.2.12__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.
- adapters/__init__.py +0 -0
- adapters/base.py +27 -0
- adapters/github_adapter.py +164 -0
- adapters/gitlab_adapter.py +207 -0
- adapters/local_adapter.py +136 -0
- banner.py +71 -0
- cli.py +606 -0
- config/__init__.py +1 -0
- config/rules.yaml +371 -0
- core/__init__.py +235 -0
- core/ast_detector.py +853 -0
- core/change.py +46 -0
- core/composer.py +93 -0
- core/evaluator.py +15 -0
- core/ignore_manager.py +71 -0
- core/knowledge.py +77 -0
- core/parser.py +181 -0
- core/parser_manager.py +104 -0
- core/quality_manager.py +117 -0
- core/renderer.py +197 -0
- core/rule_base.py +98 -0
- core/rule_runtime.py +103 -0
- core/rules.py +718 -0
- core/run_config.py +85 -0
- core/semantic_diff.py +359 -0
- core/signal_model.py +21 -0
- core/signals_registry.py +62 -0
- diffsense-2.2.12.dist-info/METADATA +18 -0
- diffsense-2.2.12.dist-info/RECORD +58 -0
- diffsense-2.2.12.dist-info/WHEEL +5 -0
- diffsense-2.2.12.dist-info/entry_points.txt +3 -0
- diffsense-2.2.12.dist-info/licenses/LICENSE +176 -0
- diffsense-2.2.12.dist-info/top_level.txt +11 -0
- diffsense_mcp/__init__.py +1 -0
- diffsense_mcp/launcher.py +28 -0
- diffsense_mcp/server.py +687 -0
- governance/lifecycle.py +54 -0
- main.py +318 -0
- rules/__init__.py +246 -0
- rules/api_compatibility.py +372 -0
- rules/collection_handling.py +349 -0
- rules/concurrency.py +194 -0
- rules/concurrency_adapter.py +250 -0
- rules/cross_language_adapter.py +444 -0
- rules/exception_handling.py +320 -0
- rules/go_rules.py +401 -0
- rules/null_safety.py +301 -0
- rules/resource_management.py +222 -0
- rules/yaml_adapter.py +195 -0
- run_audit.py +478 -0
- sdk/cpp_adapter.py +238 -0
- sdk/go_adapter.py +199 -0
- sdk/java_adapter.py +199 -0
- sdk/javascript_adapter.py +229 -0
- sdk/language_adapter.py +313 -0
- sdk/python_adapter.py +195 -0
- sdk/rule.py +63 -0
- sdk/signal.py +14 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
from sdk.rule import BaseRule
|
|
4
|
+
from sdk.signal import Signal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PublicMethodRemovedRule(BaseRule):
|
|
8
|
+
"""检测删除公共方法 - 排除测试文件"""
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
# 只匹配非测试文件中的方法删除
|
|
12
|
+
self._removed_method = re.compile(
|
|
13
|
+
r'^-\s*(?:public|protected)\s+(?!.*test)(?!.*Test)\s*(?:static\s+)?(?:\w+(?:<[^>]+>)?\s+)?\w+\s*\([^)]*\)',
|
|
14
|
+
re.MULTILINE
|
|
15
|
+
)
|
|
16
|
+
self._added_deprecated = re.compile(
|
|
17
|
+
r'^\+.*@Deprecated',
|
|
18
|
+
re.MULTILINE
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def id(self) -> str:
|
|
23
|
+
return "api.public_method_removed"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def severity(self) -> str:
|
|
27
|
+
return "critical"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def impact(self) -> str:
|
|
31
|
+
return "runtime"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def rationale(self) -> str:
|
|
35
|
+
return "Public method removed from production code, breaks API compatibility"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def rule_type(self) -> str:
|
|
39
|
+
return "regression"
|
|
40
|
+
|
|
41
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
42
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
43
|
+
files = diff_data.get('files', [])
|
|
44
|
+
|
|
45
|
+
if self._removed_method.search(raw_diff):
|
|
46
|
+
# 如果没有添加@Deprecated 作为过渡,则报告
|
|
47
|
+
if not self._added_deprecated.search(raw_diff):
|
|
48
|
+
return {"file": files[0] if files else "unknown"}
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MethodSignatureChangedRule(BaseRule):
|
|
54
|
+
"""检测方法签名变更 - 只在真正破坏 API 兼容性时触发"""
|
|
55
|
+
|
|
56
|
+
def __init__(self):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def id(self) -> str:
|
|
61
|
+
return "api.method_signature_changed"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def severity(self) -> str:
|
|
65
|
+
return "high"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def impact(self) -> str:
|
|
69
|
+
return "runtime"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def rationale(self) -> str:
|
|
73
|
+
return "Method signature changed in production code"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def rule_type(self) -> str:
|
|
77
|
+
return "regression"
|
|
78
|
+
|
|
79
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
80
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
81
|
+
files = diff_data.get('files', [])
|
|
82
|
+
|
|
83
|
+
# 只检测真正的签名变化:同一方法名,参数数量或类型不同
|
|
84
|
+
# 使用更严格的检测:必须有完整的参数列表变化
|
|
85
|
+
import re as re_module
|
|
86
|
+
|
|
87
|
+
# 查找同时有删除和添加的同一方法
|
|
88
|
+
removed_methods = re_module.findall(
|
|
89
|
+
r'^-\s*(?:public|protected)\s+(?:static\s+)?(?:\w+(?:<[^>]+>)?\s+)+(\w+)\s*\(([^)]*)\)',
|
|
90
|
+
raw_diff,
|
|
91
|
+
re_module.MULTILINE
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
added_methods = re_module.findall(
|
|
95
|
+
r'^\+\s*(?:public|protected)\s+(?:static\s+)?(?:\w+(?:<[^>]+>)?\s+)+(\w+)\s*\(([^)]*)\)',
|
|
96
|
+
raw_diff,
|
|
97
|
+
re.MULTILINE
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
for rem_name, rem_params in removed_methods:
|
|
101
|
+
for add_name, add_params in added_methods:
|
|
102
|
+
if rem_name == add_name and rem_params != add_params:
|
|
103
|
+
return {"file": files[0] if files else "unknown", "method": rem_name}
|
|
104
|
+
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class FieldRemovedRule(BaseRule):
|
|
109
|
+
"""检测删除公共字段 - 只在真正删除字段时触发"""
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
# 更严格的正则:确保是删除字段,而不是修改修饰符
|
|
113
|
+
self._removed_field = re.compile(
|
|
114
|
+
r'^-\s*(?:public|protected)\s+(?!.*\bfinal\b)(?!.*\bstatic\b).*\s+\w+\s+\w+\s*;',
|
|
115
|
+
re.MULTILINE
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FieldRemovedRule(BaseRule):
|
|
120
|
+
"""检测删除公共字段 - 只在真正删除字段时触发"""
|
|
121
|
+
|
|
122
|
+
def __init__(self):
|
|
123
|
+
self._removed_field = re.compile(
|
|
124
|
+
r'^-\s*(?:public|protected)\s+(?!.*\bfinal\b)(?!.*\bstatic\b).*\s+\w+\s+\w+\s*;',
|
|
125
|
+
re.MULTILINE
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def id(self) -> str:
|
|
130
|
+
return "api.public_field_removed"
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def severity(self) -> str:
|
|
134
|
+
return "high"
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def impact(self) -> str:
|
|
138
|
+
return "runtime"
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def rationale(self) -> str:
|
|
142
|
+
return "Public field removed, breaks direct field access"
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def rule_type(self) -> str:
|
|
146
|
+
return "regression"
|
|
147
|
+
|
|
148
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
149
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
150
|
+
|
|
151
|
+
if self._removed_field.search(raw_diff):
|
|
152
|
+
files = diff_data.get('files', [])
|
|
153
|
+
return {"file": files[0] if files else "unknown"}
|
|
154
|
+
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ConstructorRemovedRule(BaseRule):
|
|
159
|
+
"""检测删除构造函数"""
|
|
160
|
+
|
|
161
|
+
def __init__(self):
|
|
162
|
+
self._removed_ctor = re.compile(
|
|
163
|
+
r'^-\s*(?:public|protected)\s+\w+\s*\([^)]*\)',
|
|
164
|
+
re.MULTILINE
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def id(self) -> str:
|
|
169
|
+
return "api.constructor_removed"
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def severity(self) -> str:
|
|
173
|
+
return "high"
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def impact(self) -> str:
|
|
177
|
+
return "runtime"
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def rationale(self) -> str:
|
|
181
|
+
return "Constructor removed, breaks instantiation code"
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def rule_type(self) -> str:
|
|
185
|
+
return "regression"
|
|
186
|
+
|
|
187
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
188
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
189
|
+
|
|
190
|
+
if self._removed_ctor.search(raw_diff):
|
|
191
|
+
files = diff_data.get('files', [])
|
|
192
|
+
return {"file": files[0] if files else "unknown"}
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class InterfaceChangedRule(BaseRule):
|
|
198
|
+
"""检测接口变更 - 只在真正的接口文件中检测"""
|
|
199
|
+
|
|
200
|
+
def __init__(self):
|
|
201
|
+
self._interface_decl = re.compile(
|
|
202
|
+
r'^\s*(?:public\s+)?(?:abstract\s+)?(?:interface|@interface)\s+\w+',
|
|
203
|
+
re.MULTILINE
|
|
204
|
+
)
|
|
205
|
+
# 更严格的正则:只匹配方法声明,不匹配普通类的方法
|
|
206
|
+
self._method_decl = re.compile(
|
|
207
|
+
r'^\s*(?:public\s+)?(?:abstract\s+)?[\w<>,\s]+\s+\w+\s*\([^)]*\)\s*(?:;|default|{)?',
|
|
208
|
+
re.MULTILINE
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def id(self) -> str:
|
|
213
|
+
return "api.interface_changed"
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def severity(self) -> str:
|
|
217
|
+
# 降级为 medium,因为接口变更不总是破坏性的
|
|
218
|
+
return "medium"
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def impact(self) -> str:
|
|
222
|
+
return "runtime"
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def rationale(self) -> str:
|
|
226
|
+
return "Interface method added/removed in interface file"
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def rule_type(self) -> str:
|
|
230
|
+
return "regression"
|
|
231
|
+
|
|
232
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
233
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
234
|
+
files = diff_data.get('files', [])
|
|
235
|
+
|
|
236
|
+
# 关键:只检测接口文件
|
|
237
|
+
is_interface_file = False
|
|
238
|
+
for line in raw_diff.split('\n'):
|
|
239
|
+
# 检查 diff 中是否包含 interface 声明
|
|
240
|
+
if 'interface ' in line.lower() or '@interface' in line.lower():
|
|
241
|
+
is_interface_file = True
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if not is_interface_file:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
# 只在接口文件中检测方法变更
|
|
248
|
+
if self._method_decl.search(raw_diff):
|
|
249
|
+
return {"file": files[0] if files else "unknown", "change": "interface_method"}
|
|
250
|
+
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class AnnotationRemovedRule(BaseRule):
|
|
255
|
+
"""检测删除重要注解"""
|
|
256
|
+
|
|
257
|
+
def __init__(self):
|
|
258
|
+
self._important_annotations = [
|
|
259
|
+
'@Override', '@Deprecated', '@Nullable', '@Nonnull', '@NotNull',
|
|
260
|
+
'@Transactional', '@Cacheable', '@Async', '@Scheduled'
|
|
261
|
+
]
|
|
262
|
+
self._removed_annotation = re.compile(
|
|
263
|
+
r'^-\s*@(?:' + '|'.join([ann.replace('@', '') for ann in self._important_annotations]) + r')',
|
|
264
|
+
re.MULTILINE
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def id(self) -> str:
|
|
269
|
+
return "api.important_annotation_removed"
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def severity(self) -> str:
|
|
273
|
+
return "medium"
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def impact(self) -> str:
|
|
277
|
+
return "runtime"
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def rationale(self) -> str:
|
|
281
|
+
return "Important annotation removed, may change behavior"
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def rule_type(self) -> str:
|
|
285
|
+
return "regression"
|
|
286
|
+
|
|
287
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
288
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
289
|
+
|
|
290
|
+
if self._removed_annotation.search(raw_diff):
|
|
291
|
+
files = diff_data.get('files', [])
|
|
292
|
+
return {"file": files[0] if files else "unknown"}
|
|
293
|
+
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class DeprecatedApiAddedRule(BaseRule):
|
|
298
|
+
"""检测添加@Deprecated 注解(需要关注迁移)"""
|
|
299
|
+
|
|
300
|
+
def __init__(self):
|
|
301
|
+
self._added_deprecated = re.compile(
|
|
302
|
+
r'^\+\s*@Deprecated(?:\([^)]*\))?',
|
|
303
|
+
re.MULTILINE
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def id(self) -> str:
|
|
308
|
+
return "api.deprecated_added"
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def severity(self) -> str:
|
|
312
|
+
return "low"
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def impact(self) -> str:
|
|
316
|
+
return "maintenance"
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def rationale(self) -> str:
|
|
320
|
+
return "API marked as deprecated, users need migration plan"
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def rule_type(self) -> str:
|
|
324
|
+
return "absolute"
|
|
325
|
+
|
|
326
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
327
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
328
|
+
|
|
329
|
+
if self._added_deprecated.search(raw_diff):
|
|
330
|
+
files = diff_data.get('files', [])
|
|
331
|
+
return {"file": files[0] if files else "unknown"}
|
|
332
|
+
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class SerialVersionUIDChangedRule(BaseRule):
|
|
337
|
+
"""检测 SerialVersionUID 变更(破坏序列化兼容性)"""
|
|
338
|
+
|
|
339
|
+
def __init__(self):
|
|
340
|
+
self._serial_change = re.compile(
|
|
341
|
+
r'^[-+].*serialVersionUID\s*=',
|
|
342
|
+
re.MULTILINE
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def id(self) -> str:
|
|
347
|
+
return "api.serialversionuid_changed"
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def severity(self) -> str:
|
|
351
|
+
return "high"
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def impact(self) -> str:
|
|
355
|
+
return "runtime"
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def rationale(self) -> str:
|
|
359
|
+
return "serialVersionUID changed, breaks deserialization of previously serialized objects"
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def rule_type(self) -> str:
|
|
363
|
+
return "regression"
|
|
364
|
+
|
|
365
|
+
def evaluate(self, diff_data: Dict[str, Any], signals: List[Signal]) -> Optional[Dict[str, Any]]:
|
|
366
|
+
raw_diff = diff_data.get('raw_diff', "")
|
|
367
|
+
|
|
368
|
+
if self._serial_change.search(raw_diff):
|
|
369
|
+
files = diff_data.get('files', [])
|
|
370
|
+
return {"file": files[0] if files else "unknown"}
|
|
371
|
+
|
|
372
|
+
return None
|