Skip to content

fix(artifacts): a non-identifier view-dispatch target is not a dataflow variable, not a NullPointerException - #271

Merged
rahlk merged 1 commit into
mainfrom
fix/view-dispatch-null-var-npe
Sep 29, 2026
Merged

rahlk merged 1 commit into
mainfrom
fix/view-dispatch-null-var-npe

Conversation

@rahlk

@rahlk rahlk commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

ViewDispatches.resolve() ran the intra-procedural dataflow tier on s.varName() unguarded, but Site.varName() returns null for any dispatch target that is not a bare identifier — a call expression like return String.valueOf(count); inside a @RestController, as seen in Robot Shop's shipping service (Controller.count()):

@RestController
public class Controller {
    @GetMapping("/count")
    public String count() {
        long count = cityrepo.count();
        return String.valueOf(count);
    }
}

Spring entrypoint detection doesn't yet distinguish @RestController from @Controller (SpringEntrypointFinder.isEntrypointClass matches on annotation.getNameAsString().contains("Controller")), so a REST endpoint's String return is still gated into the view-name tier. The resulting null var reached DataflowTiers.intra and crashed in IntraTier.reachingLiteral's var.equals(edge.getVar()) at analysis level >= 3:

java.lang.NullPointerException: Cannot invoke "java.lang.String.equals(java.lang.Object)" because "var" is null
	at com.ibm.cldk.artifacts.DataflowTiers$IntraTier.reachingLiteral(DataflowTiers.java:174)
	at com.ibm.cldk.artifacts.DataflowTiers.intra(DataflowTiers.java:71)
	at com.ibm.cldk.artifacts.ViewDispatches.lambda$resolve$1(ViewDispatches.java:249)
	at com.ibm.cldk.artifacts.ViewDispatches.runTier(ViewDispatches.java:416)
	at com.ibm.cldk.artifacts.ViewDispatches.resolve(ViewDispatches.java:248)
	at com.ibm.cldk.artifacts.ViewDispatches.detect(ViewDispatches.java:119)

The very next tier down (interprocAll, L4) already guards the identical null with s.varName() == null ? null : ... — the L3 call was just missing the equivalent check. Reproduced directly against the public Robot Shop source at -a 4 before fixing.

Fix

Guarded var == null at the shared DataflowTiers.intra choke point rather than only at the call site, so any future caller is protected too:

static String intra(Owner owner, String useLocalId, String var) {
    if (owner == null || var == null || owner.callable.getDdg() == null
            || owner.source == null) {
        return null;
    }
    return IntraTier.reachingLiteral(owner.callable, owner.source, useLocalId, var);
}

Tests

  • ViewNameDispatchTest.aNonIdentifierReturnExpressionStaysNonLiteralInsteadOfCrashing reproduces the exact Robot Shop shape (a @RestController's return String.valueOf(n);).
  • ViewDispatchDataflowTierTest.aCallExpressionTargetStaysNonLiteralInsteadOfCrashing covers the analogous dispatcher-call-site shape (req.getRequestDispatcher(computePage()).forward(...)).

Both reproduce the NPE on the pre-fix code and pass with the guard. Full suite: 606 tests, only the Docker-only integration test unrun (no local Docker daemon in this environment).

Out of scope

@RestController return values are HTTP response bodies, not Spring view names — conflating them with @Controller in SpringEntrypointFinder is a real semantic gap (it also affects @Controller methods annotated @ResponseBody). Left untouched here since it changes detection scope rather than just fixing the crash; happy to follow up in a separate PR if wanted.

…ow variable, not a NullPointerException

ViewDispatches.resolve() ran the intra-procedural dataflow tier on
s.varName() unguarded, but Site.varName() returns null for any dispatch
target that is not a bare identifier -- a call expression like
`return String.valueOf(count);` inside a @RestController, as seen in
Robot Shop's shipping service (Controller.count()). Spring entrypoint
detection does not yet distinguish @RestController from @controller
(SpringEntrypointFinder.isEntrypointClass matches on
annotation.getNameAsString().contains("Controller")), so a REST
endpoint's String return is still gated into the view-name tier. That
null then reached DataflowTiers.intra and crashed in
IntraTier.reachingLiteral's `var.equals(edge.getVar())` at analysis
level >= 3. The very next tier down (interprocAll, L4) already guards
the identical null with `s.varName() == null ? null : ...` -- the L3
call was just missing the equivalent check.

Guarded `var == null` at the shared DataflowTiers.intra choke point
rather than only at the call site, so any future caller is protected
too. Added ViewNameDispatchTest.aNonIdentifierReturnExpressionStaysNonLiteralInsteadOfCrashing,
reproducing the exact Robot Shop shape, and
ViewDispatchDataflowTierTest.aCallExpressionTargetStaysNonLiteralInsteadOfCrashing
for the analogous dispatcher-call-site shape; both reproduce the NPE
on the old code and pass with the guard. Full suite: 606 tests, only
the Docker-only integration test unrun (no local Docker daemon).

Separately, @RestController return values are HTTP response bodies,
not Spring view names -- conflating them with @controller in
SpringEntrypointFinder is a real semantic gap, left untouched here
since it changes detection scope rather than just fixing the crash.
@rahlk

rahlk commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up filed: #272 (the @RestController/@ResponseBody view-name semantic gap called out above).

@rahlk
rahlk deleted the fix/view-dispatch-null-var-npe branch September 29, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant