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.
- sourcecode/__init__.py +1 -1
- sourcecode/cli.py +152 -3
- sourcecode/mcp/registry.py +49 -1
- sourcecode/parse_cache.py +138 -0
- sourcecode/repository_ir.py +61 -12
- sourcecode/spring_impact.py +1 -1
- sourcecode/spring_tx_analyzer.py +141 -4
- sourcecode/verify_edit.py +588 -0
- sourcecode/verify_rules.py +304 -0
- {sourcecode-2.6.2.dist-info → sourcecode-2.6.5.dist-info}/METADATA +1 -1
- {sourcecode-2.6.2.dist-info → sourcecode-2.6.5.dist-info}/RECORD +14 -11
- {sourcecode-2.6.2.dist-info → sourcecode-2.6.5.dist-info}/WHEEL +0 -0
- {sourcecode-2.6.2.dist-info → sourcecode-2.6.5.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.6.2.dist-info → sourcecode-2.6.5.dist-info}/licenses/LICENSE +0 -0
sourcecode/spring_tx_analyzer.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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", []):
|