sourcecode 1.35.24__py3-none-any.whl → 1.35.26__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/migrate_check.py +557 -47
- sourcecode/repository_ir.py +52 -1
- sourcecode/spring_event_topology.py +20 -1
- {sourcecode-1.35.24.dist-info → sourcecode-1.35.26.dist-info}/METADATA +3 -3
- {sourcecode-1.35.24.dist-info → sourcecode-1.35.26.dist-info}/RECORD +9 -9
- {sourcecode-1.35.24.dist-info → sourcecode-1.35.26.dist-info}/WHEEL +0 -0
- {sourcecode-1.35.24.dist-info → sourcecode-1.35.26.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.35.24.dist-info → sourcecode-1.35.26.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/migrate_check.py
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
"""migrate_check.py — Java 8/Spring Boot 2 migration readiness checker.
|
|
2
2
|
|
|
3
|
-
Scans Java source files
|
|
3
|
+
Scans Java source files, Spring XML config, and build descriptors for patterns
|
|
4
|
+
that must be addressed when migrating:
|
|
4
5
|
- Spring Boot 2 → 3 (javax → jakarta, Spring Security 6)
|
|
5
6
|
- Java 8 → 17 / 21 (SecurityManager, Nashorn, Unsafe, reflection, etc.)
|
|
7
|
+
- XML Spring config (applicationContext.xml, web.xml, security XML)
|
|
8
|
+
- Dependency incompatibilities (SpringFox, Hibernate 5, ByteBuddy old)
|
|
6
9
|
|
|
7
10
|
Entry point: run_migrate_check(file_paths, root) → MigrationReport
|
|
8
11
|
"""
|
|
9
12
|
from __future__ import annotations
|
|
10
13
|
|
|
14
|
+
import fnmatch
|
|
11
15
|
import hashlib
|
|
16
|
+
import os
|
|
12
17
|
import re
|
|
13
18
|
from dataclasses import dataclass, field
|
|
14
19
|
from datetime import datetime, timezone
|
|
@@ -17,7 +22,7 @@ from typing import Optional
|
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
# ---------------------------------------------------------------------------
|
|
20
|
-
# Rule catalogue
|
|
25
|
+
# Rule catalogue — Java source rules
|
|
21
26
|
# ---------------------------------------------------------------------------
|
|
22
27
|
|
|
23
28
|
@dataclass(frozen=True)
|
|
@@ -27,11 +32,11 @@ class _Rule:
|
|
|
27
32
|
title: str
|
|
28
33
|
explanation: str
|
|
29
34
|
fix_hint: str
|
|
30
|
-
migration_target: str = "spring_boot_3"
|
|
31
|
-
openrewrite_recipe: Optional[str] = None
|
|
32
|
-
import_pattern: Optional[re.Pattern] = None
|
|
33
|
-
extends_pattern: Optional[re.Pattern] = None
|
|
34
|
-
code_pattern: Optional[re.Pattern] = None
|
|
35
|
+
migration_target: str = "spring_boot_3"
|
|
36
|
+
openrewrite_recipe: Optional[str] = None
|
|
37
|
+
import_pattern: Optional[re.Pattern] = None
|
|
38
|
+
extends_pattern: Optional[re.Pattern] = None
|
|
39
|
+
code_pattern: Optional[re.Pattern] = None
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
# ---------------------------------------------------------------------------
|
|
@@ -215,7 +220,7 @@ _SPRING_SECURITY_RULES: list[_Rule] = [
|
|
|
215
220
|
]
|
|
216
221
|
|
|
217
222
|
# ---------------------------------------------------------------------------
|
|
218
|
-
# Java 11 — APIs removed from the JDK
|
|
223
|
+
# Java 11 — APIs removed from the JDK
|
|
219
224
|
# ---------------------------------------------------------------------------
|
|
220
225
|
|
|
221
226
|
_JAVA_11_RULES: list[_Rule] = [
|
|
@@ -252,6 +257,28 @@ _JAVA_11_RULES: list[_Rule] = [
|
|
|
252
257
|
openrewrite_recipe=None,
|
|
253
258
|
import_pattern=re.compile(r"^[ \t]*import\s+(javax\.xml\.ws[^;]+);", re.MULTILINE),
|
|
254
259
|
),
|
|
260
|
+
_Rule(
|
|
261
|
+
id="MIG-023",
|
|
262
|
+
severity="critical",
|
|
263
|
+
title="CORBA APIs (org.omg.* / javax.rmi.*) — removed from JDK in Java 11",
|
|
264
|
+
explanation=(
|
|
265
|
+
"The CORBA APIs (org.omg.* and javax.rmi.CORBA / javax.rmi.ssl) were deprecated "
|
|
266
|
+
"in Java 9 (JEP 289) and removed from the JDK in Java 11 (JEP 320). Applications "
|
|
267
|
+
"importing these packages will fail to compile or run on Java 11+ unless the "
|
|
268
|
+
"'org.glassfish.corba:glassfish-corba-omgapi' artifact is added explicitly."
|
|
269
|
+
),
|
|
270
|
+
fix_hint=(
|
|
271
|
+
"Remove CORBA usage where possible — CORBA is effectively dead technology. "
|
|
272
|
+
"If CORBA interop is unavoidable, add 'org.glassfish.corba:glassfish-corba-omgapi' "
|
|
273
|
+
"as an explicit Maven/Gradle dependency."
|
|
274
|
+
),
|
|
275
|
+
migration_target="java_11",
|
|
276
|
+
openrewrite_recipe=None,
|
|
277
|
+
import_pattern=re.compile(
|
|
278
|
+
r"^[ \t]*import\s+(org\.omg\.[^;]+|javax\.rmi\.CORBA\.[^;]+|javax\.rmi\.ssl\.[^;]+);",
|
|
279
|
+
re.MULTILINE,
|
|
280
|
+
),
|
|
281
|
+
),
|
|
255
282
|
]
|
|
256
283
|
|
|
257
284
|
# ---------------------------------------------------------------------------
|
|
@@ -280,7 +307,7 @@ _JAVA_15_RULES: list[_Rule] = [
|
|
|
280
307
|
]
|
|
281
308
|
|
|
282
309
|
# ---------------------------------------------------------------------------
|
|
283
|
-
# Java 17 — SecurityManager removed (JEP 411)
|
|
310
|
+
# Java 17 — SecurityManager removed (JEP 411), Thread deprecated methods
|
|
284
311
|
# ---------------------------------------------------------------------------
|
|
285
312
|
|
|
286
313
|
_JAVA_17_RULES: list[_Rule] = [
|
|
@@ -304,12 +331,37 @@ _JAVA_17_RULES: list[_Rule] = [
|
|
|
304
331
|
openrewrite_recipe=None,
|
|
305
332
|
code_pattern=re.compile(
|
|
306
333
|
r"System\.(get|set)SecurityManager\s*\(|"
|
|
307
|
-
r"\bSecurityManager\s+\w+\s*[=;({]|"
|
|
334
|
+
r"\bSecurityManager\s+\w+\s*[=;({]|"
|
|
308
335
|
r"\bnew\s+SecurityManager\s*\(|"
|
|
309
336
|
r"\bextends\s+SecurityManager\b|"
|
|
310
337
|
r"\bAccessController\.(doPrivileged|checkPermission|getContext)\s*\(",
|
|
311
338
|
),
|
|
312
339
|
),
|
|
340
|
+
_Rule(
|
|
341
|
+
id="MIG-024",
|
|
342
|
+
severity="medium",
|
|
343
|
+
title="Thread.stop / Thread.suspend / Thread.resume — deprecated for removal (Java 17+)",
|
|
344
|
+
explanation=(
|
|
345
|
+
"Thread.stop(), Thread.suspend(), and Thread.resume() are deprecated since Java 1.2 "
|
|
346
|
+
"and deprecated-for-removal since Java 17 (JEP 411 scope). Thread.stop() is "
|
|
347
|
+
"inherently unsafe — it throws ThreadDeath which can corrupt object state. "
|
|
348
|
+
"Thread.suspend/resume cause deadlocks when the suspended thread holds a monitor. "
|
|
349
|
+
"Note: detection is best-effort; confirm the variable type is java.lang.Thread."
|
|
350
|
+
),
|
|
351
|
+
fix_hint=(
|
|
352
|
+
"Use Thread.interrupt() with InterruptedException for cooperative cancellation. "
|
|
353
|
+
"Replace suspend/resume patterns with wait()/notify(), Semaphore, or a higher-level "
|
|
354
|
+
"concurrency abstraction (BlockingQueue, CountDownLatch, etc.)."
|
|
355
|
+
),
|
|
356
|
+
migration_target="java_17",
|
|
357
|
+
openrewrite_recipe=None,
|
|
358
|
+
code_pattern=re.compile(
|
|
359
|
+
r"\b(?:thread|[a-zA-Z]\w*[Tt]hread)\.(stop|suspend|resume)\s*\("
|
|
360
|
+
r"|\bnew\s+Thread\s*\([^)]{0,120}\)\s*\.(stop|suspend|resume)\s*\("
|
|
361
|
+
r"|\bThread\.currentThread\s*\(\)\s*\.(stop|suspend|resume)\s*\(",
|
|
362
|
+
re.MULTILINE,
|
|
363
|
+
),
|
|
364
|
+
),
|
|
313
365
|
]
|
|
314
366
|
|
|
315
367
|
# ---------------------------------------------------------------------------
|
|
@@ -382,6 +434,32 @@ _JAVA_9_RULES: list[_Rule] = [
|
|
|
382
434
|
openrewrite_recipe=None,
|
|
383
435
|
code_pattern=re.compile(r"\.setAccessible\s*\(\s*true\s*\)"),
|
|
384
436
|
),
|
|
437
|
+
_Rule(
|
|
438
|
+
id="MIG-025",
|
|
439
|
+
severity="medium",
|
|
440
|
+
title="ReflectionFactory / MethodHandles.privateLookupIn — deep-reflection JPMS risk",
|
|
441
|
+
explanation=(
|
|
442
|
+
"sun.reflect.ReflectionFactory bypasses module encapsulation and is not part of "
|
|
443
|
+
"the public API. MethodHandles.privateLookupIn() grants private lookup access that "
|
|
444
|
+
"requires --add-opens on Java 9+. Both patterns are common in serialization "
|
|
445
|
+
"frameworks and mocking libraries and may break under strict JPMS modules."
|
|
446
|
+
),
|
|
447
|
+
fix_hint=(
|
|
448
|
+
"Replace sun.reflect.ReflectionFactory with MethodHandles.lookup() or VarHandle. "
|
|
449
|
+
"For MethodHandles.privateLookupIn, ensure the calling module has been opened "
|
|
450
|
+
"via 'opens <package> to <module>' in module-info.java."
|
|
451
|
+
),
|
|
452
|
+
migration_target="java_9_plus",
|
|
453
|
+
openrewrite_recipe=None,
|
|
454
|
+
import_pattern=re.compile(
|
|
455
|
+
r"^[ \t]*import\s+(sun\.reflect\.ReflectionFactory[^;]*);",
|
|
456
|
+
re.MULTILINE,
|
|
457
|
+
),
|
|
458
|
+
code_pattern=re.compile(
|
|
459
|
+
r"\bReflectionFactory\s*\.\s*getReflectionFactory\s*\("
|
|
460
|
+
r"|\bMethodHandles\s*\.\s*privateLookupIn\s*\(",
|
|
461
|
+
),
|
|
462
|
+
),
|
|
385
463
|
]
|
|
386
464
|
|
|
387
465
|
# ---------------------------------------------------------------------------
|
|
@@ -404,7 +482,7 @@ _JAVA_18_RULES: list[_Rule] = [
|
|
|
404
482
|
"java.lang.ref.Cleaner for resource cleanup."
|
|
405
483
|
),
|
|
406
484
|
migration_target="java_18_plus",
|
|
407
|
-
openrewrite_recipe=
|
|
485
|
+
openrewrite_recipe="org.openrewrite.java.migrate.RemoveFinalizeMethod",
|
|
408
486
|
code_pattern=re.compile(
|
|
409
487
|
r"\b(?:protected|public)\s+void\s+finalize\s*\(\s*\)",
|
|
410
488
|
),
|
|
@@ -442,7 +520,7 @@ _LEGACY_API_RULES: list[_Rule] = [
|
|
|
442
520
|
]
|
|
443
521
|
|
|
444
522
|
# ---------------------------------------------------------------------------
|
|
445
|
-
# All rules
|
|
523
|
+
# All Java source rules
|
|
446
524
|
# ---------------------------------------------------------------------------
|
|
447
525
|
|
|
448
526
|
_ALL_RULES: list[_Rule] = (
|
|
@@ -459,19 +537,407 @@ _ALL_RULES: list[_Rule] = (
|
|
|
459
537
|
SEVERITY_ORDER: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
460
538
|
|
|
461
539
|
|
|
540
|
+
# ---------------------------------------------------------------------------
|
|
541
|
+
# XML config rules (applied to Spring XML config files)
|
|
542
|
+
# ---------------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
@dataclass(frozen=True)
|
|
545
|
+
class _XmlRule:
|
|
546
|
+
id: str
|
|
547
|
+
severity: str
|
|
548
|
+
title: str
|
|
549
|
+
explanation: str
|
|
550
|
+
fix_hint: str
|
|
551
|
+
migration_target: str
|
|
552
|
+
openrewrite_recipe: Optional[str] = None
|
|
553
|
+
pattern: Optional[re.Pattern] = None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
_XML_RULES: list[_XmlRule] = [
|
|
557
|
+
_XmlRule(
|
|
558
|
+
id="MIG-030",
|
|
559
|
+
severity="high",
|
|
560
|
+
title="javax.* class reference in Spring XML config — namespace not migrated",
|
|
561
|
+
explanation=(
|
|
562
|
+
"Spring XML bean definitions using class='javax.*' reference the old Java EE "
|
|
563
|
+
"namespace. When the application migrates to Spring Boot 3 / Jakarta EE 9+, these "
|
|
564
|
+
"bean class names must be updated to use the jakarta.* namespace equivalents. "
|
|
565
|
+
"Typical occurrences: persistence providers, validators, transaction managers."
|
|
566
|
+
),
|
|
567
|
+
fix_hint=(
|
|
568
|
+
"Update class='javax.*' attributes in XML bean definitions to the corresponding "
|
|
569
|
+
"jakarta.* class names. Run OpenRewrite or grep for 'javax.' in all XML config files."
|
|
570
|
+
),
|
|
571
|
+
migration_target="jakarta",
|
|
572
|
+
openrewrite_recipe=None,
|
|
573
|
+
pattern=re.compile(
|
|
574
|
+
r'(?:class|type|value)\s*=\s*["\'][^"\']*\bjavax\.[a-zA-Z]',
|
|
575
|
+
re.MULTILINE,
|
|
576
|
+
),
|
|
577
|
+
),
|
|
578
|
+
_XmlRule(
|
|
579
|
+
id="MIG-031",
|
|
580
|
+
severity="high",
|
|
581
|
+
title="Spring Security XML — old-style <http auto-config> or versioned schema ≤5",
|
|
582
|
+
explanation=(
|
|
583
|
+
"XML-based Spring Security configuration using <http auto-config='true'> or "
|
|
584
|
+
"pointing to a spring-security-[3-5].x.xsd schema requires significant migration "
|
|
585
|
+
"for Spring Security 6 (Spring Boot 3). The auto-config shortcut and many XML "
|
|
586
|
+
"namespace attributes were changed or removed in Spring Security 6."
|
|
587
|
+
),
|
|
588
|
+
fix_hint=(
|
|
589
|
+
"Migrate XML security config to Java-based @Configuration with SecurityFilterChain "
|
|
590
|
+
"@Bean. See the Spring Security 6 XML migration guide. "
|
|
591
|
+
"Update schema references to spring-security.xsd (no version) or use Spring Security 6 schemas."
|
|
592
|
+
),
|
|
593
|
+
migration_target="spring_security_6",
|
|
594
|
+
openrewrite_recipe="org.openrewrite.java.spring.security6.WebSecurityConfigurerAdapterToSecurityFilterChain",
|
|
595
|
+
pattern=re.compile(
|
|
596
|
+
r"<(?:\w+:)?http\s[^>]*auto-config\s*=\s*[\"']true[\"']"
|
|
597
|
+
r"|spring-security-[2345]\.\d+\.xsd",
|
|
598
|
+
re.IGNORECASE | re.MULTILINE,
|
|
599
|
+
),
|
|
600
|
+
),
|
|
601
|
+
_XmlRule(
|
|
602
|
+
id="MIG-032",
|
|
603
|
+
severity="high",
|
|
604
|
+
title="web.xml with Servlet ≤4 namespace — javax.servlet, must migrate to jakarta",
|
|
605
|
+
explanation=(
|
|
606
|
+
"A web.xml using the Java EE namespace (java.sun.com/xml/ns/javaee or "
|
|
607
|
+
"xmlns.jcp.org/xml/ns/javaee) declares a Servlet 2.x/3.x/4.x deployment descriptor. "
|
|
608
|
+
"These namespaces map to javax.servlet. Spring Boot 3 requires Jakarta Servlet 5.0+ "
|
|
609
|
+
"(namespace: jakarta.ee/xml/ns/jakartaee). The deployment descriptor must be updated."
|
|
610
|
+
),
|
|
611
|
+
fix_hint=(
|
|
612
|
+
"Update web.xml namespace from 'http://xmlns.jcp.org/xml/ns/javaee' to "
|
|
613
|
+
"'https://jakarta.ee/xml/ns/jakartaee' and set version='5.0' or '6.0'. "
|
|
614
|
+
"Update all filter-class and servlet-class entries from javax.* to jakarta.* equivalents."
|
|
615
|
+
),
|
|
616
|
+
migration_target="jakarta",
|
|
617
|
+
openrewrite_recipe=None,
|
|
618
|
+
pattern=re.compile(
|
|
619
|
+
r'xmlns\s*=\s*["\']https?://(?:java\.sun\.com|xmlns\.jcp\.org)/xml/ns/javaee["\']',
|
|
620
|
+
re.IGNORECASE | re.MULTILINE,
|
|
621
|
+
),
|
|
622
|
+
),
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
# XML files to scan: name-based heuristic (avoids scanning unrelated XML like Maven reports)
|
|
626
|
+
_XML_FILE_GLOBS: tuple[str, ...] = (
|
|
627
|
+
"web.xml",
|
|
628
|
+
"applicationContext.xml",
|
|
629
|
+
"applicationContext-*.xml",
|
|
630
|
+
"*applicationContext*.xml",
|
|
631
|
+
"*-context.xml",
|
|
632
|
+
"*Context.xml",
|
|
633
|
+
"*-config.xml",
|
|
634
|
+
"*Config.xml",
|
|
635
|
+
"*security*.xml",
|
|
636
|
+
"*Security*.xml",
|
|
637
|
+
"*servlet*.xml",
|
|
638
|
+
"*Servlet*.xml",
|
|
639
|
+
"beans.xml",
|
|
640
|
+
"*-beans.xml",
|
|
641
|
+
"*spring*.xml",
|
|
642
|
+
"*Spring*.xml",
|
|
643
|
+
"*dispatcher*.xml",
|
|
644
|
+
"*Dispatcher*.xml",
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
_SKIP_DIRS: frozenset[str] = frozenset([
|
|
648
|
+
"target", "build", ".git", ".gradle", ".mvn",
|
|
649
|
+
"node_modules", "__pycache__", ".idea", ".vscode",
|
|
650
|
+
"out", "dist", "bin", "generated-sources",
|
|
651
|
+
])
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _is_spring_xml_candidate(fname: str) -> bool:
|
|
655
|
+
return any(fnmatch.fnmatch(fname, g) for g in _XML_FILE_GLOBS)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _find_xml_config_files(root: Path) -> list[tuple[Path, str]]:
|
|
659
|
+
"""Compatibility shim — calls the combined scanner."""
|
|
660
|
+
xml_files, _ = _find_non_java_files(root)
|
|
661
|
+
return xml_files
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _find_build_files(root: Path) -> list[tuple[Path, str]]:
|
|
665
|
+
"""Compatibility shim — calls the combined scanner."""
|
|
666
|
+
_, build_files = _find_non_java_files(root)
|
|
667
|
+
return build_files
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _find_non_java_files(
|
|
671
|
+
root: Path,
|
|
672
|
+
) -> tuple[list[tuple[Path, str]], list[tuple[Path, str]]]:
|
|
673
|
+
"""Single os.walk returning (xml_config_files, build_files), excluding build dirs."""
|
|
674
|
+
xml_files: list[tuple[Path, str]] = []
|
|
675
|
+
build_files: list[tuple[Path, str]] = []
|
|
676
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
677
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
678
|
+
dp = Path(dirpath)
|
|
679
|
+
try:
|
|
680
|
+
rel_dir = dp.relative_to(root)
|
|
681
|
+
except ValueError:
|
|
682
|
+
continue
|
|
683
|
+
rel_prefix = str(rel_dir) if str(rel_dir) != "." else ""
|
|
684
|
+
for fname in filenames:
|
|
685
|
+
rel = f"{rel_prefix}/{fname}" if rel_prefix else fname
|
|
686
|
+
abs_path = dp / fname
|
|
687
|
+
if fname.endswith(".xml"):
|
|
688
|
+
if fname == "pom.xml":
|
|
689
|
+
build_files.append((abs_path, rel))
|
|
690
|
+
elif _is_spring_xml_candidate(fname):
|
|
691
|
+
xml_files.append((abs_path, rel))
|
|
692
|
+
elif fname in ("build.gradle", "build.gradle.kts"):
|
|
693
|
+
build_files.append((abs_path, rel))
|
|
694
|
+
return xml_files, build_files
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _scan_xml_file(text: str, rel_path: str) -> list["MigrationFinding"]:
|
|
698
|
+
"""Apply XML rules to raw XML text. Returns one finding per matched rule."""
|
|
699
|
+
findings: list[MigrationFinding] = []
|
|
700
|
+
for rule in _XML_RULES:
|
|
701
|
+
if rule.pattern is None:
|
|
702
|
+
continue
|
|
703
|
+
matches = list(rule.pattern.finditer(text))
|
|
704
|
+
if not matches:
|
|
705
|
+
continue
|
|
706
|
+
first_line = text[: matches[0].start()].count("\n") + 1
|
|
707
|
+
snippets = [m.group(0)[:120].strip() for m in matches[:5]]
|
|
708
|
+
findings.append(
|
|
709
|
+
MigrationFinding(
|
|
710
|
+
id=MigrationFinding.make_id(rule.id, rel_path),
|
|
711
|
+
rule_id=rule.id,
|
|
712
|
+
severity=rule.severity,
|
|
713
|
+
title=rule.title,
|
|
714
|
+
source_file=rel_path,
|
|
715
|
+
first_line=first_line,
|
|
716
|
+
imports_found=snippets,
|
|
717
|
+
explanation=rule.explanation,
|
|
718
|
+
fix_hint=rule.fix_hint,
|
|
719
|
+
migration_target=rule.migration_target,
|
|
720
|
+
openrewrite_recipe=rule.openrewrite_recipe,
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
return findings
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# ---------------------------------------------------------------------------
|
|
727
|
+
# Dependency rules (applied to pom.xml / build.gradle / build.gradle.kts)
|
|
728
|
+
# ---------------------------------------------------------------------------
|
|
729
|
+
|
|
730
|
+
@dataclass(frozen=True)
|
|
731
|
+
class _DepRule:
|
|
732
|
+
id: str
|
|
733
|
+
severity: str
|
|
734
|
+
title: str
|
|
735
|
+
explanation: str
|
|
736
|
+
fix_hint: str
|
|
737
|
+
migration_target: str
|
|
738
|
+
openrewrite_recipe: Optional[str] = None
|
|
739
|
+
# Patterns applied to raw build file text.
|
|
740
|
+
# Each is tried independently; first match wins.
|
|
741
|
+
maven_pattern: Optional[re.Pattern] = None
|
|
742
|
+
gradle_pattern: Optional[re.Pattern] = None
|
|
743
|
+
# Optional fast pre-check: skip expensive regex if this string is absent.
|
|
744
|
+
quick_filter: Optional[str] = None
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
_DEP_RULES: list[_DepRule] = [
|
|
748
|
+
_DepRule(
|
|
749
|
+
id="MIG-040",
|
|
750
|
+
severity="high",
|
|
751
|
+
title="SpringFox (io.springfox) — incompatible with Spring Boot 3 / Spring Framework 6",
|
|
752
|
+
explanation=(
|
|
753
|
+
"SpringFox relies on Spring MVC internal request mapping infrastructure that was "
|
|
754
|
+
"removed in Spring Framework 6. Applications declaring io.springfox:springfox-* "
|
|
755
|
+
"dependencies will fail to start after migration to Spring Boot 3, even if the "
|
|
756
|
+
"Java source code compiles cleanly."
|
|
757
|
+
),
|
|
758
|
+
fix_hint=(
|
|
759
|
+
"Replace springfox-swagger2 + springfox-swagger-ui with "
|
|
760
|
+
"springdoc-openapi-starter-webmvc-ui (OpenAPI 3). "
|
|
761
|
+
"Also remove @EnableSwagger2 and any SpringFox Docket configuration beans."
|
|
762
|
+
),
|
|
763
|
+
migration_target="spring_boot_3",
|
|
764
|
+
openrewrite_recipe=None,
|
|
765
|
+
maven_pattern=re.compile(r"\bio\.springfox\b", re.IGNORECASE),
|
|
766
|
+
gradle_pattern=re.compile(r"\bio\.springfox\b", re.IGNORECASE),
|
|
767
|
+
),
|
|
768
|
+
_DepRule(
|
|
769
|
+
id="MIG-041",
|
|
770
|
+
severity="high",
|
|
771
|
+
title="Hibernate 5.x explicitly pinned — Spring Boot 3 requires Hibernate 6",
|
|
772
|
+
explanation=(
|
|
773
|
+
"Spring Boot 3 ships with Hibernate 6.x as the JPA provider, which implements "
|
|
774
|
+
"Jakarta Persistence 3.0. An explicit <version>5.*</version> for hibernate-core "
|
|
775
|
+
"overrides the Spring Boot BOM and will cause runtime incompatibilities: Hibernate 5 "
|
|
776
|
+
"implements javax.persistence (not jakarta.persistence)."
|
|
777
|
+
),
|
|
778
|
+
fix_hint=(
|
|
779
|
+
"Remove the explicit Hibernate version override and let the Spring Boot 3 BOM "
|
|
780
|
+
"manage it (Hibernate 6.x). Review breaking API changes between Hibernate 5 and 6 "
|
|
781
|
+
"in the Hibernate 6 migration guide."
|
|
782
|
+
),
|
|
783
|
+
migration_target="jakarta",
|
|
784
|
+
openrewrite_recipe=None,
|
|
785
|
+
maven_pattern=re.compile(
|
|
786
|
+
r"<dependency>(?:(?!</dependency>).)*?hibernate-core(?![-\w])(?:(?!</dependency>).)*?"
|
|
787
|
+
r"<version>\s*5\.",
|
|
788
|
+
re.DOTALL | re.IGNORECASE,
|
|
789
|
+
),
|
|
790
|
+
gradle_pattern=re.compile(
|
|
791
|
+
r"""['"](org\.hibernate(?:\.orm)?):hibernate-core:5\.""",
|
|
792
|
+
re.IGNORECASE,
|
|
793
|
+
),
|
|
794
|
+
quick_filter="hibernate-core",
|
|
795
|
+
),
|
|
796
|
+
_DepRule(
|
|
797
|
+
id="MIG-042",
|
|
798
|
+
severity="medium",
|
|
799
|
+
title="ByteBuddy < 1.12.x — may not support Java 17+ strong encapsulation",
|
|
800
|
+
explanation=(
|
|
801
|
+
"ByteBuddy versions before 1.12 lack stable support for Java 17+ strong JPMS "
|
|
802
|
+
"encapsulation. Spring AOP, Mockito, and Hibernate proxies all depend on ByteBuddy "
|
|
803
|
+
"internally. If an application pins byte-buddy at 1.0–1.11.x, proxy creation "
|
|
804
|
+
"may fail with InaccessibleObjectException on Java 17+."
|
|
805
|
+
),
|
|
806
|
+
fix_hint=(
|
|
807
|
+
"Remove explicit ByteBuddy version overrides and let Spring Boot 3 BOM manage it "
|
|
808
|
+
"(ships with 1.14.x+). If you must pin it, use >= 1.12.18."
|
|
809
|
+
),
|
|
810
|
+
migration_target="java_17",
|
|
811
|
+
openrewrite_recipe=None,
|
|
812
|
+
maven_pattern=re.compile(
|
|
813
|
+
r"<dependency>(?:(?!</dependency>).)*?byte-buddy(?:(?!</dependency>).)*?"
|
|
814
|
+
r"<version>\s*1\.(?:[0-9]|1[01])\.",
|
|
815
|
+
re.DOTALL | re.IGNORECASE,
|
|
816
|
+
),
|
|
817
|
+
gradle_pattern=re.compile(
|
|
818
|
+
r"""['"](net\.bytebuddy):byte-buddy:1\.(?:[0-9]|1[01])\.""",
|
|
819
|
+
re.IGNORECASE,
|
|
820
|
+
),
|
|
821
|
+
quick_filter="byte-buddy",
|
|
822
|
+
),
|
|
823
|
+
_DepRule(
|
|
824
|
+
id="MIG-043",
|
|
825
|
+
severity="high",
|
|
826
|
+
title="EhCache 2.x — incompatible with Spring Boot 3 / JCache JSR-107 migration",
|
|
827
|
+
explanation=(
|
|
828
|
+
"EhCache 2.x (net.sf.ehcache) uses the old JSR-107 cache API and is not compatible "
|
|
829
|
+
"with the Spring Boot 3 cache abstraction. Spring Boot 3 requires EhCache 3.x "
|
|
830
|
+
"(org.ehcache) which implements JCache 1.1 and uses a different configuration format."
|
|
831
|
+
),
|
|
832
|
+
fix_hint=(
|
|
833
|
+
"Migrate from net.sf.ehcache:ehcache to org.ehcache:ehcache:3.x. "
|
|
834
|
+
"Update ehcache.xml configuration to the EhCache 3 XML format. "
|
|
835
|
+
"Add the 'org.ehcache:ehcache::jakarta' classifier for Jakarta EE compatibility."
|
|
836
|
+
),
|
|
837
|
+
migration_target="spring_boot_3",
|
|
838
|
+
openrewrite_recipe=None,
|
|
839
|
+
maven_pattern=re.compile(
|
|
840
|
+
r"<groupId>\s*net\.sf\.ehcache\s*</groupId>",
|
|
841
|
+
re.IGNORECASE,
|
|
842
|
+
),
|
|
843
|
+
gradle_pattern=re.compile(
|
|
844
|
+
r"""['"](net\.sf\.ehcache):[^'"]+""",
|
|
845
|
+
re.IGNORECASE,
|
|
846
|
+
),
|
|
847
|
+
),
|
|
848
|
+
]
|
|
849
|
+
|
|
850
|
+
_BUILD_FILE_NAMES: tuple[str, ...] = ("pom.xml", "build.gradle", "build.gradle.kts")
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _find_build_files(root: Path) -> list[tuple[Path, str]]:
|
|
854
|
+
"""Return (abs_path, rel_path) for pom.xml / build.gradle files, excluding build dirs."""
|
|
855
|
+
results: list[tuple[Path, str]] = []
|
|
856
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
857
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
858
|
+
dp = Path(dirpath)
|
|
859
|
+
try:
|
|
860
|
+
rel_dir = dp.relative_to(root)
|
|
861
|
+
except ValueError:
|
|
862
|
+
continue
|
|
863
|
+
for fname in filenames:
|
|
864
|
+
if fname in _BUILD_FILE_NAMES:
|
|
865
|
+
abs_path = dp / fname
|
|
866
|
+
rel = str(rel_dir / fname) if str(rel_dir) != "." else fname
|
|
867
|
+
results.append((abs_path, rel))
|
|
868
|
+
return results
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _resolve_maven_properties(text: str) -> str:
|
|
872
|
+
"""Substitute ${prop} references with values from the <properties> block.
|
|
873
|
+
|
|
874
|
+
Handles single-level property references that appear in the same pom.xml.
|
|
875
|
+
Multi-level references (${a} where a=${b}) are resolved up to 3 passes.
|
|
876
|
+
"""
|
|
877
|
+
props: dict[str, str] = {}
|
|
878
|
+
for m in re.finditer(r'<([A-Za-z][\w.\-]*)>\s*([^<${}]+?)\s*</\1>', text):
|
|
879
|
+
props[m.group(1)] = m.group(2).strip()
|
|
880
|
+
if not props:
|
|
881
|
+
return text
|
|
882
|
+
|
|
883
|
+
resolved = text
|
|
884
|
+
for _ in range(3):
|
|
885
|
+
def _sub(m: re.Match) -> str: # noqa: E306
|
|
886
|
+
return props.get(m.group(1), m.group(0))
|
|
887
|
+
resolved_new = re.sub(r'\$\{([\w.\-]+)\}', _sub, resolved)
|
|
888
|
+
if resolved_new == resolved:
|
|
889
|
+
break
|
|
890
|
+
resolved = resolved_new
|
|
891
|
+
return resolved
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _scan_dep_file(text: str, rel_path: str) -> list["MigrationFinding"]:
|
|
895
|
+
"""Apply dependency rules to a build file. Returns one finding per matched rule."""
|
|
896
|
+
is_gradle = rel_path.endswith((".gradle", ".gradle.kts"))
|
|
897
|
+
if not is_gradle and rel_path.endswith(".xml"):
|
|
898
|
+
text = _resolve_maven_properties(text)
|
|
899
|
+
findings: list[MigrationFinding] = []
|
|
900
|
+
for rule in _DEP_RULES:
|
|
901
|
+
if rule.quick_filter is not None and rule.quick_filter not in text:
|
|
902
|
+
continue
|
|
903
|
+
pattern = rule.gradle_pattern if is_gradle else rule.maven_pattern
|
|
904
|
+
if pattern is None:
|
|
905
|
+
continue
|
|
906
|
+
m = pattern.search(text)
|
|
907
|
+
if m is None:
|
|
908
|
+
continue
|
|
909
|
+
first_line = text[: m.start()].count("\n") + 1
|
|
910
|
+
findings.append(
|
|
911
|
+
MigrationFinding(
|
|
912
|
+
id=MigrationFinding.make_id(rule.id, rel_path),
|
|
913
|
+
rule_id=rule.id,
|
|
914
|
+
severity=rule.severity,
|
|
915
|
+
title=rule.title,
|
|
916
|
+
source_file=rel_path,
|
|
917
|
+
first_line=first_line,
|
|
918
|
+
imports_found=[m.group(0)[:120].strip()],
|
|
919
|
+
explanation=rule.explanation,
|
|
920
|
+
fix_hint=rule.fix_hint,
|
|
921
|
+
migration_target=rule.migration_target,
|
|
922
|
+
openrewrite_recipe=rule.openrewrite_recipe,
|
|
923
|
+
)
|
|
924
|
+
)
|
|
925
|
+
return findings
|
|
926
|
+
|
|
927
|
+
|
|
462
928
|
# ---------------------------------------------------------------------------
|
|
463
929
|
# Finding
|
|
464
930
|
# ---------------------------------------------------------------------------
|
|
465
931
|
|
|
466
932
|
@dataclass
|
|
467
933
|
class MigrationFinding:
|
|
468
|
-
id: str
|
|
469
|
-
rule_id: str
|
|
470
|
-
severity: str
|
|
934
|
+
id: str
|
|
935
|
+
rule_id: str
|
|
936
|
+
severity: str
|
|
471
937
|
title: str
|
|
472
|
-
source_file: str
|
|
473
|
-
first_line: int
|
|
474
|
-
imports_found: list[str] = field(default_factory=list)
|
|
938
|
+
source_file: str
|
|
939
|
+
first_line: int
|
|
940
|
+
imports_found: list[str] = field(default_factory=list)
|
|
475
941
|
explanation: str = ""
|
|
476
942
|
fix_hint: str = ""
|
|
477
943
|
migration_target: str = ""
|
|
@@ -493,11 +959,14 @@ class MigrationFinding:
|
|
|
493
959
|
"explanation": self.explanation,
|
|
494
960
|
"fix_hint": self.fix_hint,
|
|
495
961
|
"migration_target": self.migration_target,
|
|
962
|
+
"auto_fix_available": bool(self.openrewrite_recipe),
|
|
496
963
|
}
|
|
497
964
|
if self.imports_found:
|
|
498
965
|
d["imports_found"] = self.imports_found
|
|
499
966
|
if self.openrewrite_recipe:
|
|
500
967
|
d["openrewrite_recipe"] = self.openrewrite_recipe
|
|
968
|
+
else:
|
|
969
|
+
d["manual_migration"] = True
|
|
501
970
|
return d
|
|
502
971
|
|
|
503
972
|
|
|
@@ -507,14 +976,13 @@ class MigrationFinding:
|
|
|
507
976
|
|
|
508
977
|
@dataclass
|
|
509
978
|
class MigrationReport:
|
|
510
|
-
schema_version: str = "1.
|
|
979
|
+
schema_version: str = "1.2"
|
|
511
980
|
generated_at: str = ""
|
|
512
981
|
repo_id: str = ""
|
|
513
982
|
git_head: str = ""
|
|
514
983
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
blocking_count: int = 0 # critical + high finding count
|
|
984
|
+
readiness_score: int = 100
|
|
985
|
+
blocking_count: int = 0
|
|
518
986
|
estimated_effort_days: float = 0.0
|
|
519
987
|
spring_boot_2_detected: bool = False
|
|
520
988
|
|
|
@@ -540,8 +1008,6 @@ class MigrationReport:
|
|
|
540
1008
|
|
|
541
1009
|
self.blocking_count = by_severity["critical"] + by_severity["high"]
|
|
542
1010
|
|
|
543
|
-
# Score: deduct per affected-file/severity combination (not per finding, to avoid
|
|
544
|
-
# double-counting a file that imports 10 javax.persistence classes).
|
|
545
1011
|
critical_files: set[str] = set()
|
|
546
1012
|
high_files: set[str] = set()
|
|
547
1013
|
medium_files: set[str] = set()
|
|
@@ -564,7 +1030,6 @@ class MigrationReport:
|
|
|
564
1030
|
)
|
|
565
1031
|
self.readiness_score = max(0, 100 - deduction)
|
|
566
1032
|
|
|
567
|
-
# Effort: sum per distinct affected file weighted by severity
|
|
568
1033
|
self.estimated_effort_days = round(
|
|
569
1034
|
len(critical_files) * 0.5
|
|
570
1035
|
+ len(high_files) * 0.25
|
|
@@ -631,7 +1096,7 @@ class MigrationReport:
|
|
|
631
1096
|
|
|
632
1097
|
|
|
633
1098
|
# ---------------------------------------------------------------------------
|
|
634
|
-
#
|
|
1099
|
+
# Java source scanner
|
|
635
1100
|
# ---------------------------------------------------------------------------
|
|
636
1101
|
|
|
637
1102
|
def _scan_file(
|
|
@@ -642,8 +1107,6 @@ def _scan_file(
|
|
|
642
1107
|
findings: list[MigrationFinding] = []
|
|
643
1108
|
|
|
644
1109
|
for rule in rules:
|
|
645
|
-
# An import_pattern and code_pattern can coexist on the same rule (OR semantics).
|
|
646
|
-
# A finding is created if EITHER matches; we report the earliest match position.
|
|
647
1110
|
matched_imports: list[str] = []
|
|
648
1111
|
import_first_line: Optional[int] = None
|
|
649
1112
|
code_first_line: Optional[int] = None
|
|
@@ -661,14 +1124,12 @@ def _scan_file(
|
|
|
661
1124
|
code_first_line = source[: m.start()].count("\n") + 1
|
|
662
1125
|
code_snippets = [m.group(0).strip()]
|
|
663
1126
|
|
|
664
|
-
# extends_pattern is a legacy form of code_pattern
|
|
665
1127
|
extends_first_line: Optional[int] = None
|
|
666
1128
|
if rule.extends_pattern is not None:
|
|
667
1129
|
m = rule.extends_pattern.search(source)
|
|
668
1130
|
if m is not None:
|
|
669
1131
|
extends_first_line = source[: m.start()].count("\n") + 1
|
|
670
1132
|
|
|
671
|
-
# Determine overall match
|
|
672
1133
|
candidate_lines = [
|
|
673
1134
|
ln for ln in (import_first_line, code_first_line, extends_first_line)
|
|
674
1135
|
if ln is not None
|
|
@@ -708,13 +1169,17 @@ def run_migrate_check(
|
|
|
708
1169
|
*,
|
|
709
1170
|
min_severity: str = "low",
|
|
710
1171
|
) -> MigrationReport:
|
|
711
|
-
"""Scan Java
|
|
1172
|
+
"""Scan a Java repository for migration blockers.
|
|
1173
|
+
|
|
1174
|
+
Scans:
|
|
1175
|
+
- Java source files (.java) against all 24 rules (MIG-001..MIG-025)
|
|
1176
|
+
- Spring XML config files (applicationContext.xml, web.xml, security XML, etc.)
|
|
1177
|
+
- Build descriptors (pom.xml, build.gradle) for incompatible dependencies
|
|
712
1178
|
|
|
713
1179
|
Args:
|
|
714
1180
|
file_paths: Relative Java file paths (from find_java_files).
|
|
715
1181
|
root: Absolute repo root.
|
|
716
|
-
min_severity: Filter threshold
|
|
717
|
-
from the report. Choices: critical | high | medium | low.
|
|
1182
|
+
min_severity: Filter threshold. Choices: critical | high | medium | low.
|
|
718
1183
|
|
|
719
1184
|
Returns:
|
|
720
1185
|
MigrationReport with findings, readiness_score, effort estimate, and
|
|
@@ -725,6 +1190,7 @@ def run_migrate_check(
|
|
|
725
1190
|
limitations: list[str] = []
|
|
726
1191
|
read_errors = 0
|
|
727
1192
|
|
|
1193
|
+
# ── Java source scan ────────────────────────────────────────────────────
|
|
728
1194
|
for rel_path in file_paths:
|
|
729
1195
|
abs_path = root / rel_path
|
|
730
1196
|
try:
|
|
@@ -734,16 +1200,58 @@ def run_migrate_check(
|
|
|
734
1200
|
continue
|
|
735
1201
|
|
|
736
1202
|
file_findings = _scan_file(source, rel_path, _ALL_RULES)
|
|
737
|
-
# Apply min_severity filter
|
|
738
1203
|
filtered = [f for f in file_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
|
|
739
1204
|
all_findings.extend(filtered)
|
|
740
1205
|
|
|
741
1206
|
if read_errors:
|
|
742
1207
|
limitations.append(f"{read_errors} file(s) could not be read and were skipped.")
|
|
743
1208
|
|
|
1209
|
+
# ── XML + dependency scan (single tree walk) ─────────────────────────────
|
|
1210
|
+
xml_files, build_files = _find_non_java_files(root)
|
|
1211
|
+
xml_read_errors = 0
|
|
1212
|
+
for abs_path, rel_path in xml_files:
|
|
1213
|
+
try:
|
|
1214
|
+
text = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
1215
|
+
except OSError:
|
|
1216
|
+
xml_read_errors += 1
|
|
1217
|
+
continue
|
|
1218
|
+
xml_findings = _scan_xml_file(text, rel_path)
|
|
1219
|
+
filtered = [f for f in xml_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
|
|
1220
|
+
all_findings.extend(filtered)
|
|
1221
|
+
|
|
1222
|
+
if xml_read_errors:
|
|
1223
|
+
limitations.append(f"{xml_read_errors} XML file(s) could not be read and were skipped.")
|
|
1224
|
+
|
|
1225
|
+
dep_read_errors = 0
|
|
1226
|
+
raw_dep_findings: list[MigrationFinding] = []
|
|
1227
|
+
for abs_path, rel_path in build_files:
|
|
1228
|
+
try:
|
|
1229
|
+
text = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
1230
|
+
except OSError:
|
|
1231
|
+
dep_read_errors += 1
|
|
1232
|
+
continue
|
|
1233
|
+
dep_findings = _scan_dep_file(text, rel_path)
|
|
1234
|
+
filtered = [f for f in dep_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
|
|
1235
|
+
raw_dep_findings.extend(filtered)
|
|
1236
|
+
|
|
1237
|
+
# Deduplicate dep findings by rule_id: same dependency in parent + child poms
|
|
1238
|
+
# is one logical finding. Keep the first occurrence (root pom sorts first).
|
|
1239
|
+
_seen_dep_rules: dict[str, int] = {} # rule_id → count
|
|
1240
|
+
for f in raw_dep_findings:
|
|
1241
|
+
_seen_dep_rules[f.rule_id] = _seen_dep_rules.get(f.rule_id, 0) + 1
|
|
1242
|
+
_dedup_dep: list[MigrationFinding] = []
|
|
1243
|
+
_emitted: set[str] = set()
|
|
1244
|
+
for f in raw_dep_findings:
|
|
1245
|
+
if f.rule_id not in _emitted:
|
|
1246
|
+
_dedup_dep.append(f)
|
|
1247
|
+
_emitted.add(f.rule_id)
|
|
1248
|
+
all_findings.extend(_dedup_dep)
|
|
1249
|
+
|
|
1250
|
+
if dep_read_errors:
|
|
1251
|
+
limitations.append(f"{dep_read_errors} build file(s) could not be read and were skipped.")
|
|
1252
|
+
|
|
744
1253
|
limitations.extend(_STATIC_LIMITATIONS)
|
|
745
1254
|
|
|
746
|
-
# Detect Spring Boot 2 pom.xml heuristic (best-effort, non-fatal)
|
|
747
1255
|
spring_boot_2 = _detect_spring_boot_2(root)
|
|
748
1256
|
|
|
749
1257
|
report = MigrationReport(
|
|
@@ -752,12 +1260,15 @@ def run_migrate_check(
|
|
|
752
1260
|
limitations=limitations,
|
|
753
1261
|
metadata={
|
|
754
1262
|
"java_files_scanned": len(file_paths),
|
|
1263
|
+
"xml_files_scanned": len(xml_files),
|
|
1264
|
+
"build_files_scanned": len(build_files),
|
|
755
1265
|
"min_severity": min_severity,
|
|
756
1266
|
"rules_applied": [r.id for r in _ALL_RULES],
|
|
1267
|
+
"xml_rules_applied": [r.id for r in _XML_RULES],
|
|
1268
|
+
"dep_rules_applied": [r.id for r in _DEP_RULES],
|
|
757
1269
|
},
|
|
758
1270
|
)
|
|
759
1271
|
|
|
760
|
-
# Populate git_head — non-fatal
|
|
761
1272
|
try:
|
|
762
1273
|
import subprocess as _sub
|
|
763
1274
|
_r = _sub.run(
|
|
@@ -772,19 +1283,18 @@ def run_migrate_check(
|
|
|
772
1283
|
return report.finalize()
|
|
773
1284
|
|
|
774
1285
|
|
|
775
|
-
#
|
|
1286
|
+
# Remaining static limitations — things that truly require runtime analysis
|
|
776
1287
|
_STATIC_LIMITATIONS: list[str] = [
|
|
777
|
-
"Thread.stop/suspend/resume
|
|
778
|
-
"
|
|
779
|
-
"
|
|
780
|
-
"Module compatibility (JPMS): --add-opens requirements cannot be determined without "
|
|
1288
|
+
"Thread.stop/suspend/resume detection is best-effort: variable type cannot be confirmed "
|
|
1289
|
+
"without compilation. Verify that flagged variables are typed as java.lang.Thread.",
|
|
1290
|
+
"JPMS --add-opens requirements: exact set of required flags cannot be determined without "
|
|
781
1291
|
"running the application against the target JDK.",
|
|
782
|
-
"Transitive dependency compatibility: library versions
|
|
783
|
-
"
|
|
784
|
-
"
|
|
785
|
-
"
|
|
786
|
-
"
|
|
787
|
-
"
|
|
1292
|
+
"Transitive dependency compatibility: library versions resolved transitively (not declared "
|
|
1293
|
+
"directly) require 'mvn dependency:tree' or Gradle dependency insight for full analysis.",
|
|
1294
|
+
"Runtime proxy behaviour (CGLIB subclass proxies): compatibility with Java 17+ strong "
|
|
1295
|
+
"encapsulation depends on framework version at runtime, not import-level analysis.",
|
|
1296
|
+
"XML bean definitions referencing class names via property placeholders (${bean.class}) "
|
|
1297
|
+
"cannot be resolved statically.",
|
|
788
1298
|
]
|
|
789
1299
|
|
|
790
1300
|
|
sourcecode/repository_ir.py
CHANGED
|
@@ -105,7 +105,12 @@ class EvidenceBundle:
|
|
|
105
105
|
_PKG_RE = re.compile(r'^package\s+([\w.]+)\s*;', re.MULTILINE)
|
|
106
106
|
_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;', re.MULTILINE)
|
|
107
107
|
_ANN_RE = re.compile(r'^(@[\w.]+)')
|
|
108
|
-
_ANN_WITH_ARGS_RE = re.compile(
|
|
108
|
+
_ANN_WITH_ARGS_RE = re.compile(
|
|
109
|
+
r'^(@[\w.]+)\s*'
|
|
110
|
+
r'(?:\('
|
|
111
|
+
r'((?:[^()"\']*|"[^"]*"|\'[^\']*\'|\((?:[^()"\']*|"[^"]*"|\'[^\']*\')*\))*)'
|
|
112
|
+
r'\))?'
|
|
113
|
+
)
|
|
109
114
|
|
|
110
115
|
_CLASS_DECL_RE = re.compile(
|
|
111
116
|
r'(?:^|(?<=\s))'
|
|
@@ -3303,6 +3308,52 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3303
3308
|
else:
|
|
3304
3309
|
security_model = "unknown"
|
|
3305
3310
|
|
|
3311
|
+
# Detect XML-based Spring Security config. When present, per-endpoint
|
|
3312
|
+
# none_detected is expected and does NOT mean the endpoint is unsecured —
|
|
3313
|
+
# security is declared in XML (HttpSecurity rules, filter chains, web.xml
|
|
3314
|
+
# security constraints). Update security_model and re-tag affected endpoints
|
|
3315
|
+
# so the output cannot be misread as "unprotected".
|
|
3316
|
+
_XML_SECURITY_RE = re.compile(
|
|
3317
|
+
r'(?:xmlns(?::[a-z]+)?="http://www\.springframework\.org/schema/security"'
|
|
3318
|
+
r'|<security:http\b'
|
|
3319
|
+
r'|<http\s[^>]*use-expressions'
|
|
3320
|
+
r'|spring-security-[2345]'
|
|
3321
|
+
r'|xmlns:security="http://www\.springframework\.org/schema/security")',
|
|
3322
|
+
re.IGNORECASE,
|
|
3323
|
+
)
|
|
3324
|
+
_xml_security_detected = False
|
|
3325
|
+
_XML_GLOBS = (
|
|
3326
|
+
"*security*.xml", "*Security*.xml",
|
|
3327
|
+
"*applicationContext*.xml", "*-context.xml", "*Context.xml",
|
|
3328
|
+
"*spring*.xml", "*Spring*.xml",
|
|
3329
|
+
)
|
|
3330
|
+
for _glob in _XML_GLOBS:
|
|
3331
|
+
for _xf in root.rglob(_glob):
|
|
3332
|
+
if "target/" in str(_xf).replace("\\", "/"):
|
|
3333
|
+
continue
|
|
3334
|
+
try:
|
|
3335
|
+
_xt = _xf.read_text(encoding="utf-8", errors="replace")
|
|
3336
|
+
except OSError:
|
|
3337
|
+
continue
|
|
3338
|
+
if _XML_SECURITY_RE.search(_xt):
|
|
3339
|
+
_xml_security_detected = True
|
|
3340
|
+
break
|
|
3341
|
+
if _xml_security_detected:
|
|
3342
|
+
break
|
|
3343
|
+
|
|
3344
|
+
if _xml_security_detected and security_model == "unknown":
|
|
3345
|
+
security_model = "xml_or_filter_chain"
|
|
3346
|
+
# Re-tag per-endpoint none_detected → xml_or_filter_chain so the output
|
|
3347
|
+
# cannot be misread as "endpoint is unprotected".
|
|
3348
|
+
for ep in endpoints:
|
|
3349
|
+
if ep.get("security", {}).get("policy") == "none_detected":
|
|
3350
|
+
ep["security"] = {"policy": "xml_or_filter_chain"}
|
|
3351
|
+
# Recompute no_security_signal (now counts only truly unknown endpoints)
|
|
3352
|
+
no_security_signal = sum(
|
|
3353
|
+
1 for e in endpoints
|
|
3354
|
+
if e.get("security", {}).get("policy") == "none_detected"
|
|
3355
|
+
)
|
|
3356
|
+
|
|
3306
3357
|
return {
|
|
3307
3358
|
"endpoints": endpoints,
|
|
3308
3359
|
"total": len(endpoints),
|
|
@@ -197,14 +197,17 @@ def _compute_event_risk(
|
|
|
197
197
|
consumer_count: int,
|
|
198
198
|
before_commit_count: int,
|
|
199
199
|
cross_module: bool,
|
|
200
|
+
sync_in_tx_count: int = 0,
|
|
200
201
|
) -> str:
|
|
201
202
|
"""Deterministic risk scoring per spec.
|
|
202
203
|
|
|
203
204
|
high: fanout > 5 OR cross-module propagation OR BEFORE_COMMIT consumers
|
|
205
|
+
OR sync @EventListener inside @Transactional publisher
|
|
204
206
|
medium: 2–5 consumers
|
|
205
207
|
low: ≤1 consumer
|
|
206
208
|
"""
|
|
207
|
-
if consumer_count > _RISK_FANOUT_HIGH or cross_module
|
|
209
|
+
if (consumer_count > _RISK_FANOUT_HIGH or cross_module
|
|
210
|
+
or before_commit_count > 0 or sync_in_tx_count > 0):
|
|
208
211
|
return "high"
|
|
209
212
|
if consumer_count >= _RISK_FANOUT_MEDIUM:
|
|
210
213
|
return "medium"
|
|
@@ -327,9 +330,23 @@ class EventTopologyOrchestrator:
|
|
|
327
330
|
# ── 7. TX context ──────────────────────────────────────────────────
|
|
328
331
|
after_commit = [c.fqn for c in consumers if c.transactional_phase == "AFTER_COMMIT"]
|
|
329
332
|
before_commit_risks = [c.fqn for c in consumers if c.transactional_phase == "BEFORE_COMMIT"]
|
|
333
|
+
|
|
334
|
+
# Detect sync @EventListener inside @Transactional publisher.
|
|
335
|
+
# Plain @EventListener fires synchronously; if the publisher method is
|
|
336
|
+
# @Transactional the listener runs inside that TX — listener exception
|
|
337
|
+
# rolls back the outer TX, and DB state may be partially committed.
|
|
338
|
+
tx_publishers = [
|
|
339
|
+
p for p in publishers
|
|
340
|
+
if "@Transactional" in ((fqn_index.get(p) or {}).get("annotations") or [])
|
|
341
|
+
]
|
|
342
|
+
sync_in_tx_risks = [
|
|
343
|
+
c.fqn for c in consumers
|
|
344
|
+
if c.type == "spring_event" and tx_publishers
|
|
345
|
+
]
|
|
330
346
|
tx_context = {
|
|
331
347
|
"after_commit_consumers": after_commit,
|
|
332
348
|
"before_commit_risks": before_commit_risks,
|
|
349
|
+
"sync_in_tx_risks": sync_in_tx_risks,
|
|
333
350
|
}
|
|
334
351
|
|
|
335
352
|
# ── 8. Cross-module detection ──────────────────────────────────────
|
|
@@ -352,6 +369,7 @@ class EventTopologyOrchestrator:
|
|
|
352
369
|
consumer_count=len(consumers),
|
|
353
370
|
before_commit_count=len(before_commit_risks),
|
|
354
371
|
cross_module=cross_module,
|
|
372
|
+
sync_in_tx_count=len(sync_in_tx_risks),
|
|
355
373
|
)
|
|
356
374
|
|
|
357
375
|
# ── 10. Confidence ─────────────────────────────────────────────────
|
|
@@ -385,6 +403,7 @@ class EventTopologyOrchestrator:
|
|
|
385
403
|
"kafka_listeners_in_repo": kafka_count,
|
|
386
404
|
"rabbit_listeners_in_repo": rabbit_count,
|
|
387
405
|
"before_commit_risk_count": len(before_commit_risks),
|
|
406
|
+
"sync_in_tx_risk_count": len(sync_in_tx_risks),
|
|
388
407
|
"level2_events": list(level2_events.keys()),
|
|
389
408
|
"cross_module": cross_module,
|
|
390
409
|
"model_build_time_ms": model.build_time_ms,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.26
|
|
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
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.35.
|
|
117
|
+
# sourcecode 1.35.26
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=HYA42OYABbKZ43Trr1VCdpUkXMtz0AqpwPt0ENMTTjo,104
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=qh749a7ykPtGmQI1MR9y6j8TtL_jBdVYFx9YRsLqOMw,44121
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
@@ -29,7 +29,7 @@ sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,
|
|
|
29
29
|
sourcecode/license.py,sha256=3JCV2OeTVttKrOGBguU5uZC0c02Stig-KLB0mP2lNiY,22742
|
|
30
30
|
sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
|
|
31
31
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
32
|
-
sourcecode/migrate_check.py,sha256=
|
|
32
|
+
sourcecode/migrate_check.py,sha256=GuYK36DDFkwf07jbAgcoc-Ovq8ttLQNMsRqhsUilMzY,54514
|
|
33
33
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
34
34
|
sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
|
|
35
35
|
sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
|
|
@@ -40,14 +40,14 @@ sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,
|
|
|
40
40
|
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
41
41
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
42
42
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
43
|
-
sourcecode/repository_ir.py,sha256=
|
|
43
|
+
sourcecode/repository_ir.py,sha256=hl7Vc7o5LD0xWZ6Er7-x2IDrLurJZIfBKGPD-3cfraU,171754
|
|
44
44
|
sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
|
|
45
45
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
46
46
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
47
47
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
48
48
|
sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
|
|
49
49
|
sourcecode/serializer.py,sha256=7SBJIbpC_Lg0RGWq8jjNbF5TiuZwoP_fi0qhHnzQM8M,124386
|
|
50
|
-
sourcecode/spring_event_topology.py,sha256=
|
|
50
|
+
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
51
51
|
sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
|
|
52
52
|
sourcecode/spring_impact.py,sha256=Ohm2k3W4Wts8Kx8Z7DIM-J-cwGtTJBWKFBsX-WkupBQ,32943
|
|
53
53
|
sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
|
|
@@ -94,8 +94,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
94
94
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
95
95
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
96
96
|
sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
|
|
97
|
-
sourcecode-1.35.
|
|
98
|
-
sourcecode-1.35.
|
|
99
|
-
sourcecode-1.35.
|
|
100
|
-
sourcecode-1.35.
|
|
101
|
-
sourcecode-1.35.
|
|
97
|
+
sourcecode-1.35.26.dist-info/METADATA,sha256=M8Y5dAsVOUGs7ij_YJC26hy4SJtJ_4fFGZZ6NpuabKQ,21297
|
|
98
|
+
sourcecode-1.35.26.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
99
|
+
sourcecode-1.35.26.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
100
|
+
sourcecode-1.35.26.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
101
|
+
sourcecode-1.35.26.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|