sourcecode 1.35.5__tar.gz → 1.35.6__tar.gz
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-1.35.5 → sourcecode-1.35.6}/PKG-INFO +1 -1
- {sourcecode-1.35.5 → sourcecode-1.35.6}/pyproject.toml +1 -1
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/repository_ir.py +26 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/spring_impact.py +34 -9
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/spring_tx_analyzer.py +77 -17
- {sourcecode-1.35.5 → sourcecode-1.35.6}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/.gitignore +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/.ruff.toml +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/CHANGELOG.md +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/LICENSE +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/README.md +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/SECURITY.md +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/raw +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/license.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.5 → sourcecode-1.35.6}/src/sourcecode/workspace.py +0 -0
|
@@ -323,6 +323,11 @@ _SPRING_OTHER: frozenset[str] = frozenset({
|
|
|
323
323
|
|
|
324
324
|
_PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
|
|
325
325
|
|
|
326
|
+
# Two-step publish: SomeEvent var = new SomeEvent(...); publisher.publishEvent(var)
|
|
327
|
+
# Used when event is created before passing to publishEvent (common pattern).
|
|
328
|
+
_PUBLISH_EVENT_CALL_RE = re.compile(r'\.publishEvent\s*\(')
|
|
329
|
+
_NEW_EVENT_INSTANTIATION_RE = re.compile(r'\bnew\s+(\w+Event)\s*[\({]')
|
|
330
|
+
|
|
326
331
|
# Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
|
|
327
332
|
_FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
|
|
328
333
|
|
|
@@ -1195,6 +1200,27 @@ def _build_relations(
|
|
|
1195
1200
|
evidence={"type": "method_call", "value": f"publishEvent(new {event_simple})"},
|
|
1196
1201
|
))
|
|
1197
1202
|
|
|
1203
|
+
# Two-step publish: `SomeEvent var = new SomeEvent(...); publisher.publishEvent(var)`.
|
|
1204
|
+
# The inline regex above only catches publishEvent(new Evt(...)). Many Spring services
|
|
1205
|
+
# instantiate the event first and then pass the variable. When the source contains
|
|
1206
|
+
# publishEvent( (any form) we also scan for new XxxEvent instantiations that the inline
|
|
1207
|
+
# regex would have missed (i.e. not already emitted), using confidence=low.
|
|
1208
|
+
if _PUBLISH_EVENT_CALL_RE.search(_source_no_comments):
|
|
1209
|
+
inline_matched: set[str] = {m.group(1) for m in _PUBLISH_EVENT_RE.finditer(_source_no_comments)}
|
|
1210
|
+
for m in _NEW_EVENT_INSTANTIATION_RE.finditer(_source_no_comments):
|
|
1211
|
+
event_simple = m.group(1)
|
|
1212
|
+
if event_simple in inline_matched:
|
|
1213
|
+
continue # already captured by inline regex at higher confidence
|
|
1214
|
+
event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
|
|
1215
|
+
for cls_sym in _class_syms:
|
|
1216
|
+
edges.append(RelationEdge(
|
|
1217
|
+
from_symbol=cls_sym.symbol,
|
|
1218
|
+
to_symbol=event_fqn,
|
|
1219
|
+
type="publishes_event",
|
|
1220
|
+
confidence="low",
|
|
1221
|
+
evidence={"type": "method_call", "value": f"publishEvent(var) + new {event_simple}"},
|
|
1222
|
+
))
|
|
1223
|
+
|
|
1198
1224
|
# Keycloak SPI: XxxEvent.fire(...) static dispatch → publishes_event.
|
|
1199
1225
|
for m in _FIRE_EVENT_RE.finditer(_source_no_comments):
|
|
1200
1226
|
event_simple = m.group(1)
|
|
@@ -244,22 +244,48 @@ def _bfs_callers(
|
|
|
244
244
|
X#fieldName), the containing class X is also added to the caller set and BFS
|
|
245
245
|
continues from X. This resolves the DI traversal gap where contained_in edges
|
|
246
246
|
(which are skipped) were the only path from a field node back to its class.
|
|
247
|
+
|
|
248
|
+
BUG-004 fix: when BFS reaches a class-level node (no '#'), callers of that
|
|
249
|
+
class live on method-level keys (e.g. 'Foo#doWork') rather than the class key
|
|
250
|
+
('Foo'). A class→method-key index is built upfront so the BFS also traverses
|
|
251
|
+
method-level entries when processing a class-level FQN.
|
|
247
252
|
"""
|
|
248
253
|
visited: set[str] = set(seed_fqns)
|
|
249
254
|
direct: list[str] = []
|
|
250
255
|
indirect: list[str] = []
|
|
251
256
|
was_truncated = False
|
|
252
257
|
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
|
|
258
|
+
# BUG-004: index class FQN → list of method-level keys in reverse_graph.
|
|
259
|
+
# Callers of Foo#doWork are stored under reverse_graph["Foo#doWork"], never
|
|
260
|
+
# under reverse_graph["Foo"]. Without this index, BFS silently terminates
|
|
261
|
+
# whenever a class-level node is enqueued (e.g. via CH-002 expansion).
|
|
262
|
+
class_method_index: dict[str, list[str]] = {}
|
|
263
|
+
for rg_key in reverse_graph:
|
|
264
|
+
if "#" in rg_key:
|
|
265
|
+
cls = rg_key.split("#")[0]
|
|
266
|
+
class_method_index.setdefault(cls, []).append(rg_key)
|
|
267
|
+
|
|
268
|
+
def _edges_for(fqn: str) -> list[tuple[str, list[str]]]:
|
|
269
|
+
"""Return all (etype, fqn_list) pairs from reverse_graph for fqn.
|
|
270
|
+
For class-level FQNs also includes method-level entries (BUG-004)."""
|
|
271
|
+
edges: list[tuple[str, list[str]]] = list((reverse_graph.get(fqn) or {}).items())
|
|
272
|
+
if "#" not in fqn:
|
|
273
|
+
for mk in class_method_index.get(fqn, []):
|
|
274
|
+
edges.extend((reverse_graph.get(mk) or {}).items())
|
|
275
|
+
return edges
|
|
276
|
+
|
|
277
|
+
# Hub-class guard: cap depth to 1 when the UNIQUE direct caller set exceeds
|
|
278
|
+
# _BFS_CALLER_CAP, to avoid O(n^depth) BFS explosion on high-fanout seeds.
|
|
279
|
+
# Uses unique callers (not raw sum per seed) so that interface-expansion seeds
|
|
280
|
+
# (which add many method-level FQNs sharing the same callers) don't trigger the
|
|
281
|
+
# guard prematurely — a 36-method interface still has ~20 unique calling classes.
|
|
282
|
+
unique_direct_callers: set[str] = set()
|
|
256
283
|
for seed in seed_fqns:
|
|
257
|
-
|
|
258
|
-
for etype, fqn_list in entry.items():
|
|
284
|
+
for etype, fqn_list in _edges_for(seed):
|
|
259
285
|
if etype not in _SKIP_EDGE_TYPES:
|
|
260
|
-
|
|
286
|
+
unique_direct_callers.update(fqn_list)
|
|
261
287
|
|
|
262
|
-
effective_depth = 1 if
|
|
288
|
+
effective_depth = 1 if len(unique_direct_callers) > _BFS_CALLER_CAP else max_depth
|
|
263
289
|
if effective_depth < max_depth:
|
|
264
290
|
was_truncated = True
|
|
265
291
|
|
|
@@ -281,8 +307,7 @@ def _bfs_callers(
|
|
|
281
307
|
fqn, depth = queue.pop(0)
|
|
282
308
|
if depth >= effective_depth:
|
|
283
309
|
continue
|
|
284
|
-
|
|
285
|
-
for etype, fqn_list in entry.items():
|
|
310
|
+
for etype, fqn_list in _edges_for(fqn):
|
|
286
311
|
if etype in _SKIP_EDGE_TYPES:
|
|
287
312
|
continue
|
|
288
313
|
for caller in fqn_list:
|
|
@@ -52,15 +52,16 @@ _WRITE_METHOD_RE = re.compile(
|
|
|
52
52
|
re.IGNORECASE,
|
|
53
53
|
)
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
re.DOTALL,
|
|
62
|
-
)
|
|
55
|
+
# TX-005 catch-block detection helpers.
|
|
56
|
+
# _CATCH_SWALLOW_RE is retained for reference but replaced by brace-counting
|
|
57
|
+
# extraction in _has_swallowed_exception to avoid false positives from nested
|
|
58
|
+
# braces (nested if/try blocks inside catch terminating the match prematurely).
|
|
59
|
+
_CATCH_HEADER_RE = re.compile(r'\bcatch\s*\([^)]+\)\s*\{')
|
|
60
|
+
_LOG_IN_CATCH_RE = re.compile(r'\b(?:log|logger|LOG|System\.out|e\.print)\b')
|
|
63
61
|
_RETHROW_IN_CATCH_RE = re.compile(r'\bthrow\b')
|
|
62
|
+
# Non-trivial return (method call) inside a catch block indicates recovery, not
|
|
63
|
+
# silent swallowing — e.g. `return findNextId(idType)` after creating a missing row.
|
|
64
|
+
_RECOVERY_RETURN_RE = re.compile(r'\breturn\s+\w[\w.<>]*\s*\(')
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def _extract_method_body(source: str, method_name: str) -> str:
|
|
@@ -507,6 +508,10 @@ class _TX005ExceptionSwallowing:
|
|
|
507
508
|
continue
|
|
508
509
|
if not boundary.source_file:
|
|
509
510
|
continue
|
|
511
|
+
# readOnly=true transactions do not write data — swallowed exceptions
|
|
512
|
+
# cannot cause dirty commits, so TX-005 is not applicable.
|
|
513
|
+
if boundary.read_only:
|
|
514
|
+
continue
|
|
510
515
|
|
|
511
516
|
abs_path = root / boundary.source_file
|
|
512
517
|
try:
|
|
@@ -555,21 +560,76 @@ class _TX005ExceptionSwallowing:
|
|
|
555
560
|
return findings
|
|
556
561
|
|
|
557
562
|
def _has_swallowed_exception(self, source: str, symbol: str) -> bool:
|
|
558
|
-
"""Return True if
|
|
563
|
+
"""Return True if a @Transactional overload of method_name has a catch-log-no-rethrow pattern.
|
|
559
564
|
|
|
560
|
-
|
|
561
|
-
|
|
565
|
+
Guards against two false-positive sources:
|
|
566
|
+
1. Overloaded methods — only checks the overload whose declaration is immediately
|
|
567
|
+
preceded by @Transactional (preamble anchored to last '}').
|
|
568
|
+
2. Call sites — skips any match where ';' appears before the next '{', which
|
|
569
|
+
is true for call expressions (e.g. dao.method(arg);) but not definitions.
|
|
562
570
|
"""
|
|
563
571
|
method_name = symbol.split("#")[-1] if "#" in symbol else symbol.rsplit(".", 1)[-1]
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
572
|
+
pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
|
|
573
|
+
for m in pattern.finditer(source):
|
|
574
|
+
# Preamble: text between the previous closing brace and this method name.
|
|
575
|
+
# Anchoring to the last '}' prevents @Transactional from a prior method
|
|
576
|
+
# from leaking into this overload's preamble.
|
|
577
|
+
prev_brace = source.rfind('}', 0, m.start())
|
|
578
|
+
preamble_start = prev_brace + 1 if prev_brace >= 0 else 0
|
|
579
|
+
preamble = source[preamble_start:m.start()]
|
|
580
|
+
if '@Transactional' not in preamble:
|
|
581
|
+
continue
|
|
582
|
+
# Guard: if ';' comes before '{', this is a call site, not a definition.
|
|
583
|
+
brace_pos = source.find('{', m.end())
|
|
584
|
+
semi_pos = source.find(';', m.end())
|
|
585
|
+
if brace_pos < 0:
|
|
586
|
+
continue
|
|
587
|
+
if semi_pos >= 0 and semi_pos < brace_pos:
|
|
588
|
+
continue
|
|
589
|
+
# Extract this overload's body
|
|
590
|
+
depth = 1
|
|
591
|
+
i = brace_pos + 1
|
|
592
|
+
while i < len(source) and depth > 0:
|
|
593
|
+
c = source[i]
|
|
594
|
+
if c == '{':
|
|
595
|
+
depth += 1
|
|
596
|
+
elif c == '}':
|
|
597
|
+
depth -= 1
|
|
598
|
+
i += 1
|
|
599
|
+
body = source[brace_pos:i]
|
|
600
|
+
for catch_block in self._extract_catch_blocks(body):
|
|
601
|
+
if not _LOG_IN_CATCH_RE.search(catch_block):
|
|
602
|
+
continue
|
|
603
|
+
if _RETHROW_IN_CATCH_RE.search(catch_block):
|
|
604
|
+
continue
|
|
605
|
+
# Non-trivial return (method call) indicates recovery, not swallowing.
|
|
606
|
+
if _RECOVERY_RETURN_RE.search(catch_block):
|
|
607
|
+
continue
|
|
570
608
|
return True
|
|
571
609
|
return False
|
|
572
610
|
|
|
611
|
+
@staticmethod
|
|
612
|
+
def _extract_catch_blocks(body: str) -> list[str]:
|
|
613
|
+
"""Extract full catch block bodies from a method body using brace counting.
|
|
614
|
+
|
|
615
|
+
Handles nested braces correctly — unlike a simple [^}]* regex which
|
|
616
|
+
terminates at the first nested '}' inside the catch block.
|
|
617
|
+
"""
|
|
618
|
+
blocks: list[str] = []
|
|
619
|
+
for m in _CATCH_HEADER_RE.finditer(body):
|
|
620
|
+
brace_pos = m.end() - 1
|
|
621
|
+
depth = 1
|
|
622
|
+
i = brace_pos + 1
|
|
623
|
+
while i < len(body) and depth > 0:
|
|
624
|
+
c = body[i]
|
|
625
|
+
if c == '{':
|
|
626
|
+
depth += 1
|
|
627
|
+
elif c == '}':
|
|
628
|
+
depth -= 1
|
|
629
|
+
i += 1
|
|
630
|
+
blocks.append(body[brace_pos:i])
|
|
631
|
+
return blocks
|
|
632
|
+
|
|
573
633
|
|
|
574
634
|
# ---------------------------------------------------------------------------
|
|
575
635
|
# Default pattern registry
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|