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
core/run_config.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Load run config from repo .diffsense.yaml or diffsense-ignore.yaml.
|
|
3
|
+
Used as defaults for profile, auto_tune, ci_fail_level, cache, scheduler, rule_quality.
|
|
4
|
+
CLI / main / run_audit override these with explicit args when provided.
|
|
5
|
+
Also resolves pro_rules_path (super rules) for formal DiffSense runs.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
def _load_yaml(path: str) -> Optional[Dict[str, Any]]:
|
|
12
|
+
if not os.path.isfile(path):
|
|
13
|
+
return None
|
|
14
|
+
try:
|
|
15
|
+
import yaml
|
|
16
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
17
|
+
return yaml.safe_load(f) or {}
|
|
18
|
+
except Exception:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def get_run_config(repo_root: str = ".") -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Load run config from repo_root. Prefer .diffsense.yaml, then diffsense-ignore.yaml.
|
|
24
|
+
Returns dict with keys: profile, auto_tune, ci_fail_level, cache, scheduler, rule_quality.
|
|
25
|
+
Missing keys are absent (caller uses own defaults).
|
|
26
|
+
"""
|
|
27
|
+
out: Dict[str, Any] = {}
|
|
28
|
+
for fname in (".diffsense.yaml", "diffsense-ignore.yaml"):
|
|
29
|
+
path = os.path.join(repo_root, fname)
|
|
30
|
+
data = _load_yaml(path)
|
|
31
|
+
if not data:
|
|
32
|
+
continue
|
|
33
|
+
if data.get("profile") is not None:
|
|
34
|
+
out["profile"] = str(data["profile"]).strip() or None
|
|
35
|
+
if data.get("auto_tune") is not None:
|
|
36
|
+
out["auto_tune"] = bool(data["auto_tune"])
|
|
37
|
+
if data.get("ci_fail_level") is not None:
|
|
38
|
+
out["ci_fail_level"] = str(data["ci_fail_level"]).strip() or None
|
|
39
|
+
if data.get("cache") is not None:
|
|
40
|
+
out["cache"] = bool(data["cache"])
|
|
41
|
+
if data.get("scheduler") is not None:
|
|
42
|
+
out["scheduler"] = bool(data["scheduler"])
|
|
43
|
+
if isinstance(data.get("rule_quality"), dict):
|
|
44
|
+
out["rule_quality"] = dict(data["rule_quality"])
|
|
45
|
+
if data.get("pro_rules_path") is not None:
|
|
46
|
+
out["pro_rules_path"] = str(data["pro_rules_path"]).strip() or None
|
|
47
|
+
if isinstance(data.get("dependency_versions"), dict):
|
|
48
|
+
# 用户配置的依赖版本,用于 CVE 规则精确匹配:ecosystem -> package_name -> version
|
|
49
|
+
# 例如: dependency_versions: { npm: { lodash: "4.17.21" }, maven: { "org.apache.tomcat:tomcat-catalina": "9.0.72" } }
|
|
50
|
+
out["dependency_versions"] = {k: dict(v) for k, v in data["dependency_versions"].items() if isinstance(v, dict)}
|
|
51
|
+
break
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_pro_rules_path(cwd: Optional[str] = None) -> Optional[str]:
|
|
56
|
+
"""
|
|
57
|
+
解析 PRO 规则(超级规则)目录路径,供正式 audit/main 加载。
|
|
58
|
+
优先级: 环境变量 DIFFSENSE_PRO_RULES > 仓库 .diffsense.yaml 的 pro_rules_path >
|
|
59
|
+
相对 diffsense 包上级的 pro-rules(开发/同仓部署)。
|
|
60
|
+
仅当路径存在时返回,否则返回 None。
|
|
61
|
+
"""
|
|
62
|
+
cwd = cwd or os.getcwd()
|
|
63
|
+
# 1. 环境变量
|
|
64
|
+
env_path = os.environ.get("DIFFSENSE_PRO_RULES", "").strip()
|
|
65
|
+
if env_path and os.path.isdir(env_path):
|
|
66
|
+
return os.path.normpath(env_path)
|
|
67
|
+
# 2. 仓库配置
|
|
68
|
+
run_cfg = get_run_config(cwd)
|
|
69
|
+
cfg_path = run_cfg.get("pro_rules_path")
|
|
70
|
+
if cfg_path and os.path.isdir(os.path.normpath(cfg_path)):
|
|
71
|
+
return os.path.normpath(cfg_path)
|
|
72
|
+
if cfg_path:
|
|
73
|
+
abs_cfg = os.path.normpath(os.path.join(cwd, cfg_path))
|
|
74
|
+
if os.path.isdir(abs_cfg):
|
|
75
|
+
return abs_cfg
|
|
76
|
+
# 3. 默认:与 diffsense 包同级的 pro-rules(源码/同仓)
|
|
77
|
+
try:
|
|
78
|
+
import diffsense
|
|
79
|
+
base = Path(diffsense.__file__).resolve().parent.parent
|
|
80
|
+
except Exception:
|
|
81
|
+
base = Path(__file__).resolve().parent.parent.parent
|
|
82
|
+
default = base / "pro-rules"
|
|
83
|
+
if default.is_dir():
|
|
84
|
+
return str(default)
|
|
85
|
+
return None
|
core/semantic_diff.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic Analysis Engine for DiffSense
|
|
3
|
+
|
|
4
|
+
Multi-language semantic analysis supporting Java, Go, Python, C++, and JavaScript.
|
|
5
|
+
Uses language-specific parsers and adapters to extract semantic signals.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Set, Dict, Any, Tuple, Optional
|
|
9
|
+
from sdk.signal import Signal
|
|
10
|
+
|
|
11
|
+
# Import parsers registry (lazy import to avoid circular dependency)
|
|
12
|
+
def _get_parser_for_file(filename: str):
|
|
13
|
+
try:
|
|
14
|
+
from diffsense.parsers.registry import get_parser_for_file
|
|
15
|
+
return get_parser_for_file(filename)
|
|
16
|
+
except ImportError:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SemanticDiff:
|
|
21
|
+
"""
|
|
22
|
+
L2 Semantic Analysis Engine (Multi-Language Version).
|
|
23
|
+
|
|
24
|
+
Responsibility: Parse Diff -> Extract Semantic Signals.
|
|
25
|
+
Does NOT execute rules.
|
|
26
|
+
|
|
27
|
+
Supports: Java, Go, Python, C++, JavaScript
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
# Language-specific semantic analysis (currently only Java has deep support)
|
|
32
|
+
# Other languages use parser-level signals
|
|
33
|
+
self._language_extensions = {
|
|
34
|
+
'.java': 'java',
|
|
35
|
+
'.go': 'go',
|
|
36
|
+
'.py': 'python',
|
|
37
|
+
'.pyi': 'python',
|
|
38
|
+
'.cpp': 'cpp',
|
|
39
|
+
'.cc': 'cpp',
|
|
40
|
+
'.cxx': 'cpp',
|
|
41
|
+
'.h': 'cpp',
|
|
42
|
+
'.hpp': 'cpp',
|
|
43
|
+
'.js': 'javascript',
|
|
44
|
+
'.jsx': 'javascript',
|
|
45
|
+
'.ts': 'javascript',
|
|
46
|
+
'.tsx': 'javascript',
|
|
47
|
+
'.mjs': 'javascript',
|
|
48
|
+
'.cjs': 'javascript',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Pagination variables (for Java)
|
|
52
|
+
self._pagination_vars = {"pageNo", "pageSize", "start", "limit", "offset"}
|
|
53
|
+
|
|
54
|
+
# Critical calls (for Java)
|
|
55
|
+
self._critical_calls = {"encode", "decode", "validate", "check", "normalize", "sanitize"}
|
|
56
|
+
|
|
57
|
+
def get_language(self, filename: str) -> str:
|
|
58
|
+
"""Detect language from filename extension."""
|
|
59
|
+
ext = '.' + filename.split('.')[-1] if '.' in filename else ''
|
|
60
|
+
return self._language_extensions.get(ext, 'unknown')
|
|
61
|
+
|
|
62
|
+
def detect_signals(self, diff_data: Dict[str, Any]) -> List[Signal]:
|
|
63
|
+
"""
|
|
64
|
+
Main Entry Point: Returns semantic signals for all languages.
|
|
65
|
+
"""
|
|
66
|
+
signals = []
|
|
67
|
+
file_patches = diff_data.get('file_patches', [])
|
|
68
|
+
|
|
69
|
+
# Fallback if parser isn't upgraded
|
|
70
|
+
if not file_patches and 'raw_diff' in diff_data:
|
|
71
|
+
file_patches = [{'file': 'unknown', 'patch': diff_data['raw_diff']}]
|
|
72
|
+
|
|
73
|
+
# Group files by language
|
|
74
|
+
files_by_language: Dict[str, List[Dict]] = {}
|
|
75
|
+
for entry in file_patches:
|
|
76
|
+
filename = entry.get('file', 'unknown')
|
|
77
|
+
language = self.get_language(filename)
|
|
78
|
+
if language not in files_by_language:
|
|
79
|
+
files_by_language[language] = []
|
|
80
|
+
files_by_language[language].append(entry)
|
|
81
|
+
|
|
82
|
+
# Analyze each language's files
|
|
83
|
+
for language, files in files_by_language.items():
|
|
84
|
+
if language == 'java':
|
|
85
|
+
# Java: Use existing semantic analysis
|
|
86
|
+
java_signals = self._detect_java_signals(files)
|
|
87
|
+
signals.extend(java_signals)
|
|
88
|
+
elif language != 'unknown':
|
|
89
|
+
# Other languages: Use parser-level signals
|
|
90
|
+
parser_signals = self._detect_parser_signals(files, language, diff_data)
|
|
91
|
+
signals.extend(parser_signals)
|
|
92
|
+
|
|
93
|
+
return signals
|
|
94
|
+
|
|
95
|
+
def _detect_java_signals(self, file_patches: List[Dict]) -> List[Signal]:
|
|
96
|
+
"""
|
|
97
|
+
Java-specific semantic analysis (deep analysis).
|
|
98
|
+
"""
|
|
99
|
+
# Import Java-specific modules lazily to avoid issues
|
|
100
|
+
from javalang.tree import (
|
|
101
|
+
SynchronizedStatement, MethodInvocation, FieldDeclaration,
|
|
102
|
+
MethodDeclaration, LocalVariableDeclaration, VariableDeclarator,
|
|
103
|
+
ForStatement, WhileStatement, DoStatement
|
|
104
|
+
)
|
|
105
|
+
from .change import Change, ChangeKind
|
|
106
|
+
from .knowledge import is_thread_safe, is_lock_type
|
|
107
|
+
|
|
108
|
+
signals = []
|
|
109
|
+
|
|
110
|
+
# Determine Analysis Tier based on Java file count
|
|
111
|
+
if len(file_patches) > 30:
|
|
112
|
+
return [Signal(
|
|
113
|
+
id="meta.large_refactor",
|
|
114
|
+
file="meta",
|
|
115
|
+
action="detected",
|
|
116
|
+
line=None,
|
|
117
|
+
meta={"tier": 3, "language": "java"}
|
|
118
|
+
)]
|
|
119
|
+
|
|
120
|
+
analysis_mode = "deep"
|
|
121
|
+
if 10 < len(file_patches) <= 30:
|
|
122
|
+
analysis_mode = "light"
|
|
123
|
+
|
|
124
|
+
for entry in file_patches:
|
|
125
|
+
filename = entry.get('file', 'unknown')
|
|
126
|
+
patch_content = entry.get('patch', '')
|
|
127
|
+
|
|
128
|
+
if not filename.endswith('.java'):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
file_changes = self._detect_changes_in_patch(
|
|
132
|
+
filename, patch_content, mode=analysis_mode
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
for ch in file_changes:
|
|
136
|
+
sig = self._convert_change_to_signal(ch)
|
|
137
|
+
if sig:
|
|
138
|
+
signals.append(sig)
|
|
139
|
+
|
|
140
|
+
return signals
|
|
141
|
+
|
|
142
|
+
def _detect_parser_signals(self, file_patches: List[Dict], language: str, diff_data: Dict) -> List[Signal]:
|
|
143
|
+
"""
|
|
144
|
+
Use language-specific parsers to extract signals for non-Java languages.
|
|
145
|
+
"""
|
|
146
|
+
signals = []
|
|
147
|
+
|
|
148
|
+
# Get parser for this language
|
|
149
|
+
parser = None
|
|
150
|
+
for entry in file_patches:
|
|
151
|
+
filename = entry.get('file', 'unknown')
|
|
152
|
+
parser = _get_parser_for_file(filename)
|
|
153
|
+
if parser:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
if not parser:
|
|
157
|
+
return signals
|
|
158
|
+
|
|
159
|
+
# Get diff context
|
|
160
|
+
changes_context = []
|
|
161
|
+
if 'changes' in diff_data:
|
|
162
|
+
changes_context = diff_data['changes']
|
|
163
|
+
|
|
164
|
+
diff_context = {'changes': changes_context}
|
|
165
|
+
|
|
166
|
+
# Parse each file and extract signals
|
|
167
|
+
for entry in file_patches:
|
|
168
|
+
filename = entry.get('file', 'unknown')
|
|
169
|
+
content = entry.get('content', '')
|
|
170
|
+
|
|
171
|
+
if content:
|
|
172
|
+
# Parse the file
|
|
173
|
+
ast_data = parser.parse_file(filename, content)
|
|
174
|
+
|
|
175
|
+
# Extract signals
|
|
176
|
+
if hasattr(parser, 'extract_signals'):
|
|
177
|
+
file_signals = parser.extract_signals(ast_data, diff_context)
|
|
178
|
+
for sig_dict in file_signals:
|
|
179
|
+
signal = Signal(
|
|
180
|
+
id=sig_dict.get('id', 'unknown'),
|
|
181
|
+
file=filename,
|
|
182
|
+
action=sig_dict.get('action', 'unknown'),
|
|
183
|
+
line=sig_dict.get('line'),
|
|
184
|
+
meta=sig_dict.get('meta', {})
|
|
185
|
+
)
|
|
186
|
+
signals.append(signal)
|
|
187
|
+
|
|
188
|
+
return signals
|
|
189
|
+
|
|
190
|
+
def _detect_changes_in_patch(self, filename: str, patch_content: str, mode: str = "deep") -> List[Any]:
|
|
191
|
+
"""
|
|
192
|
+
Detect changes in a patch (Java-specific implementation).
|
|
193
|
+
This is the existing logic from the original semantic_diff.py
|
|
194
|
+
"""
|
|
195
|
+
# Lazy import to avoid circular dependency
|
|
196
|
+
from javalang import parse as javalang_parse
|
|
197
|
+
from javalang.tree import (
|
|
198
|
+
SynchronizedStatement, MethodInvocation, FieldDeclaration,
|
|
199
|
+
MethodDeclaration, LocalVariableDeclaration, VariableDeclarator,
|
|
200
|
+
ForStatement, WhileStatement, DoStatement
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
changes = []
|
|
204
|
+
added_lines = []
|
|
205
|
+
removed_lines = []
|
|
206
|
+
|
|
207
|
+
for line in patch_content.splitlines():
|
|
208
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
209
|
+
added_lines.append(line[1:].strip())
|
|
210
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
211
|
+
removed_lines.append(line[1:].strip())
|
|
212
|
+
|
|
213
|
+
# Analyze removed lines
|
|
214
|
+
if removed_lines:
|
|
215
|
+
self._analyze_snippet_for_changes(
|
|
216
|
+
removed_lines, filename, is_added=False,
|
|
217
|
+
changes=changes, mode=mode
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Analyze added lines
|
|
221
|
+
if added_lines:
|
|
222
|
+
self._analyze_snippet_for_changes(
|
|
223
|
+
added_lines, filename, is_added=True,
|
|
224
|
+
changes=changes, mode=mode
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return changes
|
|
228
|
+
|
|
229
|
+
def _analyze_snippet_for_changes(self, lines: List[str], filename: str, is_added: bool,
|
|
230
|
+
changes: List, mode: str = "deep"):
|
|
231
|
+
"""Analyze a code snippet for semantic changes (Java)."""
|
|
232
|
+
from .change import Change, ChangeKind
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
import javalang
|
|
236
|
+
from javalang.tree import (
|
|
237
|
+
SynchronizedStatement, MethodInvocation, FieldDeclaration,
|
|
238
|
+
MethodDeclaration, LocalVariableDeclaration
|
|
239
|
+
)
|
|
240
|
+
except ImportError:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
code_snippet = "\n".join(lines)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
tokens = list(javalang.tokenizer.tokenize(code_snippet))
|
|
247
|
+
except:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
token_values = [t.value for t in tokens]
|
|
251
|
+
|
|
252
|
+
# Quick token-based checks
|
|
253
|
+
if "synchronized" in token_values:
|
|
254
|
+
kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
|
|
255
|
+
changes.append(Change(kind=kind, file=filename, symbol="synchronized"))
|
|
256
|
+
|
|
257
|
+
if "volatile" in token_values:
|
|
258
|
+
kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
|
|
259
|
+
changes.append(Change(kind=kind, file=filename, symbol="volatile"))
|
|
260
|
+
|
|
261
|
+
# Check for pattern matches
|
|
262
|
+
for i in range(len(tokens) - 2):
|
|
263
|
+
if (tokens[i].value == "." and
|
|
264
|
+
tokens[i+1].value == "lock" and
|
|
265
|
+
tokens[i+2].value == "("):
|
|
266
|
+
kind = ChangeKind.CALL_ADDED if is_added else ChangeKind.CALL_REMOVED
|
|
267
|
+
changes.append(Change(kind=kind, file=filename, symbol="lock"))
|
|
268
|
+
|
|
269
|
+
if (tokens[i].value == "Thread" and
|
|
270
|
+
tokens[i+1].value == "." and
|
|
271
|
+
tokens[i+2].value == "sleep"):
|
|
272
|
+
kind = ChangeKind.CALL_ADDED if is_added else ChangeKind.CALL_REMOVED
|
|
273
|
+
changes.append(Change(kind=kind, file=filename, symbol="sleep"))
|
|
274
|
+
|
|
275
|
+
# Skip deep analysis in light mode
|
|
276
|
+
if mode == "light":
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Deep AST analysis
|
|
280
|
+
wrapper_class = f"class Dummy {{ {code_snippet} }}"
|
|
281
|
+
try:
|
|
282
|
+
tree = javalang_parse.parse(wrapper_class)
|
|
283
|
+
self._analyze_tree_changes(tree, filename, is_added, {}, changes)
|
|
284
|
+
except:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def _analyze_tree_changes(self, tree, filename: str, is_added: bool, var_map: Dict, changes: List):
|
|
288
|
+
"""Analyze AST tree for changes (Java)."""
|
|
289
|
+
from .change import Change, ChangeKind
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
from javalang.tree import (
|
|
293
|
+
SynchronizedStatement, MethodInvocation, FieldDeclaration,
|
|
294
|
+
MethodDeclaration, LocalVariableDeclaration, ForStatement, WhileStatement, DoStatement
|
|
295
|
+
)
|
|
296
|
+
except ImportError:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
for path, node in tree:
|
|
300
|
+
if isinstance(node, SynchronizedStatement):
|
|
301
|
+
kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
|
|
302
|
+
changes.append(Change(kind=kind, file=filename, symbol="synchronized"))
|
|
303
|
+
|
|
304
|
+
if isinstance(node, MethodDeclaration):
|
|
305
|
+
if 'synchronized' in node.modifiers:
|
|
306
|
+
kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
|
|
307
|
+
changes.append(Change(kind=kind, file=filename, symbol="synchronized"))
|
|
308
|
+
|
|
309
|
+
if isinstance(node, FieldDeclaration):
|
|
310
|
+
if 'volatile' in node.modifiers:
|
|
311
|
+
kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
|
|
312
|
+
changes.append(Change(kind=kind, file=filename, symbol="volatile"))
|
|
313
|
+
|
|
314
|
+
if node.type:
|
|
315
|
+
for declarator in node.declarators:
|
|
316
|
+
var_map[declarator.name] = node.type.name
|
|
317
|
+
|
|
318
|
+
def _convert_change_to_signal(self, change) -> Optional[Signal]:
|
|
319
|
+
"""Convert internal Change to Signal (Java)."""
|
|
320
|
+
from .change import ChangeKind
|
|
321
|
+
|
|
322
|
+
if not hasattr(change, 'kind'):
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
# Map Change -> Signal ID
|
|
326
|
+
sig_id = None
|
|
327
|
+
if hasattr(change, 'symbol'):
|
|
328
|
+
if change.symbol == "lock" or change.symbol == "unlock":
|
|
329
|
+
sig_id = "runtime.concurrency.lock"
|
|
330
|
+
elif change.symbol == "synchronized":
|
|
331
|
+
sig_id = "runtime.concurrency.synchronized"
|
|
332
|
+
elif change.symbol == "volatile":
|
|
333
|
+
sig_id = "runtime.concurrency.volatile"
|
|
334
|
+
elif change.symbol == "sleep":
|
|
335
|
+
sig_id = "runtime.performance.sleep_added"
|
|
336
|
+
|
|
337
|
+
if not sig_id and change.kind == ChangeKind.TYPE_CHANGED:
|
|
338
|
+
if change.meta.get('downgrade'):
|
|
339
|
+
sig_id = "runtime.concurrency.thread_safety_downgrade"
|
|
340
|
+
|
|
341
|
+
if not sig_id:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
# Map ChangeKind -> Action
|
|
345
|
+
action = "changed"
|
|
346
|
+
if change.kind in [ChangeKind.CALL_ADDED, ChangeKind.FIELD_ADDED, ChangeKind.MODIFIER_ADDED]:
|
|
347
|
+
action = "added"
|
|
348
|
+
elif change.kind in [ChangeKind.CALL_REMOVED, ChangeKind.FIELD_REMOVED, ChangeKind.MODIFIER_REMOVED]:
|
|
349
|
+
action = "removed"
|
|
350
|
+
elif change.kind == ChangeKind.TYPE_CHANGED:
|
|
351
|
+
action = "downgrade"
|
|
352
|
+
|
|
353
|
+
return Signal(
|
|
354
|
+
id=sig_id,
|
|
355
|
+
file=change.file,
|
|
356
|
+
action=action,
|
|
357
|
+
line=getattr(change, 'line_no', None),
|
|
358
|
+
meta=getattr(change, 'meta', {})
|
|
359
|
+
)
|
core/signal_model.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Signal:
|
|
6
|
+
id: str
|
|
7
|
+
file: str
|
|
8
|
+
confidence: float = 1.0
|
|
9
|
+
meta: Dict[str, Any] = field(default_factory=dict)
|
|
10
|
+
action: str = "added" # "added" or "removed"
|
|
11
|
+
line: Optional[int] = None
|
|
12
|
+
|
|
13
|
+
def to_dict(self):
|
|
14
|
+
return {
|
|
15
|
+
"signal_id": self.id,
|
|
16
|
+
"file": self.file,
|
|
17
|
+
"confidence": self.confidence,
|
|
18
|
+
"meta": self.meta,
|
|
19
|
+
"action": self.action,
|
|
20
|
+
"line": self.line
|
|
21
|
+
}
|
core/signals_registry.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DiffSense Signal Registry — single source of truth for all signals emitted by semantic analyzers.
|
|
3
|
+
Rules only consume these signals; they do not emit them.
|
|
4
|
+
Used by: docs/signals.md, CLI `diffsense signals`.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
# Grouped by category for docs and CLI output. Order and grouping match docs/signals.md.
|
|
9
|
+
SIGNALS_BY_GROUP: Dict[str, List[str]] = {
|
|
10
|
+
"runtime.concurrency": [
|
|
11
|
+
"runtime.concurrency.synchronized",
|
|
12
|
+
"runtime.concurrency.lock",
|
|
13
|
+
"runtime.concurrency.lock_removed",
|
|
14
|
+
"runtime.concurrency.volatile",
|
|
15
|
+
"runtime.concurrency.volatile_removed",
|
|
16
|
+
"runtime.concurrency.final_removed",
|
|
17
|
+
"runtime.concurrency.concurrent_map",
|
|
18
|
+
"runtime.concurrency.thread_safety_downgrade",
|
|
19
|
+
"runtime.concurrency.static_unsafe_collection",
|
|
20
|
+
"runtime.concurrency.atomic_to_non_atomic_write",
|
|
21
|
+
"runtime.concurrency.threadpool_param_change",
|
|
22
|
+
"runtime.concurrency.threadpool_creation",
|
|
23
|
+
"runtime.concurrency.threadpool_unbounded_queue",
|
|
24
|
+
"runtime.concurrency.executors_factory_risk",
|
|
25
|
+
"runtime.concurrency.future_get_without_timeout",
|
|
26
|
+
"runtime.concurrency.busy_wait_added",
|
|
27
|
+
],
|
|
28
|
+
"runtime.performance": [
|
|
29
|
+
"runtime.performance.sleep_added",
|
|
30
|
+
],
|
|
31
|
+
"runtime.resource": [
|
|
32
|
+
"runtime.resource.try_with_resource_removed",
|
|
33
|
+
"runtime.resource.cache_eviction_removed",
|
|
34
|
+
],
|
|
35
|
+
"runtime.network": [
|
|
36
|
+
"runtime.network.timeout_removed",
|
|
37
|
+
],
|
|
38
|
+
"runtime.data": [
|
|
39
|
+
"runtime.data.null_check_removed",
|
|
40
|
+
"runtime.data.equals_to_reference_compare",
|
|
41
|
+
],
|
|
42
|
+
"runtime": [
|
|
43
|
+
"runtime.input_normalization_removed",
|
|
44
|
+
"runtime.collection_mutation_inside_loop",
|
|
45
|
+
],
|
|
46
|
+
"data": [
|
|
47
|
+
"data.pagination_semantic_change",
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_all_signals() -> List[str]:
|
|
53
|
+
"""Flat list of all signal IDs, sorted."""
|
|
54
|
+
out = []
|
|
55
|
+
for group in SIGNALS_BY_GROUP.values():
|
|
56
|
+
out.extend(group)
|
|
57
|
+
return sorted(out)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_signals_by_group() -> Dict[str, List[str]]:
|
|
61
|
+
"""Signals grouped by category (for CLI and docs)."""
|
|
62
|
+
return SIGNALS_BY_GROUP.copy()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffsense
|
|
3
|
+
Version: 2.2.12
|
|
4
|
+
Summary: MR/PR risk audit: semantic diff + rule engine. Run in CI as image or pip.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: PyYAML
|
|
9
|
+
Requires-Dist: PyGithub
|
|
10
|
+
Requires-Dist: python-gitlab
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: javalang
|
|
13
|
+
Requires-Dist: typer>=0.9.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Provides-Extra: mcp
|
|
17
|
+
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
18
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
banner.py,sha256=JHRCec0w4eGt68IBHJ0y5df4OsuzPiXmWf-FwqJJCA4,1907
|
|
2
|
+
cli.py,sha256=Lcc9bfOTeTgHBNvJtIMAO_3hBqS8M9luIDPAfRhv4vI,27358
|
|
3
|
+
main.py,sha256=XZsJh2TUzEEOi5PkgeYTomg_dYP2Yvoj1f630msGCdc,13768
|
|
4
|
+
run_audit.py,sha256=DpeeNj0B8zIQvM6gtjaZoLUKSrkcJKORsP76-61s7Wk,21909
|
|
5
|
+
adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
adapters/base.py,sha256=8WG6uLyvpexEqyynNr7X-WuRyDzE5jOxUV9qdIH7irM,673
|
|
7
|
+
adapters/github_adapter.py,sha256=MgM0i9oGtB746CuK0c4BjrtUux0S8jWVCfes0z7-Dpo,6534
|
|
8
|
+
adapters/gitlab_adapter.py,sha256=g5OWwVLocb2UNPRxVZMZytGsgTyYI7V-8P_0hV_ZEZ0,8636
|
|
9
|
+
adapters/local_adapter.py,sha256=IIYcAWfl7WJEzDIBCC9JGJ1fjGkAUiWcqkugGUeqSLY,5074
|
|
10
|
+
config/__init__.py,sha256=3udbRIyrQB2JnFoFWqL0IAyweo36J5SsJOrMEXMAsjI,50
|
|
11
|
+
config/rules.yaml,sha256=lUGEA-S6hD8XFdCTiS_rZxWb9_xlsZKBtO2h3uPkov8,13309
|
|
12
|
+
core/__init__.py,sha256=wM1ShcscRZ9jB9gwnHsYYPMG4Mp_41FXmmFgo1_eP84,7860
|
|
13
|
+
core/ast_detector.py,sha256=fH7aNgU3oESFgy2FWiaMyVXTVUXeA0LIrAv-2TzBCP4,39590
|
|
14
|
+
core/change.py,sha256=MMQ2M0XO3NmjAQ3aTR9Xm5g651AdjqlUo-LGAlY9PT0,1364
|
|
15
|
+
core/composer.py,sha256=1tN2OEIz501JzPsuGb3PDStgjX_I1ZBxXktDs5iHEk4,3395
|
|
16
|
+
core/evaluator.py,sha256=VqSzezO2CTzIWLfhdFEXtOsm-pZtw_kEQHE1nRUG9Aw,528
|
|
17
|
+
core/ignore_manager.py,sha256=KR9Ec475CcnwAjyXciHmWLl3KZA8OSnvspiVmrVFdXM,2643
|
|
18
|
+
core/knowledge.py,sha256=83KNJu0Pq_JFEVv_J-eepgBaGyYwkA53AefOCHnxhb0,1920
|
|
19
|
+
core/parser.py,sha256=FuU2oF3kMWLNm0hKf45wBDG-r5I4531WDO0CwcTFHqk,6570
|
|
20
|
+
core/parser_manager.py,sha256=QAgU84j_mpqkDXXnG95CbjIroeoAGPMRHsJLQqFMB_Q,3954
|
|
21
|
+
core/quality_manager.py,sha256=GCpzJc643CwpmFQXvfQKs-WIf2fD5hBkmlPEwBPXgBM,4650
|
|
22
|
+
core/renderer.py,sha256=ZMRvTyA08o1sp7yQdJ55-q7w85UdMF-ejgWPHQNa6X4,7661
|
|
23
|
+
core/rule_base.py,sha256=JXaRAjYNf2OVHT5g-5jp6pn49Is0qfq9VW2FiM5yq8g,2792
|
|
24
|
+
core/rule_runtime.py,sha256=pvjPvWwryAAuVVkzMTnIb9fmJ_bQlf1tGqiZDrC1ET4,3583
|
|
25
|
+
core/rules.py,sha256=qcYy0BeVxgdMROz4Y-P72UQ0WwuH36qe_vjJe_mwOSU,30502
|
|
26
|
+
core/run_config.py,sha256=GnVcpVKfJRl3dov3rz2VtxQj2WmeEaIwdBBxJB3Lo50,3730
|
|
27
|
+
core/semantic_diff.py,sha256=OjTc7PBasD8dHQLqpVQ85fV2Q6e0iD7FaoyBnA61YKI,13894
|
|
28
|
+
core/signal_model.py,sha256=4K1yP1i8shMUj0c8Old8bWsrJb2_e5AulqPk88wlbpY,555
|
|
29
|
+
core/signals_registry.py,sha256=BzQzGZ7OMNvZpt33YKiIpbZ1E319xwXB5uXgVpM6l1E,2152
|
|
30
|
+
diffsense-2.2.12.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
31
|
+
diffsense_mcp/__init__.py,sha256=Vf1bx7eiubdiOPap5ZGtDPLdgef6gSXHwyf8JAfRfOc,23
|
|
32
|
+
diffsense_mcp/launcher.py,sha256=bYBl04TdN_A1xSizSlMvsyMDEHqKg7WNt-_PeKmN_HA,721
|
|
33
|
+
diffsense_mcp/server.py,sha256=LvyluHKcVy8hkU5rrbdu6sLi7ZJeJObQP7VOreJB4Y4,20080
|
|
34
|
+
governance/lifecycle.py,sha256=tPTbtjlZ3-nAeN7ZBXy-NSMERh90I_foPitVHHS6gmE,1643
|
|
35
|
+
rules/__init__.py,sha256=Zl9rebPBEdEmqgDW4w9c27HH0VqdbECf3ljRtVIF9jI,6653
|
|
36
|
+
rules/api_compatibility.py,sha256=3XjPH31-pLwawia3eKNECi995KB9HDgMB1U6g5PVHSI,10812
|
|
37
|
+
rules/collection_handling.py,sha256=5QCbHaeeQgV3G05oYnPokD8v1w5XS30wayOFT-hcSqE,10815
|
|
38
|
+
rules/concurrency.py,sha256=eYJiZkwB-iFoUa6W_QO-0cdRGHvtqOH2gTY8tir2swQ,6965
|
|
39
|
+
rules/concurrency_adapter.py,sha256=H3ykRPRy-B9zPatR6Iq8V1sKvqXB3rG1F1NqJYginr0,8285
|
|
40
|
+
rules/cross_language_adapter.py,sha256=hJ7ASYaGfi0Xt8VNa31icn-riFG0Xm-5yUokEnQxL9o,16016
|
|
41
|
+
rules/exception_handling.py,sha256=ewRNqq5abkl7RvS4Upsr1DpOZ2AYuVgS-R-k-iVc8Fg,9851
|
|
42
|
+
rules/go_rules.py,sha256=2bSOwNncon9Os2V3CobPxDMlhdeoyZO1HJ9RFF6TWb0,13100
|
|
43
|
+
rules/null_safety.py,sha256=EozoR8ARWfBB_ygzj_fwjnZxf_CPv42B0YntJCg5eLQ,9500
|
|
44
|
+
rules/resource_management.py,sha256=PJ9KVfNvEcJ2AMJ2pWkYoBEGTSA0KuvZeAJqtyfi7sY,7140
|
|
45
|
+
rules/yaml_adapter.py,sha256=tdXc31Fg0ksBnGYfIMgaMHs8H9X-aObEY_QX9ZRKc0k,7022
|
|
46
|
+
sdk/cpp_adapter.py,sha256=28F0x1KQpsrwlK66mNf10s-HOdZFP-cOHs9GG2psIjE,8002
|
|
47
|
+
sdk/go_adapter.py,sha256=mpT8otbYYgJMQj-Uxfr_Vhl44GOW6Ylndrl3RoHtjG0,6752
|
|
48
|
+
sdk/java_adapter.py,sha256=ykomerEDfSXUrE9yyCnoUmfNGWKCBSdLEZaQv3JjF5I,7297
|
|
49
|
+
sdk/javascript_adapter.py,sha256=C3en-rdGdqrFYPlDPgSsR7EI-Jt_cdumrYWi2zV8vFo,7651
|
|
50
|
+
sdk/language_adapter.py,sha256=iXZl-V8bkEXN0_uDPYafYHMjZRA_ZulNgh3-mcW9bV8,10210
|
|
51
|
+
sdk/python_adapter.py,sha256=6CpceOA4fjli5Vlmq2IC7i0uNAEB8lk8SrD6aFLzedo,6889
|
|
52
|
+
sdk/rule.py,sha256=OpFDygxWJbzHP3OGUOxiqhwZphHc_hgqHZdM-pL2HiA,1840
|
|
53
|
+
sdk/signal.py,sha256=9Q22WR8rAgAqjfNps0GQT7wA5Sfh-Xzhlv2rgl-o0gQ,549
|
|
54
|
+
diffsense-2.2.12.dist-info/METADATA,sha256=7FU5H830pippzhvgU4zb94a11VxGo8Ylz-JKMIRUWAg,496
|
|
55
|
+
diffsense-2.2.12.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
56
|
+
diffsense-2.2.12.dist-info/entry_points.txt,sha256=1gDOb00JZWCulfon24mvvInZabeB2tb3aBz5j7DR4Xg,80
|
|
57
|
+
diffsense-2.2.12.dist-info/top_level.txt,sha256=QnVsGcF6thk9THgrcaD0SVJak5b1gw-fBe7aMvp47FE,82
|
|
58
|
+
diffsense-2.2.12.dist-info/RECORD,,
|