sourcecode 1.66.0__tar.gz → 1.68.0__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.
- {sourcecode-1.66.0 → sourcecode-1.68.0}/.gitignore +3 -1
- {sourcecode-1.66.0 → sourcecode-1.68.0}/CHANGELOG.md +94 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/PKG-INFO +1 -1
- {sourcecode-1.66.0 → sourcecode-1.68.0}/pyproject.toml +1 -1
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/cli.py +134 -7
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/context_summarizer.py +5 -1
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/dependency_analyzer.py +50 -4
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/integration_detector.py +109 -10
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/orchestrator.py +1 -1
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/registry.py +3 -3
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/server.py +4 -4
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/migrate_check.py +185 -25
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/path_filters.py +31 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/repository_ir.py +57 -7
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/summarizer.py +1 -1
- {sourcecode-1.66.0 → sourcecode-1.68.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/.ruff.toml +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/LICENSE +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/README.md +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/SECURITY.md +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/raw +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.66.0 → sourcecode-1.68.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,99 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.68.0] — 2026-06-30
|
|
4
|
+
|
|
5
|
+
### Fixed — narrative/structured-data parity on non-Spring DI repos (Netflix Eureka field test)
|
|
6
|
+
|
|
7
|
+
A real audit of **Netflix Eureka** (Gradle multi-module, JAX-RS/Jersey 1.x + Google
|
|
8
|
+
Guice, **no Spring, no Hibernate, no persistence layer**) surfaced six reliability
|
|
9
|
+
defects that share one root cause: **prose that assumed a Spring/Hibernate/persistence
|
|
10
|
+
context even though the same JSON's own structured fields already knew it did not
|
|
11
|
+
apply.** A consumer trusting the narrative over the structured fields would have read
|
|
12
|
+
false conclusions. Guiding principle applied throughout: **a narrative term (Spring,
|
|
13
|
+
Guice, persistence, …) may only appear when the corresponding structured signal
|
|
14
|
+
confirms it.** New regression coverage across four suites.
|
|
15
|
+
|
|
16
|
+
- **SMTP false positive on a log string (BUG #1).** `export --integrations` reported
|
|
17
|
+
`kind=smtp` for `logger.warn("Transport initialization failure", e)` — the bare word
|
|
18
|
+
"Transport" in a log literal. The detector now (a) skips any token matched inside a
|
|
19
|
+
string literal and (b) gates the JavaMail tokens (`Transport`, `MimeMessage`) on a
|
|
20
|
+
`javax.mail`/`jakarta.mail`/`org.springframework.mail` import. Eureka SMTP hits 3 → 0.
|
|
21
|
+
- **JNDI DNS mislabeled as LDAP (BUG #2).** `javax.naming.directory.{Dir,Initial}Context`
|
|
22
|
+
is protocol-agnostic; it is now classified by its `INITIAL_CONTEXT_FACTORY` —
|
|
23
|
+
`DnsContextFactory` → `kind=dns` (`client=jndi-dns`), `LdapCtxFactory` → `ldap`, and an
|
|
24
|
+
unresolvable factory → `kind=naming-directory-unknown` with `confidence=low`, never an
|
|
25
|
+
assumed LDAP. Eureka `DnsResolver` now reports `dns`.
|
|
26
|
+
- **"Spring Boot 3" narrative on a non-Spring repo (BUG #3).** When `spring_present` is
|
|
27
|
+
`False`, every `migrate-check` finding's `explanation` is rewritten from Spring-Boot
|
|
28
|
+
framing into framework-neutral Jakarta-EE / servlet-container framing (the javax→jakarta
|
|
29
|
+
finding itself is unchanged). Guarantee: no explanation contains "Spring Boot" unless
|
|
30
|
+
the report confirms runtime Spring.
|
|
31
|
+
- **Inconsistent DI-framework label (BUG #4).** `impact` emitted "Spring/CDI DI pattern"
|
|
32
|
+
in the short `explanation` but "Spring/CDI/Guice" in `via_interface_note` of the *same*
|
|
33
|
+
JSON, while `direct_callers` proved the framework was Guice. Both strings now derive
|
|
34
|
+
from one label computed from caller evidence — a `*.guice.*` caller yields "Guice".
|
|
35
|
+
- **"persistence paths" for non-persistence mappers (BUG #5).** Eureka's `AzToRegionMapper`
|
|
36
|
+
classes (AWS topology, no data layer) were counted as "4 persistence paths". Each
|
|
37
|
+
`mappers_affected` entry now carries `mapper_kind` (`persistence` only for a confirmed
|
|
38
|
+
`@Repository` role or MyBatis `mapper_interface`; otherwise `name_heuristic`). The
|
|
39
|
+
explanation says "persistence path" only for confirmed entries and neutral
|
|
40
|
+
"data-mapping class" otherwise — the word "persistence" never appears for a repo with no
|
|
41
|
+
confirmed persistence signal.
|
|
42
|
+
- **Ambiguous `member_count` unit in `modernize` (BUG #6).** `subsystem_summary.member_count`
|
|
43
|
+
counts all graph members (classes + methods + fields), ~5× the class count and not
|
|
44
|
+
comparable to repo-level `total_classes`. Each entry now also carries an unambiguous
|
|
45
|
+
`class_count` (class/interface declarations only, comparable to `total_classes`), with a
|
|
46
|
+
`subsystem_summary_note` documenting both units.
|
|
47
|
+
|
|
48
|
+
## [1.67.0] — 2026-06-30
|
|
49
|
+
|
|
50
|
+
### Fixed — framing accuracy on non-Spring repos (Apache OFBiz field test)
|
|
51
|
+
|
|
52
|
+
A real audit of **Apache OFBiz** (release18.12, ~326k LOC, own framework + Service
|
|
53
|
+
Engine + widgets, **no Spring**) exposed seven defects where the tool collapsed
|
|
54
|
+
"absence of evidence" into high-confidence false framings — a less-critical LLM
|
|
55
|
+
consumer would have emitted a FALSE report (recommending a non-existent migration).
|
|
56
|
+
Guiding principle applied throughout: **degrade to N/A, never to a manufactured 0 or
|
|
57
|
+
100; declare gaps instead of inventing certainty.** New golden fixture:
|
|
58
|
+
`tests/test_ofbiz_framing_regression.py`.
|
|
59
|
+
|
|
60
|
+
- **`spring-test` no longer implies runtime Spring (BUG #1/#2).** Spring presence is
|
|
61
|
+
now scope- and artifact-aware: a test support library (`spring-test`,
|
|
62
|
+
`spring-boot-*-test`, `spring-security-test`) — even declared under a legacy
|
|
63
|
+
`compile` block — and test-tree `org.springframework.test` imports are classified
|
|
64
|
+
`spring_test_only`, never runtime Spring. `migrate-check` reports `spring_present`
|
|
65
|
+
(runtime) vs `spring_test_only` separately.
|
|
66
|
+
- **No phantom `boot3 0/100` dimension (BUG #3).** The Boot 2→3 axis is `applicable`
|
|
67
|
+
only with runtime Spring; otherwise it is N/A (`score: null`) and excluded from the
|
|
68
|
+
aggregate — it can no longer drag the readiness headline to 0.
|
|
69
|
+
- **Applicability gating + N/A readiness (BUG #4).** `jakarta` is applicable only with
|
|
70
|
+
positive evidence (migratable `javax.*` imports, or the `jakarta.*` namespace already
|
|
71
|
+
adopted); a repo with no Spring, no migratable `javax.*`, and no Hibernate 5 now
|
|
72
|
+
reports `readiness_score: null` ("N/A — no migration target detected") instead of a
|
|
73
|
+
manufactured 100. A valid `javax.servlet` surface is kept and scored on the **Jakarta
|
|
74
|
+
EE / Tomcat 10** axis, not mislabeled "Spring Boot 3".
|
|
75
|
+
- **`modernize` no longer reports framework-dispatched classes as dead code (BUG #5).**
|
|
76
|
+
Zero-static-caller classes are renamed `statically_unreferenced` (never a confident
|
|
77
|
+
"dead zone") and partitioned: classes with a dynamic-entry signature (OFBiz Service
|
|
78
|
+
Engine `Map<String,Object> name(DispatchContext, Map<…>)`, servlet/event handlers,
|
|
79
|
+
annotation-dispatched beans) or referenced from XML/SPI/properties config are listed
|
|
80
|
+
under `framework_dispatched` and excluded.
|
|
81
|
+
- **Gradle dependency parsing fixed for glob strings (BUG #6).** A string-aware comment
|
|
82
|
+
stripper no longer treats `/*` inside an Ant-style glob (`'**/*.java'`) as a block
|
|
83
|
+
comment — which had silently eaten OFBiz's entire `dependencies {}` block (0 → 59
|
|
84
|
+
deps). When a `dependencies {}` block resolves to nothing, a GAP is reported instead
|
|
85
|
+
of a confident "0 dependencies".
|
|
86
|
+
- **Vendored web assets excluded from code notes (BUG #7).** `is_vendor_path` now flags
|
|
87
|
+
vendored JS/CSS libraries by file name (`jquery-3.5.1.js`, `bootstrap.bundle.js`, …),
|
|
88
|
+
so third-party comments are no longer reported as the project's own notes.
|
|
89
|
+
- **Vendor-neutral project-type label (BUG #1, `--compact`).** `web_mvc` (triggered by
|
|
90
|
+
server-side template engines such as FreeMarker/Thymeleaf) is labeled "Server-side
|
|
91
|
+
MVC web app", not "Spring MVC web app" — OFBiz now reads "Java server-side mvc web
|
|
92
|
+
app using FreeMarker" with no false Spring claim.
|
|
93
|
+
|
|
94
|
+
No-regression: a genuine Spring Boot 2 repo keeps `boot3` applicable and a computed
|
|
95
|
+
readiness; Keycloak/Petclinic golden fixtures still pass.
|
|
96
|
+
|
|
3
97
|
## [1.66.0] — 2026-06-30
|
|
4
98
|
|
|
5
99
|
### Fixed — Spring Petclinic (Boot 4.0.3 / Java 17) false positives + aggregate consistency
|
|
@@ -4896,7 +4896,8 @@ def migrate_check_cmd(
|
|
|
4896
4896
|
output, output_path, copy,
|
|
4897
4897
|
success_msg=(
|
|
4898
4898
|
f"Migration check written to {output_path} "
|
|
4899
|
-
f"(score: {report.readiness_score
|
|
4899
|
+
f"(score: {report.readiness_score if report.readiness_score is not None else 'N/A'}"
|
|
4900
|
+
f"{'/100' if report.readiness_score is not None else ''}, {_total} findings)"
|
|
4900
4901
|
),
|
|
4901
4902
|
)
|
|
4902
4903
|
|
|
@@ -5524,6 +5525,91 @@ def fix_bug_cmd(
|
|
|
5524
5525
|
)
|
|
5525
5526
|
|
|
5526
5527
|
|
|
5528
|
+
# Method signatures that are almost always FRAMEWORK ENTRY POINTS, not dead code —
|
|
5529
|
+
# invoked by a dispatcher (reflection / XML / SPI), invisible to a static Java
|
|
5530
|
+
# call-graph. Generalizes beyond any one framework.
|
|
5531
|
+
_DYNAMIC_ENTRY_SIGNATURE_RE = __import__("re").compile(
|
|
5532
|
+
r"\(\s*DispatchContext\b" # OFBiz Service Engine service
|
|
5533
|
+
r"|HttpServletRequest\s+\w+\s*,\s*HttpServletResponse" # OFBiz event / servlet handler
|
|
5534
|
+
r"|@(?:Scheduled|PostConstruct|PreDestroy|EventListener|Bean|"
|
|
5535
|
+
r"RequestMapping|GetMapping|PostMapping|Path|GET|POST|Provider|"
|
|
5536
|
+
r"ApplicationScoped|Singleton)\b", # annotation-dispatched entry
|
|
5537
|
+
)
|
|
5538
|
+
# Config file extensions where a framework wires classes by name (XML/props/yaml).
|
|
5539
|
+
_CONFIG_REF_EXTS: frozenset = frozenset({".xml", ".properties", ".yml", ".yaml", ".groovy"})
|
|
5540
|
+
_CONFIG_SCAN_MAX_FILES: int = 12000
|
|
5541
|
+
_CONFIG_SCAN_MAX_BYTES: int = 256 * 1024
|
|
5542
|
+
|
|
5543
|
+
|
|
5544
|
+
def _partition_static_unreferenced(nodes: list[dict], root: Path) -> tuple[list[dict], list[dict]]:
|
|
5545
|
+
"""Split zero-degree classes into (truly_unreferenced, framework_dispatched).
|
|
5546
|
+
|
|
5547
|
+
A class with no static callers is NOT necessarily dead: frameworks invoke
|
|
5548
|
+
classes via reflection, XML/SPI config, or annotations that a static call-graph
|
|
5549
|
+
cannot see (e.g. Apache OFBiz Service Engine services, JAX-RS resources,
|
|
5550
|
+
ServiceLoader providers, scheduled beans). We exclude a candidate when EITHER:
|
|
5551
|
+
1. its source declares a dynamic-entry method signature, OR
|
|
5552
|
+
2. its simple name / FQN is referenced from a non-Java config file.
|
|
5553
|
+
Whatever survives is reported as *statically_unreferenced* — never a confident
|
|
5554
|
+
"dead zone".
|
|
5555
|
+
"""
|
|
5556
|
+
import os
|
|
5557
|
+
if not nodes:
|
|
5558
|
+
return [], []
|
|
5559
|
+
by_simple: dict[str, list[dict]] = {}
|
|
5560
|
+
for n in nodes:
|
|
5561
|
+
simple = (n.get("fqn") or "").rsplit(".", 1)[-1]
|
|
5562
|
+
if simple:
|
|
5563
|
+
by_simple.setdefault(simple, []).append(n)
|
|
5564
|
+
|
|
5565
|
+
dispatched_fqns: set[str] = set()
|
|
5566
|
+
|
|
5567
|
+
# 1. Source-signature allowlist (bounded — candidate set is small).
|
|
5568
|
+
for n in nodes:
|
|
5569
|
+
src = n.get("source_file")
|
|
5570
|
+
if not src:
|
|
5571
|
+
continue
|
|
5572
|
+
try:
|
|
5573
|
+
txt = (root / src).read_text(encoding="utf-8", errors="replace")
|
|
5574
|
+
except OSError:
|
|
5575
|
+
continue
|
|
5576
|
+
if _DYNAMIC_ENTRY_SIGNATURE_RE.search(txt):
|
|
5577
|
+
dispatched_fqns.add(n["fqn"])
|
|
5578
|
+
|
|
5579
|
+
# 2. Config-reference scan — find candidate names wired from XML/props/yaml.
|
|
5580
|
+
unresolved_simple = {s for s, ns in by_simple.items()
|
|
5581
|
+
if any(x["fqn"] not in dispatched_fqns for x in ns)}
|
|
5582
|
+
if unresolved_simple:
|
|
5583
|
+
files_scanned = 0
|
|
5584
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
5585
|
+
dirnames[:] = [d for d in dirnames
|
|
5586
|
+
if d not in {".git", "build", "out", "target", "node_modules", ".gradle"}]
|
|
5587
|
+
for fname in filenames:
|
|
5588
|
+
ext = os.path.splitext(fname)[1].lower()
|
|
5589
|
+
if ext not in _CONFIG_REF_EXTS:
|
|
5590
|
+
continue
|
|
5591
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
|
|
5592
|
+
break
|
|
5593
|
+
fpath = os.path.join(dirpath, fname)
|
|
5594
|
+
try:
|
|
5595
|
+
with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
|
|
5596
|
+
text = fh.read(_CONFIG_SCAN_MAX_BYTES)
|
|
5597
|
+
except OSError:
|
|
5598
|
+
continue
|
|
5599
|
+
files_scanned += 1
|
|
5600
|
+
for simple in list(unresolved_simple):
|
|
5601
|
+
if simple in text:
|
|
5602
|
+
for x in by_simple.get(simple, []):
|
|
5603
|
+
dispatched_fqns.add(x["fqn"])
|
|
5604
|
+
unresolved_simple.discard(simple)
|
|
5605
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
|
|
5606
|
+
break
|
|
5607
|
+
|
|
5608
|
+
unreferenced = [n for n in nodes if n["fqn"] not in dispatched_fqns]
|
|
5609
|
+
dispatched = [n for n in nodes if n["fqn"] in dispatched_fqns]
|
|
5610
|
+
return unreferenced, dispatched
|
|
5611
|
+
|
|
5612
|
+
|
|
5527
5613
|
@app.command("modernize")
|
|
5528
5614
|
def modernize_cmd(
|
|
5529
5615
|
path: Path = typer.Argument(
|
|
@@ -5625,13 +5711,21 @@ def modernize_cmd(
|
|
|
5625
5711
|
key=lambda n: (-n.get("in_degree", 0), n.get("fqn", "")),
|
|
5626
5712
|
)[:20]
|
|
5627
5713
|
|
|
5628
|
-
#
|
|
5629
|
-
|
|
5714
|
+
# Statically-unreferenced zones: classes with zero in-degree AND zero out-degree
|
|
5715
|
+
# in the Java call-graph. These are NOT necessarily dead — framework dispatch
|
|
5716
|
+
# (reflection / XML / SPI / annotations) is invisible to a static graph — so we
|
|
5717
|
+
# partition out framework-dispatched entry points before reporting, and never
|
|
5718
|
+
# call the survivors "dead". (Defect 5: OFBiz Service-Engine services and event
|
|
5719
|
+
# handlers were false-positive "dead zones".)
|
|
5720
|
+
_zero_degree = sorted(
|
|
5630
5721
|
[n for n in graph_nodes
|
|
5631
5722
|
if n.get("in_degree", 0) == 0 and n.get("out_degree", 0) == 0
|
|
5632
5723
|
and n.get("type") in ("class", "interface")],
|
|
5633
5724
|
key=lambda n: n.get("fqn", ""),
|
|
5634
|
-
)
|
|
5725
|
+
)
|
|
5726
|
+
dead_zones, framework_dispatched = _partition_static_unreferenced(_zero_degree, root)
|
|
5727
|
+
dead_zones = dead_zones[:20]
|
|
5728
|
+
framework_dispatched = framework_dispatched[:20]
|
|
5635
5729
|
|
|
5636
5730
|
# Hotspot candidates: high in-degree service/repository/controller nodes,
|
|
5637
5731
|
# ranked by composite score (in_degree × 2 + git_churn) for volatility signal.
|
|
@@ -5680,16 +5774,35 @@ def modernize_cmd(
|
|
|
5680
5774
|
"total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
|
|
5681
5775
|
"total_subsystems": len(subsystems),
|
|
5682
5776
|
"high_coupling_nodes": len(coupling_nodes),
|
|
5683
|
-
"
|
|
5777
|
+
"statically_unreferenced": len(dead_zones),
|
|
5778
|
+
"framework_dispatched": len(framework_dispatched),
|
|
5684
5779
|
}
|
|
5780
|
+
# BUG #6 (v1.68.0): `member_count` counts ALL graph members in the subsystem —
|
|
5781
|
+
# classes, methods AND fields — so it runs ~5x higher than the class count and
|
|
5782
|
+
# is not comparable to the repo-level `total_classes` (which counts only
|
|
5783
|
+
# class/interface nodes). Surface an unambiguous `class_count` alongside it,
|
|
5784
|
+
# computed against the same class/interface node set as total_classes, so a
|
|
5785
|
+
# reader does not have to reverse-engineer the unit. `member_count` is retained
|
|
5786
|
+
# for backward compatibility but is now documented as the graph-member tally.
|
|
5787
|
+
_class_fqns = {n["fqn"] for n in graph_nodes if n.get("type") in ("class", "interface")}
|
|
5788
|
+
|
|
5789
|
+
def _class_count(members: list) -> int:
|
|
5790
|
+
return sum(1 for m in members if m in _class_fqns)
|
|
5791
|
+
|
|
5685
5792
|
_subsystem_summary = [
|
|
5686
5793
|
{
|
|
5687
5794
|
"label": s.get("label") or s.get("name") or "",
|
|
5688
5795
|
"package_prefix": s.get("package_prefix") or s.get("pkg") or "",
|
|
5796
|
+
"class_count": _class_count(s.get("members") or []),
|
|
5689
5797
|
"member_count": len(s.get("members") or []),
|
|
5690
5798
|
}
|
|
5691
5799
|
for s in subsystems[:15]
|
|
5692
5800
|
]
|
|
5801
|
+
_subsystem_summary_note = (
|
|
5802
|
+
"class_count = class/interface declarations only (comparable to "
|
|
5803
|
+
"summary.total_classes). member_count = all graph members in the subsystem "
|
|
5804
|
+
"including methods and fields, so it is larger and not a class tally."
|
|
5805
|
+
)
|
|
5693
5806
|
|
|
5694
5807
|
if (not _mod_is_pro) and _mod_large(str(root)):
|
|
5695
5808
|
# Large monolith, free tier: structural discovery preview only — no dead
|
|
@@ -5706,6 +5819,7 @@ def modernize_cmd(
|
|
|
5706
5819
|
),
|
|
5707
5820
|
"summary": _summary,
|
|
5708
5821
|
"subsystem_summary": _subsystem_summary,
|
|
5822
|
+
"subsystem_summary_note": _subsystem_summary_note,
|
|
5709
5823
|
"hotspot_candidates": hotspots[:3],
|
|
5710
5824
|
"high_coupling_nodes": [
|
|
5711
5825
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
@@ -5723,14 +5837,26 @@ def modernize_cmd(
|
|
|
5723
5837
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
5724
5838
|
for n in coupling_nodes
|
|
5725
5839
|
],
|
|
5726
|
-
"
|
|
5840
|
+
"statically_unreferenced": [
|
|
5727
5841
|
{"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
|
|
5728
5842
|
for n in dead_zones
|
|
5729
5843
|
],
|
|
5844
|
+
"statically_unreferenced_note": (
|
|
5845
|
+
"Zero static callers in the Java call-graph. NOT confirmed dead: verify "
|
|
5846
|
+
"no framework dispatch (reflection, XML/SPI config, annotations) before "
|
|
5847
|
+
"removing. Classes detected as framework-dispatched are listed separately "
|
|
5848
|
+
"under framework_dispatched and excluded from this list."
|
|
5849
|
+
),
|
|
5850
|
+
"framework_dispatched": [
|
|
5851
|
+
{"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
|
|
5852
|
+
for n in framework_dispatched
|
|
5853
|
+
],
|
|
5730
5854
|
"subsystem_summary": _subsystem_summary,
|
|
5855
|
+
"subsystem_summary_note": _subsystem_summary_note,
|
|
5731
5856
|
"cross_module_tangles": [
|
|
5732
5857
|
{
|
|
5733
5858
|
"label": s.get("label") or s.get("name") or "",
|
|
5859
|
+
"class_count": _class_count(s.get("members") or []),
|
|
5734
5860
|
"member_count": len(s.get("members") or []),
|
|
5735
5861
|
}
|
|
5736
5862
|
for s in tangle_modules
|
|
@@ -5742,7 +5868,8 @@ def modernize_cmd(
|
|
|
5742
5868
|
if hotspots else
|
|
5743
5869
|
"high_coupling_nodes shows the most-referenced classes — start there. "
|
|
5744
5870
|
)
|
|
5745
|
-
+ "
|
|
5871
|
+
+ "statically_unreferenced lists classes with no Java callers — review "
|
|
5872
|
+
+ "for framework dispatch (XML/reflection/SPI) before removing. "
|
|
5746
5873
|
+ "Cross-module tangles indicate coupling worth decomposing."
|
|
5747
5874
|
),
|
|
5748
5875
|
}
|
|
@@ -28,7 +28,11 @@ _STACK_LABELS: dict[str, str] = {
|
|
|
28
28
|
|
|
29
29
|
_TYPE_LABELS: dict[str, str] = {
|
|
30
30
|
"api": "REST API",
|
|
31
|
-
|
|
31
|
+
# Vendor-neutral: web_mvc is triggered by server-side template engines
|
|
32
|
+
# (Thymeleaf/FreeMarker), which are NOT exclusive to Spring (e.g. Apache OFBiz
|
|
33
|
+
# uses FreeMarker with its own framework). The detected frameworks list carries
|
|
34
|
+
# the actual stack; the type label must not assert "Spring".
|
|
35
|
+
"web_mvc": "Server-side MVC web app",
|
|
32
36
|
"webapp": "Web app",
|
|
33
37
|
"fullstack": "Full-stack app",
|
|
34
38
|
"cli": "CLI tool",
|
|
@@ -1278,9 +1278,42 @@ class DependencyAnalyzer:
|
|
|
1278
1278
|
return records, limitations
|
|
1279
1279
|
|
|
1280
1280
|
def _strip_gradle_comments(self, content: str) -> str:
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1281
|
+
"""Strip // and /* */ comments WITHOUT touching string literals.
|
|
1282
|
+
|
|
1283
|
+
A naive regex treats `/*` inside a string as a block-comment open. Gradle
|
|
1284
|
+
build files routinely embed `/*` in Ant-style glob strings (e.g.
|
|
1285
|
+
'**/*.java') and `//` in URL strings — a regex stripper then eats the entire
|
|
1286
|
+
dependencies block up to the next literal `*/` (real defect on Apache OFBiz,
|
|
1287
|
+
where ~100 deps vanished). This scanner skips quoted strings (single, double,
|
|
1288
|
+
and triple-quoted, with backslash escapes) so only genuine comments are removed.
|
|
1289
|
+
"""
|
|
1290
|
+
out: list[str] = []
|
|
1291
|
+
i, n = 0, len(content)
|
|
1292
|
+
quote: Optional[str] = None # active string delimiter
|
|
1293
|
+
while i < n:
|
|
1294
|
+
if quote is not None:
|
|
1295
|
+
# Inside a string literal — copy verbatim until the matching delimiter.
|
|
1296
|
+
if len(quote) == 1 and content[i] == "\\" and i + 1 < n:
|
|
1297
|
+
out.append(content[i:i + 2]); i += 2; continue
|
|
1298
|
+
if content.startswith(quote, i):
|
|
1299
|
+
out.append(quote); i += len(quote); quote = None; continue
|
|
1300
|
+
out.append(content[i]); i += 1; continue
|
|
1301
|
+
if content.startswith("//", i):
|
|
1302
|
+
j = content.find("\n", i)
|
|
1303
|
+
if j == -1:
|
|
1304
|
+
break
|
|
1305
|
+
i = j; continue
|
|
1306
|
+
if content.startswith("/*", i):
|
|
1307
|
+
j = content.find("*/", i + 2)
|
|
1308
|
+
if j == -1:
|
|
1309
|
+
break
|
|
1310
|
+
i = j + 2; continue
|
|
1311
|
+
for q in ('"""', "'''", '"', "'"):
|
|
1312
|
+
if content.startswith(q, i):
|
|
1313
|
+
quote = q; out.append(q); i += len(q); break
|
|
1314
|
+
else:
|
|
1315
|
+
out.append(content[i]); i += 1
|
|
1316
|
+
return "".join(out)
|
|
1284
1317
|
|
|
1285
1318
|
def _analyze_gradle(self, root: Path) -> tuple[list[DependencyRecord], list[str]]:
|
|
1286
1319
|
for filename in ("build.gradle", "build.gradle.kts"):
|
|
@@ -1307,7 +1340,20 @@ class DependencyAnalyzer:
|
|
|
1307
1340
|
except OSError:
|
|
1308
1341
|
pass
|
|
1309
1342
|
|
|
1310
|
-
|
|
1343
|
+
deduped = self._dedupe(records)
|
|
1344
|
+
gradle_limitations = [
|
|
1345
|
+
"gradle: no compatible lockfile found; transitive dependencies unavailable"
|
|
1346
|
+
]
|
|
1347
|
+
# Honest-zero guard: if the build file declares a dependencies block but
|
|
1348
|
+
# nothing was resolved, report a GAP rather than a confident "0 deps".
|
|
1349
|
+
if not deduped and re.search(r"\bdependencies\s*\{", content):
|
|
1350
|
+
jar_count = sum(1 for _ in root.glob("lib/**/*.jar"))
|
|
1351
|
+
gap = ("gradle: dependencies declared but none resolved by static "
|
|
1352
|
+
"parsing (non-standard declaration syntax)")
|
|
1353
|
+
if jar_count:
|
|
1354
|
+
gap += f"; {jar_count} jar(s) present under lib/ (not parsed)"
|
|
1355
|
+
gradle_limitations.append(gap)
|
|
1356
|
+
return deduped, gradle_limitations
|
|
1311
1357
|
return [], []
|
|
1312
1358
|
|
|
1313
1359
|
def _parse_gradle_properties(self, root: Path, content: str) -> dict[str, str]:
|
|
@@ -7,8 +7,14 @@ deterministic source-text matching (same approach as the JNDI datasource scan in
|
|
|
7
7
|
|
|
8
8
|
Covered clients:
|
|
9
9
|
|
|
10
|
-
* HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative)
|
|
11
|
-
|
|
10
|
+
* HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative),
|
|
11
|
+
JDK/Apache/OkHttp clients
|
|
12
|
+
* LDAP — ``LdapTemplate``, JNDI ``InitialLdapContext``/``LdapContext``
|
|
13
|
+
* DNS — JNDI ``DirContext`` configured with ``DnsContextFactory`` (BUG #2:
|
|
14
|
+
``DirContext`` is protocol-agnostic and is classified by its
|
|
15
|
+
``INITIAL_CONTEXT_FACTORY``, not assumed to be LDAP)
|
|
16
|
+
* SMTP — JavaMail / Jakarta Mail (BUG #1: gated on a mail import so the bare
|
|
17
|
+
word "Transport" in a log string is not a false positive)
|
|
12
18
|
* JMS — ``JmsTemplate``, ActiveMQ connection factories
|
|
13
19
|
|
|
14
20
|
Each hit is reported with a ``file:line`` evidence anchor and, when a literal URL
|
|
@@ -28,6 +34,24 @@ _URL_RE = re.compile(r'"((?:https?|ldaps?|tcp|amqp|jms|nio)://[^"]*)"')
|
|
|
28
34
|
# First string literal on a line (fallback target, e.g. WebClient.create("x")).
|
|
29
35
|
_STR_RE = re.compile(r'"([^"]+)"')
|
|
30
36
|
|
|
37
|
+
# BUG #1 (v1.68.0): a JavaMail/Jakarta-Mail import is required before an SMTP token
|
|
38
|
+
# (Transport, MimeMessage) is trusted. The bare word "Transport" is also a common
|
|
39
|
+
# English noun that appears in log strings ( logger.warn("Transport initialization
|
|
40
|
+
# failure") in non-mail code), so a mail-import gate plus a string-literal skip
|
|
41
|
+
# stops those false positives.
|
|
42
|
+
_MAIL_IMPORT_RE = re.compile(
|
|
43
|
+
r"^\s*import\s+(?:(?:javax|jakarta)\.mail|org\.springframework\.mail)\b",
|
|
44
|
+
re.MULTILINE,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# BUG #2 (v1.68.0): javax.naming.directory.{DirContext,InitialDirContext} is NOT
|
|
48
|
+
# LDAP-specific — the actual protocol is decided by the value bound to
|
|
49
|
+
# Context.INITIAL_CONTEXT_FACTORY. com.sun.jndi.dns.DnsContextFactory means DNS
|
|
50
|
+
# (SRV/A record lookups), com.sun.jndi.ldap.LdapCtxFactory means LDAP. Classify by
|
|
51
|
+
# the factory class present in the file instead of defaulting to LDAP.
|
|
52
|
+
_DNS_FACTORY_RE = re.compile(r"jndi\.dns|DnsContextFactory", re.IGNORECASE)
|
|
53
|
+
_LDAP_FACTORY_RE = re.compile(r"jndi\.ldap|LdapCtxFactory", re.IGNORECASE)
|
|
54
|
+
|
|
31
55
|
# Declarative HTTP client. Attrs may span multiple lines, so matched on full text.
|
|
32
56
|
_FEIGN_RE = re.compile(r"@FeignClient\s*\(([^)]*)\)", re.DOTALL)
|
|
33
57
|
_ATTR_URL_RE = re.compile(r'url\s*=\s*"([^"]*)"')
|
|
@@ -82,6 +106,39 @@ def _extract_target(line: str) -> Optional[str]:
|
|
|
82
106
|
return None
|
|
83
107
|
|
|
84
108
|
|
|
109
|
+
def _in_string_literal(line: str, idx: int) -> bool:
|
|
110
|
+
"""True if char offset ``idx`` falls inside a double-quoted string on ``line``.
|
|
111
|
+
|
|
112
|
+
BUG #1 (v1.68.0): a Java type token never legitimately appears inside a string
|
|
113
|
+
literal, so a match there (e.g. the word "Transport" in a log message) is noise,
|
|
114
|
+
not a real client construct. Counts unescaped quotes before ``idx``.
|
|
115
|
+
"""
|
|
116
|
+
quote_count = 0
|
|
117
|
+
i = 0
|
|
118
|
+
while i < idx and i < len(line):
|
|
119
|
+
ch = line[i]
|
|
120
|
+
if ch == "\\":
|
|
121
|
+
i += 2
|
|
122
|
+
continue
|
|
123
|
+
if ch == '"':
|
|
124
|
+
quote_count += 1
|
|
125
|
+
i += 1
|
|
126
|
+
return quote_count % 2 == 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _classify_naming_factory(text: str) -> Optional[str]:
|
|
130
|
+
"""Classify a javax.naming.directory usage by its INITIAL_CONTEXT_FACTORY.
|
|
131
|
+
|
|
132
|
+
Returns ``"dns"``, ``"ldap"``, or ``None`` (factory not statically resolvable).
|
|
133
|
+
See BUG #2 — DirContext alone does not imply LDAP.
|
|
134
|
+
"""
|
|
135
|
+
if _DNS_FACTORY_RE.search(text):
|
|
136
|
+
return "dns"
|
|
137
|
+
if _LDAP_FACTORY_RE.search(text):
|
|
138
|
+
return "ldap"
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
85
142
|
def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
86
143
|
"""Detect outbound integrations across ``file_paths`` (relative to ``root``).
|
|
87
144
|
|
|
@@ -93,18 +150,31 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
93
150
|
seen: "set[tuple[str, str, Optional[str], str]]" = set()
|
|
94
151
|
records: "list[dict]" = []
|
|
95
152
|
|
|
96
|
-
def _add(
|
|
153
|
+
def _add(
|
|
154
|
+
kind: str,
|
|
155
|
+
client: str,
|
|
156
|
+
target: Optional[str],
|
|
157
|
+
rel: str,
|
|
158
|
+
line: int,
|
|
159
|
+
confidence: Optional[str] = None,
|
|
160
|
+
) -> None:
|
|
97
161
|
evidence = f"{rel}:{line}"
|
|
98
162
|
key = (kind, client, target, evidence)
|
|
99
163
|
if key in seen:
|
|
100
164
|
return
|
|
101
165
|
seen.add(key)
|
|
102
|
-
|
|
166
|
+
rec = {
|
|
103
167
|
"kind": kind,
|
|
104
168
|
"client": client,
|
|
105
169
|
"target": target,
|
|
106
170
|
"evidence": evidence,
|
|
107
|
-
}
|
|
171
|
+
}
|
|
172
|
+
# Per-record confidence is emitted only when the classification is uncertain
|
|
173
|
+
# (e.g. a JNDI DirContext whose factory could not be resolved). Confident hits
|
|
174
|
+
# stay schema-clean without the field.
|
|
175
|
+
if confidence is not None:
|
|
176
|
+
rec["confidence"] = confidence
|
|
177
|
+
records.append(rec)
|
|
108
178
|
|
|
109
179
|
for rel in file_paths:
|
|
110
180
|
try:
|
|
@@ -126,6 +196,10 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
126
196
|
)
|
|
127
197
|
_add("http", "feign", target, rel, _line_of(text, m.start()))
|
|
128
198
|
|
|
199
|
+
# Per-file context used to disambiguate / gate uncertain tokens (BUG #1/#2).
|
|
200
|
+
has_mail_import = bool(_MAIL_IMPORT_RE.search(text))
|
|
201
|
+
naming_factory = _classify_naming_factory(text)
|
|
202
|
+
|
|
129
203
|
# Token clients — per line, skipping imports/package/comment noise.
|
|
130
204
|
# First pass records the declaration site and any variable name bound to
|
|
131
205
|
# the client, so a later call site (where the URL literal usually lives)
|
|
@@ -146,7 +220,28 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
146
220
|
m = token_re.search(line)
|
|
147
221
|
if not m:
|
|
148
222
|
continue
|
|
149
|
-
|
|
223
|
+
# BUG #1: a token matched inside a string literal (log message, doc
|
|
224
|
+
# comment text) is never a real client construct — skip it.
|
|
225
|
+
if _in_string_literal(line, m.start()):
|
|
226
|
+
continue
|
|
227
|
+
confidence: Optional[str] = None
|
|
228
|
+
# BUG #1: SMTP tokens require a JavaMail/Jakarta-Mail import to be
|
|
229
|
+
# trusted — "Transport" / "MimeMessage" are too generic otherwise.
|
|
230
|
+
if kind == "smtp" and not has_mail_import:
|
|
231
|
+
continue
|
|
232
|
+
# BUG #2: javax.naming.directory.{Dir,Initial}Context is protocol-
|
|
233
|
+
# agnostic. Reclassify by the configured context factory; default to
|
|
234
|
+
# an explicit low-confidence "unknown" rather than assuming LDAP.
|
|
235
|
+
if client == "jndi-ldap" and "Dir" in token_re.pattern:
|
|
236
|
+
if naming_factory == "dns":
|
|
237
|
+
kind, client = "dns", "jndi-dns"
|
|
238
|
+
elif naming_factory == "ldap":
|
|
239
|
+
kind, client = "ldap", "jndi-ldap"
|
|
240
|
+
else:
|
|
241
|
+
kind, client, confidence = (
|
|
242
|
+
"naming-directory-unknown", "jndi-dircontext", "low",
|
|
243
|
+
)
|
|
244
|
+
_add(kind, client, _extract_target(line), rel, lineno, confidence)
|
|
150
245
|
tok = m.group(0)
|
|
151
246
|
# `Type name` (field/local decl) and `name = new Type(` forms.
|
|
152
247
|
decl = re.search(re.escape(tok) + r"\s+(\w+)\b", line)
|
|
@@ -186,9 +281,13 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
186
281
|
"confidence": confidence,
|
|
187
282
|
"coverage_note": (
|
|
188
283
|
"Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
|
|
189
|
-
"+ JNDI),
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"statically
|
|
284
|
+
"+ JNDI), DNS (JNDI DirContext w/ DnsContextFactory), SMTP (JavaMail, "
|
|
285
|
+
"import-gated), and JMS client constructs by source-text matching. JNDI "
|
|
286
|
+
"DirContext usage is classified by its INITIAL_CONTEXT_FACTORY (dns vs "
|
|
287
|
+
"ldap); when the factory is not statically resolvable the kind is "
|
|
288
|
+
"'naming-directory-unknown' with confidence='low', never assumed LDAP. A "
|
|
289
|
+
"count of 0 means no such construct was found, not that the system has no "
|
|
290
|
+
"outbound integrations — runtime/DI-wired clients are not statically "
|
|
291
|
+
"visible."
|
|
193
292
|
),
|
|
194
293
|
}
|
|
@@ -774,7 +774,7 @@ def run_migrate_flow_impl(repo_path: str, min_severity: str = "low") -> dict[str
|
|
|
774
774
|
if not _is_java_repo(repo_path):
|
|
775
775
|
quality_warnings.append(
|
|
776
776
|
"not_a_java_repo: migration analysis targets Spring Boot / Java repos. "
|
|
777
|
-
"Result will be empty (readiness_score=
|
|
777
|
+
"Result will be empty (readiness_score=null/N/A, no findings)."
|
|
778
778
|
)
|
|
779
779
|
|
|
780
780
|
report = _exec(["migrate-check", repo_path, "--min-severity", min_severity])
|
|
@@ -798,7 +798,7 @@ depth: BFS depth for indirect caller traversal (1–8, default: 4).
|
|
|
798
798
|
Analyzes codebase for modernization opportunities: dead zones, hotspot scores, upgrade candidates.
|
|
799
799
|
|
|
800
800
|
Maps to: sourcecode modernize <repo_path>
|
|
801
|
-
Returns: hotspot_candidates (high fan-in + git churn),
|
|
801
|
+
Returns: hotspot_candidates (high fan-in + git churn), statically_unreferenced (zero-caller classes — NOT confirmed dead; verify no framework dispatch) + framework_dispatched,
|
|
802
802
|
high_coupling_nodes, subsystem_summary, cross_module_tangles, recommendation.
|
|
803
803
|
|
|
804
804
|
Best for: refactor planning, identifying where to start, finding safe removal candidates.
|
|
@@ -1403,7 +1403,7 @@ Spring Boot 2→3 migration readiness: javax→jakarta namespace blockers. JAVA
|
|
|
1403
1403
|
When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
|
|
1404
1404
|
or upgrading from Spring Boot 2.x to 3.x. Use BEFORE get_spring_audit when the goal
|
|
1405
1405
|
is migration planning rather than ongoing Spring semantic audit.
|
|
1406
|
-
Do NOT call on non-Java repositories — returns readiness_score=
|
|
1406
|
+
Do NOT call on non-Java repositories — returns readiness_score=null (N/A) with no findings.
|
|
1407
1407
|
|
|
1408
1408
|
Rules detected:
|
|
1409
1409
|
MIG-001 critical — javax.persistence imports (JPA; will not compile after migration)
|
|
@@ -1415,7 +1415,7 @@ Rules detected:
|
|
|
1415
1415
|
MIG-007 medium — javax.inject imports (DI annotations)
|
|
1416
1416
|
MIG-008 medium — javax.ws.rs imports (JAX-RS API)
|
|
1417
1417
|
|
|
1418
|
-
Returns: schema_version, readiness_score (0–100; 100=ready to migrate),
|
|
1418
|
+
Returns: schema_version, readiness_score (0–100, or null=N/A when no migration target applies; 100=ready to migrate),
|
|
1419
1419
|
jakarta_readiness / boot3_readiness / jdk_modernization (per-dimension 0–100),
|
|
1420
1420
|
blocking_count, estimated_effort_days, spring_boot_2_detected (true|false|null —
|
|
1421
1421
|
null=undetermined, never assumed true), spring_boot_version_detected,
|