sourcecode 1.35.24__tar.gz → 1.35.25__tar.gz

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.
Files changed (107) hide show
  1. {sourcecode-1.35.24 → sourcecode-1.35.25}/PKG-INFO +3 -3
  2. {sourcecode-1.35.24 → sourcecode-1.35.25}/README.md +2 -2
  3. {sourcecode-1.35.24 → sourcecode-1.35.25}/pyproject.toml +1 -1
  4. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/migrate_check.py +518 -47
  6. {sourcecode-1.35.24 → sourcecode-1.35.25}/.github/workflows/build-windows.yml +0 -0
  7. {sourcecode-1.35.24 → sourcecode-1.35.25}/.gitignore +0 -0
  8. {sourcecode-1.35.24 → sourcecode-1.35.25}/.ruff.toml +0 -0
  9. {sourcecode-1.35.24 → sourcecode-1.35.25}/CHANGELOG.md +0 -0
  10. {sourcecode-1.35.24 → sourcecode-1.35.25}/CONTRIBUTING.md +0 -0
  11. {sourcecode-1.35.24 → sourcecode-1.35.25}/LICENSE +0 -0
  12. {sourcecode-1.35.24 → sourcecode-1.35.25}/SECURITY.md +0 -0
  13. {sourcecode-1.35.24 → sourcecode-1.35.25}/raw +0 -0
  14. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/adaptive_scanner.py +0 -0
  15. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/architecture_analyzer.py +0 -0
  16. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/architecture_summary.py +0 -0
  17. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/ast_extractor.py +0 -0
  18. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/cache.py +0 -0
  19. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/canonical_ir.py +0 -0
  20. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/cir_graphs.py +0 -0
  21. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/classifier.py +0 -0
  22. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/cli.py +0 -0
  23. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/code_notes_analyzer.py +0 -0
  24. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/confidence_analyzer.py +0 -0
  25. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/context_scorer.py +0 -0
  26. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/context_summarizer.py +0 -0
  27. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/contract_model.py +0 -0
  28. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/contract_pipeline.py +0 -0
  29. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/coverage_parser.py +0 -0
  30. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/dependency_analyzer.py +0 -0
  31. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/__init__.py +0 -0
  32. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/base.py +0 -0
  33. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/csproj_parser.py +0 -0
  34. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/dart.py +0 -0
  35. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/dotnet.py +0 -0
  36. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/elixir.py +0 -0
  37. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/go.py +0 -0
  38. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/heuristic.py +0 -0
  39. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/hybrid.py +0 -0
  40. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/java.py +0 -0
  41. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/jvm_ext.py +0 -0
  42. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/nodejs.py +0 -0
  43. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/parsers.py +0 -0
  44. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/php.py +0 -0
  45. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/project.py +0 -0
  46. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/python.py +0 -0
  47. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/ruby.py +0 -0
  48. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/rust.py +0 -0
  49. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/systems.py +0 -0
  50. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/terraform.py +0 -0
  51. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/detectors/tooling.py +0 -0
  52. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/doc_analyzer.py +0 -0
  53. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/entrypoint_classifier.py +0 -0
  54. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/env_analyzer.py +0 -0
  55. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/error_schema.py +0 -0
  56. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/explain.py +0 -0
  57. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/flow_analyzer.py +0 -0
  59. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/fqn_utils.py +0 -0
  60. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/git_analyzer.py +0 -0
  61. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/graph_analyzer.py +0 -0
  62. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/license.py +0 -0
  63. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/__init__.py +0 -0
  64. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  65. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  66. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  67. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  68. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  69. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/orchestrator.py +0 -0
  70. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/registry.py +0 -0
  71. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/runner.py +0 -0
  72. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp/server.py +0 -0
  73. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/mcp_nudge.py +0 -0
  74. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/metrics_analyzer.py +0 -0
  75. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/output_budget.py +0 -0
  76. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/path_filters.py +0 -0
  77. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/pr_comment_renderer.py +0 -0
  78. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/pr_impact.py +0 -0
  79. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/prepare_context.py +0 -0
  80. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/progress.py +0 -0
  81. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/ranking_engine.py +0 -0
  82. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/redactor.py +0 -0
  83. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/relevance_scorer.py +0 -0
  84. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/repo_classifier.py +0 -0
  85. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/repository_ir.py +0 -0
  86. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/ris.py +0 -0
  87. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/runtime_classifier.py +0 -0
  88. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/scanner.py +0 -0
  89. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/schema.py +0 -0
  90. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/semantic_analyzer.py +0 -0
  91. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/serializer.py +0 -0
  92. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/spring_event_topology.py +0 -0
  93. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/spring_findings.py +0 -0
  94. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/spring_impact.py +0 -0
  95. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/spring_model.py +0 -0
  96. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/spring_security_audit.py +0 -0
  97. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/spring_semantic.py +0 -0
  98. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/spring_tx_analyzer.py +0 -0
  99. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/summarizer.py +0 -0
  100. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/telemetry/__init__.py +0 -0
  101. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/telemetry/config.py +0 -0
  102. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/telemetry/consent.py +0 -0
  103. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/telemetry/events.py +0 -0
  104. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/telemetry/filters.py +0 -0
  105. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/telemetry/transport.py +0 -0
  106. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/tree_utils.py +0 -0
  107. {sourcecode-1.35.24 → sourcecode-1.35.25}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.24
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.24-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.24
117
+ # sourcecode 1.35.25
118
118
  ```
119
119
 
120
120
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.35.24-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.25-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.35.24
79
+ # sourcecode 1.35.25
80
80
  ```
81
81
 
82
82
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.35.24"
7
+ version = "1.35.25"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.24"
3
+ __version__ = "1.35.25"
@@ -1,14 +1,19 @@
1
1
  """migrate_check.py — Java 8/Spring Boot 2 migration readiness checker.
2
2
 
3
- Scans Java source files for patterns that must be addressed when migrating:
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" # jakarta | spring_security_6 | java_11 | java_15 | java_17 | java_9_plus | java_18_plus
31
- openrewrite_recipe: Optional[str] = None # Official OpenRewrite recipe ID, if one exists
32
- import_pattern: Optional[re.Pattern] = None # matches the import statement
33
- extends_pattern: Optional[re.Pattern] = None # matches an extends clause
34
- code_pattern: Optional[re.Pattern] = None # matches arbitrary code anywhere in the file
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 (must add as explicit deps)
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*[=;({]|" # variable declaration, requires code-context char to avoid Javadoc FPs
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=None,
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 list
523
+ # All Java source rules
446
524
  # ---------------------------------------------------------------------------
447
525
 
448
526
  _ALL_RULES: list[_Rule] = (
@@ -459,19 +537,382 @@ _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 _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
+
462
903
  # ---------------------------------------------------------------------------
463
904
  # Finding
464
905
  # ---------------------------------------------------------------------------
465
906
 
466
907
  @dataclass
467
908
  class MigrationFinding:
468
- id: str # deterministic: "{rule_id}-{file_hash[:12]}"
469
- rule_id: str # "MIG-001" .. "MIG-022"
470
- severity: str # "critical" | "high" | "medium" | "low"
909
+ id: str
910
+ rule_id: str
911
+ severity: str
471
912
  title: str
472
- source_file: str # relative path
473
- first_line: int # 1-based line number of first match
474
- imports_found: list[str] = field(default_factory=list) # matched import stmts or code snippets
913
+ source_file: str
914
+ first_line: int
915
+ imports_found: list[str] = field(default_factory=list)
475
916
  explanation: str = ""
476
917
  fix_hint: str = ""
477
918
  migration_target: str = ""
@@ -493,11 +934,14 @@ class MigrationFinding:
493
934
  "explanation": self.explanation,
494
935
  "fix_hint": self.fix_hint,
495
936
  "migration_target": self.migration_target,
937
+ "auto_fix_available": bool(self.openrewrite_recipe),
496
938
  }
497
939
  if self.imports_found:
498
940
  d["imports_found"] = self.imports_found
499
941
  if self.openrewrite_recipe:
500
942
  d["openrewrite_recipe"] = self.openrewrite_recipe
943
+ else:
944
+ d["manual_migration"] = True
501
945
  return d
502
946
 
503
947
 
@@ -507,14 +951,13 @@ class MigrationFinding:
507
951
 
508
952
  @dataclass
509
953
  class MigrationReport:
510
- schema_version: str = "1.1"
954
+ schema_version: str = "1.2"
511
955
  generated_at: str = ""
512
956
  repo_id: str = ""
513
957
  git_head: str = ""
514
958
 
515
- # Core metrics
516
- readiness_score: int = 100 # 0–100; 100 = ready to migrate
517
- blocking_count: int = 0 # critical + high finding count
959
+ readiness_score: int = 100
960
+ blocking_count: int = 0
518
961
  estimated_effort_days: float = 0.0
519
962
  spring_boot_2_detected: bool = False
520
963
 
@@ -540,8 +983,6 @@ class MigrationReport:
540
983
 
541
984
  self.blocking_count = by_severity["critical"] + by_severity["high"]
542
985
 
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
986
  critical_files: set[str] = set()
546
987
  high_files: set[str] = set()
547
988
  medium_files: set[str] = set()
@@ -564,7 +1005,6 @@ class MigrationReport:
564
1005
  )
565
1006
  self.readiness_score = max(0, 100 - deduction)
566
1007
 
567
- # Effort: sum per distinct affected file weighted by severity
568
1008
  self.estimated_effort_days = round(
569
1009
  len(critical_files) * 0.5
570
1010
  + len(high_files) * 0.25
@@ -631,7 +1071,7 @@ class MigrationReport:
631
1071
 
632
1072
 
633
1073
  # ---------------------------------------------------------------------------
634
- # Scanner
1074
+ # Java source scanner
635
1075
  # ---------------------------------------------------------------------------
636
1076
 
637
1077
  def _scan_file(
@@ -642,8 +1082,6 @@ def _scan_file(
642
1082
  findings: list[MigrationFinding] = []
643
1083
 
644
1084
  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
1085
  matched_imports: list[str] = []
648
1086
  import_first_line: Optional[int] = None
649
1087
  code_first_line: Optional[int] = None
@@ -661,14 +1099,12 @@ def _scan_file(
661
1099
  code_first_line = source[: m.start()].count("\n") + 1
662
1100
  code_snippets = [m.group(0).strip()]
663
1101
 
664
- # extends_pattern is a legacy form of code_pattern
665
1102
  extends_first_line: Optional[int] = None
666
1103
  if rule.extends_pattern is not None:
667
1104
  m = rule.extends_pattern.search(source)
668
1105
  if m is not None:
669
1106
  extends_first_line = source[: m.start()].count("\n") + 1
670
1107
 
671
- # Determine overall match
672
1108
  candidate_lines = [
673
1109
  ln for ln in (import_first_line, code_first_line, extends_first_line)
674
1110
  if ln is not None
@@ -708,13 +1144,17 @@ def run_migrate_check(
708
1144
  *,
709
1145
  min_severity: str = "low",
710
1146
  ) -> MigrationReport:
711
- """Scan Java files for migration blockers (Spring Boot 2→3, Java 8→17/21).
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
712
1153
 
713
1154
  Args:
714
1155
  file_paths: Relative Java file paths (from find_java_files).
715
1156
  root: Absolute repo root.
716
- min_severity: Filter threshold findings below this severity are excluded
717
- from the report. Choices: critical | high | medium | low.
1157
+ min_severity: Filter threshold. Choices: critical | high | medium | low.
718
1158
 
719
1159
  Returns:
720
1160
  MigrationReport with findings, readiness_score, effort estimate, and
@@ -725,6 +1165,7 @@ def run_migrate_check(
725
1165
  limitations: list[str] = []
726
1166
  read_errors = 0
727
1167
 
1168
+ # ── Java source scan ────────────────────────────────────────────────────
728
1169
  for rel_path in file_paths:
729
1170
  abs_path = root / rel_path
730
1171
  try:
@@ -734,16 +1175,44 @@ def run_migrate_check(
734
1175
  continue
735
1176
 
736
1177
  file_findings = _scan_file(source, rel_path, _ALL_RULES)
737
- # Apply min_severity filter
738
1178
  filtered = [f for f in file_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
739
1179
  all_findings.extend(filtered)
740
1180
 
741
1181
  if read_errors:
742
1182
  limitations.append(f"{read_errors} file(s) could not be read and were skipped.")
743
1183
 
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
+
744
1214
  limitations.extend(_STATIC_LIMITATIONS)
745
1215
 
746
- # Detect Spring Boot 2 pom.xml heuristic (best-effort, non-fatal)
747
1216
  spring_boot_2 = _detect_spring_boot_2(root)
748
1217
 
749
1218
  report = MigrationReport(
@@ -752,12 +1221,15 @@ def run_migrate_check(
752
1221
  limitations=limitations,
753
1222
  metadata={
754
1223
  "java_files_scanned": len(file_paths),
1224
+ "xml_files_scanned": len(xml_files),
1225
+ "build_files_scanned": len(build_files),
755
1226
  "min_severity": min_severity,
756
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],
757
1230
  },
758
1231
  )
759
1232
 
760
- # Populate git_head — non-fatal
761
1233
  try:
762
1234
  import subprocess as _sub
763
1235
  _r = _sub.run(
@@ -772,19 +1244,18 @@ def run_migrate_check(
772
1244
  return report.finalize()
773
1245
 
774
1246
 
775
- # Items that static analysis cannot determine, always emitted as limitations
1247
+ # Remaining static limitations things that truly require runtime analysis
776
1248
  _STATIC_LIMITATIONS: list[str] = [
777
- "Thread.stop/suspend/resume deprecation: cannot reliably detect without type resolution "
778
- "(requires knowing that a variable is typed as java.lang.Thread).",
779
- "CORBA removal (Java 11): org.omg.* usage not scanned; add manually if project uses CORBA.",
780
- "Module compatibility (JPMS): --add-opens requirements cannot be determined without "
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 "
781
1252
  "running the application against the target JDK.",
782
- "Transitive dependency compatibility: library versions (Hibernate, Jackson, etc.) must be "
783
- "verified separately against Spring Boot 3 BOM.",
784
- "XML-based Spring config (applicationContext.xml, web.xml): not scanned bean class names "
785
- "and servlet filter chains in XML may reference javax.* classes.",
786
- "Runtime proxy behaviour (CGLIB/ByteBuddy subclass proxies): compatibility with Java 17+ "
787
- "strong encapsulation depends on framework version, not detectable via import scanning.",
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.",
788
1259
  ]
789
1260
 
790
1261
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes