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.
Files changed (58) hide show
  1. adapters/__init__.py +0 -0
  2. adapters/base.py +27 -0
  3. adapters/github_adapter.py +164 -0
  4. adapters/gitlab_adapter.py +207 -0
  5. adapters/local_adapter.py +136 -0
  6. banner.py +71 -0
  7. cli.py +606 -0
  8. config/__init__.py +1 -0
  9. config/rules.yaml +371 -0
  10. core/__init__.py +235 -0
  11. core/ast_detector.py +853 -0
  12. core/change.py +46 -0
  13. core/composer.py +93 -0
  14. core/evaluator.py +15 -0
  15. core/ignore_manager.py +71 -0
  16. core/knowledge.py +77 -0
  17. core/parser.py +181 -0
  18. core/parser_manager.py +104 -0
  19. core/quality_manager.py +117 -0
  20. core/renderer.py +197 -0
  21. core/rule_base.py +98 -0
  22. core/rule_runtime.py +103 -0
  23. core/rules.py +718 -0
  24. core/run_config.py +85 -0
  25. core/semantic_diff.py +359 -0
  26. core/signal_model.py +21 -0
  27. core/signals_registry.py +62 -0
  28. diffsense-2.2.12.dist-info/METADATA +18 -0
  29. diffsense-2.2.12.dist-info/RECORD +58 -0
  30. diffsense-2.2.12.dist-info/WHEEL +5 -0
  31. diffsense-2.2.12.dist-info/entry_points.txt +3 -0
  32. diffsense-2.2.12.dist-info/licenses/LICENSE +176 -0
  33. diffsense-2.2.12.dist-info/top_level.txt +11 -0
  34. diffsense_mcp/__init__.py +1 -0
  35. diffsense_mcp/launcher.py +28 -0
  36. diffsense_mcp/server.py +687 -0
  37. governance/lifecycle.py +54 -0
  38. main.py +318 -0
  39. rules/__init__.py +246 -0
  40. rules/api_compatibility.py +372 -0
  41. rules/collection_handling.py +349 -0
  42. rules/concurrency.py +194 -0
  43. rules/concurrency_adapter.py +250 -0
  44. rules/cross_language_adapter.py +444 -0
  45. rules/exception_handling.py +320 -0
  46. rules/go_rules.py +401 -0
  47. rules/null_safety.py +301 -0
  48. rules/resource_management.py +222 -0
  49. rules/yaml_adapter.py +195 -0
  50. run_audit.py +478 -0
  51. sdk/cpp_adapter.py +238 -0
  52. sdk/go_adapter.py +199 -0
  53. sdk/java_adapter.py +199 -0
  54. sdk/javascript_adapter.py +229 -0
  55. sdk/language_adapter.py +313 -0
  56. sdk/python_adapter.py +195 -0
  57. sdk/rule.py +63 -0
  58. 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