sourcecode 1.64.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 +102 -2
- sourcecode/integration_detector.py +31 -0
- sourcecode/migrate_check.py +266 -68
- sourcecode/path_filters.py +33 -0
- {sourcecode-1.64.0.dist-info → sourcecode-1.65.0.dist-info}/METADATA +33 -3
- {sourcecode-1.64.0.dist-info → sourcecode-1.65.0.dist-info}/RECORD +10 -10
- {sourcecode-1.64.0.dist-info → sourcecode-1.65.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.64.0.dist-info → sourcecode-1.65.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.64.0.dist-info → sourcecode-1.65.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/hibernate_strat.py
CHANGED
|
@@ -253,6 +253,75 @@ def _scan_build_dependency(root: Path) -> tuple[bool, list[str]]:
|
|
|
253
253
|
samples.append(snippet)
|
|
254
254
|
return found, samples
|
|
255
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
|
+
|
|
256
325
|
# Escalation markers — dynamic / reflection-based persistence construction.
|
|
257
326
|
_ABSTRACTION_CLASS_RE = re.compile(
|
|
258
327
|
r"\b(DynamicEntityDao|DynamicEntityDaoImpl|BasicPersistenceModule|"
|
|
@@ -406,12 +475,23 @@ class HibernateStratification:
|
|
|
406
475
|
# when backed by a build dependency or a parsed org.hibernate import, "none"
|
|
407
476
|
# when detection was vetoed for lack of evidence.
|
|
408
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
|
|
409
485
|
|
|
410
486
|
def to_dict(self) -> dict:
|
|
411
487
|
return {
|
|
412
488
|
"schema_version": HIBERNATE_SCHEMA_VERSION,
|
|
413
489
|
"detected": self.detected,
|
|
414
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,
|
|
415
495
|
"classification": self.classification,
|
|
416
496
|
"classification_label": self.classification_label,
|
|
417
497
|
"stratified": True,
|
|
@@ -763,6 +843,7 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
763
843
|
return strat
|
|
764
844
|
|
|
765
845
|
dep_present, dep_samples = _scan_build_dependency(root)
|
|
846
|
+
eff_version, version_major, version_conf = _resolve_hibernate_version(root)
|
|
766
847
|
|
|
767
848
|
has_evidence = dep_present or any_hibernate_import or any_jpa_import
|
|
768
849
|
evidence: dict = {
|
|
@@ -771,6 +852,9 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
771
852
|
"hibernate_import_present": any_hibernate_import,
|
|
772
853
|
"hibernate_import_sample": hibernate_import_sample,
|
|
773
854
|
"jpa_import_present": any_jpa_import,
|
|
855
|
+
"effective_version": eff_version,
|
|
856
|
+
"version_major": version_major,
|
|
857
|
+
"version_confidence": version_conf,
|
|
774
858
|
"confidence": "high" if has_evidence else "none",
|
|
775
859
|
}
|
|
776
860
|
|
|
@@ -959,6 +1043,12 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
959
1043
|
strat = HibernateStratification()
|
|
960
1044
|
strat.findings = findings
|
|
961
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)
|
|
962
1052
|
strat.rewrite_targets = sorted(
|
|
963
1053
|
targets, key=lambda t: (t.source_file, t.line_start, t.layer, t.id)
|
|
964
1054
|
)
|
|
@@ -1126,13 +1216,23 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
|
|
|
1126
1216
|
stops.append("SQL/HQL shape not statically inferable (string concatenation)")
|
|
1127
1217
|
strat.stop_conditions_triggered = stops
|
|
1128
1218
|
|
|
1129
|
-
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:
|
|
1130
1229
|
strat.classification = CLASS_REWRITE
|
|
1131
1230
|
elif layer_files[LAYER_CRITERIA] or concat_query_global or jpa_deprecated_seen:
|
|
1132
1231
|
strat.classification = CLASS_UPGRADE_CARE
|
|
1133
1232
|
else:
|
|
1134
1233
|
strat.classification = CLASS_UPGRADE
|
|
1135
|
-
|
|
1234
|
+
if strat.migration_applicable:
|
|
1235
|
+
strat.classification_label = _CLASS_LABELS[strat.classification]
|
|
1136
1236
|
|
|
1137
1237
|
# ── Risk separation: observable vs inferred ─────────────────────────────
|
|
1138
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
|
}
|
sourcecode/migrate_check.py
CHANGED
|
@@ -22,6 +22,8 @@ from datetime import datetime, timezone
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Optional, TYPE_CHECKING
|
|
24
24
|
|
|
25
|
+
from sourcecode.path_filters import is_test_or_fixture_path
|
|
26
|
+
|
|
25
27
|
if TYPE_CHECKING:
|
|
26
28
|
from sourcecode.hibernate_strat import HibernateStratification
|
|
27
29
|
|
|
@@ -582,6 +584,54 @@ def _is_no_migrate_javax(fqn: str) -> bool:
|
|
|
582
584
|
"""True if a javax FQN belongs to a JDK/permanent namespace (no jakarta move)."""
|
|
583
585
|
return any(fqn.startswith(p) for p in _JAKARTA_NO_MIGRATE_PREFIXES)
|
|
584
586
|
|
|
587
|
+
|
|
588
|
+
# BUG #8: autogenerated source markers — path fragments and the JSR-250 marker.
|
|
589
|
+
_GENERATED_PATH_FRAGMENTS: tuple[str, ...] = (
|
|
590
|
+
"/generated-sources/", "/generated/", "/target/generated",
|
|
591
|
+
"/build/generated", "/.apt_generated/",
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _classify_code_context(finding: "MigrationFinding") -> str:
|
|
596
|
+
"""Bucket a finding as main / test / generated for blocking-count segregation."""
|
|
597
|
+
path = finding.source_file
|
|
598
|
+
if is_test_or_fixture_path(path):
|
|
599
|
+
return "test"
|
|
600
|
+
norm = path.replace("\\", "/").lower()
|
|
601
|
+
if any(frag in norm for frag in _GENERATED_PATH_FRAGMENTS):
|
|
602
|
+
return "generated"
|
|
603
|
+
# javax.annotation.Generated is the marker emitted into autogenerated code.
|
|
604
|
+
if finding.rule_id == "MIG-006" and finding.imports_found and all(
|
|
605
|
+
imp.rsplit(".", 1)[-1] == "Generated" for imp in finding.imports_found
|
|
606
|
+
):
|
|
607
|
+
return "generated"
|
|
608
|
+
return "main"
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
# BUG #2: Spring presence signal. The Boot3 dimension only applies to repos that
|
|
612
|
+
# actually use Spring — a Quarkus/Micronaut/Helidon/Jakarta-pure repo has no
|
|
613
|
+
# Spring Boot 2→3 axis. Detected from build coordinates or framework imports.
|
|
614
|
+
_SPRING_BUILD_RE: re.Pattern = re.compile(
|
|
615
|
+
r"org\.springframework(?:\.boot)?\b|spring-boot-starter", re.IGNORECASE
|
|
616
|
+
)
|
|
617
|
+
_SPRING_IMPORT_RE: re.Pattern = re.compile(
|
|
618
|
+
r"^[ \t]*import\s+org\.springframework\.", re.MULTILINE
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _detect_spring_present(root: Path, spring_import_seen: bool) -> bool:
|
|
623
|
+
"""True when Spring is actually used (build coordinate or org.springframework import)."""
|
|
624
|
+
if spring_import_seen:
|
|
625
|
+
return True
|
|
626
|
+
for abs_path, _rel in _find_build_files(root):
|
|
627
|
+
try:
|
|
628
|
+
text = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
629
|
+
except OSError:
|
|
630
|
+
continue
|
|
631
|
+
if _SPRING_BUILD_RE.search(text):
|
|
632
|
+
return True
|
|
633
|
+
return False
|
|
634
|
+
|
|
585
635
|
# G-1: cap on total readiness deduction from low-severity (advisory, non-blocking)
|
|
586
636
|
# findings, so optional modernization cleanups cannot collapse the migration-readiness
|
|
587
637
|
# headline on a repo with zero blockers. See MigrationReport.finalize.
|
|
@@ -594,6 +644,10 @@ _JAKARTA_TARGETS: frozenset[str] = frozenset({"jakarta"})
|
|
|
594
644
|
_BOOT3_MIGRATION_TARGETS: frozenset[str] = frozenset(
|
|
595
645
|
{"jakarta", "spring_boot_3", "spring_security_6"}
|
|
596
646
|
)
|
|
647
|
+
# BUG #6: best-practice hygiene (java.util.Date → java.time) blocks NO version
|
|
648
|
+
# upgrade. It is advisory only and must never sink a readiness dimension to 0 —
|
|
649
|
+
# reported as a separate hygiene metric, excluded from JDK-modernization scoring.
|
|
650
|
+
_BEST_PRACTICE_TARGETS: frozenset[str] = frozenset({"java_8_best_practice"})
|
|
597
651
|
# Cap on total readiness deduction from JDK-modernization findings (medium/low),
|
|
598
652
|
# so reflection/date cleanups cannot collapse a jakarta-ready repo's headline.
|
|
599
653
|
_JDK_ADVISORY_DEDUCTION_CAP: int = 15
|
|
@@ -1013,6 +1067,10 @@ class MigrationFinding:
|
|
|
1013
1067
|
fix_hint: str = ""
|
|
1014
1068
|
migration_target: str = ""
|
|
1015
1069
|
openrewrite_recipe: Optional[str] = None
|
|
1070
|
+
# BUG #8: where the finding lives — "main" (product), "test" (test/fixture
|
|
1071
|
+
# harness), or "generated" (autogenerated source). Only "main" counts toward
|
|
1072
|
+
# blocking_count / readiness; test+generated are reported in a separate bucket.
|
|
1073
|
+
code_context: str = "main"
|
|
1016
1074
|
|
|
1017
1075
|
@staticmethod
|
|
1018
1076
|
def make_id(rule_id: str, source_file: str) -> str:
|
|
@@ -1031,6 +1089,7 @@ class MigrationFinding:
|
|
|
1031
1089
|
"fix_hint": self.fix_hint,
|
|
1032
1090
|
"migration_target": self.migration_target,
|
|
1033
1091
|
"auto_fix_available": bool(self.openrewrite_recipe),
|
|
1092
|
+
"code_context": self.code_context,
|
|
1034
1093
|
}
|
|
1035
1094
|
if self.imports_found:
|
|
1036
1095
|
d["imports_found"] = self.imports_found
|
|
@@ -1061,8 +1120,12 @@ def _dimension_score(
|
|
|
1061
1120
|
low: set[str] = set()
|
|
1062
1121
|
for f in findings:
|
|
1063
1122
|
if targets is None:
|
|
1123
|
+
# JDK-modernization axis: everything NOT a Boot 2→3 blocker, EXCEPT
|
|
1124
|
+
# best-practice hygiene (BUG #6) which is advisory and scored separately.
|
|
1064
1125
|
if f.migration_target in _BOOT3_MIGRATION_TARGETS:
|
|
1065
1126
|
continue
|
|
1127
|
+
if f.migration_target in _BEST_PRACTICE_TARGETS:
|
|
1128
|
+
continue
|
|
1066
1129
|
elif f.migration_target not in targets:
|
|
1067
1130
|
continue
|
|
1068
1131
|
if f.severity == "critical":
|
|
@@ -1113,6 +1176,13 @@ class MigrationReport:
|
|
|
1113
1176
|
# None = could not determine. Absence of evidence is never reported as True.
|
|
1114
1177
|
spring_boot_2_detected: Optional[bool] = None
|
|
1115
1178
|
spring_boot_version_detected: Optional[str] = None
|
|
1179
|
+
# BUG #2: whether Spring is used at all. The Boot3 dimension is N/A without it.
|
|
1180
|
+
spring_present: bool = True
|
|
1181
|
+
# BUG #6 / #8: findings that do NOT count toward blocking_count or readiness —
|
|
1182
|
+
# best-practice hygiene (java.util.Date…) and test/fixture/generated buckets,
|
|
1183
|
+
# surfaced separately so the headline reflects real product migration risk.
|
|
1184
|
+
hygiene_findings: int = 0
|
|
1185
|
+
non_blocking: dict = field(default_factory=dict)
|
|
1116
1186
|
|
|
1117
1187
|
findings: list[MigrationFinding] = field(default_factory=list)
|
|
1118
1188
|
limitations: list[str] = field(default_factory=list)
|
|
@@ -1138,82 +1208,122 @@ class MigrationReport:
|
|
|
1138
1208
|
by_target[f.migration_target] = by_target.get(f.migration_target, 0) + 1
|
|
1139
1209
|
affected_files.add(f.source_file)
|
|
1140
1210
|
|
|
1141
|
-
|
|
1211
|
+
# BUG #8: only product ("main") code counts toward blocking_count, the
|
|
1212
|
+
# readiness dimensions, and effort. Test harnesses/fixtures (deprecated
|
|
1213
|
+
# web.xml descriptors, test-only sun.* HTTP servers) and autogenerated
|
|
1214
|
+
# sources are reported in a separate non-blocking bucket — counting them
|
|
1215
|
+
# as product blockers inflates the verdict.
|
|
1216
|
+
main_findings = [f for f in self.findings if f.code_context == "main"]
|
|
1217
|
+
|
|
1218
|
+
def _files_by_sev(items: list["MigrationFinding"], sev: str,
|
|
1219
|
+
targets: "Optional[frozenset[str]]" = None,
|
|
1220
|
+
exclude: "Optional[frozenset[str]]" = None) -> set[str]:
|
|
1221
|
+
out: set[str] = set()
|
|
1222
|
+
for f in items:
|
|
1223
|
+
if f.severity != sev:
|
|
1224
|
+
continue
|
|
1225
|
+
if targets is not None and f.migration_target not in targets:
|
|
1226
|
+
continue
|
|
1227
|
+
if exclude is not None and f.migration_target in exclude:
|
|
1228
|
+
continue
|
|
1229
|
+
out.add(f.source_file)
|
|
1230
|
+
return out
|
|
1231
|
+
|
|
1232
|
+
critical_files = _files_by_sev(main_findings, "critical")
|
|
1233
|
+
high_files = _files_by_sev(main_findings, "high")
|
|
1234
|
+
medium_files = _files_by_sev(main_findings, "medium")
|
|
1235
|
+
low_files = _files_by_sev(main_findings, "low")
|
|
1236
|
+
|
|
1237
|
+
self.blocking_count = sum(
|
|
1238
|
+
1 for f in main_findings if f.severity in ("critical", "high")
|
|
1239
|
+
)
|
|
1142
1240
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
# no longer force 0/100).
|
|
1164
|
-
mig_med = {f.source_file for f in self.findings
|
|
1165
|
-
if f.severity == "medium" and f.migration_target in _BOOT3_MIGRATION_TARGETS}
|
|
1166
|
-
mig_low = {f.source_file for f in self.findings
|
|
1167
|
-
if f.severity == "low" and f.migration_target in _BOOT3_MIGRATION_TARGETS}
|
|
1168
|
-
jdk_med = {f.source_file for f in self.findings
|
|
1169
|
-
if f.severity == "medium" and f.migration_target not in _BOOT3_MIGRATION_TARGETS}
|
|
1170
|
-
jdk_low = {f.source_file for f in self.findings
|
|
1171
|
-
if f.severity == "low" and f.migration_target not in _BOOT3_MIGRATION_TARGETS}
|
|
1241
|
+
# BUG #4 / #6: readiness deduction. FRAMEWORK blockers (jakarta / Boot3 /
|
|
1242
|
+
# Security — critical+high) are UNCAPPED so a genuinely blocked repo floors
|
|
1243
|
+
# at 0. Orthogonal JDK-modernization debt (SecurityManager, sun.*, reflection)
|
|
1244
|
+
# is advisory-CAPPED so it can never sink the framework headline. Best-practice
|
|
1245
|
+
# hygiene (java.util.Date) is EXCLUDED entirely — it blocks no version upgrade.
|
|
1246
|
+
fw = _BOOT3_MIGRATION_TARGETS
|
|
1247
|
+
fw_crit = _files_by_sev(main_findings, "critical", targets=fw)
|
|
1248
|
+
fw_high = _files_by_sev(main_findings, "high", targets=fw)
|
|
1249
|
+
fw_med = _files_by_sev(main_findings, "medium", targets=fw)
|
|
1250
|
+
fw_low = _files_by_sev(main_findings, "low", targets=fw)
|
|
1251
|
+
_jdk_exclude = fw | _BEST_PRACTICE_TARGETS
|
|
1252
|
+
jdk_crit = {f.source_file for f in main_findings
|
|
1253
|
+
if f.severity == "critical" and f.migration_target not in _jdk_exclude}
|
|
1254
|
+
jdk_high = {f.source_file for f in main_findings
|
|
1255
|
+
if f.severity == "high" and f.migration_target not in _jdk_exclude}
|
|
1256
|
+
jdk_med = {f.source_file for f in main_findings
|
|
1257
|
+
if f.severity == "medium" and f.migration_target not in _jdk_exclude}
|
|
1258
|
+
jdk_low = {f.source_file for f in main_findings
|
|
1259
|
+
if f.severity == "low" and f.migration_target not in _jdk_exclude}
|
|
1260
|
+
jdk_raw = len(jdk_crit) * 15 + len(jdk_high) * 8 + len(jdk_med) * 3 + len(jdk_low) * 1
|
|
1172
1261
|
|
|
1173
1262
|
deduction = (
|
|
1174
|
-
len(
|
|
1175
|
-
+ len(
|
|
1176
|
-
+ len(
|
|
1177
|
-
+ min(len(
|
|
1178
|
-
+ min(
|
|
1263
|
+
len(fw_crit) * 15
|
|
1264
|
+
+ len(fw_high) * 8
|
|
1265
|
+
+ len(fw_med) * 3
|
|
1266
|
+
+ min(len(fw_low) * 1, _LOW_SEVERITY_DEDUCTION_CAP)
|
|
1267
|
+
+ min(jdk_raw, _JDK_ADVISORY_DEDUCTION_CAP)
|
|
1179
1268
|
)
|
|
1180
1269
|
self.readiness_score = max(0, 100 - deduction)
|
|
1181
1270
|
|
|
1182
|
-
# Per-dimension readiness — independent severity-weighted scores
|
|
1183
|
-
|
|
1184
|
-
self.
|
|
1185
|
-
self.
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1190
|
-
#
|
|
1191
|
-
|
|
1271
|
+
# Per-dimension readiness — independent severity-weighted scores (MAIN only).
|
|
1272
|
+
self.jakarta_readiness = _dimension_score(main_findings, _JAKARTA_TARGETS)
|
|
1273
|
+
self.boot3_readiness = _dimension_score(main_findings, _BOOT3_MIGRATION_TARGETS)
|
|
1274
|
+
self.jdk_modernization = _dimension_score(main_findings, None)
|
|
1275
|
+
|
|
1276
|
+
# BUG #1: Hibernate 5→6 is its own rewrite axis and applies ONLY when the
|
|
1277
|
+
# repo is actually on Hibernate < 6. A resolved Hibernate 6+ (Keycloak:
|
|
1278
|
+
# 6.2.13) means the migration is already done → N/A, never a blocker. An
|
|
1279
|
+
# unresolved version degrades to a low-confidence hypothesis (no headline).
|
|
1280
|
+
hib = self.hibernate
|
|
1281
|
+
_hibernate_applies = (
|
|
1282
|
+
hib is not None and hib.detected and hib.migration_applicable
|
|
1283
|
+
)
|
|
1284
|
+
if hib is not None and hib.detected:
|
|
1285
|
+
# Already on H6+ → nothing to migrate → 100; else the rewrite score.
|
|
1286
|
+
self.hibernate_readiness = hib.readiness if hib.migration_applicable else 100
|
|
1287
|
+
if _hibernate_applies and hib.classification == "rewrite_zone" \
|
|
1288
|
+
and hib.version_confidence == "high":
|
|
1289
|
+
# Only a CONFIRMED Hibernate-5 rewrite zone earns the headline blocker.
|
|
1290
|
+
self.headline_blocker = "hibernate_rewrite"
|
|
1291
|
+
|
|
1292
|
+
# BUG #2 / #3: declare which dimensions apply. jakarta + JDK are always
|
|
1293
|
+
# applicable to a Java repo; boot3 applies only when Spring is actually used
|
|
1294
|
+
# (Quarkus/Micronaut/Jakarta-pure repos have no Boot 2→3 axis); hibernate
|
|
1295
|
+
# applies only for a real Hibernate < 6. N/A dimensions carry score=None and
|
|
1296
|
+
# are excluded from the aggregate — never scored as 0.
|
|
1297
|
+
if not self.spring_present:
|
|
1298
|
+
self.boot3_readiness = 100
|
|
1192
1299
|
if _hibernate_applies:
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
# Hibernate dependency/import was found. An inapplicable dimension is N/A
|
|
1200
|
-
# (score=None) and is excluded from the aggregate — never scored as 0.
|
|
1300
|
+
_hib_reason = f"Hibernate 5→6 rewrite axis (detected {hib.effective_version or 'version unknown'})"
|
|
1301
|
+
elif hib is not None and hib.detected and not hib.migration_applicable:
|
|
1302
|
+
_hib_reason = (f"N/A — already on Hibernate {hib.version_major} "
|
|
1303
|
+
f"({hib.effective_version}); no 5→6 migration pending")
|
|
1304
|
+
else:
|
|
1305
|
+
_hib_reason = "N/A — no Hibernate dependency or import detected"
|
|
1201
1306
|
self.applicable_dimensions = {
|
|
1202
1307
|
"jakarta": {"applicable": True, "score": self.jakarta_readiness,
|
|
1203
1308
|
"reason": "javax→jakarta namespace migration"},
|
|
1204
|
-
"boot3": {
|
|
1205
|
-
|
|
1309
|
+
"boot3": {
|
|
1310
|
+
"applicable": self.spring_present,
|
|
1311
|
+
"score": self.boot3_readiness if self.spring_present else None,
|
|
1312
|
+
"reason": ("Spring Boot 2→3 / Security 6 migration"
|
|
1313
|
+
if self.spring_present
|
|
1314
|
+
else "N/A — no Spring usage detected (non-Spring stack)"),
|
|
1315
|
+
},
|
|
1206
1316
|
"jdk_modernization": {"applicable": True, "score": self.jdk_modernization,
|
|
1207
1317
|
"reason": "orthogonal JDK modernization debt"},
|
|
1208
1318
|
"hibernate": {
|
|
1209
1319
|
"applicable": _hibernate_applies,
|
|
1210
1320
|
"score": self.hibernate_readiness if _hibernate_applies else None,
|
|
1211
|
-
"reason":
|
|
1212
|
-
if _hibernate_applies
|
|
1213
|
-
else "N/A — no Hibernate dependency or import detected"),
|
|
1321
|
+
"reason": _hib_reason,
|
|
1214
1322
|
},
|
|
1215
1323
|
}
|
|
1216
1324
|
|
|
1325
|
+
# BUG #5: effort over MAIN findings only — N/A axes (Hibernate-6 phantom,
|
|
1326
|
+
# test fixtures) no longer pad the estimate.
|
|
1217
1327
|
self.estimated_effort_days = round(
|
|
1218
1328
|
len(critical_files) * 0.5
|
|
1219
1329
|
+ len(high_files) * 0.25
|
|
@@ -1222,12 +1332,34 @@ class MigrationReport:
|
|
|
1222
1332
|
1,
|
|
1223
1333
|
)
|
|
1224
1334
|
|
|
1335
|
+
# BUG #6 / #8: hygiene + non-blocking buckets, surfaced separately.
|
|
1336
|
+
self.hygiene_findings = sum(
|
|
1337
|
+
1 for f in main_findings if f.migration_target in _BEST_PRACTICE_TARGETS
|
|
1338
|
+
)
|
|
1339
|
+
_nb_by_ctx: dict[str, int] = {}
|
|
1340
|
+
_nb_by_rule: dict[str, int] = {}
|
|
1341
|
+
for f in self.findings:
|
|
1342
|
+
if f.code_context == "main":
|
|
1343
|
+
continue
|
|
1344
|
+
_nb_by_ctx[f.code_context] = _nb_by_ctx.get(f.code_context, 0) + 1
|
|
1345
|
+
_nb_by_rule[f.rule_id] = _nb_by_rule.get(f.rule_id, 0) + 1
|
|
1346
|
+
self.non_blocking = {
|
|
1347
|
+
"count": sum(_nb_by_ctx.values()),
|
|
1348
|
+
"by_context": _nb_by_ctx,
|
|
1349
|
+
"by_rule": _nb_by_rule,
|
|
1350
|
+
"note": ("Findings in test/fixture harnesses or autogenerated sources. "
|
|
1351
|
+
"Excluded from blocking_count, readiness, and effort."),
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1225
1354
|
self.summary = {
|
|
1226
1355
|
"total_findings": len(self.findings),
|
|
1227
1356
|
"affected_files": len(affected_files),
|
|
1228
1357
|
"by_severity": by_severity,
|
|
1229
1358
|
"by_rule": by_rule,
|
|
1230
1359
|
"by_migration_target": by_target,
|
|
1360
|
+
"main_findings": len(main_findings),
|
|
1361
|
+
"non_blocking_findings": self.non_blocking["count"],
|
|
1362
|
+
"hygiene_findings": self.hygiene_findings,
|
|
1231
1363
|
}
|
|
1232
1364
|
return self
|
|
1233
1365
|
|
|
@@ -1251,6 +1383,9 @@ class MigrationReport:
|
|
|
1251
1383
|
"headline_blocker": self.headline_blocker,
|
|
1252
1384
|
"blocking_count": self.blocking_count,
|
|
1253
1385
|
"estimated_effort_days": self.estimated_effort_days,
|
|
1386
|
+
"hygiene_findings": self.hygiene_findings,
|
|
1387
|
+
"non_blocking": self.non_blocking,
|
|
1388
|
+
"spring_present": self.spring_present,
|
|
1254
1389
|
"spring_boot_2_detected": self.spring_boot_2_detected,
|
|
1255
1390
|
"spring_boot_version_detected": self.spring_boot_version_detected,
|
|
1256
1391
|
"summary": self.summary,
|
|
@@ -1270,19 +1405,31 @@ class MigrationReport:
|
|
|
1270
1405
|
_boot = f"Boot {self.spring_boot_version_detected or '3+'} detected"
|
|
1271
1406
|
else:
|
|
1272
1407
|
_boot = "unknown"
|
|
1408
|
+
|
|
1409
|
+
def _dim(name: str) -> str:
|
|
1410
|
+
d = self.applicable_dimensions.get(name, {})
|
|
1411
|
+
if not d.get("applicable", True):
|
|
1412
|
+
return f"{name}: N/A"
|
|
1413
|
+
return f"{name}: {d.get('score')}/100"
|
|
1414
|
+
|
|
1415
|
+
# Blocking parenthetical reflects MAIN (product) findings only — matching
|
|
1416
|
+
# blocking_count — so the text headline cannot contradict the JSON.
|
|
1417
|
+
main_crit = sum(1 for f in self.findings
|
|
1418
|
+
if f.code_context == "main" and f.severity == "critical")
|
|
1419
|
+
main_high = sum(1 for f in self.findings
|
|
1420
|
+
if f.code_context == "main" and f.severity == "high")
|
|
1421
|
+
nb = self.non_blocking.get("count", 0)
|
|
1273
1422
|
lines: list[str] = [
|
|
1274
1423
|
f"Migration Readiness: {self.readiness_score}/100",
|
|
1275
|
-
f" jakarta
|
|
1276
|
-
f"
|
|
1277
|
-
f"jdk-modernization: {self.jdk_modernization}/100 "
|
|
1278
|
-
f"hibernate: {self.hibernate_readiness}/100",
|
|
1424
|
+
f" {_dim('jakarta')} {_dim('boot3')} "
|
|
1425
|
+
f"{_dim('jdk_modernization')} {_dim('hibernate')}",
|
|
1279
1426
|
*([f" ⚠ Headline blocker: {self.headline_blocker} "
|
|
1280
1427
|
f"(readiness_score reflects jakarta/Boot3 only — Hibernate is a separate rewrite axis)"]
|
|
1281
1428
|
if self.headline_blocker else []),
|
|
1282
|
-
f"Spring Boot 2 detected: {_boot}",
|
|
1283
|
-
f"Blocking issues: {self.blocking_count} "
|
|
1284
|
-
f"(critical: {
|
|
1285
|
-
f"
|
|
1429
|
+
f"Spring present: {self.spring_present} Spring Boot 2 detected: {_boot}",
|
|
1430
|
+
f"Blocking issues (product code): {self.blocking_count} "
|
|
1431
|
+
f"(critical: {main_crit}, high: {main_high})"
|
|
1432
|
+
+ (f" [+{nb} in test/generated, non-blocking]" if nb else ""),
|
|
1286
1433
|
f"Affected files: {self.summary.get('affected_files', 0)}",
|
|
1287
1434
|
f"Estimated effort: {self.estimated_effort_days}d",
|
|
1288
1435
|
"",
|
|
@@ -1314,6 +1461,42 @@ class MigrationReport:
|
|
|
1314
1461
|
# Java source scanner
|
|
1315
1462
|
# ---------------------------------------------------------------------------
|
|
1316
1463
|
|
|
1464
|
+
# BUG #7: javax.annotation.Generated is the JSR-250 marker emitted into
|
|
1465
|
+
# AUTOGENERATED sources (SCIM, JAXB, MapStruct…). It maps mechanically to
|
|
1466
|
+
# jakarta.annotation.Generated and carries low migration value — it must not be
|
|
1467
|
+
# described with the generic "@PostConstruct/@PreDestroy/@Resource" blurb, nor
|
|
1468
|
+
# rated medium alongside real CDI lifecycle annotations.
|
|
1469
|
+
_MIG006_LIFECYCLE = ("PostConstruct", "PreDestroy", "Resource",
|
|
1470
|
+
"ManagedBean", "Priority", "Resources")
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _refine_mig006(matched_imports: list[str]) -> tuple[str, str]:
|
|
1474
|
+
"""Return (severity, explanation) tailored to the actual javax.annotation symbols.
|
|
1475
|
+
|
|
1476
|
+
Explanation names the concrete symbols detected, not a fixed list. When the
|
|
1477
|
+
only symbol is `Generated`, severity degrades to low (mechanical, generated
|
|
1478
|
+
code); otherwise the CDI lifecycle annotations keep medium severity.
|
|
1479
|
+
"""
|
|
1480
|
+
symbols = [imp.rsplit(".", 1)[-1] for imp in matched_imports]
|
|
1481
|
+
lifecycle = [s for s in symbols if s in _MIG006_LIFECYCLE]
|
|
1482
|
+
only_generated = bool(symbols) and all(s == "Generated" for s in symbols)
|
|
1483
|
+
sym_list = ", ".join(f"@{s}" for s in dict.fromkeys(symbols)) or "javax.annotation"
|
|
1484
|
+
if only_generated:
|
|
1485
|
+
return (
|
|
1486
|
+
"low",
|
|
1487
|
+
"javax.annotation.Generated (JSR-250) maps mechanically to "
|
|
1488
|
+
"jakarta.annotation.Generated. It is emitted into autogenerated sources "
|
|
1489
|
+
"and carries low migration value — re-run the generator on the Jakarta "
|
|
1490
|
+
"toolchain rather than hand-editing.",
|
|
1491
|
+
)
|
|
1492
|
+
affected = ", ".join(f"@{s}" for s in dict.fromkeys(lifecycle)) or sym_list
|
|
1493
|
+
return (
|
|
1494
|
+
"medium",
|
|
1495
|
+
f"jakarta.annotation replaces javax.annotation in Jakarta EE 9+. "
|
|
1496
|
+
f"Detected symbol(s): {sym_list}. {affected} are affected.",
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
|
|
1317
1500
|
def _scan_file(
|
|
1318
1501
|
source: str,
|
|
1319
1502
|
rel_path: str,
|
|
@@ -1360,16 +1543,21 @@ def _scan_file(
|
|
|
1360
1543
|
first_line = min(candidate_lines)
|
|
1361
1544
|
all_matches = matched_imports + code_snippets
|
|
1362
1545
|
|
|
1546
|
+
severity = rule.severity
|
|
1547
|
+
explanation = rule.explanation
|
|
1548
|
+
if rule.id == "MIG-006" and matched_imports:
|
|
1549
|
+
severity, explanation = _refine_mig006(matched_imports)
|
|
1550
|
+
|
|
1363
1551
|
findings.append(
|
|
1364
1552
|
MigrationFinding(
|
|
1365
1553
|
id=MigrationFinding.make_id(rule.id, rel_path),
|
|
1366
1554
|
rule_id=rule.id,
|
|
1367
|
-
severity=
|
|
1555
|
+
severity=severity,
|
|
1368
1556
|
title=rule.title,
|
|
1369
1557
|
source_file=rel_path,
|
|
1370
1558
|
first_line=first_line,
|
|
1371
1559
|
imports_found=all_matches,
|
|
1372
|
-
explanation=
|
|
1560
|
+
explanation=explanation,
|
|
1373
1561
|
fix_hint=rule.fix_hint,
|
|
1374
1562
|
migration_target=rule.migration_target,
|
|
1375
1563
|
openrewrite_recipe=rule.openrewrite_recipe,
|
|
@@ -1410,6 +1598,7 @@ def run_migrate_check(
|
|
|
1410
1598
|
limitations: list[str] = []
|
|
1411
1599
|
read_errors = 0
|
|
1412
1600
|
jakarta_import_count = 0
|
|
1601
|
+
spring_import_seen = False
|
|
1413
1602
|
|
|
1414
1603
|
# ── Java source scan ────────────────────────────────────────────────────
|
|
1415
1604
|
for rel_path in file_paths:
|
|
@@ -1422,6 +1611,8 @@ def run_migrate_check(
|
|
|
1422
1611
|
|
|
1423
1612
|
# Jakarta EE 9+ namespace adoption signal (vetoes a false Boot-2 verdict).
|
|
1424
1613
|
jakarta_import_count += len(_JAKARTA_IMPORT_RE.findall(source))
|
|
1614
|
+
if not spring_import_seen and _SPRING_IMPORT_RE.search(source):
|
|
1615
|
+
spring_import_seen = True
|
|
1425
1616
|
|
|
1426
1617
|
file_findings = _scan_file(source, rel_path, _ALL_RULES)
|
|
1427
1618
|
filtered = [f for f in file_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
|
|
@@ -1477,6 +1668,12 @@ def run_migrate_check(
|
|
|
1477
1668
|
limitations.extend(_STATIC_LIMITATIONS)
|
|
1478
1669
|
|
|
1479
1670
|
spring_boot_2, spring_boot_version = _detect_spring_boot(root, jakarta_import_count)
|
|
1671
|
+
spring_present = _detect_spring_present(root, spring_import_seen)
|
|
1672
|
+
|
|
1673
|
+
# BUG #8: classify each finding's code context (main / test / generated) so the
|
|
1674
|
+
# report can keep test fixtures and autogenerated sources out of blocking_count.
|
|
1675
|
+
for f in all_findings:
|
|
1676
|
+
f.code_context = _classify_code_context(f)
|
|
1480
1677
|
|
|
1481
1678
|
# Hibernate 5→6 stratification (independent of min_severity — it is its own
|
|
1482
1679
|
# risk model, not a severity-filtered finding stream).
|
|
@@ -1486,6 +1683,7 @@ def run_migrate_check(
|
|
|
1486
1683
|
report = MigrationReport(
|
|
1487
1684
|
spring_boot_2_detected=spring_boot_2,
|
|
1488
1685
|
spring_boot_version_detected=spring_boot_version,
|
|
1686
|
+
spring_present=spring_present,
|
|
1489
1687
|
findings=all_findings,
|
|
1490
1688
|
hibernate=hibernate_strat,
|
|
1491
1689
|
limitations=limitations,
|
sourcecode/path_filters.py
CHANGED
|
@@ -12,6 +12,16 @@ _TEST_SEGMENTS = frozenset({
|
|
|
12
12
|
"integrationtest", "integrationtests",
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
+
# Whole-module directories that are test harness / fixtures even though their
|
|
16
|
+
# code lives under src/main (e.g. a Maven module that ships a test framework).
|
|
17
|
+
# A finding under one of these modules is test infrastructure, not the product.
|
|
18
|
+
_TEST_MODULE_SEGMENTS = frozenset({
|
|
19
|
+
"testsuite", "test-framework", "testframework",
|
|
20
|
+
"integration-arquillian", "arquillian",
|
|
21
|
+
"test-utils", "test-util", "testutils",
|
|
22
|
+
"test-support", "testsupport",
|
|
23
|
+
})
|
|
24
|
+
|
|
15
25
|
_VENDOR_SEGMENTS = frozenset({
|
|
16
26
|
"vendor", "vendors",
|
|
17
27
|
"third_party", "thirdparty",
|
|
@@ -86,6 +96,29 @@ def is_test_path(path: str) -> bool:
|
|
|
86
96
|
return False
|
|
87
97
|
|
|
88
98
|
|
|
99
|
+
def is_test_or_fixture_path(path: str) -> bool:
|
|
100
|
+
"""Return True when *path* is test code OR test-harness/fixture infrastructure.
|
|
101
|
+
|
|
102
|
+
Broader than is_test_path: also catches whole modules that are test frameworks
|
|
103
|
+
or integration-test harnesses (testsuite/, test-framework/, integration-arquillian/,
|
|
104
|
+
*-test*, *-it modules) even when their sources sit under src/main. Migration
|
|
105
|
+
tooling uses this to keep test fixtures (deprecated web.xml deployment
|
|
106
|
+
descriptors, test-only sun.* HTTP servers) out of the product's blocking count.
|
|
107
|
+
"""
|
|
108
|
+
if is_test_path(path):
|
|
109
|
+
return True
|
|
110
|
+
norm = path.replace("\\", "/").lower()
|
|
111
|
+
parts = norm.split("/")
|
|
112
|
+
for part in parts[:-1]: # skip the filename
|
|
113
|
+
bare = part.rstrip("/")
|
|
114
|
+
if bare in _TEST_MODULE_SEGMENTS:
|
|
115
|
+
return True
|
|
116
|
+
# module dirs like "adapter-test", "foo-tests", "bar-itests"
|
|
117
|
+
if bare.endswith(("-test", "-tests", "-it", "-itest", "-itests")):
|
|
118
|
+
return True
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
89
122
|
def is_vendor_path(path: str) -> bool:
|
|
90
123
|
"""Return True when *path* is inside a vendored / third-party directory.
|
|
91
124
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.65.0
|
|
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
|
|
@@ -411,7 +411,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
|
|
|
411
411
|
|------|--------|
|
|
412
412
|
| `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
|
|
413
413
|
| `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
|
|
414
|
-
| `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `
|
|
414
|
+
| `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`) + `coverage_note`: a count of `0` means "no client construct found", **not** "no integrations" (runtime/DI-wired clients are not statically visible). |
|
|
415
415
|
| `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
|
|
416
416
|
|
|
417
417
|
The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
|
|
@@ -610,7 +610,7 @@ Detects migration blockers across Java source files, Spring XML config files, an
|
|
|
610
610
|
| `MIG-002` | high | `javax.servlet` import — Servlet API changed |
|
|
611
611
|
| `MIG-003` | high | `javax.validation` import — Bean Validation changed |
|
|
612
612
|
| `MIG-004` | high | `javax.transaction` import — TX API changed |
|
|
613
|
-
| `MIG-006` | medium | `javax.annotation` import — CDI annotations changed |
|
|
613
|
+
| `MIG-006` | medium¹ | `javax.annotation` import — CDI annotations changed (¹`javax.annotation.Generated` in autogenerated code → **low**, bucketed `generated`; explanation names the actual symbol) |
|
|
614
614
|
| `MIG-007` | medium | `javax.inject` import — DI annotations changed |
|
|
615
615
|
| `MIG-008` | medium | `javax.ws.rs` import — JAX-RS changed |
|
|
616
616
|
| `MIG-009` | medium | `javax.jms` import — JMS API changed |
|
|
@@ -722,9 +722,39 @@ reported as **N/A** (`score: null`), never as `0`. The top-level
|
|
|
722
722
|
states that `readiness_score` is a derived aggregate over applicable dimensions
|
|
723
723
|
only — for decisions, read the per-dimension breakdown + `blocking_count`.
|
|
724
724
|
|
|
725
|
+
#### Reliability guarantees (no version, no verdict)
|
|
726
|
+
|
|
727
|
+
A migration verdict shown to a decision-maker must never contradict its own
|
|
728
|
+
evidence. `migrate-check` enforces:
|
|
729
|
+
|
|
730
|
+
- **Version-gated framework axes.** The Hibernate 5→6 axis applies only when the
|
|
731
|
+
effective Hibernate version (resolved from `pom.xml`/Gradle, following Maven
|
|
732
|
+
`${properties}` and BOM-managed `<hibernate*.version>`) is `< 6`. A resolved
|
|
733
|
+
Hibernate **6+** repo is reported `migration_applicable: false` (N/A) — no
|
|
734
|
+
`hibernate_rewrite` headline. An **unresolved** version degrades to a
|
|
735
|
+
low-confidence hypothesis with no headline blocker. See
|
|
736
|
+
`hibernate.effective_version` / `version_major` / `version_confidence`.
|
|
737
|
+
- **Boot3 auto-disables without Spring.** `boot3.applicable` tracks real Spring
|
|
738
|
+
usage; Quarkus / Micronaut / Helidon / Jakarta-pure repos report `boot3` as N/A
|
|
739
|
+
(`spring_present: false`), not a contradictory `applicable: true`.
|
|
740
|
+
- **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
|
|
741
|
+
(`javax.xml.*`, `javax.crypto.*`, `javax.naming.*`, `javax.sql.*`,
|
|
742
|
+
`javax.management.*`, `javax.security.auth.*`, …) are allowlisted across every
|
|
743
|
+
jakarta scorer.
|
|
744
|
+
- **Three buckets: blocker ≠ hygiene ≠ test.** Only product (`main`) code counts
|
|
745
|
+
toward `blocking_count`, readiness, and effort. Test harnesses
|
|
746
|
+
(`testsuite/`, `test-framework/`, `integration-arquillian/`, `*-test*`,
|
|
747
|
+
`src/test/`) and autogenerated sources are tagged `code_context` and surfaced in
|
|
748
|
+
`non_blocking{}`. Best-practice hygiene (`java.util.Date`) is reported as
|
|
749
|
+
`hygiene_findings` and never sinks a dimension. Each finding carries
|
|
750
|
+
`code_context: main | test | generated`.
|
|
751
|
+
|
|
725
752
|
```bash
|
|
726
753
|
# inspect only the Hibernate rewrite targets
|
|
727
754
|
sourcecode migrate-check . --format json | jq '.hibernate.rewrite_targets[]'
|
|
755
|
+
|
|
756
|
+
# product blockers only (excludes test/fixtures + hygiene)
|
|
757
|
+
sourcecode migrate-check . --format json | jq '.blocking_count, .non_blocking, .hygiene_findings'
|
|
728
758
|
```
|
|
729
759
|
|
|
730
760
|
### `rename-class` — Java class rename
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=p8-GKiFQV7kVIfsCmKNXmTPDXKUpvscoIAbYoLSteFs,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
@@ -28,15 +28,15 @@ sourcecode/format_contract.py,sha256=1cTNqwP8geA2hbQoBHUPgX3_vSh3l8guJT_jmgEnFF8
|
|
|
28
28
|
sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
|
|
29
29
|
sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
|
|
30
30
|
sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,62403
|
|
31
|
-
sourcecode/hibernate_strat.py,sha256=
|
|
32
|
-
sourcecode/integration_detector.py,sha256=
|
|
31
|
+
sourcecode/hibernate_strat.py,sha256=ZyI0sikxh7IemU8uqn1Z73z-tHeFeCJuYhJnyQL8pzU,57765
|
|
32
|
+
sourcecode/integration_detector.py,sha256=mQulsXN1P-4V_3ueh3xy_N9x3Aqlvm7GQ-3YGqlBEYM,8223
|
|
33
33
|
sourcecode/license.py,sha256=wckiLuiwaE35KMCStUf1gYzleJuFe6qsSyxUQJvit3s,23500
|
|
34
34
|
sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
|
|
35
35
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
36
|
-
sourcecode/migrate_check.py,sha256=
|
|
36
|
+
sourcecode/migrate_check.py,sha256=pv7jZGhHRaw160sWYk0NWR-jWZERl_2S_gFAJ72maFI,80133
|
|
37
37
|
sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
|
|
38
38
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
39
|
-
sourcecode/path_filters.py,sha256=
|
|
39
|
+
sourcecode/path_filters.py,sha256=DMea0KIlmj2pzTkuy-3YYFF4ktP0q5zP9mH4v4uXh84,5237
|
|
40
40
|
sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
|
|
41
41
|
sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
42
42
|
sourcecode/prepare_context.py,sha256=PsRaKeABMh964niCS578WRgcccW9bajnm2yHS9zcKuM,222655
|
|
@@ -103,8 +103,8 @@ sourcecode/telemetry/consent.py,sha256=H5z2Wu63pZqbaKucRPoJQJ0zCo4cke9ZlBrJC-MaW
|
|
|
103
103
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
104
104
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
105
105
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
106
|
-
sourcecode-1.
|
|
107
|
-
sourcecode-1.
|
|
108
|
-
sourcecode-1.
|
|
109
|
-
sourcecode-1.
|
|
110
|
-
sourcecode-1.
|
|
106
|
+
sourcecode-1.65.0.dist-info/METADATA,sha256=tuUMAq5_nLV0J0Zrsl8TAS1NdxLSgunoAtnhlFoYv2U,46300
|
|
107
|
+
sourcecode-1.65.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
108
|
+
sourcecode-1.65.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
109
|
+
sourcecode-1.65.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
110
|
+
sourcecode-1.65.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|