sourcecode 1.35.5__py3-none-any.whl → 1.35.6__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.5"
3
+ __version__ = "1.35.6"
@@ -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
- # Hub-class guard: if seeds have > _BFS_CALLER_CAP direct callers combined,
254
- # cap effective depth to 1 to avoid O(n^depth) explosion.
255
- total_direct_count = 0
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
- entry = reverse_graph.get(seed) or {}
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
- total_direct_count += len(fqn_list)
286
+ unique_direct_callers.update(fqn_list)
261
287
 
262
- effective_depth = 1 if total_direct_count > _BFS_CALLER_CAP else max_depth
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
- entry = reverse_graph.get(fqn) or {}
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
- # Exception swallowing pattern for TX-005:
56
- # catch block that contains a log/print but no throw/rethrow
57
- _CATCH_SWALLOW_RE = re.compile(
58
- r'catch\s*\([^)]+\)\s*\{[^}]*' # catch(...) {
59
- r'(?:log|logger|LOG|System\.out|e\.print)' # logging call
60
- r'[^}]*\}', # closing brace (no throw inside)
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 the specific method body has a catch-log-no-rethrow pattern.
563
+ """Return True if a @Transactional overload of method_name has a catch-log-no-rethrow pattern.
559
564
 
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.
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
- body = _extract_method_body(source, method_name)
565
- if not body:
566
- return False
567
- for match in _CATCH_SWALLOW_RE.finditer(body):
568
- block = match.group(0)
569
- if not _RETHROW_IN_CATCH_RE.search(block):
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.5
3
+ Version: 1.35.6
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
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=QCdv6kJhqRAzIEK9ewWpwfq2rcWG3YpzJnjAJDMG_pc,103
1
+ sourcecode/__init__.py,sha256=TcLh06FmiHFO9CNvHqSENDrpMzVidT6Co88A6deoVv8,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
@@ -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=uLIewoLBg4nknI1JlI8bPo_kl9SVyT-GFQqENTeFz1M,167673
39
+ sourcecode/repository_ir.py,sha256=SOACZ4viYG8tpmL_-l4XqoX6bN4GmCeX5ZIr9tnuQyA,169298
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=-ET4oB9tZQYfcyhfI781mMJCU-przz6x4Ejwr3hejA8,31743
48
+ sourcecode/spring_impact.py,sha256=vb_cOk9dFU7YcGPeqivFcpS4OYSETT72oJYn4WC5al0,33266
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=eBcYLRKhlUllHl195CVGNWeg2vMext4u1Tezu_Mwrdg,27143
52
+ sourcecode/spring_tx_analyzer.py,sha256=ZI_sG0oQi_iQAbm0hwIzjwGPMV2oS0RSsz2dGYCz__k,30103
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.5.dist-info/METADATA,sha256=ZrHgEgvVfhprWb98X8P4rB5PUJZ7Av4uDsrHfzVgovU,21263
94
- sourcecode-1.35.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
95
- sourcecode-1.35.5.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
96
- sourcecode-1.35.5.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
97
- sourcecode-1.35.5.dist-info/RECORD,,
93
+ sourcecode-1.35.6.dist-info/METADATA,sha256=yC_5VR0K7bAsP10Dli2yOR3RAPA8ylm3slY150Z9O6g,21263
94
+ sourcecode-1.35.6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
95
+ sourcecode-1.35.6.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
96
+ sourcecode-1.35.6.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
97
+ sourcecode-1.35.6.dist-info/RECORD,,