sourcecode 1.63.0__py3-none-any.whl → 1.65.0__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/hibernate_strat.py +296 -15
- sourcecode/integration_detector.py +31 -0
- sourcecode/migrate_check.py +334 -58
- sourcecode/path_filters.py +33 -0
- sourcecode/repository_ir.py +70 -0
- {sourcecode-1.63.0.dist-info → sourcecode-1.65.0.dist-info}/METADATA +52 -4
- {sourcecode-1.63.0.dist-info → sourcecode-1.65.0.dist-info}/RECORD +11 -11
- {sourcecode-1.63.0.dist-info → sourcecode-1.65.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.63.0.dist-info → sourcecode-1.65.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.63.0.dist-info → sourcecode-1.65.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/hibernate_strat.py
CHANGED
|
@@ -36,7 +36,7 @@ from typing import Optional
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# Sub-schema version for the `hibernate` output section. Bump on shape changes.
|
|
39
|
-
HIBERNATE_SCHEMA_VERSION = "2.
|
|
39
|
+
HIBERNATE_SCHEMA_VERSION = "2.1"
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
# ---------------------------------------------------------------------------
|
|
@@ -107,14 +107,24 @@ _JPA_DEPRECATED_RE = re.compile(
|
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
# Layer 2 — JPA Criteria API + legacy Hibernate Criteria.
|
|
110
|
+
# BUG #1: dropped over-generic tokens (\bPredicate\b, \bRoot\s*<, \bCriterion\b,
|
|
111
|
+
# \bConjunction\b, \bDisjunction\b) that collide with project-owned domain classes
|
|
112
|
+
# (e.g. Alfresco's own query model has Predicate / Conjunction / Disjunction /
|
|
113
|
+
# Criterion classes with NO Hibernate dependency). Real JPA Criteria code always
|
|
114
|
+
# carries CriteriaBuilder / CriteriaQuery, so detection is preserved via those.
|
|
110
115
|
_CRITERIA_JPA_RE = re.compile(
|
|
111
116
|
r"\bCriteriaBuilder\b|\bCriteriaQuery\b|\bCriteriaUpdate\b|"
|
|
112
|
-
r"\bCriteriaDelete\b
|
|
117
|
+
r"\bCriteriaDelete\b|persistence\.criteria"
|
|
113
118
|
)
|
|
119
|
+
# Legacy Hibernate Criteria. org.hibernate.Criteria / org.hibernate.criterion.*
|
|
120
|
+
# are FQN-anchored (safe). The bare-name tokens (createCriteria, Restrictions,
|
|
121
|
+
# Projections, DetachedCriteria) only count when the file actually imports
|
|
122
|
+
# org.hibernate (see _file_imports_hibernate gate) — they are otherwise common
|
|
123
|
+
# enough to collide with unrelated builder APIs.
|
|
114
124
|
_CRITERIA_LEGACY_RE = re.compile(
|
|
115
|
-
r"org\.hibernate\.Criteria\b
|
|
116
|
-
r"\
|
|
117
|
-
r"\
|
|
125
|
+
r"org\.hibernate\.Criteria\b|org\.hibernate\.criterion\b|"
|
|
126
|
+
r"\.createCriteria\s*\(|\bDetachedCriteria\b|"
|
|
127
|
+
r"\bRestrictions\.|\bProjections\."
|
|
118
128
|
)
|
|
119
129
|
|
|
120
130
|
# Layer 3 — HQL / native string queries. createQuery requires a string literal so
|
|
@@ -131,20 +141,187 @@ _HQL_CONCAT_RE = re.compile(
|
|
|
131
141
|
)
|
|
132
142
|
|
|
133
143
|
# Layer 4 — Hibernate SPI / internal API (CRITICAL blocker).
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
144
|
+
# BUG #1: split into FQN-anchored matches (proof of Hibernate origin — always
|
|
145
|
+
# counted) and bare-name matches (e.g. `implements XInterceptor`, `extends
|
|
146
|
+
# XEventListener`) that collide with project-owned AOP interceptors / event
|
|
147
|
+
# listeners. Bare-name matches only count when the file actually imports
|
|
148
|
+
# org.hibernate (see _file_imports_hibernate gate).
|
|
149
|
+
_SPI_FQN_RE = re.compile(
|
|
137
150
|
r"\borg\.hibernate\.(?:type|engine|internal|persister|metamodel|"
|
|
138
|
-
r"boot\.spi|boot\.internal|event|tuple|property\.access|loader|sql\.ast
|
|
151
|
+
r"boot\.spi|boot\.internal|event|tuple|property\.access|loader|sql\.ast|"
|
|
152
|
+
r"usertype)\b|"
|
|
139
153
|
r"\bEmptyInterceptor\b|"
|
|
154
|
+
r"\bSessionFactoryImpl\b|\bSessionImplementor\b|"
|
|
155
|
+
r"\bSharedSessionContractImplementor\b"
|
|
156
|
+
)
|
|
157
|
+
_SPI_SIMPLE_RE = re.compile(
|
|
158
|
+
r"\b(?:implements|extends)\s+\w*(?:UserType|CompositeUserType|UserCollectionType)\b|"
|
|
140
159
|
r"\bimplements\s+\w*Interceptor\b|"
|
|
141
160
|
r"\b(?:implements|extends)\s+\w*EventListener\b|"
|
|
142
|
-
r"\bSessionFactoryImpl\b|\bSessionImplementor\b|"
|
|
143
|
-
r"\bSharedSessionContractImplementor\b|"
|
|
144
161
|
r"\bsetPropertyAccessStrategy\b|\bPropertyAccessStrategy\b|"
|
|
145
162
|
r"\bImplicitNamingStrategy\b|\bPhysicalNamingStrategy\b"
|
|
146
163
|
)
|
|
147
164
|
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# BUG #1 — comment/string stripping + real-import & dependency evidence
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
#
|
|
169
|
+
# A Hibernate 5→6 verdict is a high-cost blocker. It must be PROVEN from resolved
|
|
170
|
+
# AST signals, never from a substring inside a comment, Javadoc, string literal,
|
|
171
|
+
# or classpath resource path. Two guards enforce this:
|
|
172
|
+
# 1. _strip_comments_strings() — all pattern scanning runs on code with comments
|
|
173
|
+
# and string/char literal CONTENT removed (quotes/newlines preserved so line
|
|
174
|
+
# numbers and call-shape tokens survive).
|
|
175
|
+
# 2. _collect_hibernate_evidence() — detection only proceeds when there is real
|
|
176
|
+
# evidence: an org.hibernate:* build dependency, a parsed `import org.hibernate.*`,
|
|
177
|
+
# or a parsed `import {jakarta,javax}.persistence.*` (JPA provider in play).
|
|
178
|
+
|
|
179
|
+
_JAVA_TOKEN_RE = re.compile(
|
|
180
|
+
r"(//[^\n]*)" # line comment
|
|
181
|
+
r"|(/\*.*?\*/)" # block comment
|
|
182
|
+
r"|(\"(?:\\.|[^\"\\\n])*\")" # string literal
|
|
183
|
+
r"|('(?:\\.|[^'\\\n])*')", # char literal
|
|
184
|
+
re.DOTALL,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _strip_comments_strings(src: str) -> str:
|
|
189
|
+
"""Remove comment and string/char-literal CONTENT, preserving line count.
|
|
190
|
+
|
|
191
|
+
Line comments are blanked in place (the trailing newline is outside the match,
|
|
192
|
+
so line numbers are unaffected). Block comments are replaced by an equal number
|
|
193
|
+
of newlines. String/char literals collapse to empty `""` / `''` tokens so that
|
|
194
|
+
call-shape detection (e.g. createQuery("...")) and concatenation (`"" + x`) still
|
|
195
|
+
match, but no pattern can match a substring that lived inside a literal.
|
|
196
|
+
"""
|
|
197
|
+
def _repl(m: re.Match) -> str:
|
|
198
|
+
if m.group(1) is not None:
|
|
199
|
+
return ""
|
|
200
|
+
if m.group(2) is not None:
|
|
201
|
+
return "\n" * m.group(2).count("\n")
|
|
202
|
+
if m.group(3) is not None:
|
|
203
|
+
return '""'
|
|
204
|
+
if m.group(4) is not None:
|
|
205
|
+
return "''"
|
|
206
|
+
return m.group(0)
|
|
207
|
+
return _JAVA_TOKEN_RE.sub(_repl, src)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Real `import org.hibernate.*` — anchored to an import statement, not a substring.
|
|
211
|
+
_REAL_HIBERNATE_IMPORT_RE = re.compile(
|
|
212
|
+
r"^[ \t]*import\s+(?:static\s+)?org\.hibernate\.", re.MULTILINE
|
|
213
|
+
)
|
|
214
|
+
# Real JPA import (jakarta or javax persistence) — a JPA provider is in play.
|
|
215
|
+
_JPA_IMPORT_RE = re.compile(
|
|
216
|
+
r"^[ \t]*import\s+(?:static\s+)?(?:jakarta|javax)\.persistence\.", re.MULTILINE
|
|
217
|
+
)
|
|
218
|
+
# org.hibernate:* coordinate in a Maven/Gradle build descriptor.
|
|
219
|
+
_HIBERNATE_DEP_RE = re.compile(
|
|
220
|
+
r"<groupId>\s*org\.hibernate(?:\.orm)?\s*</groupId>"
|
|
221
|
+
r"|['\"]org\.hibernate(?:\.orm)?:",
|
|
222
|
+
re.IGNORECASE,
|
|
223
|
+
)
|
|
224
|
+
_BUILD_FILE_NAMES: tuple[str, ...] = ("pom.xml", "build.gradle", "build.gradle.kts")
|
|
225
|
+
_SKIP_BUILD_SCAN_DIRS: frozenset[str] = frozenset(
|
|
226
|
+
{"target", "build", ".git", ".gradle", "node_modules", "__pycache__", "out", "dist", "bin"}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _file_imports_hibernate(stripped_source: str) -> bool:
|
|
231
|
+
return bool(_REAL_HIBERNATE_IMPORT_RE.search(stripped_source))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _scan_build_dependency(root: Path) -> tuple[bool, list[str]]:
|
|
235
|
+
"""Return (org.hibernate dependency present, sample coordinate strings)."""
|
|
236
|
+
import os
|
|
237
|
+
samples: list[str] = []
|
|
238
|
+
found = False
|
|
239
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
240
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_BUILD_SCAN_DIRS]
|
|
241
|
+
for fname in filenames:
|
|
242
|
+
if fname not in _BUILD_FILE_NAMES:
|
|
243
|
+
continue
|
|
244
|
+
try:
|
|
245
|
+
text = (Path(dirpath) / fname).read_text(encoding="utf-8", errors="replace")
|
|
246
|
+
except OSError:
|
|
247
|
+
continue
|
|
248
|
+
text = _strip_comments_strings(text) if fname.endswith((".gradle", ".kts")) else text
|
|
249
|
+
for m in _HIBERNATE_DEP_RE.finditer(text):
|
|
250
|
+
found = True
|
|
251
|
+
snippet = m.group(0).strip()
|
|
252
|
+
if snippet not in samples and len(samples) < 5:
|
|
253
|
+
samples.append(snippet)
|
|
254
|
+
return found, samples
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ── BUG #1: effective Hibernate version resolution ─────────────────────────
|
|
258
|
+
# A 5→6 rewrite verdict is only valid when the project is actually on Hibernate 5.
|
|
259
|
+
# Resolve the effective major version from build descriptors, following Maven
|
|
260
|
+
# ${properties} (incl. BOM-managed values declared as <hibernate*.version>) and
|
|
261
|
+
# explicit hibernate-core / hibernate-orm coordinates. When the resolved major is
|
|
262
|
+
# ≥ 6 the 5→6 axis does NOT apply (the migration is already done). When it cannot
|
|
263
|
+
# be resolved the axis degrades to a low-confidence hypothesis (no headline).
|
|
264
|
+
_HIB_VER = r"(\d+\.\d+(?:\.\d+)?(?:[.\-][\w]+)*)" # 6.2.13.Final, 5.6.15, 6.2
|
|
265
|
+
_HIB_PROP_RE = re.compile(
|
|
266
|
+
r"<((?:[\w.\-]*hibernate[\w.\-]*?)\.?version)>\s*" + _HIB_VER + r"\s*</\1>",
|
|
267
|
+
re.IGNORECASE,
|
|
268
|
+
)
|
|
269
|
+
_HIB_COORD_VERSION_RE = re.compile(
|
|
270
|
+
r"<artifactId>\s*hibernate-(?:core|orm)\s*</artifactId>\s*"
|
|
271
|
+
r"(?:<[^>]+>[^<]*</[^>]+>\s*)*?<version>\s*" + _HIB_VER,
|
|
272
|
+
re.IGNORECASE | re.DOTALL,
|
|
273
|
+
)
|
|
274
|
+
_HIB_GRADLE_VERSION_RE = re.compile(
|
|
275
|
+
r"org\.hibernate(?:\.orm)?:hibernate-(?:core|orm):" + _HIB_VER,
|
|
276
|
+
re.IGNORECASE,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _ver_key(full: str) -> tuple[int, int]:
|
|
281
|
+
parts = full.split(".")
|
|
282
|
+
try:
|
|
283
|
+
major = int(parts[0])
|
|
284
|
+
except (ValueError, IndexError):
|
|
285
|
+
return (0, 0)
|
|
286
|
+
try:
|
|
287
|
+
minor = int(parts[1]) if len(parts) > 1 else 0
|
|
288
|
+
except ValueError:
|
|
289
|
+
minor = 0
|
|
290
|
+
return (major, minor)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _resolve_hibernate_version(root: Path) -> tuple[Optional[str], Optional[int], str]:
|
|
294
|
+
"""Return (full_version_string, major, confidence) for the effective Hibernate.
|
|
295
|
+
|
|
296
|
+
confidence is "high" when a version was resolved from a build descriptor,
|
|
297
|
+
"none" when none could be determined (degrade to hypothesis). The newest
|
|
298
|
+
declared Hibernate wins in a multi-module build.
|
|
299
|
+
"""
|
|
300
|
+
import os
|
|
301
|
+
best_full: Optional[str] = None
|
|
302
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
303
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_BUILD_SCAN_DIRS]
|
|
304
|
+
for fname in filenames:
|
|
305
|
+
if fname not in _BUILD_FILE_NAMES:
|
|
306
|
+
continue
|
|
307
|
+
try:
|
|
308
|
+
text = (Path(dirpath) / fname).read_text(encoding="utf-8", errors="replace")
|
|
309
|
+
except OSError:
|
|
310
|
+
continue
|
|
311
|
+
fulls: list[str] = []
|
|
312
|
+
for m in _HIB_PROP_RE.finditer(text):
|
|
313
|
+
fulls.append(m.group(2))
|
|
314
|
+
for m in _HIB_COORD_VERSION_RE.finditer(text):
|
|
315
|
+
fulls.append(m.group(1))
|
|
316
|
+
for m in _HIB_GRADLE_VERSION_RE.finditer(text):
|
|
317
|
+
fulls.append(m.group(1))
|
|
318
|
+
for full in fulls:
|
|
319
|
+
if best_full is None or _ver_key(full) > _ver_key(best_full):
|
|
320
|
+
best_full = full
|
|
321
|
+
if best_full is None:
|
|
322
|
+
return None, None, "none"
|
|
323
|
+
return best_full, _ver_key(best_full)[0], "high"
|
|
324
|
+
|
|
148
325
|
# Escalation markers — dynamic / reflection-based persistence construction.
|
|
149
326
|
_ABSTRACTION_CLASS_RE = re.compile(
|
|
150
327
|
r"\b(DynamicEntityDao|DynamicEntityDaoImpl|BasicPersistenceModule|"
|
|
@@ -294,11 +471,27 @@ class HibernateStratification:
|
|
|
294
471
|
total_effort_range_days: dict = field(default_factory=dict)
|
|
295
472
|
effort_model: dict = field(default_factory=dict)
|
|
296
473
|
findings: list[HibernateFinding] = field(default_factory=list)
|
|
474
|
+
# BUG #1: auditable proof behind the detected verdict. confidence is "high"
|
|
475
|
+
# when backed by a build dependency or a parsed org.hibernate import, "none"
|
|
476
|
+
# when detection was vetoed for lack of evidence.
|
|
477
|
+
evidence: dict = field(default_factory=dict)
|
|
478
|
+
# BUG #1: effective Hibernate version + whether the 5→6 axis applies at all.
|
|
479
|
+
# When the resolved major is ≥ 6 the migration is already done → not applicable
|
|
480
|
+
# (never a blocker). When unresolved, the axis is a low-confidence hypothesis.
|
|
481
|
+
effective_version: Optional[str] = None
|
|
482
|
+
version_major: Optional[int] = None
|
|
483
|
+
version_confidence: str = "none"
|
|
484
|
+
migration_applicable: bool = True
|
|
297
485
|
|
|
298
486
|
def to_dict(self) -> dict:
|
|
299
487
|
return {
|
|
300
488
|
"schema_version": HIBERNATE_SCHEMA_VERSION,
|
|
301
489
|
"detected": self.detected,
|
|
490
|
+
"evidence": self.evidence,
|
|
491
|
+
"effective_version": self.effective_version,
|
|
492
|
+
"version_major": self.version_major,
|
|
493
|
+
"version_confidence": self.version_confidence,
|
|
494
|
+
"migration_applicable": self.migration_applicable,
|
|
302
495
|
"classification": self.classification,
|
|
303
496
|
"classification_label": self.classification_label,
|
|
304
497
|
"stratified": True,
|
|
@@ -607,15 +800,75 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
607
800
|
concat_query_global = False
|
|
608
801
|
incompatible: set[str] = set()
|
|
609
802
|
|
|
803
|
+
# ── BUG #1: evidence gate ───────────────────────────────────────────────
|
|
804
|
+
# Pre-read each Java source ONCE, strip comments + string/char literals, and
|
|
805
|
+
# record real-import evidence. Detection only proceeds when there is genuine
|
|
806
|
+
# proof of Hibernate/JPA — never a substring inside a comment or a classpath
|
|
807
|
+
# resource path (e.g. Alfresco's "org.hibernate.dialect.MySQLInnoDBDialect"
|
|
808
|
+
# appears only in Javadoc + iBatis resource directory names).
|
|
809
|
+
parsed: list[tuple[str, str, bool]] = [] # (rel_path, stripped_source, imports_hibernate)
|
|
810
|
+
any_hibernate_import = False
|
|
811
|
+
any_jpa_import = False
|
|
812
|
+
hibernate_import_sample: Optional[str] = None
|
|
610
813
|
for rel_path in sorted(file_paths):
|
|
611
814
|
if not rel_path.endswith(".java"):
|
|
612
815
|
continue
|
|
613
816
|
abs_path = root / rel_path
|
|
614
817
|
try:
|
|
615
|
-
|
|
818
|
+
raw = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
616
819
|
except OSError:
|
|
617
820
|
continue
|
|
821
|
+
source = _strip_comments_strings(raw)
|
|
822
|
+
imports_hib = _file_imports_hibernate(source)
|
|
823
|
+
if imports_hib:
|
|
824
|
+
any_hibernate_import = True
|
|
825
|
+
if hibernate_import_sample is None:
|
|
826
|
+
hm = _REAL_HIBERNATE_IMPORT_RE.search(source)
|
|
827
|
+
# capture the full import line for the evidence record
|
|
828
|
+
line = source[hm.start():].split("\n", 1)[0].strip().rstrip(";")
|
|
829
|
+
hibernate_import_sample = line
|
|
830
|
+
if _JPA_IMPORT_RE.search(source):
|
|
831
|
+
any_jpa_import = True
|
|
832
|
+
parsed.append((rel_path, source, imports_hib))
|
|
833
|
+
|
|
834
|
+
# No Java sources → nothing to stratify; skip the (potentially large) build walk.
|
|
835
|
+
if not parsed:
|
|
836
|
+
strat = HibernateStratification()
|
|
837
|
+
strat.detected = False
|
|
838
|
+
strat.classification = CLASS_NONE
|
|
839
|
+
strat.classification_label = _CLASS_LABELS[CLASS_NONE]
|
|
840
|
+
strat.evidence = {"dependency_present": False, "dependency_coordinates": [],
|
|
841
|
+
"hibernate_import_present": False, "hibernate_import_sample": None,
|
|
842
|
+
"jpa_import_present": False, "confidence": "none"}
|
|
843
|
+
return strat
|
|
618
844
|
|
|
845
|
+
dep_present, dep_samples = _scan_build_dependency(root)
|
|
846
|
+
eff_version, version_major, version_conf = _resolve_hibernate_version(root)
|
|
847
|
+
|
|
848
|
+
has_evidence = dep_present or any_hibernate_import or any_jpa_import
|
|
849
|
+
evidence: dict = {
|
|
850
|
+
"dependency_present": dep_present,
|
|
851
|
+
"dependency_coordinates": dep_samples,
|
|
852
|
+
"hibernate_import_present": any_hibernate_import,
|
|
853
|
+
"hibernate_import_sample": hibernate_import_sample,
|
|
854
|
+
"jpa_import_present": any_jpa_import,
|
|
855
|
+
"effective_version": eff_version,
|
|
856
|
+
"version_major": version_major,
|
|
857
|
+
"version_confidence": version_conf,
|
|
858
|
+
"confidence": "high" if has_evidence else "none",
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if not has_evidence:
|
|
862
|
+
# No resolved Hibernate/JPA evidence → never a blocker. Any org.hibernate
|
|
863
|
+
# substring lives in comments/strings/classpath paths only.
|
|
864
|
+
strat = HibernateStratification()
|
|
865
|
+
strat.detected = False
|
|
866
|
+
strat.classification = CLASS_NONE
|
|
867
|
+
strat.classification_label = _CLASS_LABELS[CLASS_NONE]
|
|
868
|
+
strat.evidence = evidence
|
|
869
|
+
return strat
|
|
870
|
+
|
|
871
|
+
for rel_path, source, file_imports_hib in parsed:
|
|
619
872
|
nl = _line_index(source)
|
|
620
873
|
src_lines = source.split("\n")
|
|
621
874
|
classes, methods = _index_symbols(source, nl)
|
|
@@ -736,7 +989,16 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
736
989
|
"Hibernate 6 HQL parser changes")
|
|
737
990
|
|
|
738
991
|
# ── Layer 4: Hibernate SPI / internal ──────────────────────────────
|
|
739
|
-
|
|
992
|
+
# FQN-anchored SPI matches are proof on their own. Bare-name matches
|
|
993
|
+
# (`implements XInterceptor`, `extends XEventListener`) only count when
|
|
994
|
+
# the file actually imports org.hibernate — otherwise they are the
|
|
995
|
+
# project's own AOP interceptors / event listeners (BUG #1).
|
|
996
|
+
spi_matches = list(_SPI_FQN_RE.finditer(source))
|
|
997
|
+
if file_imports_hib:
|
|
998
|
+
spi_matches = sorted(
|
|
999
|
+
spi_matches + list(_SPI_SIMPLE_RE.finditer(source)),
|
|
1000
|
+
key=lambda m: m.start(),
|
|
1001
|
+
)
|
|
740
1002
|
if spi_matches:
|
|
741
1003
|
spi_global = True
|
|
742
1004
|
mod["has_spi"] = True
|
|
@@ -780,6 +1042,13 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
780
1042
|
|
|
781
1043
|
strat = HibernateStratification()
|
|
782
1044
|
strat.findings = findings
|
|
1045
|
+
strat.evidence = evidence
|
|
1046
|
+
strat.effective_version = eff_version
|
|
1047
|
+
strat.version_major = version_major
|
|
1048
|
+
strat.version_confidence = version_conf
|
|
1049
|
+
# BUG #1: the 5→6 axis only applies when NOT already on Hibernate 6+. A
|
|
1050
|
+
# resolved major ≥ 6 means the migration is complete — never a blocker.
|
|
1051
|
+
strat.migration_applicable = not (version_major is not None and version_major >= 6)
|
|
783
1052
|
strat.rewrite_targets = sorted(
|
|
784
1053
|
targets, key=lambda t: (t.source_file, t.line_start, t.layer, t.id)
|
|
785
1054
|
)
|
|
@@ -789,6 +1058,8 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
789
1058
|
)[:25]
|
|
790
1059
|
|
|
791
1060
|
if not findings:
|
|
1061
|
+
# Evidence existed (dependency/import) but no concrete migration pattern
|
|
1062
|
+
# fired — Hibernate present yet nothing in the rewrite/upgrade surface.
|
|
792
1063
|
strat.detected = False
|
|
793
1064
|
strat.classification = CLASS_NONE
|
|
794
1065
|
strat.classification_label = _CLASS_LABELS[CLASS_NONE]
|
|
@@ -945,13 +1216,23 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
945
1216
|
stops.append("SQL/HQL shape not statically inferable (string concatenation)")
|
|
946
1217
|
strat.stop_conditions_triggered = stops
|
|
947
1218
|
|
|
948
|
-
if
|
|
1219
|
+
if not strat.migration_applicable:
|
|
1220
|
+
# Already on Hibernate 6+ — the 5→6 stratification below is retained as an
|
|
1221
|
+
# informational exposure map, but it is NOT a migration and NOT a blocker.
|
|
1222
|
+
strat.classification = CLASS_NONE
|
|
1223
|
+
strat.classification_label = (
|
|
1224
|
+
f"ALREADY ON HIBERNATE {strat.version_major} "
|
|
1225
|
+
f"({strat.effective_version}) — NO 5→6 MIGRATION PENDING"
|
|
1226
|
+
)
|
|
1227
|
+
strat.stop_conditions_triggered = []
|
|
1228
|
+
elif stops:
|
|
949
1229
|
strat.classification = CLASS_REWRITE
|
|
950
1230
|
elif layer_files[LAYER_CRITERIA] or concat_query_global or jpa_deprecated_seen:
|
|
951
1231
|
strat.classification = CLASS_UPGRADE_CARE
|
|
952
1232
|
else:
|
|
953
1233
|
strat.classification = CLASS_UPGRADE
|
|
954
|
-
|
|
1234
|
+
if strat.migration_applicable:
|
|
1235
|
+
strat.classification_label = _CLASS_LABELS[strat.classification]
|
|
955
1236
|
|
|
956
1237
|
# ── Risk separation: observable vs inferred ─────────────────────────────
|
|
957
1238
|
observable: list[str] = []
|
|
@@ -35,10 +35,28 @@ _ATTR_NAME_RE = re.compile(r'(?:name|value)\s*=\s*"([^"]*)"')
|
|
|
35
35
|
_FIRST_LITERAL_RE = re.compile(r'^\s*"([^"]*)"')
|
|
36
36
|
|
|
37
37
|
# token -> (kind, client). Matched as whole-word usage outside imports/comments.
|
|
38
|
+
# Covers Spring AND plain-Java/Jakarta stacks (Quarkus, Micronaut, Keycloak SPI):
|
|
39
|
+
# the detector must not be Spring-centric, or a non-Spring repo with real LDAP /
|
|
40
|
+
# SMTP / HTTP integrations falsely reports "0 integrations".
|
|
38
41
|
_TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
|
|
42
|
+
# Spring HTTP clients
|
|
39
43
|
("RestTemplate", "http", "resttemplate"),
|
|
40
44
|
("WebClient", "http", "webclient"),
|
|
45
|
+
# Plain-Java / third-party HTTP clients
|
|
46
|
+
("HttpClient", "http", "jdk-httpclient"), # java.net.http.HttpClient
|
|
47
|
+
("CloseableHttpClient", "http", "apache-httpclient"),
|
|
48
|
+
("HttpClients", "http", "apache-httpclient"),
|
|
49
|
+
("OkHttpClient", "http", "okhttp"),
|
|
50
|
+
# LDAP / directory (Spring + JNDI)
|
|
41
51
|
("LdapTemplate", "ldap", "ldaptemplate"),
|
|
52
|
+
("InitialLdapContext", "ldap", "jndi-ldap"),
|
|
53
|
+
("InitialDirContext", "ldap", "jndi-ldap"),
|
|
54
|
+
("LdapContext", "ldap", "jndi-ldap"),
|
|
55
|
+
# Mail / SMTP (JavaMail / Jakarta Mail)
|
|
56
|
+
("JavaMailSender", "smtp", "spring-mail"),
|
|
57
|
+
("MimeMessage", "smtp", "javamail"),
|
|
58
|
+
("Transport", "smtp", "javamail"),
|
|
59
|
+
# JMS / messaging
|
|
42
60
|
("JmsTemplate", "jms", "jmstemplate"),
|
|
43
61
|
("ActiveMQConnectionFactory", "jms", "activemq"),
|
|
44
62
|
)
|
|
@@ -156,8 +174,21 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
156
174
|
for r in records:
|
|
157
175
|
by_kind[r["kind"]] = by_kind.get(r["kind"], 0) + 1
|
|
158
176
|
|
|
177
|
+
# BUG #9: honest confidence. A zero count means "no detectable client
|
|
178
|
+
# construct was found", NOT "this system has no integrations" — runtime-wired
|
|
179
|
+
# clients (DI, config-driven endpoints, JCA connectors) are invisible to static
|
|
180
|
+
# text matching. Report that explicitly instead of an authoritative "0".
|
|
181
|
+
confidence = "observed" if records else "not_analyzed"
|
|
159
182
|
return {
|
|
160
183
|
"integrations": records,
|
|
161
184
|
"by_kind": {k: by_kind[k] for k in sorted(by_kind)},
|
|
162
185
|
"count": len(records),
|
|
186
|
+
"confidence": confidence,
|
|
187
|
+
"coverage_note": (
|
|
188
|
+
"Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
|
|
189
|
+
"+ JNDI), SMTP (JavaMail), and JMS client constructs by source-text "
|
|
190
|
+
"matching. A count of 0 means no such construct was found, not that the "
|
|
191
|
+
"system has no outbound integrations — runtime/DI-wired clients are not "
|
|
192
|
+
"statically visible."
|
|
193
|
+
),
|
|
163
194
|
}
|