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
governance/lifecycle.py
ADDED
|
@@ -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
|