sourcecode 1.71.0__tar.gz → 1.73.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.71.0 → sourcecode-1.73.0}/CHANGELOG.md +114 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/PKG-INFO +1 -1
- {sourcecode-1.71.0 → sourcecode-1.73.0}/pyproject.toml +1 -1
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/canonical_ir.py +14 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/cli.py +111 -19
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/java.py +99 -8
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/parsers.py +23 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/explain.py +7 -2
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/integration_detector.py +54 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/migrate_check.py +54 -13
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/repository_ir.py +346 -10
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/serializer.py +6 -1
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/spring_security_audit.py +11 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/spring_tx_analyzer.py +70 -9
- {sourcecode-1.71.0 → sourcecode-1.73.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/.gitignore +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/.ruff.toml +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/LICENSE +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/README.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/SECURITY.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/raw +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.71.0 → sourcecode-1.73.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,119 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.73.0] — 2026-07-01
|
|
4
|
+
|
|
5
|
+
### Fixed — audit reliability on a large non-Boot Spring monolith (Alfresco Community Repo field test)
|
|
6
|
+
|
|
7
|
+
A real architecture audit of **alfresco-community-repo** (`fe6dd26c3f`; 6,549
|
|
8
|
+
classes, 24 Maven modules, Spring 7 **without Spring Boot**, iBatis persistence,
|
|
9
|
+
Camel/ActiveMQ messaging, Acegi-derived security, WebScript REST surface)
|
|
10
|
+
surfaced seven defects — each capable of leading an automated consultant to a
|
|
11
|
+
false conclusion. All fixed at root cause with regression coverage and
|
|
12
|
+
re-verified on the live repo.
|
|
13
|
+
|
|
14
|
+
- **`--compact` did not resolve `language_version` from a Maven property (BUG #1).**
|
|
15
|
+
Alfresco declares `<maven.compiler.source>${java.version}</...>` chained to
|
|
16
|
+
`<java.version>21</java.version>`; the Java-stack detector picked the first key
|
|
17
|
+
and emitted the literal `${java.version}` while `migrate-check` resolved it. A
|
|
18
|
+
shared `substitute_maven_properties` resolver (in `detectors/parsers.py`) now
|
|
19
|
+
backs BOTH paths — `--compact` reports `21`.
|
|
20
|
+
- **`explain --output` wrote Markdown to a `.json` file with no warning (BUG #2).**
|
|
21
|
+
Broke any consumer assuming JSON parity across subcommands. `explain` now infers
|
|
22
|
+
JSON from a `.json` output extension (an explicit `--format` always wins), and
|
|
23
|
+
`--help` documents that its default output is human-readable text/Markdown.
|
|
24
|
+
- **`entry_points.bootstrap` false positives on `*Application.java` by name (BUG #3).**
|
|
25
|
+
Alfresco's XSD/JAXB-generated `Application.java` model classes (no `main()`, no
|
|
26
|
+
bootstrap annotation, in a WAR) were listed as application entry points. A file
|
|
27
|
+
now qualifies only with a real `main(String[])` or a bootstrap annotation
|
|
28
|
+
(`@SpringBootApplication` / WAR `SpringBootServletInitializer` / `@QuarkusMain`
|
|
29
|
+
/ …); the serializer's name-based bootstrap fallback was removed.
|
|
30
|
+
- **"MyBatis" framework declared against the tool's own zero evidence (BUG #4).**
|
|
31
|
+
A `org.mybatis:mybatis` pom coordinate alone produced a "MyBatis" framework label
|
|
32
|
+
while the `mybatis` block reported `mapper_interfaces: 0, xml_files: 0` (Alfresco's
|
|
33
|
+
SQL layer is iBatis-style `*-SqlMap.xml`, surfaced separately). MyBatis is now
|
|
34
|
+
emitted only with real usage evidence — an `@Mapper` interface or a `*Mapper.xml`.
|
|
35
|
+
- **`export --integrations` missed ActiveMQ/Camel messaging (BUG #5).**
|
|
36
|
+
JMS detection recognized little beyond a direct `JmsTemplate`, reporting 1
|
|
37
|
+
integration on a repo with an ActiveMQ connection-factory config and Camel routes.
|
|
38
|
+
Added `ActiveMQSslConnectionFactory` / pooled / caching connection-factory tokens
|
|
39
|
+
and an Apache Camel route pass (literal `activemq:`/`jms:` endpoint URIs, plus
|
|
40
|
+
variable-URI `from()/to()` routes inside a `RouteBuilder` as honest low-confidence
|
|
41
|
+
`camel-route` messaging records). Alfresco JMS/messaging surface: 1 → 5.
|
|
42
|
+
- **`readiness_score` Hibernate false-positive — regression test (BUG #6).**
|
|
43
|
+
v1.63.0 once scored this Hibernate-free repo 42/100 with a phantom
|
|
44
|
+
`hibernate_rewrite` headline (~46 days). Already fixed by v1.72.0; added an
|
|
45
|
+
explicit regression test (`no Hibernate/JPA imports → hibernate.detected False,
|
|
46
|
+
headline_blocker never hibernate_rewrite`) so a future detector refactor cannot
|
|
47
|
+
reintroduce it.
|
|
48
|
+
- **`endpoints` / C4 `api_surface` returned 0 with no explanation (BUG #7).**
|
|
49
|
+
On an obvious API server (WebScripts: XML descriptor + handler class, not
|
|
50
|
+
`@RestController`), a bare `total: 0` reads as "no public API". `endpoints` now
|
|
51
|
+
emits a first-class `zero_result_reason` naming the routing patterns searched and
|
|
52
|
+
any non-Spring surface detected (Alfresco: 482 WebScript descriptors / 901 handler
|
|
53
|
+
classes), and the C4 export's `api_surface` carries the same explanation.
|
|
54
|
+
|
|
55
|
+
## [1.72.0] — 2026-07-01
|
|
56
|
+
|
|
57
|
+
### Fixed — correctness on a large real-world Spring Boot 3 monolith (Broadleaf Commerce CE field test)
|
|
58
|
+
|
|
59
|
+
A downstream architecture audit of **BroadleafCommerce** (`97705634ac`, branch
|
|
60
|
+
`develop-7.0.x`; ~3,000 Java classes, 13 Maven poms, Spring Boot 3.5 / Spring 6.2 /
|
|
61
|
+
Hibernate 5.6) hand-verified against source surfaced seven reproducible
|
|
62
|
+
correctness defects across `modernize`, `spring-audit`, `migrate-check`, and
|
|
63
|
+
`export --integrations`. Each fix targets the root cause and adds regression
|
|
64
|
+
coverage; all were re-verified against the live repo.
|
|
65
|
+
|
|
66
|
+
- **`spring-audit` TX-005 — 100% false-positive rate (4/4) on expected-exception idioms.**
|
|
67
|
+
The "swallowed exception in `@Transactional`" rule flagged the standard JPA
|
|
68
|
+
control-flow idiom (`Query.getSingleResult()` → `catch (NoResultException)` →
|
|
69
|
+
insert; tolerant insert → `catch (EntityExistsException)`) and catches carrying an
|
|
70
|
+
explicit `// do nothing — acceptable condition` intent comment. TX-005 now skips
|
|
71
|
+
catches whose caught type is a well-known expected-control-flow exception
|
|
72
|
+
(`NoResultException`, `NonUniqueResultException`, `EntityExistsException`,
|
|
73
|
+
`EmptyResultDataAccessException`, …) and catches whose body documents a deliberate
|
|
74
|
+
no-op, while still flagging a genuine unexpected-exception swallow.
|
|
75
|
+
- **`modernize` `statically_unreferenced` — ~95% false positives on annotation-constant nested classes.**
|
|
76
|
+
Two compounding defects: (a) the pre-scan fast path emitted nested types as
|
|
77
|
+
`pkg.Inner`, dropping the enclosing class, so distinct nested types with the same
|
|
78
|
+
simple name (Broadleaf has 25 `GroupName`s) collapsed onto one colliding FQN;
|
|
79
|
+
(b) static-constant reads inside annotation arguments
|
|
80
|
+
(`@AdminPresentation(group = GroupName.General)`, incl. the fully-qualified
|
|
81
|
+
`Outer.Nested.CONST` form and class-level annotations) were never counted as
|
|
82
|
+
references, so the constant holders read as zero-caller dead code. Now: nested
|
|
83
|
+
symbols are binary-qualified (`Outer.Inner`); a new `references` edge pass resolves
|
|
84
|
+
annotation-value reads (bare, inherited-nested via supertypes, and qualified
|
|
85
|
+
chains) in both parsed and pre-scan-skipped files. Presentation-holder false
|
|
86
|
+
positives on Broadleaf went 11→0 (the survivors are genuinely-unused declarations).
|
|
87
|
+
- **`modernize` `cross_module_tangles` — re-emitted the subsystem list instead of coupling.**
|
|
88
|
+
The field echoed `{label, class_count, member_count}` per subsystem with no edge
|
|
89
|
+
data. It now measures real directed inter-subsystem coupling from structural graph
|
|
90
|
+
edges, with `edge_count`, `reverse_edge_count`, and `mutual` (bidirectional/cyclic
|
|
91
|
+
= the actual tangle) — e.g. Broadleaf `profile↔common` is now correctly flagged
|
|
92
|
+
mutual.
|
|
93
|
+
- **`migrate-check` MIG-031 — test-scope config counted as a production blocker + wrong explanation.**
|
|
94
|
+
A test-only `bl-applicationContext-test-security.xml` (under `src/main/resources`,
|
|
95
|
+
loaded only by test `@ContextConfiguration`) inflated `blocking_count`; XML config
|
|
96
|
+
with a `test` filename marker is now classified `code_context: "test"`
|
|
97
|
+
(blocking_count 1→0). The explanation was a static OR-template asserting both
|
|
98
|
+
triggers; it now reports only the sub-pattern that actually matched (auto-config
|
|
99
|
+
vs versioned legacy schema).
|
|
100
|
+
- **`export --integrations` — missed in-code Spring Security LDAP (false negative).**
|
|
101
|
+
Subclassing `LdapUserDetailsMapper` and using `DirContextOperations` — the
|
|
102
|
+
idiomatic Spring Security LDAP auth mapper — was undetected (Broadleaf reported 0
|
|
103
|
+
LDAP). Added `LdapUserDetailsMapper` / `DirContextOperations` / context-source
|
|
104
|
+
tokens; Broadleaf now reports 5 LDAP integration points.
|
|
105
|
+
- **`explain` vs `modernize` in-degree — silently contradictory numbers.**
|
|
106
|
+
The same class showed different fan-in in the two commands with no explanation.
|
|
107
|
+
Both metrics are now self-describing: `modernize.high_coupling_nodes` documents
|
|
108
|
+
`in_degree` as the raw all-edge-type incoming count; `explain` labels its value
|
|
109
|
+
as "N distinct caller classes".
|
|
110
|
+
- **`spring-audit` vs `endpoints` — unreconcilable endpoint counts.**
|
|
111
|
+
The canonical IR (feeding `spring-audit`, `impact`, `validation`) did not apply the
|
|
112
|
+
`endpoints` command's filter for framework dynamic-admin FQN-shaped paths, so its
|
|
113
|
+
count was silently inflated. The filter is now shared; `endpoints_analyzed` is
|
|
114
|
+
documented as the deduplicated canonical count (≤ `endpoints` total, difference =
|
|
115
|
+
multi-prefix collapse), reconciling 225 vs 220 on Broadleaf.
|
|
116
|
+
|
|
3
117
|
## [1.71.0] — 2026-07-01
|
|
4
118
|
|
|
5
119
|
### Fixed — framework-framing & migration reliability on a large non-Boot Spring repo (Jenkins core field test)
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import hashlib
|
|
18
18
|
import json
|
|
19
|
+
import re
|
|
19
20
|
from dataclasses import dataclass, field
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Any, Optional
|
|
@@ -37,6 +38,11 @@ IR_SCHEMA_VERSION = "1.0.0"
|
|
|
37
38
|
# Edge types excluded from reverse graph (mirrors repository_ir._REVERSE_EXCLUDE)
|
|
38
39
|
_REVERSE_EXCLUDE: frozenset[str] = frozenset({"annotated_with", "mapped_to"})
|
|
39
40
|
|
|
41
|
+
# A route path that looks like a Java FQN is framework dynamic-admin routing, not a
|
|
42
|
+
# real REST endpoint (mirrors the filter in repository_ir.extract_java_endpoints so
|
|
43
|
+
# the CIR endpoint count reconciles with the `endpoints` command). See BUG #7.
|
|
44
|
+
_FQN_PATH_RE: re.Pattern = re.compile(r"/(org|com|net|io|edu)\.[a-z][a-z0-9]*\.[a-zA-Z]")
|
|
45
|
+
|
|
40
46
|
|
|
41
47
|
# ---------------------------------------------------------------------------
|
|
42
48
|
# CanonicalSecurity
|
|
@@ -324,9 +330,17 @@ def ir_dict_to_canonical(
|
|
|
324
330
|
# Build canonical endpoints from route_surface — stable ordering
|
|
325
331
|
route_surface = ir.get("route_surface") or []
|
|
326
332
|
# Deduplicate by endpoint id (route_surface can have duplicates from multi-prefix)
|
|
333
|
+
# BUG #7 (Broadleaf field test): the `endpoints` command (extract_java_endpoints)
|
|
334
|
+
# drops routes whose path looks like a Java FQN — framework dynamic-admin routing
|
|
335
|
+
# (Broadleaf @AdminSection registers entity class FQNs as URL segments) is not a
|
|
336
|
+
# real REST surface. The CIR endpoint list feeds `spring-audit` (endpoints_analyzed),
|
|
337
|
+
# `impact` and `validation`; it must apply the SAME filter so its count reconciles
|
|
338
|
+
# with `endpoints` instead of being silently inflated by those pseudo-routes.
|
|
327
339
|
_seen_ids: set[str] = set()
|
|
328
340
|
raw_endpoints: list[CanonicalEndpoint] = []
|
|
329
341
|
for r in route_surface:
|
|
342
|
+
if _FQN_PATH_RE.search(r.get("path", "") or ""):
|
|
343
|
+
continue
|
|
330
344
|
ep = _route_to_canonical_endpoint(r)
|
|
331
345
|
if ep.id not in _seen_ids:
|
|
332
346
|
_seen_ids.add(ep.id)
|
|
@@ -4221,6 +4221,7 @@ def _build_c4_export(
|
|
|
4221
4221
|
edges: "list[dict]",
|
|
4222
4222
|
endpoints: "list[dict]",
|
|
4223
4223
|
integrations: "dict",
|
|
4224
|
+
endpoint_meta: "Optional[dict]" = None,
|
|
4224
4225
|
) -> "dict":
|
|
4225
4226
|
"""Assemble a unified, tool-agnostic C4 architecture export + incremental manifest.
|
|
4226
4227
|
|
|
@@ -4236,6 +4237,15 @@ def _build_c4_export(
|
|
|
4236
4237
|
# consumer counts/classifies modules instead of inferring them from leaf dirs.
|
|
4237
4238
|
module_graph["module_roots"] = _detect_module_roots(by_directory)
|
|
4238
4239
|
api_surface = _group_endpoints_by_controller(endpoints)
|
|
4240
|
+
# BUG #7 (Alfresco field test): carry the endpoint extractor's structured
|
|
4241
|
+
# zero-result explanation and non-Spring REST surface into api_surface, so a
|
|
4242
|
+
# C4 consumer seeing 0 controllers understands WHY (unrecognized routing, e.g.
|
|
4243
|
+
# WebScripts) instead of concluding the system has no public API.
|
|
4244
|
+
if endpoint_meta:
|
|
4245
|
+
if endpoint_meta.get("zero_result_reason"):
|
|
4246
|
+
api_surface["zero_result_reason"] = endpoint_meta["zero_result_reason"]
|
|
4247
|
+
if endpoint_meta.get("non_spring_rest_surface"):
|
|
4248
|
+
api_surface["non_spring_rest_surface"] = endpoint_meta["non_spring_rest_surface"]
|
|
4239
4249
|
containers = _detect_containers(root)
|
|
4240
4250
|
|
|
4241
4251
|
limitations: "list[str]" = []
|
|
@@ -4364,7 +4374,8 @@ def export_cmd(
|
|
|
4364
4374
|
from sourcecode.repository_ir import extract_java_endpoints
|
|
4365
4375
|
ir = build_repo_ir(file_list, root)
|
|
4366
4376
|
graph = ir.get("graph", {})
|
|
4367
|
-
|
|
4377
|
+
_ep_data = extract_java_endpoints(root)
|
|
4378
|
+
endpoints = _ep_data.get("endpoints", [])
|
|
4368
4379
|
data = _build_c4_export(
|
|
4369
4380
|
root,
|
|
4370
4381
|
file_list,
|
|
@@ -4372,6 +4383,7 @@ def export_cmd(
|
|
|
4372
4383
|
graph.get("edges", []),
|
|
4373
4384
|
endpoints,
|
|
4374
4385
|
detect_integrations(file_list, root),
|
|
4386
|
+
endpoint_meta=_ep_data,
|
|
4375
4387
|
)
|
|
4376
4388
|
output = _serialize_dict(data, format)
|
|
4377
4389
|
_emit_command_output(output, output_path, copy,
|
|
@@ -5215,14 +5227,16 @@ def explain_cmd(
|
|
|
5215
5227
|
Path("."),
|
|
5216
5228
|
help="Repository root (default: current directory)",
|
|
5217
5229
|
),
|
|
5218
|
-
format: str = typer.Option(
|
|
5219
|
-
|
|
5220
|
-
help="Output format: text (default) or json."
|
|
5221
|
-
|
|
5230
|
+
format: Optional[str] = typer.Option(
|
|
5231
|
+
None, "--format", "-f",
|
|
5232
|
+
help="Output format: text (default) or json. If unset and --output ends "
|
|
5233
|
+
"in .json, JSON is written automatically.",
|
|
5222
5234
|
),
|
|
5223
5235
|
output_path: Optional[Path] = typer.Option(
|
|
5224
5236
|
None, "--output", "-o",
|
|
5225
|
-
help="Write output to a file instead of stdout."
|
|
5237
|
+
help="Write output to a file instead of stdout. NOTE: the default format is "
|
|
5238
|
+
"human-readable text/Markdown, NOT JSON — pass --format json (or use a "
|
|
5239
|
+
".json output path) for machine-readable output.",
|
|
5226
5240
|
),
|
|
5227
5241
|
copy: bool = typer.Option(
|
|
5228
5242
|
False, "--copy", "-c",
|
|
@@ -5231,6 +5245,11 @@ def explain_cmd(
|
|
|
5231
5245
|
) -> None:
|
|
5232
5246
|
"""Human-readable architectural summary for a class.
|
|
5233
5247
|
|
|
5248
|
+
\b
|
|
5249
|
+
OUTPUT FORMAT: defaults to text/Markdown (human-oriented). Unlike most
|
|
5250
|
+
subcommands this is NOT JSON by default — pass --format json, or give an
|
|
5251
|
+
--output path ending in .json, for a machine-readable envelope.
|
|
5252
|
+
|
|
5234
5253
|
\b
|
|
5235
5254
|
Generates a structured explanation derived entirely from static analysis:
|
|
5236
5255
|
- Purpose and Spring stereotype
|
|
@@ -5278,6 +5297,15 @@ def explain_cmd(
|
|
|
5278
5297
|
)
|
|
5279
5298
|
raise typer.Exit(code=1)
|
|
5280
5299
|
|
|
5300
|
+
# BUG #2 (Alfresco field test): `explain -o out.json` previously wrote Markdown
|
|
5301
|
+
# to a .json file, breaking any programmatic consumer that assumed JSON parity
|
|
5302
|
+
# with the other subcommands. Infer JSON from a .json output extension when
|
|
5303
|
+
# --format is unset; an explicit --format always wins.
|
|
5304
|
+
if format is None:
|
|
5305
|
+
if output_path and str(output_path).lower().endswith(".json"):
|
|
5306
|
+
format = "json"
|
|
5307
|
+
else:
|
|
5308
|
+
format = "text"
|
|
5281
5309
|
_enforce_format("explain", format)
|
|
5282
5310
|
|
|
5283
5311
|
file_list = find_java_files(target)
|
|
@@ -5764,11 +5792,63 @@ def modernize_cmd(
|
|
|
5764
5792
|
key=lambda h: (-h["hotspot_score"], h["fqn"]),
|
|
5765
5793
|
)[:15]
|
|
5766
5794
|
|
|
5767
|
-
# Cross-module tangles:
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
)
|
|
5795
|
+
# Cross-module tangles: actual inter-subsystem coupling measured from the
|
|
5796
|
+
# dependency graph, NOT a re-labelled subsystem list. For every structural
|
|
5797
|
+
# edge whose endpoints live in two different subsystems we tally a directed
|
|
5798
|
+
# (from_subsystem → to_subsystem) coupling count; pairs coupled in BOTH
|
|
5799
|
+
# directions are the real tangles (bidirectional/cyclic module coupling).
|
|
5800
|
+
_graph_edges: list = (ir.get("graph") or {}).get("edges") or []
|
|
5801
|
+
# Structural coupling edge types. annotated_with / mapped_to / event edges are
|
|
5802
|
+
# excluded (metadata + separate topology, not module-decomposition coupling).
|
|
5803
|
+
_TANGLE_EDGE_TYPES = frozenset({
|
|
5804
|
+
"imports", "injects", "extends", "implements",
|
|
5805
|
+
"references", "calls", "instantiates", "returns",
|
|
5806
|
+
})
|
|
5807
|
+
_fqn_to_pkg: dict[str, str] = {}
|
|
5808
|
+
_pkg_to_label: dict[str, str] = {}
|
|
5809
|
+
for _s in subsystems:
|
|
5810
|
+
_pp = _s.get("package_prefix") or _s.get("pkg") or ""
|
|
5811
|
+
if not _pp:
|
|
5812
|
+
continue
|
|
5813
|
+
_pkg_to_label[_pp] = _s.get("label") or _s.get("name") or _pp
|
|
5814
|
+
for _m in (_s.get("members") or []):
|
|
5815
|
+
_fqn_to_pkg[_m] = _pp
|
|
5816
|
+
|
|
5817
|
+
from collections import Counter as _Counter
|
|
5818
|
+
_pair_counts: "_Counter[tuple[str, str]]" = _Counter()
|
|
5819
|
+
_pair_example: dict[tuple[str, str], str] = {}
|
|
5820
|
+
for _e in _graph_edges:
|
|
5821
|
+
if _e.get("type") not in _TANGLE_EDGE_TYPES:
|
|
5822
|
+
continue
|
|
5823
|
+
_a = _fqn_to_pkg.get(_e.get("from"))
|
|
5824
|
+
_b = _fqn_to_pkg.get(_e.get("to"))
|
|
5825
|
+
if not _a or not _b or _a == _b:
|
|
5826
|
+
continue
|
|
5827
|
+
_pair_counts[(_a, _b)] += 1
|
|
5828
|
+
_pair_example.setdefault((_a, _b), f"{_e.get('from')} → {_e.get('to')}")
|
|
5829
|
+
|
|
5830
|
+
_cross_module_tangles = []
|
|
5831
|
+
for (_a, _b), _cnt in sorted(
|
|
5832
|
+
_pair_counts.items(), key=lambda kv: (-kv[1], kv[0])
|
|
5833
|
+
):
|
|
5834
|
+
_reverse = _pair_counts.get((_b, _a), 0)
|
|
5835
|
+
_cross_module_tangles.append({
|
|
5836
|
+
"from_subsystem": _pkg_to_label.get(_a, _a),
|
|
5837
|
+
"to_subsystem": _pkg_to_label.get(_b, _b),
|
|
5838
|
+
"from_package": _a,
|
|
5839
|
+
"to_package": _b,
|
|
5840
|
+
"edge_count": _cnt,
|
|
5841
|
+
"reverse_edge_count": _reverse,
|
|
5842
|
+
# A mutual (cyclic) dependency is the actual "tangle" — both modules
|
|
5843
|
+
# reference each other, so neither can be extracted independently.
|
|
5844
|
+
"mutual": _reverse > 0,
|
|
5845
|
+
"example": _pair_example.get((_a, _b), ""),
|
|
5846
|
+
})
|
|
5847
|
+
# Surface mutual tangles first (highest decomposition risk), then by strength.
|
|
5848
|
+
_cross_module_tangles.sort(
|
|
5849
|
+
key=lambda t: (not t["mutual"], -t["edge_count"], t["from_package"])
|
|
5850
|
+
)
|
|
5851
|
+
_cross_module_tangles = _cross_module_tangles[:15]
|
|
5772
5852
|
|
|
5773
5853
|
_summary = {
|
|
5774
5854
|
"total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
|
|
@@ -5837,6 +5917,17 @@ def modernize_cmd(
|
|
|
5837
5917
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
5838
5918
|
for n in coupling_nodes
|
|
5839
5919
|
],
|
|
5920
|
+
# BUG #6 (Broadleaf field test): make the in_degree metric self-describing
|
|
5921
|
+
# so it is not confused with `explain`'s caller count. They differ by design.
|
|
5922
|
+
"high_coupling_nodes_note": (
|
|
5923
|
+
"in_degree = raw count of incoming graph edges across ALL edge types "
|
|
5924
|
+
"(imports, injects, extends/implements, references, annotations), "
|
|
5925
|
+
"counted at symbol level. This is deliberately larger than and NOT "
|
|
5926
|
+
"equal to `sourcecode explain`'s caller count, which reports DISTINCT "
|
|
5927
|
+
"dependent classes (DI dependents + reverse-call-graph callers, "
|
|
5928
|
+
"deduplicated to class level). Use in_degree for blast-radius ranking, "
|
|
5929
|
+
"explain's caller list for the concrete dependents to inspect."
|
|
5930
|
+
),
|
|
5840
5931
|
"statically_unreferenced": [
|
|
5841
5932
|
{"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
|
|
5842
5933
|
for n in dead_zones
|
|
@@ -5853,14 +5944,15 @@ def modernize_cmd(
|
|
|
5853
5944
|
],
|
|
5854
5945
|
"subsystem_summary": _subsystem_summary,
|
|
5855
5946
|
"subsystem_summary_note": _subsystem_summary_note,
|
|
5856
|
-
"cross_module_tangles":
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5947
|
+
"cross_module_tangles": _cross_module_tangles,
|
|
5948
|
+
"cross_module_tangles_note": (
|
|
5949
|
+
"Directed inter-subsystem coupling measured from structural graph "
|
|
5950
|
+
"edges (imports/injects/extends/implements/references/calls). "
|
|
5951
|
+
"edge_count = edges from_package→to_package; mutual=true marks a "
|
|
5952
|
+
"bidirectional (cyclic) dependency — the actual tangle that blocks "
|
|
5953
|
+
"independent module extraction. Empty means no cross-subsystem "
|
|
5954
|
+
"structural coupling was detected."
|
|
5955
|
+
),
|
|
5864
5956
|
# BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
|
|
5865
5957
|
"recommendation": (
|
|
5866
5958
|
(
|
|
@@ -10,7 +10,11 @@ from sourcecode.detectors.base import (
|
|
|
10
10
|
EntryPoint,
|
|
11
11
|
StackDetection,
|
|
12
12
|
)
|
|
13
|
-
from sourcecode.detectors.parsers import
|
|
13
|
+
from sourcecode.detectors.parsers import (
|
|
14
|
+
read_text_lines,
|
|
15
|
+
substitute_maven_properties as _resolve_maven_property,
|
|
16
|
+
unique_strings,
|
|
17
|
+
)
|
|
14
18
|
from sourcecode.schema import FrameworkDetection
|
|
15
19
|
from sourcecode.tree_utils import flatten_file_tree
|
|
16
20
|
|
|
@@ -55,6 +59,14 @@ _JAX_RS_PROVIDER_RE = re.compile(r'@Provider\b')
|
|
|
55
59
|
|
|
56
60
|
# --- CDI / Jakarta EE scopes ---
|
|
57
61
|
_CDI_SCOPED_RE = re.compile(r'@(?:ApplicationScoped|RequestScoped|SessionScoped|Singleton|Dependent)\b')
|
|
62
|
+
# BUG #3: a genuine bootstrap entry declares a real main() or a bootstrap annotation
|
|
63
|
+
# / WAR initializer supertype — never merely a `*Application.java` filename.
|
|
64
|
+
_MAIN_METHOD_RE = re.compile(r'\bstatic\b[^;{}()]*\bmain\s*\(\s*(?:final\s+)?String')
|
|
65
|
+
_BOOTSTRAP_ANNOTATION_RE = re.compile(
|
|
66
|
+
r'@(?:SpringBootApplication|QuarkusMain|MicronautApplication|'
|
|
67
|
+
r'EnableAutoConfiguration|SpringBootConfiguration)\b'
|
|
68
|
+
r'|\bextends\s+SpringBootServletInitializer\b'
|
|
69
|
+
)
|
|
58
70
|
|
|
59
71
|
# --- Keycloak / Quarkus SPI ---
|
|
60
72
|
# Matches "implements EventListenerProvider" or "implements Foo, EventListenerProvider, Bar"
|
|
@@ -151,6 +163,18 @@ class JavaDetector(AbstractDetector):
|
|
|
151
163
|
# Spring profiles — check src/main/options/, src/main/resources/
|
|
152
164
|
spring_profiles = self._detect_spring_profiles(context.root, all_paths)
|
|
153
165
|
|
|
166
|
+
# BUG #4 (Alfresco field test): a `org.mybatis` coordinate in the pom is not
|
|
167
|
+
# proof the app USES MyBatis mappers. Alfresco declares org.mybatis:mybatis
|
|
168
|
+
# yet has zero @Mapper interfaces and zero *Mapper.xml — its SQL layer is
|
|
169
|
+
# iBatis-style *-SqlMap.xml (surfaced separately). Emitting "MyBatis" as an
|
|
170
|
+
# architecture framework then contradicted the tool's own evidence block
|
|
171
|
+
# (mapper_interfaces:0, xml_files:0) and mislabeled bean DTO mappers. Keep
|
|
172
|
+
# the label only with real usage evidence: a *Mapper.xml or an @Mapper interface.
|
|
173
|
+
if any(f.name == "MyBatis" for f in frameworks) and not self._has_mybatis_usage(
|
|
174
|
+
context.root, all_paths
|
|
175
|
+
):
|
|
176
|
+
frameworks = [f for f in frameworks if f.name != "MyBatis"]
|
|
177
|
+
|
|
154
178
|
entry_points = self._collect_entry_points(context)
|
|
155
179
|
transactional_classes = self._collect_transactional_classes(context, all_paths)
|
|
156
180
|
stack = StackDetection(
|
|
@@ -211,6 +235,17 @@ class JavaDetector(AbstractDetector):
|
|
|
211
235
|
break
|
|
212
236
|
break
|
|
213
237
|
|
|
238
|
+
# BUG #1 (Alfresco field test): a picked value can be a Maven property
|
|
239
|
+
# reference — Alfresco's <maven.compiler.source>${java.version}</...> chains
|
|
240
|
+
# to <java.version>21</java.version> in the SAME <properties> block. Resolve
|
|
241
|
+
# ${...} references against the pom's own properties so --compact reports the
|
|
242
|
+
# concrete version ("21") instead of the literal "${java.version}", matching
|
|
243
|
+
# what migrate-check already surfaces. Bounded iteration guards ref cycles.
|
|
244
|
+
if "language_version" in result:
|
|
245
|
+
result["language_version"] = _resolve_maven_property(
|
|
246
|
+
result["language_version"], props
|
|
247
|
+
)
|
|
248
|
+
|
|
214
249
|
return result
|
|
215
250
|
|
|
216
251
|
def _detect_spring_profiles(self, root: Path, all_paths: list[str]) -> list[str]:
|
|
@@ -425,15 +460,25 @@ class JavaDetector(AbstractDetector):
|
|
|
425
460
|
# Exclude test trees: test helpers like AdminApplication.java in
|
|
426
461
|
# integration/src/test/java/ must not be treated as production entrypoints.
|
|
427
462
|
from sourcecode.path_filters import is_test_path as _is_test_path
|
|
463
|
+
# BUG #3 (Alfresco field test): a `*Application.java` / `*Main.java` NAME does
|
|
464
|
+
# not make a file a bootstrap entry point. Alfresco has XSD-generated JAXB
|
|
465
|
+
# `Application.java` model classes (no main(), no bootstrap annotation) in a
|
|
466
|
+
# WAR with no own entry point — flagging them as `application` polluted the
|
|
467
|
+
# executive summary. Emit `kind="application"` ONLY when the file actually
|
|
468
|
+
# declares a `main(String[])` method or a bootstrap annotation.
|
|
428
469
|
app_candidates = [
|
|
429
|
-
p for p in
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
entry_points: list[EntryPoint] = [
|
|
434
|
-
EntryPoint(path=p, stack="java", kind="application", source="manifest")
|
|
435
|
-
for p in unique_strings(app_candidates)
|
|
470
|
+
p for p in unique_strings([
|
|
471
|
+
q for q in all_java
|
|
472
|
+
if q.endswith(("Application.java", "Main.java")) and not _is_test_path(q)
|
|
473
|
+
])
|
|
436
474
|
]
|
|
475
|
+
entry_points: list[EntryPoint] = []
|
|
476
|
+
for p in app_candidates:
|
|
477
|
+
verified = self._verify_bootstrap_entry(context.root / p)
|
|
478
|
+
if verified:
|
|
479
|
+
entry_points.append(EntryPoint(
|
|
480
|
+
path=p, stack="java", kind="application", source=verified,
|
|
481
|
+
))
|
|
437
482
|
|
|
438
483
|
# 2. Annotation-based scan: @RestController, @WebFilter, FilterRegistrationBean
|
|
439
484
|
# Prioritize Controller-named files so all REST controllers are detected
|
|
@@ -481,6 +526,52 @@ class JavaDetector(AbstractDetector):
|
|
|
481
526
|
unique_eps.append(ep)
|
|
482
527
|
return unique_eps
|
|
483
528
|
|
|
529
|
+
def _has_mybatis_usage(self, root: Path, all_paths: list[str]) -> bool:
|
|
530
|
+
"""True when the repo shows real MyBatis usage, not just a pom coordinate.
|
|
531
|
+
|
|
532
|
+
Evidence = a *Mapper.xml file OR a *Mapper.java carrying the MyBatis
|
|
533
|
+
@Mapper annotation. iBatis-style *-SqlMap.xml is deliberately NOT counted
|
|
534
|
+
here — it is a distinct legacy pattern surfaced elsewhere (BUG #4).
|
|
535
|
+
"""
|
|
536
|
+
if any(p.endswith("Mapper.xml") for p in all_paths):
|
|
537
|
+
return True
|
|
538
|
+
from sourcecode.path_filters import is_test_path as _is_test_path
|
|
539
|
+
mapper_java = [
|
|
540
|
+
p for p in all_paths
|
|
541
|
+
if p.endswith("Mapper.java") and not _is_test_path(p)
|
|
542
|
+
]
|
|
543
|
+
for rel in mapper_java[:_MAX_JAVA_ENTRY_SCAN]:
|
|
544
|
+
try:
|
|
545
|
+
abs_path = root / rel
|
|
546
|
+
if abs_path.stat().st_size > _MAX_FILE_SIZE:
|
|
547
|
+
continue
|
|
548
|
+
content = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
549
|
+
except OSError:
|
|
550
|
+
continue
|
|
551
|
+
if "org.apache.ibatis.annotations.Mapper" in content or "@Mapper" in content:
|
|
552
|
+
return True
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
def _verify_bootstrap_entry(self, abs_path: Path) -> "str | None":
|
|
556
|
+
"""Return the evidence source when *abs_path* is a real bootstrap entry.
|
|
557
|
+
|
|
558
|
+
A file is a genuine application entry point only if it declares a
|
|
559
|
+
`main(String[])` method OR carries a recognized bootstrap annotation /
|
|
560
|
+
WAR initializer supertype. Returns "main_method" / "bootstrap_annotation"
|
|
561
|
+
accordingly, else None (name alone is never sufficient — see BUG #3).
|
|
562
|
+
"""
|
|
563
|
+
try:
|
|
564
|
+
if abs_path.stat().st_size > _MAX_FILE_SIZE:
|
|
565
|
+
return None
|
|
566
|
+
content = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
567
|
+
except OSError:
|
|
568
|
+
return None
|
|
569
|
+
if _BOOTSTRAP_ANNOTATION_RE.search(content):
|
|
570
|
+
return "bootstrap_annotation"
|
|
571
|
+
if _MAIN_METHOD_RE.search(content):
|
|
572
|
+
return "main_method"
|
|
573
|
+
return None
|
|
574
|
+
|
|
484
575
|
def _augment_deep_java_controllers(self, context: DetectionContext, all_java: list[str]) -> None:
|
|
485
576
|
"""Scan standard Java source roots for *Controller*.java files not in all_java.
|
|
486
577
|
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
from collections.abc import Iterable
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any, Optional
|
|
7
8
|
|
|
9
|
+
_MAVEN_PROP_REF_RE = re.compile(r'\$\{([\w.\-]+)\}')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def substitute_maven_properties(text: str, props: "dict[str, str]") -> str:
|
|
13
|
+
"""Resolve Maven ${prop} references in *text* against *props*.
|
|
14
|
+
|
|
15
|
+
Shared single source of truth for Maven property resolution across
|
|
16
|
+
subcommands (BUG #1, Alfresco field test): both the --compact Java-stack
|
|
17
|
+
detector and migrate-check must resolve `${java.version}` → `21` identically.
|
|
18
|
+
Multi-level references (${a} where a=${b}) resolve in up to 3 passes; a
|
|
19
|
+
reference with no matching property is left verbatim (honest, not blanked).
|
|
20
|
+
"""
|
|
21
|
+
if not props or "${" not in text:
|
|
22
|
+
return text
|
|
23
|
+
resolved = text
|
|
24
|
+
for _ in range(3):
|
|
25
|
+
new = _MAVEN_PROP_REF_RE.sub(lambda m: props.get(m.group(1), m.group(0)), resolved)
|
|
26
|
+
if new == resolved:
|
|
27
|
+
break
|
|
28
|
+
resolved = new
|
|
29
|
+
return resolved
|
|
30
|
+
|
|
8
31
|
|
|
9
32
|
def load_json_file(path: Path) -> Optional[dict[str, Any]]:
|
|
10
33
|
"""Carga un fichero JSON sin lanzar si el contenido es invalido."""
|
|
@@ -298,10 +298,15 @@ def _structural_purpose(
|
|
|
298
298
|
head = f"Likely {role} (no DI annotations found)" if role else \
|
|
299
299
|
"Likely a structurally significant class (no DI annotations found)"
|
|
300
300
|
signals: list[str] = []
|
|
301
|
+
# BUG #6 (Broadleaf field test): this counts DISTINCT dependent classes (DI
|
|
302
|
+
# dependents + reverse-call-graph callers, deduped to class level), which is a
|
|
303
|
+
# different metric from modernize's node.in_degree (raw incoming-edge count
|
|
304
|
+
# across ALL edge types, symbol-level). Name it explicitly so the two numbers
|
|
305
|
+
# are not read as a contradiction when cross-referenced.
|
|
301
306
|
if in_degree >= 3:
|
|
302
|
-
signals.append(f"high in
|
|
307
|
+
signals.append(f"high fan-in ({in_degree} distinct caller classes)")
|
|
303
308
|
elif in_degree:
|
|
304
|
-
signals.append(f"
|
|
309
|
+
signals.append(f"{in_degree} distinct caller class{'es' if in_degree != 1 else ''}")
|
|
305
310
|
if lifecycle:
|
|
306
311
|
signals.append(f"lifecycle methods detected ({'/'.join(lifecycle)})")
|
|
307
312
|
suffix = f" — {', '.join(signals)}" if signals else ""
|