sourcecode 2.6.2__py3-none-any.whl → 2.6.5__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.

Potentially problematic release.


This version of sourcecode might be problematic. Click here for more details.

@@ -6,9 +6,8 @@ Patterns implemented (Phase 2):
6
6
  TX-003 readOnly=true boundary propagating into write-capable callee
7
7
  TX-004 NOT_SUPPORTED or NEVER invoked from transactional call chain
8
8
  TX-005 Exception swallowing inside @Transactional method (regex)
9
-
10
- Self-invocation via this.method() intentionally excluded from Phase 2:
11
- requires AST-level analysis, regex produces too many false positives.
9
+ TX-006 Self-invocation of a @Transactional sibling — proxy bypass (Semantic-IR
10
+ invocation atoms; unambiguous same-class calls only)
12
11
 
13
12
  All patterns are deterministic and never raise.
14
13
  """
@@ -683,6 +682,142 @@ class _TX005ExceptionSwallowing:
683
682
  return blocks
684
683
 
685
684
 
685
+ # ---------------------------------------------------------------------------
686
+ # TX-006: self-invocation of a @Transactional sibling — proxy bypass
687
+ # ---------------------------------------------------------------------------
688
+
689
+ class _TX006SelfInvocation:
690
+ """Same-instance call to a @Transactional sibling method bypasses the proxy.
691
+
692
+ Spring TX advice is applied by a proxy that wraps the bean. A call that does
693
+ NOT go through that proxy — an unqualified `foo()` or an explicit `this.foo()`
694
+ to a method of the SAME class — reaches the target directly, so its
695
+ method-level @Transactional is silently ignored at runtime (Sonar S2229).
696
+
697
+ Detection is structural and VAI-clean, reading the Semantic-IR invocation
698
+ atoms already produced by build_canonical_ir (cir.body_facts):
699
+
700
+ - the atom's receiver is None (unqualified) or "this" → not via a proxy
701
+ - resolved_fqn is set → unambiguous sibling
702
+ (the IR only resolves a sibling when receiver∈{None,"this"} and exactly one
703
+ same-class method matches — overloaded/ambiguous targets stay None → skipped)
704
+ - the resolved sibling carries a METHOD-LEVEL @Transactional → advice would apply
705
+
706
+ Class-level-only @Transactional is deliberately NOT flagged: the enclosing
707
+ proxy already applies it to the outer entry method, so the self-call runs in
708
+ that same transaction — no advice is lost. The self-injection workaround
709
+ (`self.foo()` where `self` is an injected same-type field) uses a named field
710
+ receiver, never None/"this", so it never resolves as a sibling → no false
711
+ positive on the legitimate escape hatch.
712
+ """
713
+ pattern_id = "TX-006"
714
+ severity = "high" # nominal (Protocol); real severity is tiered per finding
715
+
716
+ # Propagations whose whole purpose is a distinct transaction boundary: a bypass
717
+ # demonstrably breaks them (a new/suspended/forbidden TX that never happens, or a
718
+ # runtime IllegalTransactionStateException). Everything else — a plain REQUIRED/
719
+ # SUPPORTS callee, or a readOnly getter (bypass loses only the read-only
720
+ # optimization, not correctness) — degrades to running in the caller's TX: a real
721
+ # bypass with lower blast radius → medium, matching TX-002/003 discipline.
722
+ _HIGH_IMPACT_PROPAGATION = frozenset({
723
+ "REQUIRES_NEW", "NESTED", "NOT_SUPPORTED", "NEVER", "MANDATORY",
724
+ })
725
+
726
+ def analyze(
727
+ self,
728
+ cir: "CanonicalRepositoryIR",
729
+ tx_index: TransactionBoundaryIndex,
730
+ root: Optional[Path],
731
+ *,
732
+ model: Optional[SpringSemanticModel] = None,
733
+ ) -> list[SpringFinding]:
734
+ findings: list[SpringFinding] = []
735
+ body_facts = getattr(cir, "body_facts", {}) or {}
736
+ _seen_ids: set[str] = set()
737
+
738
+ for caller_fqn, atoms in body_facts.items():
739
+ if not isinstance(atoms, list):
740
+ continue
741
+ for atom in atoms:
742
+ if not isinstance(atom, dict) or atom.get("fact") != "invocation":
743
+ continue
744
+ # Only same-instance calls bypass the proxy: unqualified or `this.`.
745
+ if atom.get("receiver") not in (None, "this"):
746
+ continue
747
+ target = atom.get("resolved_fqn")
748
+ if not target:
749
+ continue # ambiguous/overloaded/unresolved sibling — honest skip
750
+ # The bypass only matters when the sibling declares its OWN
751
+ # method-level @Transactional; class-level advice already covers
752
+ # the outer entry method and is not lost by the self-call.
753
+ tb = tx_index.by_symbol.get(target)
754
+ if tb is None or tb.scope != "method":
755
+ continue
756
+
757
+ _fid = SpringFinding.make_id(self.pattern_id, f"{caller_fqn}→{target}")
758
+ if _fid in _seen_ids:
759
+ continue
760
+ _seen_ids.add(_fid)
761
+
762
+ caller_simple = caller_fqn.rsplit(".", 1)[-1].replace("#", ".")
763
+ callee_simple = target.rsplit(".", 1)[-1].replace("#", ".")
764
+
765
+ # Tier severity by what the bypass demonstrably breaks. A propagation
766
+ # that mandates a distinct boundary (or forbids an active TX) is
767
+ # high; a plain REQUIRED/SUPPORTS/readOnly callee degrades to the
768
+ # caller's TX — still a real bypass, lower blast radius → medium.
769
+ if tb.propagation in self._HIGH_IMPACT_PROPAGATION:
770
+ sev = "high"
771
+ else:
772
+ sev = "medium"
773
+
774
+ findings.append(SpringFinding(
775
+ id=_fid,
776
+ pattern_id=self.pattern_id,
777
+ category="tx",
778
+ severity=sev,
779
+ confidence="high",
780
+ title="Self-invocation of @Transactional method — Spring proxy bypass",
781
+ symbol=target,
782
+ source_file=tb.source_file,
783
+ evidence={
784
+ "caller": caller_fqn,
785
+ "callee": target,
786
+ "receiver": atom.get("receiver") or "(unqualified)",
787
+ "annotation": "@Transactional",
788
+ "callee_propagation": tb.propagation,
789
+ "callee_read_only": tb.read_only,
790
+ "proxy_mechanism": "proxy self-invocation",
791
+ "reason": (
792
+ "same-instance call does not pass through the Spring proxy"
793
+ ),
794
+ },
795
+ explanation=(
796
+ f"{caller_simple} calls sibling {callee_simple} on the same "
797
+ "instance (unqualified / this.). The call does not pass through "
798
+ f"the Spring proxy, so the @Transactional on {callee_simple} "
799
+ f"(propagation={tb.propagation}"
800
+ f"{', readOnly=true' if tb.read_only else ''}) is silently "
801
+ "ignored — it executes in the caller's transaction context "
802
+ "(or none at all), not the one its annotation declares. "
803
+ "No new/independent transaction, propagation, or rollback boundary is applied."
804
+ ),
805
+ fix_hint=(
806
+ "Move the @Transactional method to a separate Spring bean and "
807
+ "call it through the injected dependency, or inject the bean into "
808
+ "itself and call via the injected proxy reference, or use "
809
+ "TransactionTemplate for programmatic transaction control."
810
+ ),
811
+ limitations=[
812
+ "Only unambiguous sibling calls are analyzed — overloaded targets "
813
+ "are reported as unresolved and skipped",
814
+ ],
815
+ related_symbols=[target.split("#")[0]],
816
+ ))
817
+
818
+ return findings
819
+
820
+
686
821
  # ---------------------------------------------------------------------------
687
822
  # Default pattern registry
688
823
  # ---------------------------------------------------------------------------
@@ -693,6 +828,7 @@ _DEFAULT_TX_PATTERNS: list[TxPattern] = [
693
828
  _TX003ReadOnlyWritePropagation(), # type: ignore[list-item]
694
829
  _TX004TxSuspensionRisk(), # type: ignore[list-item]
695
830
  _TX005ExceptionSwallowing(), # type: ignore[list-item]
831
+ _TX006SelfInvocation(), # type: ignore[list-item]
696
832
  ]
697
833
 
698
834
 
@@ -777,7 +913,8 @@ def run_tx_audit(
777
913
  _spring_detected = tx_index.stats()["total"] > 0 or model.bean_graph.has_spring_beans()
778
914
 
779
915
  _tx_limitations = [
780
- "Self-invocation via this.method() not detected — requires AST-level analysis",
916
+ "Self-invocation (this.method()) detected for unambiguous same-class calls "
917
+ "(TX-006); overloaded targets are reported as unresolved and skipped",
781
918
  "Dynamic dispatch (interface/polymorphic calls) may produce incomplete call chains",
782
919
  ]
783
920
  for _err in getattr(engine, "_last_analysis_errors", []):