Lukasz Lenart created WW-5657:
---------------------------------
Summary: Reduce cognitive complexity in XWorkConverter
Key: WW-5657
URL: https://issues.apache.org/jira/browse/WW-5657
Project: Struts 2
Issue Type: Improvement
Components: XML Validators
Reporter: Lukasz Lenart
Fix For: 7.3.0
h2. Summary
SonarCloud reports three methods in
{{core/src/main/java/org/apache/struts2/conversion/impl/XWorkConverter.java}}
above the cognitive-complexity threshold ({{java:S3776}}, limit 15), plus a
handful of smaller findings in the same file. None is a correctness bug; all
are maintainability debt in a class that sits on the type-conversion path.
h2. Current state
Measured from the SonarCloud API on 2026-07-25
({{componentKeys=apache_struts:core/src/main/java/org/apache/struts2/conversion/impl/XWorkConverter.java}}).
|| Method || Cognitive complexity || Rule || Origin ||
| {{convertValue(Map, Object, Member, String, Object, Class)}} | *34* |
java:S3776 CRITICAL | long-standing |
| {{processMethodAnnotations(Map, Class)}} | *26* | java:S3776 CRITICAL |
introduced by WW-3871 |
| {{processFieldAnnotations(Map, Class)}} | *21* | java:S3776 CRITICAL |
introduced by WW-3871 |
WW-3871 replaced a single {{addConverterMapping}} of complexity 34 with four
named passes. That was a net improvement in readability, but two of the passes
still exceed the threshold on their own, because each carries the same
five-step pipeline of guards.
Also open on this file:
|| Rule || Location || Note ||
| java:S135 MINOR (x3) | the three annotation passes | "at most one
break/continue per loop" — the guard-clause {{continue}}s |
| java:S1192 CRITICAL | {{convertValue}} | literal {{"Unable to convert value
using type converter [{}]"}} repeated 3 times |
| java:S1172 MAJOR | {{handleConversionException}} | unused parameter
{{object}} |
| java:S112 MAJOR | {{buildConverterMapping}} | throws generic {{Exception}} |
| java:S1181 MAJOR | {{getConverter}} | "Catch Exception instead of Throwable"
— *see the warning below, do not apply blindly* |
h2. Proposed work
h3. 1. Extract the shared annotation-registration pipeline
{{processMethodAnnotations}} and {{processFieldAnnotations}} are the same five
steps in the same order, differing only in how the fallback name is derived,
the wording of two WARNs, and whether logging is gated:
# skip non-{{@TypeConversion}} annotations
# skip APPLICATION-scoped annotations with no explicit key (WARN)
# derive the name: explicit {{key()}}, else property name / field name
# resolve the key, skip if unresolvable (WARN)
# skip if the key is already mapped, otherwise delegate to
{{annotationProcessor}}
Extracting steps 2-5 into one helper taking the {{TypeConversion}}, the
{{Member}}, the derived fallback name and a "should log" flag collapses both
passes to a loop plus a delegate call. The guards become early {{return}}s in
the helper, which drops both methods under the S3776 threshold *and* clears the
three S135 findings — the {{continue}} statements disappear along with the
duplication.
This is the higher-value half of the ticket: it removes a copy-paste pair,
which is worth more than the Sonar number.
h3. 2. Decompose {{convertValue}}
The 87-line body does five separable jobs: unwrap a single-element
{{String[]}}, find a member-level converter, fall back to a class-level
converter, invoke it, and translate failures into conversion errors. Each is
independently nameable and testable. This is the older and larger of the two
problems, and the riskier one — see the constraint below.
h3. 3. Smaller findings
{{S1192}} (extract the repeated literal into a constant) and {{S1172}} (drop
the unused {{object}} parameter — check callers first, it is {{protected}} and
may be overridden downstream) are cheap and can ride along.
h2. Constraints
* *No behavioural change.* The conversion mapping registered for a given class,
and the precedence between the four passes, must stay identical. WW-3871 added
tests that pin the key-derivation rules, the class > method > field
precedence and first-writer-wins; they must pass untouched, as must
{{MyBeanActionTest}}, which is the backward-compatibility evidence for the
annotation forms.
* *Do not "fix" {{java:S1181}} by narrowing {{catch (Throwable)}} to {{catch
(Exception)}} in {{getConverter}}.* That catch is load-bearing:
{{getDeclaredFields()}} and {{getMethods()}} can throw {{NoClassDefFoundError}}
for a class with a member whose type is absent from the classpath, and the
handler turns it into {{addNoMapping(clazz)}}. Narrowing it would let an
{{Error}} escape into the OGNL property-access path. If the rule is to be
silenced, silence it with a comment explaining why, not by changing the catch.
* {{convertValue}} is {{public}} and
{{getConverter}}/{{handleConversionException}}/{{buildConverterMapping}} are
{{protected}} — this class is extended in the wild. Signature changes belong in
a major release; prefer extracting {{private}} helpers.
* Core module tests are JUnit 3/4, never JUnit 5: classes extending
{{XWorkTestCase}} use {{public void testXxx()}} with no {{@Test}} annotation,
which silently never runs there.
h2. Acceptance criteria
* No {{java:S3776}} issue remains open on {{XWorkConverter.java}} in SonarCloud.
* {{mvn test -DskipAssembly -pl core}} passes with no test modified to
accommodate the refactor. A test that had to change is a signal that behaviour
changed.
* No public or protected signature changes.
h2. Notes
Found while reviewing [PR #1812|https://github.com/apache/struts/pull/1812]
(WW-3871). Item 1 covers debt that PR introduced; item 2 predates it. If item 1
is folded into WW-3871 before it merges, this ticket narrows to
{{convertValue}} and the smaller findings.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)