sourcecode 1.35.23__py3-none-any.whl → 1.35.25__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.23"
3
+ __version__ = "1.35.25"
@@ -1,13 +1,19 @@
1
- """migrate_check.py — Spring Boot 2→3 (javax→jakarta) migration readiness checker.
1
+ """migrate_check.py — Java 8/Spring Boot 2 migration readiness checker.
2
2
 
3
- Scans Java source files for import namespaces and class patterns that must be
4
- updated when migrating from Spring Boot 2.x (javax.*) to Spring Boot 3.x (jakarta.*).
3
+ Scans Java source files, Spring XML config, and build descriptors for patterns
4
+ that must be addressed when migrating:
5
+ - Spring Boot 2 → 3 (javax → jakarta, Spring Security 6)
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)
5
9
 
6
10
  Entry point: run_migrate_check(file_paths, root) → MigrationReport
7
11
  """
8
12
  from __future__ import annotations
9
13
 
14
+ import fnmatch
10
15
  import hashlib
16
+ import os
11
17
  import re
12
18
  from dataclasses import dataclass, field
13
19
  from datetime import datetime, timezone
@@ -16,7 +22,7 @@ from typing import Optional
16
22
 
17
23
 
18
24
  # ---------------------------------------------------------------------------
19
- # Rule catalogue
25
+ # Rule catalogue — Java source rules
20
26
  # ---------------------------------------------------------------------------
21
27
 
22
28
  @dataclass(frozen=True)
@@ -26,11 +32,18 @@ class _Rule:
26
32
  title: str
27
33
  explanation: str
28
34
  fix_hint: str
29
- import_pattern: Optional[re.Pattern] = None # matches the import statement
30
- extends_pattern: Optional[re.Pattern] = None # matches an extends clause
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
31
40
 
32
41
 
33
- _IMPORT_RULES: list[_Rule] = [
42
+ # ---------------------------------------------------------------------------
43
+ # Jakarta namespace rules (Spring Boot 2 → 3)
44
+ # ---------------------------------------------------------------------------
45
+
46
+ _JAKARTA_RULES: list[_Rule] = [
34
47
  _Rule(
35
48
  id="MIG-001",
36
49
  severity="critical",
@@ -40,6 +53,8 @@ _IMPORT_RULES: list[_Rule] = [
40
53
  "namespace. Files importing javax.persistence will not compile after migration."
41
54
  ),
42
55
  fix_hint="Replace 'javax.persistence' with 'jakarta.persistence' across all affected files.",
56
+ migration_target="jakarta",
57
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxPersistenceToJakartaPersistence",
43
58
  import_pattern=re.compile(r"^[ \t]*import\s+(javax\.persistence[^;]+);", re.MULTILINE),
44
59
  ),
45
60
  _Rule(
@@ -51,6 +66,8 @@ _IMPORT_RULES: list[_Rule] = [
51
66
  "HttpServletResponse referencing javax.servlet will break after migration."
52
67
  ),
53
68
  fix_hint="Replace 'javax.servlet' with 'jakarta.servlet' and update the servlet-api dependency.",
69
+ migration_target="jakarta",
70
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxServletToJakartaServlet",
54
71
  import_pattern=re.compile(r"^[ \t]*import\s+(javax\.servlet[^;]+);", re.MULTILINE),
55
72
  ),
56
73
  _Rule(
@@ -63,6 +80,8 @@ _IMPORT_RULES: list[_Rule] = [
63
80
  "picked up by the validator after migration."
64
81
  ),
65
82
  fix_hint="Replace 'javax.validation' with 'jakarta.validation'.",
83
+ migration_target="jakarta",
84
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxValidationToJakartaValidation",
66
85
  import_pattern=re.compile(r"^[ \t]*import\s+(javax\.validation[^;]+);", re.MULTILINE),
67
86
  ),
68
87
  _Rule(
@@ -75,6 +94,8 @@ _IMPORT_RULES: list[_Rule] = [
75
94
  "will resolve to the wrong class after migration."
76
95
  ),
77
96
  fix_hint="Replace 'javax.transaction' with 'jakarta.transaction'.",
97
+ migration_target="jakarta",
98
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxTransactionToJakartaTransaction",
78
99
  import_pattern=re.compile(r"^[ \t]*import\s+(javax\.transaction[^;]+);", re.MULTILINE),
79
100
  ),
80
101
  _Rule(
@@ -86,6 +107,8 @@ _IMPORT_RULES: list[_Rule] = [
86
107
  "@PostConstruct, @PreDestroy, @Resource are affected."
87
108
  ),
88
109
  fix_hint="Replace 'javax.annotation' with 'jakarta.annotation'.",
110
+ migration_target="jakarta",
111
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxAnnotationPackageToJakarta",
89
112
  import_pattern=re.compile(r"^[ \t]*import\s+(javax\.annotation[^;]+);", re.MULTILINE),
90
113
  ),
91
114
  _Rule(
@@ -97,6 +120,8 @@ _IMPORT_RULES: list[_Rule] = [
97
120
  "@Inject and @Named from javax.inject are affected."
98
121
  ),
99
122
  fix_hint="Replace 'javax.inject' with 'jakarta.inject'.",
123
+ migration_target="jakarta",
124
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxInjectToJakartaInject",
100
125
  import_pattern=re.compile(r"^[ \t]*import\s+(javax\.inject[^;]+);", re.MULTILINE),
101
126
  ),
102
127
  _Rule(
@@ -108,11 +133,30 @@ _IMPORT_RULES: list[_Rule] = [
108
133
  "JAX-RS resource classes, Response, and client code are affected."
109
134
  ),
110
135
  fix_hint="Replace 'javax.ws.rs' with 'jakarta.ws.rs'.",
136
+ migration_target="jakarta",
137
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxWsRsToJakartaWsRs",
111
138
  import_pattern=re.compile(r"^[ \t]*import\s+(javax\.ws\.rs[^;]+);", re.MULTILINE),
112
139
  ),
140
+ _Rule(
141
+ id="MIG-009",
142
+ severity="medium",
143
+ title="javax.jms import — JMS API not migrated to jakarta",
144
+ explanation=(
145
+ "jakarta.jms replaces javax.jms in Jakarta EE 9+. "
146
+ "Message listeners, ConnectionFactory, and Queue references are affected."
147
+ ),
148
+ fix_hint="Replace 'javax.jms' with 'jakarta.jms' and ensure messaging provider supports Jakarta EE 9.",
149
+ migration_target="jakarta",
150
+ openrewrite_recipe="org.openrewrite.java.migrate.jakarta.JavaxJmsToJakartaJms",
151
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.jms[^;]+);", re.MULTILINE),
152
+ ),
113
153
  ]
114
154
 
115
- _EXTENDS_RULES: list[_Rule] = [
155
+ # ---------------------------------------------------------------------------
156
+ # Spring Security 6 rules
157
+ # ---------------------------------------------------------------------------
158
+
159
+ _SPRING_SECURITY_RULES: list[_Rule] = [
116
160
  _Rule(
117
161
  id="MIG-005",
118
162
  severity="high",
@@ -126,30 +170,753 @@ _EXTENDS_RULES: list[_Rule] = [
126
170
  "Remove the class extension and expose a SecurityFilterChain @Bean instead. "
127
171
  "See the Spring Security 6 migration guide."
128
172
  ),
173
+ migration_target="spring_security_6",
174
+ openrewrite_recipe="org.openrewrite.java.spring.security6.WebSecurityConfigurerAdapterToSecurityFilterChain",
129
175
  extends_pattern=re.compile(r"\bextends\s+WebSecurityConfigurerAdapter\b"),
130
176
  ),
177
+ _Rule(
178
+ id="MIG-020",
179
+ severity="high",
180
+ title="antMatchers / authorizeRequests — deprecated Spring Security 6 patterns",
181
+ explanation=(
182
+ "antMatchers() was replaced by requestMatchers() and authorizeRequests() was replaced "
183
+ "by authorizeHttpRequests() in Spring Security 6. The old methods are removed. "
184
+ "Also, AuthenticationManagerBuilder-based configuration is superseded by "
185
+ "UserDetailsService and PasswordEncoder beans."
186
+ ),
187
+ fix_hint=(
188
+ "Replace antMatchers() with requestMatchers(), authorizeRequests() with "
189
+ "authorizeHttpRequests(). Migrate HttpSecurity config to Lambda DSL style."
190
+ ),
191
+ migration_target="spring_security_6",
192
+ openrewrite_recipe="org.openrewrite.java.spring.security6.HttpSecurityLambdaDsl",
193
+ code_pattern=re.compile(
194
+ r"\.antMatchers\s*\(|\.authorizeRequests\s*\(\)|"
195
+ r"\bAuthenticationManagerBuilder\b",
196
+ re.MULTILINE,
197
+ ),
198
+ ),
199
+ _Rule(
200
+ id="MIG-019",
201
+ severity="high",
202
+ title="SpringFox / Swagger 2 — incompatible with Spring Boot 3 / Spring MVC 6",
203
+ explanation=(
204
+ "SpringFox (io.springfox) requires Spring MVC internals that were removed in "
205
+ "Spring Framework 6. Applications using @EnableSwagger2 or springfox.documentation "
206
+ "will fail to start after migration to Spring Boot 3."
207
+ ),
208
+ fix_hint=(
209
+ "Migrate to springdoc-openapi-starter-webmvc-ui (OpenAPI 3). "
210
+ "Replace springfox-swagger2 + springfox-swagger-ui dependencies."
211
+ ),
212
+ migration_target="spring_security_6",
213
+ openrewrite_recipe=None,
214
+ import_pattern=re.compile(
215
+ r"^[ \t]*import\s+(springfox\.[^;]+);",
216
+ re.MULTILINE,
217
+ ),
218
+ code_pattern=re.compile(r"@EnableSwagger2\b"),
219
+ ),
220
+ ]
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Java 11 — APIs removed from the JDK
224
+ # ---------------------------------------------------------------------------
225
+
226
+ _JAVA_11_RULES: list[_Rule] = [
227
+ _Rule(
228
+ id="MIG-021",
229
+ severity="high",
230
+ title="javax.xml.bind (JAXB) — removed from JDK in Java 11",
231
+ explanation=(
232
+ "JAXB was part of the JDK in Java 8 (java.xml.bind module) but removed in Java 11. "
233
+ "Code importing javax.xml.bind will fail to compile on Java 11+ unless the "
234
+ "jakarta.xml.bind-api and jaxb-impl artifacts are added as dependencies."
235
+ ),
236
+ fix_hint=(
237
+ "Add 'jakarta.xml.bind:jakarta.xml.bind-api' and 'org.glassfish.jaxb:jaxb-runtime' "
238
+ "as dependencies. Also migrate javax.xml.bind → jakarta.xml.bind for Spring Boot 3."
239
+ ),
240
+ migration_target="java_11",
241
+ openrewrite_recipe=None,
242
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.xml\.bind[^;]+);", re.MULTILINE),
243
+ ),
244
+ _Rule(
245
+ id="MIG-022",
246
+ severity="high",
247
+ title="javax.xml.ws (JAX-WS) — removed from JDK in Java 11",
248
+ explanation=(
249
+ "JAX-WS was bundled with the JDK in Java 8 but removed in Java 11. "
250
+ "Applications importing javax.xml.ws require an explicit jaxws-rt dependency."
251
+ ),
252
+ fix_hint=(
253
+ "Add 'com.sun.xml.ws:jaxws-rt' as a dependency. "
254
+ "Also migrate javax.xml.ws → jakarta.xml.ws for Spring Boot 3 targets."
255
+ ),
256
+ migration_target="java_11",
257
+ openrewrite_recipe=None,
258
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.xml\.ws[^;]+);", re.MULTILINE),
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
+ ),
282
+ ]
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # Java 15 — Nashorn removed
286
+ # ---------------------------------------------------------------------------
287
+
288
+ _JAVA_15_RULES: list[_Rule] = [
289
+ _Rule(
290
+ id="MIG-012",
291
+ severity="high",
292
+ title="Nashorn ScriptEngine — removed in Java 15",
293
+ explanation=(
294
+ "The Nashorn JavaScript engine was deprecated in Java 11 (JEP 335) and removed "
295
+ "in Java 15 (JEP 372). Code importing jdk.nashorn.* or obtaining the Nashorn engine "
296
+ "via ScriptEngineManager will fail at runtime on Java 15+."
297
+ ),
298
+ fix_hint=(
299
+ "Replace Nashorn with GraalVM Polyglot API (org.graalvm.sdk:polyglot) or "
300
+ "Mozilla Rhino (org.mozilla:rhino). Remove jdk.nashorn.* imports."
301
+ ),
302
+ migration_target="java_15",
303
+ openrewrite_recipe=None,
304
+ import_pattern=re.compile(r"^[ \t]*import\s+(jdk\.nashorn[^;]+);", re.MULTILINE),
305
+ code_pattern=re.compile(r'getEngineByName\s*\(\s*["\']nashorn["\']'),
306
+ ),
307
+ ]
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # Java 17 — SecurityManager removed (JEP 411), Thread deprecated methods
311
+ # ---------------------------------------------------------------------------
312
+
313
+ _JAVA_17_RULES: list[_Rule] = [
314
+ _Rule(
315
+ id="MIG-010",
316
+ severity="critical",
317
+ title="SecurityManager / AccessController — removed in Java 17 (JEP 411)",
318
+ explanation=(
319
+ "The Security Manager and its associated APIs (SecurityManager, AccessController, "
320
+ "System.setSecurityManager, System.getSecurityManager, SecurityPermission, "
321
+ "RuntimePermission) were deprecated for removal in Java 17 and are non-functional. "
322
+ "In Java 17, setSecurityManager() throws UnsupportedOperationException unless "
323
+ "the 'java.security.manager=allow' system property is set."
324
+ ),
325
+ fix_hint=(
326
+ "Remove SecurityManager installation and AccessController.doPrivileged() calls. "
327
+ "Replace with proper module-based access control or Jakarta Security. "
328
+ "See JEP 411 migration guide."
329
+ ),
330
+ migration_target="java_17",
331
+ openrewrite_recipe=None,
332
+ code_pattern=re.compile(
333
+ r"System\.(get|set)SecurityManager\s*\(|"
334
+ r"\bSecurityManager\s+\w+\s*[=;({]|"
335
+ r"\bnew\s+SecurityManager\s*\(|"
336
+ r"\bextends\s+SecurityManager\b|"
337
+ r"\bAccessController\.(doPrivileged|checkPermission|getContext)\s*\(",
338
+ ),
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
+ ),
365
+ ]
366
+
367
+ # ---------------------------------------------------------------------------
368
+ # Java 9+ — Strong encapsulation (JPMS) — internal APIs
369
+ # ---------------------------------------------------------------------------
370
+
371
+ _JAVA_9_RULES: list[_Rule] = [
372
+ _Rule(
373
+ id="MIG-011",
374
+ severity="high",
375
+ title="JDK internal API imports (sun.* / com.sun.net.*) — strong encapsulation since Java 9",
376
+ explanation=(
377
+ "Imports from sun.* and com.sun.net.* reference JDK-internal APIs that are "
378
+ "not part of the public specification. Since Java 9 (JPMS), these packages are "
379
+ "strongly encapsulated and require --add-exports / --add-opens JVM flags, "
380
+ "which are cumbersome and may be removed in future Java releases."
381
+ ),
382
+ fix_hint=(
383
+ "Replace internal API usage with public equivalents. "
384
+ "For com.sun.net.httpserver, migrate to java.net.http.HttpServer or a framework. "
385
+ "Add '--add-exports java.base/sun.misc=ALL-UNNAMED' only as a last resort."
386
+ ),
387
+ migration_target="java_9_plus",
388
+ openrewrite_recipe=None,
389
+ import_pattern=re.compile(
390
+ r"^[ \t]*import\s+(sun\.[^;]+|com\.sun\.(?:net|tools|jdi|source|management)[^;]+);",
391
+ re.MULTILINE,
392
+ ),
393
+ ),
394
+ _Rule(
395
+ id="MIG-013",
396
+ severity="high",
397
+ title="sun.misc.Unsafe — direct access requires --add-opens since Java 9",
398
+ explanation=(
399
+ "sun.misc.Unsafe is a JDK-internal class not exposed by the public module system. "
400
+ "Accessing it via reflection or direct import requires "
401
+ "'--add-opens java.base/sun.misc=ALL-UNNAMED' on Java 9+. "
402
+ "Many frameworks (ByteBuddy, CGLIB, ASM) use Unsafe internally."
403
+ ),
404
+ fix_hint=(
405
+ "Remove direct Unsafe usage and rely on VarHandle (java.lang.invoke.VarHandle) "
406
+ "as the public replacement. Ensure framework versions used are Java 17+ compatible."
407
+ ),
408
+ migration_target="java_9_plus",
409
+ openrewrite_recipe=None,
410
+ import_pattern=re.compile(
411
+ r"^[ \t]*import\s+(sun\.misc\.Unsafe[^;]*);",
412
+ re.MULTILINE,
413
+ ),
414
+ code_pattern=re.compile(
415
+ r'Unsafe\.getUnsafe\s*\(|"theUnsafe"|getDeclaredField\s*\(\s*["\']theUnsafe["\']',
416
+ ),
417
+ ),
418
+ _Rule(
419
+ id="MIG-014",
420
+ severity="medium",
421
+ title="setAccessible(true) — may throw InaccessibleObjectException on Java 9+",
422
+ explanation=(
423
+ "Reflective access via setAccessible(true) to JDK-internal classes throws "
424
+ "InaccessibleObjectException on Java 9+ unless the owning module grants access. "
425
+ "This is an 'illegal reflective access' warning in Java 9-15 and a hard failure "
426
+ "in Java 17+ for strongly-encapsulated modules."
427
+ ),
428
+ fix_hint=(
429
+ "Ensure setAccessible() calls target application code, not JDK internal classes. "
430
+ "Add necessary '--add-opens' flags for unavoidable cases. "
431
+ "Prefer public APIs to avoid reflection on JDK internals entirely."
432
+ ),
433
+ migration_target="java_9_plus",
434
+ openrewrite_recipe=None,
435
+ code_pattern=re.compile(r"\.setAccessible\s*\(\s*true\s*\)"),
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
+ ),
463
+ ]
464
+
465
+ # ---------------------------------------------------------------------------
466
+ # Java 18+ — finalize() deprecated for removal
467
+ # ---------------------------------------------------------------------------
468
+
469
+ _JAVA_18_RULES: list[_Rule] = [
470
+ _Rule(
471
+ id="MIG-015",
472
+ severity="medium",
473
+ title="finalize() override — deprecated for removal since Java 9, removed in Java 18",
474
+ explanation=(
475
+ "Object.finalize() was deprecated in Java 9 and deprecated-for-removal in Java 18 "
476
+ "(JEP 421). Overriding finalize() is unreliable, may delay GC, and the mechanism "
477
+ "is being removed from the platform. Java 18+ emits warnings; future JDK versions "
478
+ "will not call finalizers."
479
+ ),
480
+ fix_hint=(
481
+ "Replace finalize() with try-with-resources (AutoCloseable/Closeable) or "
482
+ "java.lang.ref.Cleaner for resource cleanup."
483
+ ),
484
+ migration_target="java_18_plus",
485
+ openrewrite_recipe="org.openrewrite.java.migrate.RemoveFinalizeMethod",
486
+ code_pattern=re.compile(
487
+ r"\b(?:protected|public)\s+void\s+finalize\s*\(\s*\)",
488
+ ),
489
+ ),
490
+ ]
491
+
492
+ # ---------------------------------------------------------------------------
493
+ # Best-practice / low-severity — legacy date/time API
494
+ # ---------------------------------------------------------------------------
495
+
496
+ _LEGACY_API_RULES: list[_Rule] = [
497
+ _Rule(
498
+ id="MIG-016",
499
+ severity="low",
500
+ title="Legacy date/time API (java.util.Date / Calendar / SimpleDateFormat)",
501
+ explanation=(
502
+ "java.util.Date, java.util.Calendar, and java.text.SimpleDateFormat are "
503
+ "thread-unsafe and error-prone. They are superseded by java.time (JSR-310) "
504
+ "introduced in Java 8. While not removed, they cause issues in multi-threaded "
505
+ "Spring applications and should be migrated before upgrading."
506
+ ),
507
+ fix_hint=(
508
+ "Replace Date with LocalDate/LocalDateTime/ZonedDateTime, "
509
+ "Calendar with java.time.Calendar, "
510
+ "SimpleDateFormat with DateTimeFormatter (thread-safe)."
511
+ ),
512
+ migration_target="java_8_best_practice",
513
+ openrewrite_recipe="org.openrewrite.java.migrate.JavaTimeAPIs",
514
+ import_pattern=re.compile(
515
+ r"^[ \t]*import\s+(java\.util\.(?:Date|Calendar|GregorianCalendar)"
516
+ r"|java\.text\.(?:SimpleDateFormat|DateFormat))[^;]*;",
517
+ re.MULTILINE,
518
+ ),
519
+ ),
131
520
  ]
132
521
 
133
- _ALL_RULES: list[_Rule] = _IMPORT_RULES + _EXTENDS_RULES
522
+ # ---------------------------------------------------------------------------
523
+ # All Java source rules
524
+ # ---------------------------------------------------------------------------
525
+
526
+ _ALL_RULES: list[_Rule] = (
527
+ _JAKARTA_RULES
528
+ + _SPRING_SECURITY_RULES
529
+ + _JAVA_11_RULES
530
+ + _JAVA_15_RULES
531
+ + _JAVA_17_RULES
532
+ + _JAVA_9_RULES
533
+ + _JAVA_18_RULES
534
+ + _LEGACY_API_RULES
535
+ )
134
536
 
135
537
  SEVERITY_ORDER: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3}
136
538
 
137
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 _scan_dep_file(text: str, rel_path: str) -> list["MigrationFinding"]:
872
+ """Apply dependency rules to a build file. Returns one finding per matched rule."""
873
+ is_gradle = rel_path.endswith((".gradle", ".gradle.kts"))
874
+ findings: list[MigrationFinding] = []
875
+ for rule in _DEP_RULES:
876
+ if rule.quick_filter is not None and rule.quick_filter not in text:
877
+ continue
878
+ pattern = rule.gradle_pattern if is_gradle else rule.maven_pattern
879
+ if pattern is None:
880
+ continue
881
+ m = pattern.search(text)
882
+ if m is None:
883
+ continue
884
+ first_line = text[: m.start()].count("\n") + 1
885
+ findings.append(
886
+ MigrationFinding(
887
+ id=MigrationFinding.make_id(rule.id, rel_path),
888
+ rule_id=rule.id,
889
+ severity=rule.severity,
890
+ title=rule.title,
891
+ source_file=rel_path,
892
+ first_line=first_line,
893
+ imports_found=[m.group(0)[:120].strip()],
894
+ explanation=rule.explanation,
895
+ fix_hint=rule.fix_hint,
896
+ migration_target=rule.migration_target,
897
+ openrewrite_recipe=rule.openrewrite_recipe,
898
+ )
899
+ )
900
+ return findings
901
+
902
+
138
903
  # ---------------------------------------------------------------------------
139
904
  # Finding
140
905
  # ---------------------------------------------------------------------------
141
906
 
142
907
  @dataclass
143
908
  class MigrationFinding:
144
- id: str # deterministic: "{rule_id}-{file_hash[:12]}"
145
- rule_id: str # "MIG-001" .. "MIG-008"
146
- severity: str # "critical" | "high" | "medium" | "low"
909
+ id: str
910
+ rule_id: str
911
+ severity: str
147
912
  title: str
148
- source_file: str # relative path
149
- first_line: int # 1-based line number of first match
150
- imports_found: list[str] = field(default_factory=list) # matched import statements
913
+ source_file: str
914
+ first_line: int
915
+ imports_found: list[str] = field(default_factory=list)
151
916
  explanation: str = ""
152
917
  fix_hint: str = ""
918
+ migration_target: str = ""
919
+ openrewrite_recipe: Optional[str] = None
153
920
 
154
921
  @staticmethod
155
922
  def make_id(rule_id: str, source_file: str) -> str:
@@ -166,9 +933,15 @@ class MigrationFinding:
166
933
  "first_line": self.first_line,
167
934
  "explanation": self.explanation,
168
935
  "fix_hint": self.fix_hint,
936
+ "migration_target": self.migration_target,
937
+ "auto_fix_available": bool(self.openrewrite_recipe),
169
938
  }
170
939
  if self.imports_found:
171
940
  d["imports_found"] = self.imports_found
941
+ if self.openrewrite_recipe:
942
+ d["openrewrite_recipe"] = self.openrewrite_recipe
943
+ else:
944
+ d["manual_migration"] = True
172
945
  return d
173
946
 
174
947
 
@@ -178,14 +951,13 @@ class MigrationFinding:
178
951
 
179
952
  @dataclass
180
953
  class MigrationReport:
181
- schema_version: str = "1.0"
954
+ schema_version: str = "1.2"
182
955
  generated_at: str = ""
183
956
  repo_id: str = ""
184
957
  git_head: str = ""
185
958
 
186
- # Core metrics
187
- readiness_score: int = 100 # 0–100; 100 = ready to migrate
188
- blocking_count: int = 0 # critical + high finding count
959
+ readiness_score: int = 100
960
+ blocking_count: int = 0
189
961
  estimated_effort_days: float = 0.0
190
962
  spring_boot_2_detected: bool = False
191
963
 
@@ -200,17 +972,17 @@ class MigrationReport:
200
972
 
201
973
  by_severity: dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "low": 0}
202
974
  by_rule: dict[str, int] = {}
975
+ by_target: dict[str, int] = {}
203
976
  affected_files: set[str] = set()
204
977
 
205
978
  for f in self.findings:
206
979
  by_severity[f.severity] = by_severity.get(f.severity, 0) + 1
207
980
  by_rule[f.rule_id] = by_rule.get(f.rule_id, 0) + 1
981
+ by_target[f.migration_target] = by_target.get(f.migration_target, 0) + 1
208
982
  affected_files.add(f.source_file)
209
983
 
210
984
  self.blocking_count = by_severity["critical"] + by_severity["high"]
211
985
 
212
- # Score: deduct per affected-file/severity combination (not per finding, to avoid
213
- # double-counting a file that imports 10 javax.persistence classes).
214
986
  critical_files: set[str] = set()
215
987
  high_files: set[str] = set()
216
988
  medium_files: set[str] = set()
@@ -233,7 +1005,6 @@ class MigrationReport:
233
1005
  )
234
1006
  self.readiness_score = max(0, 100 - deduction)
235
1007
 
236
- # Effort: sum per distinct affected file weighted by severity
237
1008
  self.estimated_effort_days = round(
238
1009
  len(critical_files) * 0.5
239
1010
  + len(high_files) * 0.25
@@ -247,6 +1018,7 @@ class MigrationReport:
247
1018
  "affected_files": len(affected_files),
248
1019
  "by_severity": by_severity,
249
1020
  "by_rule": by_rule,
1021
+ "by_migration_target": by_target,
250
1022
  }
251
1023
  return self
252
1024
 
@@ -287,16 +1059,19 @@ class MigrationReport:
287
1059
  for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
288
1060
  lines.append(
289
1061
  f"{f.rule_id} [{f.severity.upper()}] {f.source_file}:{f.first_line}"
1062
+ f" [{f.migration_target}]"
290
1063
  )
291
1064
  lines.append(f" {f.title}")
292
1065
  lines.append(f" Fix: {f.fix_hint}")
1066
+ if f.openrewrite_recipe:
1067
+ lines.append(f" OpenRewrite: {f.openrewrite_recipe}")
293
1068
  lines.append("")
294
1069
 
295
1070
  return "\n".join(lines)
296
1071
 
297
1072
 
298
1073
  # ---------------------------------------------------------------------------
299
- # Scanner
1074
+ # Java source scanner
300
1075
  # ---------------------------------------------------------------------------
301
1076
 
302
1077
  def _scan_file(
@@ -307,44 +1082,54 @@ def _scan_file(
307
1082
  findings: list[MigrationFinding] = []
308
1083
 
309
1084
  for rule in rules:
1085
+ matched_imports: list[str] = []
1086
+ import_first_line: Optional[int] = None
1087
+ code_first_line: Optional[int] = None
1088
+ code_snippets: list[str] = []
1089
+
310
1090
  if rule.import_pattern is not None:
311
1091
  matches = list(rule.import_pattern.finditer(source))
312
- if not matches:
313
- continue
314
- # Compute 1-based line number of first match
315
- first_line = source[: matches[0].start()].count("\n") + 1
316
- imports_found = [m.group(1) for m in matches]
317
- findings.append(
318
- MigrationFinding(
319
- id=MigrationFinding.make_id(rule.id, rel_path),
320
- rule_id=rule.id,
321
- severity=rule.severity,
322
- title=rule.title,
323
- source_file=rel_path,
324
- first_line=first_line,
325
- imports_found=imports_found,
326
- explanation=rule.explanation,
327
- fix_hint=rule.fix_hint,
328
- )
329
- )
1092
+ if matches:
1093
+ import_first_line = source[: matches[0].start()].count("\n") + 1
1094
+ matched_imports = [m.group(1) for m in matches]
1095
+
1096
+ if rule.code_pattern is not None:
1097
+ m = rule.code_pattern.search(source)
1098
+ if m is not None:
1099
+ code_first_line = source[: m.start()].count("\n") + 1
1100
+ code_snippets = [m.group(0).strip()]
330
1101
 
331
- elif rule.extends_pattern is not None:
1102
+ extends_first_line: Optional[int] = None
1103
+ if rule.extends_pattern is not None:
332
1104
  m = rule.extends_pattern.search(source)
333
- if m is None:
334
- continue
335
- first_line = source[: m.start()].count("\n") + 1
336
- findings.append(
337
- MigrationFinding(
338
- id=MigrationFinding.make_id(rule.id, rel_path),
339
- rule_id=rule.id,
340
- severity=rule.severity,
341
- title=rule.title,
342
- source_file=rel_path,
343
- first_line=first_line,
344
- explanation=rule.explanation,
345
- fix_hint=rule.fix_hint,
346
- )
1105
+ if m is not None:
1106
+ extends_first_line = source[: m.start()].count("\n") + 1
1107
+
1108
+ candidate_lines = [
1109
+ ln for ln in (import_first_line, code_first_line, extends_first_line)
1110
+ if ln is not None
1111
+ ]
1112
+ if not candidate_lines:
1113
+ continue
1114
+
1115
+ first_line = min(candidate_lines)
1116
+ all_matches = matched_imports + code_snippets
1117
+
1118
+ findings.append(
1119
+ MigrationFinding(
1120
+ id=MigrationFinding.make_id(rule.id, rel_path),
1121
+ rule_id=rule.id,
1122
+ severity=rule.severity,
1123
+ title=rule.title,
1124
+ source_file=rel_path,
1125
+ first_line=first_line,
1126
+ imports_found=all_matches,
1127
+ explanation=rule.explanation,
1128
+ fix_hint=rule.fix_hint,
1129
+ migration_target=rule.migration_target,
1130
+ openrewrite_recipe=rule.openrewrite_recipe,
347
1131
  )
1132
+ )
348
1133
 
349
1134
  return findings
350
1135
 
@@ -359,22 +1144,28 @@ def run_migrate_check(
359
1144
  *,
360
1145
  min_severity: str = "low",
361
1146
  ) -> MigrationReport:
362
- """Scan Java files for Spring Boot 2→3 migration blockers.
1147
+ """Scan a Java repository for migration blockers.
1148
+
1149
+ Scans:
1150
+ - Java source files (.java) against all 24 rules (MIG-001..MIG-025)
1151
+ - Spring XML config files (applicationContext.xml, web.xml, security XML, etc.)
1152
+ - Build descriptors (pom.xml, build.gradle) for incompatible dependencies
363
1153
 
364
1154
  Args:
365
1155
  file_paths: Relative Java file paths (from find_java_files).
366
1156
  root: Absolute repo root.
367
- min_severity: Filter threshold findings below this severity are excluded
368
- from the report. Choices: critical | high | medium | low.
1157
+ min_severity: Filter threshold. Choices: critical | high | medium | low.
369
1158
 
370
1159
  Returns:
371
- MigrationReport with findings, readiness_score, and effort estimate.
1160
+ MigrationReport with findings, readiness_score, effort estimate, and
1161
+ migration_target breakdown.
372
1162
  """
373
1163
  min_order = SEVERITY_ORDER.get(min_severity, 3)
374
1164
  all_findings: list[MigrationFinding] = []
375
1165
  limitations: list[str] = []
376
1166
  read_errors = 0
377
1167
 
1168
+ # ── Java source scan ────────────────────────────────────────────────────
378
1169
  for rel_path in file_paths:
379
1170
  abs_path = root / rel_path
380
1171
  try:
@@ -384,14 +1175,44 @@ def run_migrate_check(
384
1175
  continue
385
1176
 
386
1177
  file_findings = _scan_file(source, rel_path, _ALL_RULES)
387
- # Apply min_severity filter
388
1178
  filtered = [f for f in file_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
389
1179
  all_findings.extend(filtered)
390
1180
 
391
1181
  if read_errors:
392
1182
  limitations.append(f"{read_errors} file(s) could not be read and were skipped.")
393
1183
 
394
- # Detect Spring Boot 2 pom.xml heuristic (best-effort, non-fatal)
1184
+ # ── XML + dependency scan (single tree walk) ─────────────────────────────
1185
+ xml_files, build_files = _find_non_java_files(root)
1186
+ xml_read_errors = 0
1187
+ for abs_path, rel_path in xml_files:
1188
+ try:
1189
+ text = abs_path.read_text(encoding="utf-8", errors="replace")
1190
+ except OSError:
1191
+ xml_read_errors += 1
1192
+ continue
1193
+ xml_findings = _scan_xml_file(text, rel_path)
1194
+ filtered = [f for f in xml_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
1195
+ all_findings.extend(filtered)
1196
+
1197
+ if xml_read_errors:
1198
+ limitations.append(f"{xml_read_errors} XML file(s) could not be read and were skipped.")
1199
+
1200
+ dep_read_errors = 0
1201
+ for abs_path, rel_path in build_files:
1202
+ try:
1203
+ text = abs_path.read_text(encoding="utf-8", errors="replace")
1204
+ except OSError:
1205
+ dep_read_errors += 1
1206
+ continue
1207
+ dep_findings = _scan_dep_file(text, rel_path)
1208
+ filtered = [f for f in dep_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
1209
+ all_findings.extend(filtered)
1210
+
1211
+ if dep_read_errors:
1212
+ limitations.append(f"{dep_read_errors} build file(s) could not be read and were skipped.")
1213
+
1214
+ limitations.extend(_STATIC_LIMITATIONS)
1215
+
395
1216
  spring_boot_2 = _detect_spring_boot_2(root)
396
1217
 
397
1218
  report = MigrationReport(
@@ -400,12 +1221,15 @@ def run_migrate_check(
400
1221
  limitations=limitations,
401
1222
  metadata={
402
1223
  "java_files_scanned": len(file_paths),
1224
+ "xml_files_scanned": len(xml_files),
1225
+ "build_files_scanned": len(build_files),
403
1226
  "min_severity": min_severity,
404
1227
  "rules_applied": [r.id for r in _ALL_RULES],
1228
+ "xml_rules_applied": [r.id for r in _XML_RULES],
1229
+ "dep_rules_applied": [r.id for r in _DEP_RULES],
405
1230
  },
406
1231
  )
407
1232
 
408
- # Populate git_head — non-fatal
409
1233
  try:
410
1234
  import subprocess as _sub
411
1235
  _r = _sub.run(
@@ -420,9 +1244,30 @@ def run_migrate_check(
420
1244
  return report.finalize()
421
1245
 
422
1246
 
1247
+ # Remaining static limitations — things that truly require runtime analysis
1248
+ _STATIC_LIMITATIONS: list[str] = [
1249
+ "Thread.stop/suspend/resume detection is best-effort: variable type cannot be confirmed "
1250
+ "without compilation. Verify that flagged variables are typed as java.lang.Thread.",
1251
+ "JPMS --add-opens requirements: exact set of required flags cannot be determined without "
1252
+ "running the application against the target JDK.",
1253
+ "Transitive dependency compatibility: library versions resolved transitively (not declared "
1254
+ "directly) require 'mvn dependency:tree' or Gradle dependency insight for full analysis.",
1255
+ "Runtime proxy behaviour (CGLIB subclass proxies): compatibility with Java 17+ strong "
1256
+ "encapsulation depends on framework version at runtime, not import-level analysis.",
1257
+ "XML bean definitions referencing class names via property placeholders (${bean.class}) "
1258
+ "cannot be resolved statically.",
1259
+ ]
1260
+
1261
+
423
1262
  def _detect_spring_boot_2(root: Path) -> bool:
424
1263
  """Return True if any pom.xml or build.gradle declares spring-boot 2.x."""
425
- _SB2 = re.compile(r"spring[-.]boot[^\"'\n]*[\"']?2\.\d+", re.IGNORECASE)
1264
+ _SB2 = re.compile(
1265
+ r"(?:spring[.\-]boot[.\-]?(?:version|starter|parent)[^=\n]*[=:\s>\"']?\s*)"
1266
+ r"2\.\d+[\.\d]*|"
1267
+ r"<version>\s*2\.\d+[\.\d]*\s*</version>.*spring.boot|"
1268
+ r"spring.boot.*<version>\s*2\.\d+",
1269
+ re.IGNORECASE | re.DOTALL,
1270
+ )
426
1271
  for name in ("pom.xml", "build.gradle", "build.gradle.kts"):
427
1272
  candidate = root / name
428
1273
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.23
3
+ Version: 1.35.25
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
- ![Version](https://img.shields.io/badge/version-1.35.23-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.25-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.35.23
117
+ # sourcecode 1.35.25
118
118
  ```
119
119
 
120
120
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=_3RZgCJrBxKTnf4cMy8WI2Ca9TZsi_5ac6JucEkUIVM,104
1
+ sourcecode/__init__.py,sha256=1fGjuVJyBU95TOEnJBXLcVgGYs_QYMvaKwPypDXVP9g,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=5vsO7YJiXkn6HKealy2n7qoc99-eD2EW9YAE8Jm1HR0,16341
32
+ sourcecode/migrate_check.py,sha256=-jghKewJwMO0VCXML-ZY1KI_RQO5gd5-pyLCMg5u8jA,52971
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
@@ -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.23.dist-info/METADATA,sha256=SPVcfkrBpRzffHQfMhEbeyM4fxShVvN6gdmSMOJs35Q,21297
98
- sourcecode-1.35.23.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
99
- sourcecode-1.35.23.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
100
- sourcecode-1.35.23.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
101
- sourcecode-1.35.23.dist-info/RECORD,,
97
+ sourcecode-1.35.25.dist-info/METADATA,sha256=XdPhns9JN4tVct_5ARdKlbsb3w76e9rBFXZ_SI4-VC0,21297
98
+ sourcecode-1.35.25.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
99
+ sourcecode-1.35.25.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
100
+ sourcecode-1.35.25.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
101
+ sourcecode-1.35.25.dist-info/RECORD,,