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,54 @@
1
+ from enum import Enum
2
+ from typing import Dict, Any
3
+
4
+ class RuleStatus(Enum):
5
+ EXPERIMENTAL = "experimental"
6
+ BETA = "beta"
7
+ STABLE = "stable"
8
+ DEPRECATED = "deprecated"
9
+ DISABLED = "disabled"
10
+
11
+ class LifecycleManager:
12
+ """
13
+ Manages the lifecycle of rules.
14
+ Decides if a rule should run based on its status and configuration.
15
+ """
16
+
17
+ def __init__(self, config: Dict[str, Any] = None):
18
+ self.config = config or {}
19
+ # Default policy: Run everything except DISABLED
20
+ self.min_status = RuleStatus.EXPERIMENTAL
21
+
22
+ def should_run(self, rule) -> bool:
23
+ """
24
+ Determines if a rule should be executed.
25
+ """
26
+ status_str = getattr(rule, 'status', 'experimental').lower()
27
+
28
+ # Check explicit config override
29
+ # rules:
30
+ # my.rule.id:
31
+ # enabled: false
32
+ rule_config = self.config.get(rule.id, {})
33
+ if rule_config.get('enabled') is False:
34
+ return False
35
+
36
+ try:
37
+ status = RuleStatus(status_str)
38
+ except ValueError:
39
+ status = RuleStatus.EXPERIMENTAL
40
+
41
+ if status == RuleStatus.DISABLED:
42
+ return False
43
+
44
+ return True
45
+
46
+ def adjust_severity(self, rule, original_severity: str) -> str:
47
+ """
48
+ Adjust severity based on lifecycle (e.g. DEPRECATED rules might be downgraded).
49
+ """
50
+ status_str = getattr(rule, 'status', 'experimental').lower()
51
+ if status_str == "deprecated":
52
+ return "low" # Deprecated rules are always low severity
53
+
54
+ return original_severity
main.py ADDED
@@ -0,0 +1,318 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ import os
5
+ import re
6
+ import time
7
+ from typing import Dict, Any, List, Tuple
8
+ from core.parser import DiffParser
9
+ from core.rules import RuleEngine
10
+ from core.evaluator import ImpactEvaluator
11
+ from core.composer import DecisionComposer
12
+ from core.renderer import MarkdownRenderer, HtmlRenderer
13
+ from core.ast_detector import ASTDetector
14
+
15
+ def _baseline_key(rule: Dict[str, Any]) -> str:
16
+ return f"{rule.get('id', '')}::{rule.get('matched_file', '')}"
17
+
18
+ def _load_baseline(path: str) -> Dict[str, Any]:
19
+ if not os.path.exists(path):
20
+ return {"items": []}
21
+ try:
22
+ with open(path, "r", encoding="utf-8") as f:
23
+ data = json.load(f)
24
+ if isinstance(data, dict) and isinstance(data.get("items"), list):
25
+ return data
26
+ except Exception:
27
+ return {"items": []}
28
+ return {"items": []}
29
+
30
+ def _save_baseline(path: str, items: List[Dict[str, Any]]) -> None:
31
+ data = {"items": items}
32
+ with open(path, "w", encoding="utf-8") as f:
33
+ json.dump(data, f, ensure_ascii=False, indent=2)
34
+
35
+ def _baseline_items(triggered_rules: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
36
+ items = []
37
+ for r in triggered_rules:
38
+ items.append({
39
+ "rule_id": r.get("id", ""),
40
+ "file": r.get("matched_file", "")
41
+ })
42
+ return items
43
+
44
+ def _baseline_set(data: Dict[str, Any]) -> set:
45
+ items = data.get("items", [])
46
+ return {f"{i.get('rule_id', '')}::{i.get('file', '')}" for i in items}
47
+
48
+ def _first_added_position(patch_text: str) -> Tuple[int, int]:
49
+ lines = patch_text.splitlines()
50
+ position = 1
51
+ new_line = None
52
+ for i, line in enumerate(lines, start=1):
53
+ if line.startswith("@@"):
54
+ m = re.search(r"\+(\d+)", line)
55
+ if m:
56
+ try:
57
+ new_line = int(m.group(1))
58
+ except Exception:
59
+ new_line = None
60
+ position = i
61
+ continue
62
+ if line.startswith("+") and not line.startswith("+++"):
63
+ if new_line is None:
64
+ new_line = 1
65
+ return i, new_line
66
+ if line.startswith("-") and not line.startswith("---"):
67
+ continue
68
+ if new_line is not None:
69
+ new_line += 1
70
+ return position, new_line or 1
71
+
72
+ def _build_inline_comments(triggered_rules: List[Dict[str, Any]], diff_data: Dict[str, Any]) -> List[Dict[str, Any]]:
73
+ patches = {p.get("file"): p.get("patch", "") for p in diff_data.get("file_patches", [])}
74
+ comments = []
75
+ for r in triggered_rules:
76
+ path = r.get("matched_file", "")
77
+ patch_text = patches.get(path, "")
78
+ if not patch_text and diff_data.get("file_patches"):
79
+ for p in diff_data.get("file_patches", []):
80
+ if p.get("file"):
81
+ path = p.get("file")
82
+ patch_text = p.get("patch", "")
83
+ break
84
+ position, line = _first_added_position(patch_text) if patch_text else (1, 1)
85
+ body = f"{r.get('severity', '').upper()} {r.get('id', '')}: {r.get('rationale', '')}"
86
+ comments.append({
87
+ "path": path,
88
+ "position": position,
89
+ "line": line,
90
+ "body": body,
91
+ "rule_id": r.get("id", "")
92
+ })
93
+ return comments
94
+
95
+ def _write_json(path: str, data: Any) -> None:
96
+ with open(path, "w", encoding="utf-8") as f:
97
+ json.dump(data, f, ensure_ascii=False, indent=2)
98
+
99
+ def main():
100
+ parser = argparse.ArgumentParser(description="DiffSense: Event-driven MR Audit Analyzer")
101
+ parser.add_argument("diff_file", help="Path to the diff file")
102
+ parser.add_argument("--rules", default="config", help="Path to rules: single YAML file or directory of YAML files")
103
+ parser.add_argument("--format", choices=["json", "markdown"], default="json", help="Output format")
104
+ parser.add_argument("--profile", default=None, help="Profile: strict or lightweight")
105
+ parser.add_argument("--baseline", action="store_true", help="Generate baseline file for existing issues")
106
+ parser.add_argument("--since-baseline", action="store_true", help="Only report findings not in baseline")
107
+ parser.add_argument("--baseline-file", default=".diffsense-baseline.json", help="Baseline file path")
108
+ parser.add_argument("--report-json", default="diffsense-report.json", help="Report JSON output path")
109
+ parser.add_argument("--report-html", default="diffsense-report.html", help="Report HTML output path")
110
+ parser.add_argument("--comments-json", default="diffsense-comments.json", help="Inline comments JSON output path")
111
+ parser.add_argument("--quality-auto-tune", action="store_true", help="Enable quality auto tune (skip/downgrade)")
112
+ parser.add_argument("--quality-disable-threshold", type=float, default=0.3, help="Disable threshold")
113
+ parser.add_argument("--quality-downgrade-threshold", type=float, default=0.5, help="Downgrade threshold")
114
+ parser.add_argument("--quality-min-samples", type=int, default=30, help="Minimum samples before actions")
115
+ parser.add_argument("--experimental", action="store_true", help="Include experimental rules (report-only by default)")
116
+ parser.add_argument("--experimental-report-only", dest="experimental_report_only", action="store_true", default=True, help="Do not affect decision with experimental rules")
117
+ parser.add_argument("--experimental-affect-decision", dest="experimental_report_only", action="store_false", help="Allow experimental rules to affect decision")
118
+
119
+ args = parser.parse_args()
120
+ # Apply official recommended config from .diffsense.yaml when not overridden by CLI
121
+ try:
122
+ from core.run_config import get_run_config
123
+ run_cfg = get_run_config(os.getcwd())
124
+ if args.profile is None and run_cfg.get("profile"):
125
+ args.profile = run_cfg["profile"]
126
+ if not args.quality_auto_tune and run_cfg.get("auto_tune"):
127
+ args.quality_auto_tune = True
128
+ rq = run_cfg.get("rule_quality") or {}
129
+ if args.quality_downgrade_threshold == 0.5 and "degrade_threshold" in rq:
130
+ try:
131
+ args.quality_downgrade_threshold = float(rq["degrade_threshold"])
132
+ except (TypeError, ValueError):
133
+ pass
134
+ if args.quality_disable_threshold == 0.3 and "disable_threshold" in rq:
135
+ try:
136
+ args.quality_disable_threshold = float(rq["disable_threshold"])
137
+ except (TypeError, ValueError):
138
+ pass
139
+ if args.quality_min_samples == 30 and "min_samples" in rq:
140
+ try:
141
+ args.quality_min_samples = int(rq["min_samples"])
142
+ except (TypeError, ValueError):
143
+ pass
144
+ except Exception:
145
+ pass
146
+
147
+ wall_start = time.perf_counter()
148
+
149
+ # 1. Read Diff
150
+ try:
151
+ with open(args.diff_file, 'r', encoding='utf-8') as f:
152
+ diff_content = f.read()
153
+ except FileNotFoundError:
154
+ print(f"Error: File {args.diff_file} not found.")
155
+ sys.exit(1)
156
+
157
+ # 2. Parse Diff
158
+ diff_parser = DiffParser()
159
+ diff_data = diff_parser.parse(diff_content)
160
+
161
+ # 2.5 Detect AST Signals
162
+ ast_detector = ASTDetector()
163
+ ast_signals = ast_detector.detect_signals(diff_data)
164
+
165
+ # 3. Init Engine & Evaluator
166
+ # Use absolute path for default rules if relative path fails
167
+ rules_path = args.rules
168
+ if not os.path.exists(rules_path):
169
+ # try relative to script
170
+ script_dir = os.path.dirname(os.path.abspath(__file__))
171
+ rules_path = os.path.join(script_dir, args.rules)
172
+
173
+ quality_config = {
174
+ "auto_tune": args.quality_auto_tune,
175
+ "disable_threshold": args.quality_disable_threshold,
176
+ "degrade_threshold": args.quality_downgrade_threshold,
177
+ "min_samples": args.quality_min_samples
178
+ }
179
+ pro_rules_path = None
180
+ try:
181
+ from core.run_config import get_pro_rules_path
182
+ pro_rules_path = get_pro_rules_path(os.getcwd())
183
+ except Exception:
184
+ pass
185
+ engine_config = {
186
+ "rule_quality": quality_config,
187
+ "experimental": {"enabled": args.experimental, "report_only": args.experimental_report_only},
188
+ }
189
+ try:
190
+ from core.run_config import get_run_config
191
+ _rc = get_run_config(os.getcwd())
192
+ if _rc.get("dependency_versions"):
193
+ engine_config["dependency_versions"] = _rc["dependency_versions"]
194
+ except Exception:
195
+ pass
196
+ rule_engine = RuleEngine(
197
+ rules_path,
198
+ profile=args.profile,
199
+ config=engine_config,
200
+ pro_rules_path=pro_rules_path,
201
+ )
202
+ evaluator = ImpactEvaluator(rule_engine)
203
+
204
+ # 4. Evaluate Impact
205
+ # Now returns a list of triggered rules (List[Dict])
206
+ triggered_rules = evaluator.evaluate(diff_data, ast_signals)
207
+ if args.baseline:
208
+ _save_baseline(args.baseline_file, _baseline_items(triggered_rules))
209
+ if args.since_baseline:
210
+ baseline_data = _load_baseline(args.baseline_file)
211
+ baseline_keys = _baseline_set(baseline_data)
212
+ triggered_rules = [r for r in triggered_rules if _baseline_key(r) not in baseline_keys]
213
+
214
+ # 5. Compose Decision
215
+ composer = DecisionComposer()
216
+ # Now takes triggered_rules and list of files
217
+ result = composer.compose(triggered_rules, diff_data.get('files', []))
218
+
219
+ # Add Rule Performance & Cache Metrics (copy so we don't mutate engine.metrics)
220
+ result['_metrics'] = dict(rule_engine.get_metrics())
221
+ result['_metrics']['cache'] = {
222
+ "diff": diff_parser.metrics,
223
+ "ast": ast_detector.metrics
224
+ }
225
+ result["_metrics"]["rule_stats"] = rule_engine.get_rule_stats()
226
+ result["_rule_quality"] = rule_engine.get_rule_quality_metrics()
227
+ result["_quality_warnings"] = rule_engine.get_quality_warnings()
228
+
229
+ # Structured performance for CI (machine-readable)
230
+ wall_s = time.perf_counter() - wall_start
231
+ d_m = diff_parser.metrics
232
+ a_m = ast_detector.metrics
233
+ d_total = d_m["hits"] + d_m["misses"]
234
+ a_total = a_m["hits"] + a_m["misses"]
235
+ total_ops = d_total + a_total
236
+ cache_hit_rate_pct = ((d_m["hits"] + a_m["hits"]) / total_ops * 100) if total_ops > 0 else 0.0
237
+ r_stats = result["_metrics"].get("rule_stats", {})
238
+ total_rules = r_stats.get("total_rules", 0)
239
+ executed_count = r_stats.get("executed_count", 0)
240
+ rules_executed_pct = (executed_count / total_rules * 100) if total_rules else 0.0
241
+ result["_performance"] = {
242
+ "wall_clock_s": round(wall_s, 3),
243
+ "cache": {"diff": dict(d_m), "ast": dict(a_m)},
244
+ "cache_hit_rate_pct": round(cache_hit_rate_pct, 2),
245
+ "rules_executed": executed_count,
246
+ "rules_total": total_rules,
247
+ "rules_executed_pct": round(rules_executed_pct, 2),
248
+ }
249
+
250
+ # 6. Output report summary to stderr for CI visibility
251
+ sys.stderr.write("\n" + "="*40 + "\n")
252
+ sys.stderr.write("🚀 DiffSense Performance Report\n")
253
+ sys.stderr.write("="*40 + "\n")
254
+
255
+ # Diff Cache
256
+ d_m = diff_parser.metrics
257
+ d_total = d_m["hits"] + d_m["misses"]
258
+ d_rate = (d_m["hits"] / d_total * 100) if d_total > 0 else 0
259
+ sys.stderr.write(f"🔹 Diff Cache Hit: {d_rate:.1f}% ({d_m['hits']}/{d_total})\n")
260
+
261
+ # AST Cache
262
+ a_m = ast_detector.metrics
263
+ a_total = a_m["hits"] + a_m["misses"]
264
+ a_rate = (a_m["hits"] / a_total * 100) if a_total > 0 else 0
265
+ sys.stderr.write(f"🔹 AST Cache Hit: {a_rate:.1f}% ({a_m['hits']}/{a_total})\n")
266
+
267
+ # Saved Time (Estimated)
268
+ # Total saved = (hits * avg_parse_time_from_misses)
269
+ # Since we only track duration on miss, we use it as the estimate for hits.
270
+ d_saved = d_m["hits"] * d_m["saved_ms"]
271
+ a_saved = a_m["hits"] * (a_m["saved_ms"] / a_m["misses"] if a_m["misses"] > 0 else 0)
272
+ total_saved = (d_saved + a_saved) / 1000
273
+ sys.stderr.write(f"⏱️ Estimated Saved Time: {total_saved:.2f}s\n")
274
+
275
+ # Rules executed (Q1/Q2 visibility)
276
+ r_stats = result["_metrics"].get("rule_stats", {})
277
+ total_rules = r_stats.get("total_rules", 0)
278
+ executed_count = r_stats.get("executed_count", 0)
279
+ exec_pct = (executed_count / total_rules * 100) if total_rules else 0
280
+ sys.stderr.write(f"🔹 Rules executed: {executed_count} / {total_rules} ({exec_pct:.0f}%)\n")
281
+ if total_rules and exec_pct > 30:
282
+ sys.stderr.write(" 💡 Consider enabling more profile/scheduler filters to reduce executed rules.\n")
283
+
284
+ # Slowest Rules
285
+ sys.stderr.write("\n🐢 Top 3 Slowest Rules:\n")
286
+ r_stats = result["_metrics"].get("rule_stats", {}).get("top_slow", [])
287
+ for r in r_stats[:3]:
288
+ r_id = r.get("rule_id")
289
+ r_time_ms = r.get("time_ms", 0)
290
+ sys.stderr.write(f" - {r_id}: {r_time_ms:.2f}ms\n")
291
+ for w in result["_quality_warnings"]:
292
+ sys.stderr.write(f"⚠️ Low quality rule: {w.get('rule_id')} precision {w.get('precision'):.2f} (hits {w.get('hits')})\n")
293
+
294
+ # Triggered rules summary
295
+ if triggered_rules:
296
+ sys.stderr.write("\n🎯 Triggered Rules Summary:\n")
297
+ for r in triggered_rules:
298
+ sys.stderr.write(f" - {r.get('severity', '').upper()} {r.get('id', '')}: {r.get('matched_file', '')}\n")
299
+
300
+ sys.stderr.write("="*40 + "\n\n")
301
+ rule_engine.persist_rule_quality()
302
+
303
+ # 7. Output Result
304
+ inline_comments = _build_inline_comments(triggered_rules, diff_data)
305
+ _write_json(args.report_json, result)
306
+ html_report = HtmlRenderer().render(result)
307
+ with open(args.report_html, "w", encoding="utf-8") as f:
308
+ f.write(html_report)
309
+ _write_json(args.comments_json, inline_comments)
310
+
311
+ if args.format == "json":
312
+ print(json.dumps(result, indent=2))
313
+ elif args.format == "markdown":
314
+ renderer = MarkdownRenderer()
315
+ print(renderer.render(result))
316
+
317
+ if __name__ == "__main__":
318
+ main()
rules/__init__.py ADDED
@@ -0,0 +1,246 @@
1
+ """
2
+ DiffSense Rules Package
3
+
4
+ This package contains built-in rules for DiffSense code review tool.
5
+ Rules are organized by category:
6
+
7
+ - concurrency.py: Thread safety and concurrency rules
8
+ - resource_management.py: Resource leak detection rules
9
+ - exception_handling.py: Exception handling best practices
10
+ - null_safety.py: Null pointer exception prevention
11
+ - collection_handling.py: Collection usage best practices
12
+ - api_compatibility.py: API breaking change detection
13
+ - yaml_adapter.py: Legacy YAML rule adapter
14
+
15
+ Usage:
16
+ Rules are automatically loaded by the RuleEngine.
17
+ Custom rules can be added by extending BaseRule class.
18
+ """
19
+
20
+ from rules.concurrency import (
21
+ ThreadPoolSemanticChangeRule,
22
+ ConcurrencyRegressionRule,
23
+ ThreadSafetyRemovalRule,
24
+ LatchMisuseRule,
25
+ )
26
+
27
+ from rules.resource_management import (
28
+ CloseableResourceLeakRule,
29
+ DatabaseConnectionLeakRule,
30
+ StreamWrapperRule,
31
+ IOStreamChainingRule,
32
+ ExecutorServiceShutdownRule,
33
+ )
34
+
35
+ from rules.exception_handling import (
36
+ SwallowedExceptionRule,
37
+ GenericExceptionRule,
38
+ ThrowRuntimeExceptionRule,
39
+ ThrowsClauseRemovedRule,
40
+ FinallyBlockMissingRule,
41
+ ExceptionLoggingRule,
42
+ )
43
+
44
+ from rules.null_safety import (
45
+ NullReturnIgnoredRule,
46
+ OptionalUnwrapRule,
47
+ AutoboxingNPERule,
48
+ ChainedMethodCallNPERule,
49
+ ArrayIndexOutOfBoundsRule,
50
+ StringConcatNPERule,
51
+ )
52
+
53
+ from rules.collection_handling import (
54
+ RawTypeUsageRule,
55
+ UnmodifiableCollectionRule,
56
+ ConcurrentModificationRule,
57
+ MapComputeRule,
58
+ StreamCollectorRule,
59
+ ImmutableCollectionRule,
60
+ ListResizeRule,
61
+ )
62
+
63
+ from rules.api_compatibility import (
64
+ PublicMethodRemovedRule,
65
+ MethodSignatureChangedRule,
66
+ FieldRemovedRule,
67
+ ConstructorRemovedRule,
68
+ InterfaceChangedRule,
69
+ AnnotationRemovedRule,
70
+ DeprecatedApiAddedRule,
71
+ SerialVersionUIDChangedRule,
72
+ )
73
+
74
+ # Python、C++、JavaScript、Go 规则已迁移到 YAML 配置
75
+ # 参见 diffsense/config/rules/ 目录
76
+
77
+ # Cross-language rules (support multiple languages)
78
+ try:
79
+ from rules.cross_language_adapter import (
80
+ CrossLanguageRuleFactory,
81
+ GenericResourceLeakRule,
82
+ GenericNullSafetyRule,
83
+ GenericExceptionHandlingRule,
84
+ )
85
+ CROSS_LANGUAGE_RULES_AVAILABLE = True
86
+ except ImportError:
87
+ CROSS_LANGUAGE_RULES_AVAILABLE = False
88
+
89
+ # Registry of all built-in rules
90
+ BUILTIN_RULES = [
91
+ # Concurrency (4 rules)
92
+ ThreadPoolSemanticChangeRule,
93
+ ConcurrencyRegressionRule,
94
+ ThreadSafetyRemovalRule,
95
+ LatchMisuseRule,
96
+
97
+ # Resource Management (5 rules)
98
+ CloseableResourceLeakRule,
99
+ DatabaseConnectionLeakRule,
100
+ StreamWrapperRule,
101
+ IOStreamChainingRule,
102
+ ExecutorServiceShutdownRule,
103
+
104
+ # Exception Handling (6 rules)
105
+ SwallowedExceptionRule,
106
+ GenericExceptionRule,
107
+ ThrowRuntimeExceptionRule,
108
+ ThrowsClauseRemovedRule,
109
+ FinallyBlockMissingRule,
110
+ ExceptionLoggingRule,
111
+
112
+ # Null Safety (6 rules)
113
+ NullReturnIgnoredRule,
114
+ OptionalUnwrapRule,
115
+ AutoboxingNPERule,
116
+ ChainedMethodCallNPERule,
117
+ ArrayIndexOutOfBoundsRule,
118
+ StringConcatNPERule,
119
+
120
+ # Collection Handling (7 rules)
121
+ RawTypeUsageRule,
122
+ UnmodifiableCollectionRule,
123
+ ConcurrentModificationRule,
124
+ MapComputeRule,
125
+ StreamCollectorRule,
126
+ ImmutableCollectionRule,
127
+ ListResizeRule,
128
+
129
+ # API Compatibility (8 rules)
130
+ PublicMethodRemovedRule,
131
+ MethodSignatureChangedRule,
132
+ FieldRemovedRule,
133
+ ConstructorRemovedRule,
134
+ InterfaceChangedRule,
135
+ AnnotationRemovedRule,
136
+ DeprecatedApiAddedRule,
137
+ SerialVersionUIDChangedRule,
138
+
139
+ # Python/C++/JavaScript/Go 规则已迁移到 YAML 配置
140
+ # 参见 diffsense/config/rules/ 目录
141
+ ]
142
+
143
+ # Total: 36 built-in rules + cross-language rules
144
+
145
+ # Cross-language rule support (Python, JavaScript, C++)
146
+ CROSS_LANGUAGE_RULES_LIST = []
147
+
148
+ if CROSS_LANGUAGE_RULES_AVAILABLE:
149
+ # Create rules for each supported language
150
+ for language in ['python', 'javascript', 'cpp', 'c']:
151
+ CROSS_LANGUAGE_RULES_LIST.extend(
152
+ CrossLanguageRuleFactory.create_all_rules_for_language(language)
153
+ )
154
+
155
+
156
+ def get_all_builtin_rules():
157
+ """
158
+ Instantiate and return all built-in rules.
159
+
160
+ Returns:
161
+ List[BaseRule]: List of instantiated rule objects
162
+ """
163
+ return [rule_class() for rule_class in BUILTIN_RULES]
164
+
165
+
166
+ def get_rules_by_category(category: str):
167
+ """
168
+ Get rules filtered by category.
169
+
170
+ Args:
171
+ category: Category name ('concurrency', 'resource', 'exception',
172
+ 'null_safety', 'collection', 'api')
173
+
174
+ Returns:
175
+ List[BaseRule]: List of rules in the specified category
176
+ """
177
+ category_rules = {
178
+ 'concurrency': [
179
+ ThreadPoolSemanticChangeRule,
180
+ ConcurrencyRegressionRule,
181
+ ThreadSafetyRemovalRule,
182
+ LatchMisuseRule,
183
+ ],
184
+ 'resource': [
185
+ CloseableResourceLeakRule,
186
+ DatabaseConnectionLeakRule,
187
+ StreamWrapperRule,
188
+ IOStreamChainingRule,
189
+ ExecutorServiceShutdownRule,
190
+ ],
191
+ 'exception': [
192
+ SwallowedExceptionRule,
193
+ GenericExceptionRule,
194
+ ThrowRuntimeExceptionRule,
195
+ ThrowsClauseRemovedRule,
196
+ FinallyBlockMissingRule,
197
+ ExceptionLoggingRule,
198
+ ],
199
+ 'null_safety': [
200
+ NullReturnIgnoredRule,
201
+ OptionalUnwrapRule,
202
+ AutoboxingNPERule,
203
+ ChainedMethodCallNPERule,
204
+ ArrayIndexOutOfBoundsRule,
205
+ StringConcatNPERule,
206
+ ],
207
+ 'collection': [
208
+ RawTypeUsageRule,
209
+ UnmodifiableCollectionRule,
210
+ ConcurrentModificationRule,
211
+ MapComputeRule,
212
+ StreamCollectorRule,
213
+ ImmutableCollectionRule,
214
+ ListResizeRule,
215
+ ],
216
+ 'api': [
217
+ PublicMethodRemovedRule,
218
+ MethodSignatureChangedRule,
219
+ FieldRemovedRule,
220
+ ConstructorRemovedRule,
221
+ InterfaceChangedRule,
222
+ AnnotationRemovedRule,
223
+ DeprecatedApiAddedRule,
224
+ SerialVersionUIDChangedRule,
225
+ ],
226
+ }
227
+
228
+ rules = category_rules.get(category.lower(), [])
229
+ return [rule_class() for rule_class in rules]
230
+
231
+
232
+ def get_rule_by_id(rule_id: str):
233
+ """
234
+ Get a specific rule by its ID.
235
+
236
+ Args:
237
+ rule_id: Rule ID (e.g., 'resource.closeable_leak')
238
+
239
+ Returns:
240
+ BaseRule: The rule instance, or None if not found
241
+ """
242
+ for rule_class in BUILTIN_RULES:
243
+ rule = rule_class()
244
+ if rule.id == rule_id:
245
+ return rule
246
+ return None