sourcecode 1.35.3__py3-none-any.whl → 1.35.4__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.
- sourcecode/__init__.py +1 -1
- sourcecode/cir_graphs.py +20 -4
- sourcecode/repository_ir.py +100 -8
- sourcecode/spring_impact.py +54 -10
- sourcecode/spring_tx_analyzer.py +34 -2
- {sourcecode-1.35.3.dist-info → sourcecode-1.35.4.dist-info}/METADATA +99 -6
- {sourcecode-1.35.3.dist-info → sourcecode-1.35.4.dist-info}/RECORD +10 -10
- {sourcecode-1.35.3.dist-info → sourcecode-1.35.4.dist-info}/WHEEL +0 -0
- {sourcecode-1.35.3.dist-info → sourcecode-1.35.4.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.35.3.dist-info → sourcecode-1.35.4.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cir_graphs.py
CHANGED
|
@@ -68,11 +68,23 @@ class ImplementationGraph:
|
|
|
68
68
|
dependencies: cir.dependencies — list of edge dicts with 'from'/'to'/'type'
|
|
69
69
|
known_symbols: set(cir.symbols) — only in-repo FQNs
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
(e.g.
|
|
71
|
+
The Java parser stores 'implements' edges with the simple class name in the 'to'
|
|
72
|
+
field (e.g. 'OrderService') rather than the FQN. We resolve these via a
|
|
73
|
+
precomputed simple-name → FQN map built from known_symbols. Only unambiguous
|
|
74
|
+
resolutions are accepted; external framework interfaces and ambiguous names are
|
|
75
|
+
excluded.
|
|
76
|
+
|
|
73
77
|
Includes edges where the implementing class (from_fqn) is NOT in known_symbols
|
|
74
78
|
only when the interface IS known — this handles partial-parse edge cases.
|
|
75
79
|
"""
|
|
80
|
+
# Pre-build simple-name → [FQN] lookup for class-level symbols only (no '#').
|
|
81
|
+
# Used to resolve unqualified interface names (BUG-IC-001).
|
|
82
|
+
_simple_to_fqn: dict[str, list[str]] = {}
|
|
83
|
+
for sym in known_symbols:
|
|
84
|
+
if "#" not in sym and "." in sym:
|
|
85
|
+
simple = sym.rsplit(".", 1)[1]
|
|
86
|
+
_simple_to_fqn.setdefault(simple, []).append(sym)
|
|
87
|
+
|
|
76
88
|
impl_of: dict[str, list[str]] = {}
|
|
77
89
|
ifaces_of: dict[str, list[str]] = {}
|
|
78
90
|
|
|
@@ -83,9 +95,13 @@ class ImplementationGraph:
|
|
|
83
95
|
to_fqn = (edge.get("to") or "").strip()
|
|
84
96
|
if not from_fqn or not to_fqn:
|
|
85
97
|
continue
|
|
86
|
-
#
|
|
98
|
+
# Resolve to_fqn: prefer exact known-symbol match, then try simple-name lookup.
|
|
99
|
+
# Rejects external interfaces (java.*, org.springframework.*) and ambiguous names.
|
|
87
100
|
if to_fqn not in known_symbols:
|
|
88
|
-
|
|
101
|
+
candidates = _simple_to_fqn.get(to_fqn, [])
|
|
102
|
+
if len(candidates) != 1:
|
|
103
|
+
continue
|
|
104
|
+
to_fqn = candidates[0]
|
|
89
105
|
# Ignore malformed FQNs (e.g. generic type fragments like "Long>")
|
|
90
106
|
if ">" in to_fqn or "<" in to_fqn:
|
|
91
107
|
continue
|
sourcecode/repository_ir.py
CHANGED
|
@@ -202,10 +202,13 @@ _FILTER_SECURITY_ANNOTATIONS: frozenset[str] = frozenset({
|
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
# Programmatic security: method-call patterns that indicate runtime auth enforcement.
|
|
205
|
+
# Requires method-call or field-access context — bare class name mentions (imports,
|
|
206
|
+
# type declarations) must NOT match or IAM/auth-domain repos generate false positives.
|
|
205
207
|
_PROGRAMMATIC_SECURITY_RE = re.compile(
|
|
206
208
|
r"\b(?:hasRole|hasAuthority|isAuthenticated|requirePermission|checkPermission"
|
|
207
209
|
r"|assertAuthorized|authenticate)\s*\("
|
|
208
|
-
r"|
|
|
210
|
+
r"|SecurityContextHolder\."
|
|
211
|
+
r"|\.(?:getAuthentication|getSecurityContext|getPrincipal|isAuthorized|checkAccess)\s*\("
|
|
209
212
|
r"|throw\s+new\s+(?:AccessDeniedException|UnauthorizedException|ForbiddenException|AuthenticationException)\b",
|
|
210
213
|
re.MULTILINE,
|
|
211
214
|
)
|
|
@@ -323,6 +326,30 @@ _PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
|
|
|
323
326
|
# Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
|
|
324
327
|
_FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
|
|
325
328
|
|
|
329
|
+
# Class-level consumer detection from class signature (not annotations).
|
|
330
|
+
# Pattern 1: implements [Prefix]ApplicationListener<EventType>
|
|
331
|
+
# Matches both the standard Spring interface (ApplicationListener<E>) and
|
|
332
|
+
# framework-specific subinterfaces (BroadleafApplicationListener<E>,
|
|
333
|
+
# SmartApplicationListener<E>, etc.). Uses \w* prefix instead of \b so
|
|
334
|
+
# that "Broadleaf" prefix does not break the word boundary. (BUG-EVT-001)
|
|
335
|
+
_APP_LISTENER_RE = re.compile(r'\w*ApplicationListener\s*<\s*(\w+)\s*>')
|
|
336
|
+
# Pattern 2: extends AbstractXxxEventListener<EventType> — abstract base class pattern
|
|
337
|
+
# (Broadleaf's AbstractBroadleafApplicationEventListener and similar).
|
|
338
|
+
# Matches any parent class name that contains "EventListener".
|
|
339
|
+
_ABSTRACT_LISTENER_RE = re.compile(r'\bextends\s+\w+EventListener\w*\s*<\s*(\w+)\s*>')
|
|
340
|
+
|
|
341
|
+
# Block comment stripper — removes /* ... */ (including Javadoc) to prevent
|
|
342
|
+
# _PUBLISH_EVENT_RE / _FIRE_EVENT_RE from matching example code in comments.
|
|
343
|
+
_BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
|
|
344
|
+
_LINE_COMMENT_RE = re.compile(r'//[^\n]*')
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _strip_java_comments(source: str) -> str:
|
|
348
|
+
"""Remove // line comments and /* */ block comments from Java source."""
|
|
349
|
+
source = _BLOCK_COMMENT_RE.sub(' ', source)
|
|
350
|
+
source = _LINE_COMMENT_RE.sub(' ', source)
|
|
351
|
+
return source
|
|
352
|
+
|
|
326
353
|
# Edge types used for subsystem grouping — semantic hierarchy only, not imports
|
|
327
354
|
_SUBSYSTEM_STRUCTURAL_EDGES: frozenset[str] = frozenset({
|
|
328
355
|
"extends", "implements", "injects", "contained_in",
|
|
@@ -546,9 +573,37 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
|
|
|
546
573
|
pending_ann_values: dict[str, str] = {}
|
|
547
574
|
in_block_comment = False
|
|
548
575
|
|
|
576
|
+
# BUG-PARSER-001: normalize multi-line class declarations where the opening brace
|
|
577
|
+
# appears on a continuation line (e.g. "implements A,\n B, C {").
|
|
578
|
+
# _CLASS_DECL_RE requires '{' on the same line as 'class' — joining the continuation
|
|
579
|
+
# here makes the regex work without changing the per-line brace-depth counter.
|
|
580
|
+
_raw_lines = source.splitlines()
|
|
581
|
+
_joined: list[str] = []
|
|
582
|
+
_i = 0
|
|
583
|
+
_CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum)\s+[A-Z]')
|
|
584
|
+
while _i < len(_raw_lines):
|
|
585
|
+
_line = _raw_lines[_i]
|
|
586
|
+
_stripped = _line.strip()
|
|
587
|
+
if (_CLASS_KW_RE.search(_stripped) and '{' not in _stripped
|
|
588
|
+
and not _stripped.startswith('//')
|
|
589
|
+
and not _stripped.startswith('*')):
|
|
590
|
+
# Continuation: join until we hit a line containing '{'
|
|
591
|
+
_buf = _line
|
|
592
|
+
_i += 1
|
|
593
|
+
while _i < len(_raw_lines):
|
|
594
|
+
_cont = _raw_lines[_i]
|
|
595
|
+
_buf = _buf.rstrip() + ' ' + _cont.strip()
|
|
596
|
+
_i += 1
|
|
597
|
+
if '{' in _cont:
|
|
598
|
+
break
|
|
599
|
+
_joined.append(_buf)
|
|
600
|
+
else:
|
|
601
|
+
_joined.append(_line)
|
|
602
|
+
_i += 1
|
|
603
|
+
|
|
549
604
|
# P1 fix: normalize multiline annotations (e.g. @RequestMapping(\n value="..."\n))
|
|
550
605
|
# into single lines so the per-line regex can capture annotation args correctly.
|
|
551
|
-
_normalized_lines = _normalize_multiline_annotations(
|
|
606
|
+
_normalized_lines = _normalize_multiline_annotations(_joined)
|
|
552
607
|
|
|
553
608
|
for line in _normalized_lines:
|
|
554
609
|
stripped = line.strip()
|
|
@@ -1122,10 +1177,15 @@ def _build_relations(
|
|
|
1122
1177
|
|
|
1123
1178
|
_class_syms = [s for s in symbols if s.type in ("class", "interface") and "#" not in s.symbol]
|
|
1124
1179
|
|
|
1180
|
+
# Strip comments before event scanning to prevent Javadoc examples from
|
|
1181
|
+
# generating false publisher edges (BUG-003).
|
|
1182
|
+
_source_no_comments = _strip_java_comments(source)
|
|
1183
|
+
|
|
1125
1184
|
# Spring: class that calls publishEvent(new XxxEvent(...)) → event type FQN.
|
|
1126
|
-
for m in _PUBLISH_EVENT_RE.finditer(
|
|
1185
|
+
for m in _PUBLISH_EVENT_RE.finditer(_source_no_comments):
|
|
1127
1186
|
event_simple = m.group(1)
|
|
1128
|
-
|
|
1187
|
+
# BUG-004: try import_map first, then same-package map, then keep simple name.
|
|
1188
|
+
event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
|
|
1129
1189
|
for cls_sym in _class_syms:
|
|
1130
1190
|
edges.append(RelationEdge(
|
|
1131
1191
|
from_symbol=cls_sym.symbol,
|
|
@@ -1136,9 +1196,9 @@ def _build_relations(
|
|
|
1136
1196
|
))
|
|
1137
1197
|
|
|
1138
1198
|
# Keycloak SPI: XxxEvent.fire(...) static dispatch → publishes_event.
|
|
1139
|
-
for m in _FIRE_EVENT_RE.finditer(
|
|
1199
|
+
for m in _FIRE_EVENT_RE.finditer(_source_no_comments):
|
|
1140
1200
|
event_simple = m.group(1)
|
|
1141
|
-
event_fqn = import_map.get(event_simple
|
|
1201
|
+
event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
|
|
1142
1202
|
for cls_sym in _class_syms:
|
|
1143
1203
|
edges.append(RelationEdge(
|
|
1144
1204
|
from_symbol=cls_sym.symbol,
|
|
@@ -1161,6 +1221,34 @@ def _build_relations(
|
|
|
1161
1221
|
evidence={"type": "signature", "value": f"implements {_ELP_IFACE}"},
|
|
1162
1222
|
))
|
|
1163
1223
|
|
|
1224
|
+
# Class-level consumer detection via class signature (EVT-003 / EVT-004).
|
|
1225
|
+
# Pattern A: class Foo implements ApplicationListener<XxxEvent>
|
|
1226
|
+
# → standard Spring interface, event type = generic param.
|
|
1227
|
+
# Pattern B: class Foo extends AbstractXxxEventListener<XxxEvent>
|
|
1228
|
+
# → abstract base class pattern (Broadleaf and similar frameworks),
|
|
1229
|
+
# event type = generic param of the parent class.
|
|
1230
|
+
for sym in _class_syms:
|
|
1231
|
+
sig = sym.signature or ""
|
|
1232
|
+
for pattern, ev_label in (
|
|
1233
|
+
(_APP_LISTENER_RE, "implements ApplicationListener"),
|
|
1234
|
+
(_ABSTRACT_LISTENER_RE, "extends *EventListener"),
|
|
1235
|
+
):
|
|
1236
|
+
m = pattern.search(sig)
|
|
1237
|
+
if m:
|
|
1238
|
+
event_simple = m.group(1)
|
|
1239
|
+
event_fqn = (
|
|
1240
|
+
import_map.get(event_simple)
|
|
1241
|
+
or _same_pkg.get(event_simple)
|
|
1242
|
+
or event_simple
|
|
1243
|
+
)
|
|
1244
|
+
edges.append(RelationEdge(
|
|
1245
|
+
from_symbol=sym.symbol,
|
|
1246
|
+
to_symbol=event_fqn,
|
|
1247
|
+
type="listens_to_event",
|
|
1248
|
+
confidence="high",
|
|
1249
|
+
evidence={"type": "signature", "value": f"{ev_label}<{event_simple}>"},
|
|
1250
|
+
))
|
|
1251
|
+
|
|
1164
1252
|
seen: set[tuple[str, str, str]] = set()
|
|
1165
1253
|
unique: list[RelationEdge] = []
|
|
1166
1254
|
for e in edges:
|
|
@@ -2321,8 +2409,12 @@ def _assemble(
|
|
|
2321
2409
|
for sym in _class_syms_asm
|
|
2322
2410
|
)
|
|
2323
2411
|
)
|
|
2412
|
+
# Only real annotation-based policies count (not "programmatic" fallback).
|
|
2413
|
+
# Programmatic security does not mean every unannotated endpoint is unsecured.
|
|
2324
2414
|
_has_ann_sec_asm = any(
|
|
2325
|
-
r.get("security_annotations")
|
|
2415
|
+
isinstance(r.get("security_annotations"), dict)
|
|
2416
|
+
and r["security_annotations"].get("policy") not in (None, "programmatic", "none_detected")
|
|
2417
|
+
for r in _route_surface
|
|
2326
2418
|
if isinstance(r, dict)
|
|
2327
2419
|
)
|
|
2328
2420
|
if _filter_based_asm and _has_ann_sec_asm:
|
|
@@ -3172,7 +3264,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3172
3264
|
)
|
|
3173
3265
|
)
|
|
3174
3266
|
_has_annotation_security = any(
|
|
3175
|
-
e.get("security", {}).get("policy")
|
|
3267
|
+
e.get("security", {}).get("policy") not in (None, "none_detected", "programmatic")
|
|
3176
3268
|
for e in endpoints
|
|
3177
3269
|
)
|
|
3178
3270
|
if _filter_based and _has_annotation_security:
|
sourcecode/spring_impact.py
CHANGED
|
@@ -289,9 +289,16 @@ def _bfs_callers(
|
|
|
289
289
|
_add_caller(caller, depth)
|
|
290
290
|
# CH-002: injects edge to a field/constructor node → also traverse
|
|
291
291
|
# the containing class, bypassing the skipped contained_in edge.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
# Two formats emitted by the CIR parser:
|
|
293
|
+
# Constructor injection: pkg.Class#<init> (hash separator)
|
|
294
|
+
# Field injection: pkg.Class.field (dot, lowercase last segment)
|
|
295
|
+
if etype == "injects":
|
|
296
|
+
if "#" in caller:
|
|
297
|
+
_add_caller(caller.rsplit("#", 1)[0], depth)
|
|
298
|
+
elif "." in caller:
|
|
299
|
+
last_seg = caller.rsplit(".", 1)[1]
|
|
300
|
+
if last_seg and last_seg[0].islower():
|
|
301
|
+
_add_caller(caller.rsplit(".", 1)[0], depth)
|
|
295
302
|
|
|
296
303
|
return direct, indirect, was_truncated
|
|
297
304
|
|
|
@@ -316,10 +323,16 @@ def _collect_endpoints(
|
|
|
316
323
|
"""
|
|
317
324
|
all_fqns = set(seed_fqns) | set(all_callers)
|
|
318
325
|
|
|
319
|
-
# Class-level
|
|
320
|
-
#
|
|
326
|
+
# Class-level controller FQNs (no '#') that appear anywhere in the chain.
|
|
327
|
+
# Two cases produce a class-level controller node in the chain:
|
|
328
|
+
# 1. Seed is the controller class itself (user queried the whole controller).
|
|
329
|
+
# 2. Caller is the controller class node — happens when a service/repository
|
|
330
|
+
# is injected into a controller: the BFS reverse edge lands on the class
|
|
331
|
+
# node (e.g. "OwnerController" with no '#'), not a specific method.
|
|
332
|
+
# All endpoints of that controller are affected because any change to the
|
|
333
|
+
# injected dependency impacts every handler that uses it.
|
|
321
334
|
class_level_controllers: set[str] = {
|
|
322
|
-
fqn for fqn in
|
|
335
|
+
fqn for fqn in all_fqns
|
|
323
336
|
if "#" not in fqn and fqn in model.endpoint_index.controller_fqns
|
|
324
337
|
}
|
|
325
338
|
|
|
@@ -327,7 +340,7 @@ def _collect_endpoints(
|
|
|
327
340
|
seen_ep_ids: set[str] = set()
|
|
328
341
|
|
|
329
342
|
# Collect candidate controllers: those whose handler_symbol is in the chain
|
|
330
|
-
# OR whose class node
|
|
343
|
+
# OR whose class node appears in the chain (class-level).
|
|
331
344
|
candidate_controllers: set[str] = set(class_level_controllers)
|
|
332
345
|
for fqn in all_fqns:
|
|
333
346
|
cls = _class_of(fqn)
|
|
@@ -339,8 +352,8 @@ def _collect_endpoints(
|
|
|
339
352
|
handler = getattr(ep, "handler_symbol", "") or ""
|
|
340
353
|
ep_id = getattr(ep, "id", "") or ""
|
|
341
354
|
|
|
342
|
-
# Include if: handler is
|
|
343
|
-
# class-level
|
|
355
|
+
# Include if: handler method is in call chain OR the controller's class
|
|
356
|
+
# node appears at class-level in the chain (seed or DI-injected class).
|
|
344
357
|
if handler not in all_fqns and controller not in class_level_controllers:
|
|
345
358
|
continue
|
|
346
359
|
|
|
@@ -561,6 +574,32 @@ class ImpactOrchestrator:
|
|
|
561
574
|
f"added {n_syms} symbol(s) from {n_classes} implementation(s)."
|
|
562
575
|
)
|
|
563
576
|
|
|
577
|
+
# CH-001b: expand impl seeds to include their interfaces for BFS (BUG-IC-002).
|
|
578
|
+
# Callers typically inject the interface type, so reverse-graph edges live on
|
|
579
|
+
# the interface node, not on the implementation node. Without this expansion,
|
|
580
|
+
# querying 'OrderServiceImpl' finds 0 callers even though 36 classes inject it.
|
|
581
|
+
if impl_graph is not None:
|
|
582
|
+
current_seed_classes = {_class_of(s) for s in seed_fqns}
|
|
583
|
+
iface_seeds: list[str] = []
|
|
584
|
+
iface_classes_added: set[str] = set()
|
|
585
|
+
for seed_class in sorted(current_seed_classes):
|
|
586
|
+
ifaces = impl_graph.interfaces_of(seed_class)
|
|
587
|
+
for iface_class in ifaces:
|
|
588
|
+
if iface_class in iface_classes_added or iface_class in current_seed_classes:
|
|
589
|
+
continue
|
|
590
|
+
iface_classes_added.add(iface_class)
|
|
591
|
+
for sym in cir.symbols:
|
|
592
|
+
if _class_of(sym) == iface_class and sym not in set(seed_fqns):
|
|
593
|
+
iface_seeds.append(sym)
|
|
594
|
+
if iface_seeds:
|
|
595
|
+
seed_fqns = list(dict.fromkeys(seed_fqns + iface_seeds))
|
|
596
|
+
n_classes = len(iface_classes_added)
|
|
597
|
+
n_syms = len(iface_seeds)
|
|
598
|
+
warnings.append(
|
|
599
|
+
f"Implementation-to-interface expansion (CH-001b): "
|
|
600
|
+
f"added {n_syms} symbol(s) from {n_classes} interface(s) for caller BFS."
|
|
601
|
+
)
|
|
602
|
+
|
|
564
603
|
# ── 2. BFS through reverse graph ─────────────────────────────────
|
|
565
604
|
direct_callers, indirect_callers, truncated = _bfs_callers(
|
|
566
605
|
seed_fqns, cir.reverse_graph, depth
|
|
@@ -580,8 +619,13 @@ class ImpactOrchestrator:
|
|
|
580
619
|
try:
|
|
581
620
|
boundary = model.tx_index.effective_boundary(resolved_symbol)
|
|
582
621
|
if boundary is None and "#" not in resolved_symbol:
|
|
583
|
-
# Class-level symbol — try class_level directly
|
|
622
|
+
# Class-level symbol — try class_level directly, then fall back
|
|
623
|
+
# to first method-level boundary if class has only method-level TX.
|
|
584
624
|
boundary = model.tx_index.class_level.get(resolved_symbol)
|
|
625
|
+
if boundary is None:
|
|
626
|
+
method_boundaries = model.tx_index.by_class.get(resolved_symbol, [])
|
|
627
|
+
if method_boundaries:
|
|
628
|
+
boundary = method_boundaries[0]
|
|
585
629
|
if boundary is not None:
|
|
586
630
|
tx_boundary = boundary.to_dict()
|
|
587
631
|
except Exception:
|
sourcecode/spring_tx_analyzer.py
CHANGED
|
@@ -63,6 +63,30 @@ _CATCH_SWALLOW_RE = re.compile(
|
|
|
63
63
|
_RETHROW_IN_CATCH_RE = re.compile(r'\bthrow\b')
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def _extract_method_body(source: str, method_name: str) -> str:
|
|
67
|
+
"""Extract the first method body matching method_name using brace counting.
|
|
68
|
+
|
|
69
|
+
Returns the text from '{' to the matching '}', or empty string if not found.
|
|
70
|
+
Needed to scope TX-005 regex to the specific method instead of the whole file.
|
|
71
|
+
"""
|
|
72
|
+
pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
|
|
73
|
+
for m in pattern.finditer(source):
|
|
74
|
+
brace_pos = source.find('{', m.end())
|
|
75
|
+
if brace_pos < 0:
|
|
76
|
+
continue
|
|
77
|
+
depth = 1
|
|
78
|
+
i = brace_pos + 1
|
|
79
|
+
while i < len(source) and depth > 0:
|
|
80
|
+
c = source[i]
|
|
81
|
+
if c == '{':
|
|
82
|
+
depth += 1
|
|
83
|
+
elif c == '}':
|
|
84
|
+
depth -= 1
|
|
85
|
+
i += 1
|
|
86
|
+
return source[brace_pos:i]
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
66
90
|
# ---------------------------------------------------------------------------
|
|
67
91
|
# Pattern protocol
|
|
68
92
|
# ---------------------------------------------------------------------------
|
|
@@ -531,8 +555,16 @@ class _TX005ExceptionSwallowing:
|
|
|
531
555
|
return findings
|
|
532
556
|
|
|
533
557
|
def _has_swallowed_exception(self, source: str, symbol: str) -> bool:
|
|
534
|
-
"""Return True if
|
|
535
|
-
|
|
558
|
+
"""Return True if the specific method body has a catch-log-no-rethrow pattern.
|
|
559
|
+
|
|
560
|
+
Scopes the search to the method body only (not the whole file) to avoid
|
|
561
|
+
false positives when other methods in the same file have swallowed exceptions.
|
|
562
|
+
"""
|
|
563
|
+
method_name = symbol.split("#")[-1] if "#" in symbol else symbol.rsplit(".", 1)[-1]
|
|
564
|
+
body = _extract_method_body(source, method_name)
|
|
565
|
+
if not body:
|
|
566
|
+
return False
|
|
567
|
+
for match in _CATCH_SWALLOW_RE.finditer(body):
|
|
536
568
|
block = match.group(0)
|
|
537
569
|
if not _RETHROW_IN_CATCH_RE.search(block):
|
|
538
570
|
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.4
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
|
|
40
40
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
41
41
|
|
|
42
|
-

|
|
43
43
|

|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -113,7 +113,7 @@ pipx install sourcecode
|
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
115
|
sourcecode version
|
|
116
|
-
# sourcecode 1.
|
|
116
|
+
# sourcecode 1.35.4
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
---
|
|
@@ -133,6 +133,15 @@ sourcecode --agent
|
|
|
133
133
|
# Blast radius: what breaks if this class changes?
|
|
134
134
|
sourcecode impact OrderService /path/to/repo
|
|
135
135
|
|
|
136
|
+
# Spring semantic audit: TX anomalies + security surface (free)
|
|
137
|
+
sourcecode spring-audit /path/to/repo
|
|
138
|
+
|
|
139
|
+
# Impact chain: systemic blast radius with TX/SEC enrichment (free)
|
|
140
|
+
sourcecode impact-chain OrderService /path/to/repo
|
|
141
|
+
|
|
142
|
+
# Event topology: publisher → event → consumer graph (free)
|
|
143
|
+
sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
144
|
+
|
|
136
145
|
# REST endpoint surface
|
|
137
146
|
sourcecode endpoints /path/to/repo
|
|
138
147
|
|
|
@@ -187,15 +196,21 @@ sourcecode /repo --agent # ~4,500–5,500 tokens — more detail
|
|
|
187
196
|
sourcecode onboard /repo # task-structured: entry points, key files, gaps
|
|
188
197
|
```
|
|
189
198
|
|
|
190
|
-
### Before every change — blast radius check
|
|
199
|
+
### Before every change — blast radius + TX/SEC check
|
|
191
200
|
|
|
192
201
|
```bash
|
|
193
202
|
# Always target the INTERFACE in Spring projects, not the implementation:
|
|
194
203
|
sourcecode impact OrderService /repo # ✓ 30 callers, 11 endpoints
|
|
195
204
|
sourcecode impact OrderServiceImpl /repo # ✗ 0 callers (Spring DI blindness)
|
|
196
205
|
|
|
197
|
-
#
|
|
198
|
-
sourcecode impact
|
|
206
|
+
# Impact chain: blast radius enriched with TX boundary and security surfaces
|
|
207
|
+
sourcecode impact-chain OrderService /repo
|
|
208
|
+
|
|
209
|
+
# Event topology: who publishes/consumes this event, and in what TX phase?
|
|
210
|
+
sourcecode impact-chain OrderPlacedEvent /repo --type events
|
|
211
|
+
|
|
212
|
+
# Spring audit: catch TX anomalies before they hit production
|
|
213
|
+
sourcecode spring-audit /repo --scope tx
|
|
199
214
|
```
|
|
200
215
|
|
|
201
216
|
### Continuous agent loop — delta context
|
|
@@ -262,6 +277,9 @@ Specifically:
|
|
|
262
277
|
- Endpoint recall for JAX-RS subresource locator pattern is ~65%
|
|
263
278
|
- `impact` on implementation classes (e.g. `OrderServiceImpl`) returns 0 callers in Spring Boot — callers inject the interface via `@Autowired`. Always target the interface. When `direct_callers: []` with `confidence_level: high` for a `@Service` class, re-query the interface.
|
|
264
279
|
- `no_security_signal` on endpoints means no method-level annotations found — does **not** mean the endpoint is unsecured. Projects using Spring Security filter chains show 100% `no_security_signal` even when fully secured.
|
|
280
|
+
- `spring-audit` and `impact-chain` are **Java/Spring only** — non-Java repos return `spring_detected: false`
|
|
281
|
+
- Event topology via `--type events` does not resolve Kafka/RabbitMQ/Redis message routes — only Spring ApplicationEvent and `@EventListener` chains
|
|
282
|
+
- Self-invocation TX bypass (calling `@Transactional` method from the same class without going through the proxy) is not detected
|
|
265
283
|
|
|
266
284
|
---
|
|
267
285
|
|
|
@@ -312,6 +330,81 @@ sourcecode endpoints /path/to/repo --output endpoints.json
|
|
|
312
330
|
|
|
313
331
|
Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
|
|
314
332
|
|
|
333
|
+
### `spring-audit` — Spring semantic audit [free]
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
sourcecode spring-audit /path/to/repo
|
|
337
|
+
sourcecode spring-audit /path/to/repo --scope tx # TX anomalies only
|
|
338
|
+
sourcecode spring-audit /path/to/repo --scope security # security surface only
|
|
339
|
+
sourcecode spring-audit /path/to/repo --min-severity high
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Detects structural Spring anomalies that survive code review and tests, but cause production failures:
|
|
343
|
+
|
|
344
|
+
| Pattern | Description |
|
|
345
|
+
|---------|-------------|
|
|
346
|
+
| `TX-001` | `@Transactional` on private/final method — CGLIB proxy bypass, TX silently ignored |
|
|
347
|
+
| `TX-002` | `REQUIRES_NEW` nested inside `REQUIRED` call chain — unexpected transaction nesting |
|
|
348
|
+
| `TX-003` | `readOnly=true` boundary propagating to write operation |
|
|
349
|
+
| `TX-004` | `NOT_SUPPORTED`/`NEVER` called within active TX chain |
|
|
350
|
+
| `TX-005` | Exception swallowing inside `@Transactional` — silent TX rollback suppression |
|
|
351
|
+
| `SEC-001` | Unsecured endpoint in annotation-based security model |
|
|
352
|
+
| `SEC-002` | CVE-2025-41248: `@PreAuthorize` on inherited method from generic supertype |
|
|
353
|
+
| `SEC-003` | `@Transactional` on `@Controller`/`@RestController` — TX in wrong layer |
|
|
354
|
+
|
|
355
|
+
Returns structured findings with `severity`, `confidence`, `symbol`, `source_file`, `evidence`, `explanation`, and `fix_hint`. JAVA/SPRING ONLY.
|
|
356
|
+
|
|
357
|
+
### `impact-chain` — systemic blast radius with TX/SEC enrichment [free]
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
sourcecode impact-chain OrderService /path/to/repo
|
|
361
|
+
sourcecode impact-chain com.example.OrderService#placeOrder /path/to/repo
|
|
362
|
+
sourcecode impact-chain PaymentService . --depth 6
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Unlike `impact` (which traces the caller graph), `impact-chain` builds on the SpringSemanticModel to enrich every step of the blast cone with transaction and security context:
|
|
366
|
+
|
|
367
|
+
| Field | Description |
|
|
368
|
+
|-------|-------------|
|
|
369
|
+
| `direct_callers` | Symbols that directly call the target |
|
|
370
|
+
| `indirect_callers` | Transitive callers (BFS up to `--depth` hops, default: 4) |
|
|
371
|
+
| `endpoints_affected` | HTTP endpoints reachable through the call chain |
|
|
372
|
+
| `transaction_boundary` | `@Transactional` semantics on the target: propagation, isolation, readOnly |
|
|
373
|
+
| `security_surfaces` | Per-endpoint security policy + SEC finding IDs |
|
|
374
|
+
| `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
|
|
375
|
+
| `risk_level` | `critical` \| `high` \| `medium` \| `low` |
|
|
376
|
+
|
|
377
|
+
**Event topology** — query the publisher/consumer graph for a Spring event class:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
| Field | Description |
|
|
384
|
+
|-------|-------------|
|
|
385
|
+
| `publishers` | FQNs that publish this event class |
|
|
386
|
+
| `consumers` | Listeners with TX phase metadata (`AFTER_COMMIT`, `BEFORE_COMMIT`, etc.) |
|
|
387
|
+
| `event_graph` | Publisher → event → consumer edges (BFS ≤ 2 hops) |
|
|
388
|
+
| `transaction_context` | `AFTER_COMMIT` consumers, `BEFORE_COMMIT` risks |
|
|
389
|
+
| `risk_level` | Derived from TX phase and consumer count |
|
|
390
|
+
|
|
391
|
+
**Limitations of event topology:**
|
|
392
|
+
- Resolves Spring `ApplicationEvent` / `@EventListener` chains only
|
|
393
|
+
- Does not trace Kafka, RabbitMQ, Redis, or other message brokers
|
|
394
|
+
- Does not detect self-invocation proxy bypass
|
|
395
|
+
- Conditional beans (`@ConditionalOnProperty`) are not evaluated at analysis time
|
|
396
|
+
|
|
397
|
+
### `cold-start` — RIS bootstrap context
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
sourcecode cold-start /path/to/repo
|
|
401
|
+
sourcecode cold-start /path/to/repo --compact # ~10K token subset
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Returns the Repository Intelligence Snapshot (RIS) instantly — zero re-analysis. The RIS is built by a prior warm cache pass and includes stacks, entry points, endpoint surface, and Spring semantic signals. Status field: `cold_start_ready` | `cold_start_stale` | `no_ris`.
|
|
405
|
+
|
|
406
|
+
Use `--compact` to get a ~10K token subset safe for direct LLM injection. Full snapshot can exceed 100K tokens on medium repos — use `--output FILE` for local search tooling.
|
|
407
|
+
|
|
315
408
|
### `repo-ir` — symbol-level IR
|
|
316
409
|
|
|
317
410
|
```bash
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=HvT-OD6BpIxm8uMM8dF8IJUh4mkn6RKbB0__ghb_vHI,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=qh749a7ykPtGmQI1MR9y6j8TtL_jBdVYFx9YRsLqOMw,44121
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=_btmeOJIe3t-NicF94D5ZAesa2YIJ0_QNExGnbHxGFE,50578
|
|
6
6
|
sourcecode/cache.py,sha256=wAyPrXN5DqiGivnMpeEuun2xHDKfBer2_oBsh6kj_vc,30447
|
|
7
7
|
sourcecode/canonical_ir.py,sha256=uwpwCnJxMh_eiIVg4cOLv7-aZthvmDFcG4azCOycLkw,24281
|
|
8
|
-
sourcecode/cir_graphs.py,sha256=
|
|
8
|
+
sourcecode/cir_graphs.py,sha256=wuRjW0MjSr9KLq03L1TzYqkIeY_phUmIbVA3FQDfHKk,8603
|
|
9
9
|
sourcecode/classifier.py,sha256=2lYoSH3vOTkXZYPU7Go2WIet1-IuNzTWVhc-ULnXtgw,8024
|
|
10
10
|
sourcecode/cli.py,sha256=UI80E2S1g6Ui300-vICvn4B7NXIuGYWE4a0FXrBa-8E,216875
|
|
11
11
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
@@ -36,7 +36,7 @@ sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,
|
|
|
36
36
|
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
37
37
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
38
38
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
39
|
-
sourcecode/repository_ir.py,sha256=
|
|
39
|
+
sourcecode/repository_ir.py,sha256=uLIewoLBg4nknI1JlI8bPo_kl9SVyT-GFQqENTeFz1M,167673
|
|
40
40
|
sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
|
|
41
41
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
42
42
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -45,11 +45,11 @@ sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGp
|
|
|
45
45
|
sourcecode/serializer.py,sha256=ooNZW2_fqx__BXII25eAWq-BomodvqQ6opUT_niQYCA,123403
|
|
46
46
|
sourcecode/spring_event_topology.py,sha256=LvGv5RXtU_O-fVB_OO9eDD2UmZM72Jn2oUHgOo50Qm0,17157
|
|
47
47
|
sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
|
|
48
|
-
sourcecode/spring_impact.py,sha256
|
|
48
|
+
sourcecode/spring_impact.py,sha256=-ET4oB9tZQYfcyhfI781mMJCU-przz6x4Ejwr3hejA8,31743
|
|
49
49
|
sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
|
|
50
50
|
sourcecode/spring_security_audit.py,sha256=RPr491FGAmWNxqe-uN0FS2gmjQ0M5ryRyXjLNMMzKFk,20077
|
|
51
51
|
sourcecode/spring_semantic.py,sha256=CiAf77p48-RFrUF0zbgww4w2Xigrbo1t5M3ZCDIfV_g,12032
|
|
52
|
-
sourcecode/spring_tx_analyzer.py,sha256=
|
|
52
|
+
sourcecode/spring_tx_analyzer.py,sha256=eBcYLRKhlUllHl195CVGNWeg2vMext4u1Tezu_Mwrdg,27143
|
|
53
53
|
sourcecode/summarizer.py,sha256=YspHEVeYJVmltq0FMtGZF8kIP3qiR2KLcanGL6Y7uTI,20747
|
|
54
54
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
55
55
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -90,8 +90,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
90
90
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
91
91
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
92
92
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
93
|
-
sourcecode-1.35.
|
|
94
|
-
sourcecode-1.35.
|
|
95
|
-
sourcecode-1.35.
|
|
96
|
-
sourcecode-1.35.
|
|
97
|
-
sourcecode-1.35.
|
|
93
|
+
sourcecode-1.35.4.dist-info/METADATA,sha256=Pl9s6-m8pSLDCwawxAneFDVHpQvJLxehMZvruYMWnr0,21263
|
|
94
|
+
sourcecode-1.35.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
95
|
+
sourcecode-1.35.4.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
96
|
+
sourcecode-1.35.4.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
97
|
+
sourcecode-1.35.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|