<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Braham</title>
    <description>The latest articles on DEV Community by Braham (@brahamshakti).</description>
    <link>https://dev.to/brahamshakti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4095624%2F6f445660-9ad9-4430-9603-95353b4f0299.png</url>
      <title>DEV Community: Braham</title>
      <link>https://dev.to/brahamshakti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brahamshakti"/>
    <language>en</language>
    <item>
      <title>What I gave up to run Spring static analysis in a browser</title>
      <dc:creator>Braham</dc:creator>
      <pubDate>Wed, 26 Aug 2026 11:24:48 +0000</pubDate>
      <link>https://dev.to/brahamshakti/what-i-gave-up-to-run-spring-static-analysis-in-a-browser-6f8</link>
      <guid>https://dev.to/brahamshakti/what-i-gave-up-to-run-spring-static-analysis-in-a-browser-6f8</guid>
      <description>&lt;p&gt;There is a question that costs me half an hour every time I have to answer it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I change this controller method, what else breaks?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The honest process was always the same. Find Usages on the handler. Follow the&lt;br&gt;
service call. Find Usages again. Open the repository, squint at a derived query&lt;br&gt;
name, guess which table it lands on. Three greps for a method name that turns&lt;br&gt;
out to be declared in eleven places. Then make the change anyway, because half&lt;br&gt;
an hour is what the estimate had left in it.&lt;/p&gt;

&lt;p&gt;I wanted the machine to do that walk. What I did not want was to upload a codebase I&lt;br&gt;
did not own to somebody else's analysis service to get it — not out of&lt;br&gt;
principle, but because "we send your code to a third party" is the sentence&lt;br&gt;
that ends the conversation before it starts. So I set myself a constraint:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The analysis runs in the browser. There is no server. There is nothing to&lt;br&gt;
upload to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is about what that constraint costs, because it turned out to cost&lt;br&gt;
much more than a bit of bundle size, and the interesting parts of the tool are&lt;br&gt;
all consequences of paying it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The thing you lose is types
&lt;/h2&gt;

&lt;p&gt;A Java static analyser normally starts by not being alone. &lt;code&gt;javac&lt;/code&gt; will hand&lt;br&gt;
you a resolved AST. The classpath tells you what &lt;code&gt;org.springframework.data.&lt;br&gt;
repository.CrudRepository&lt;/code&gt; actually declares. Your jars are on disk. When you&lt;br&gt;
see &lt;code&gt;repo.save(owner)&lt;/code&gt;, you can ask the compiler what &lt;code&gt;repo&lt;/code&gt; is and get a real&lt;br&gt;
answer.&lt;/p&gt;

&lt;p&gt;In a browser tab you have none of that. No JDK. No Maven cache. No classpath.&lt;br&gt;
You have the files the user picked with the File System Access API, and a&lt;br&gt;
parser you can compile to WebAssembly. I used&lt;br&gt;
&lt;a href="https://tree-sitter.github.io/tree-sitter/" rel="noopener noreferrer"&gt;tree-sitter&lt;/a&gt; with the Java grammar,&lt;br&gt;
which gives you a fast, error-tolerant concrete syntax tree.&lt;/p&gt;

&lt;p&gt;What tree-sitter gives you is &lt;em&gt;syntax&lt;/em&gt;. What it does not give you is &lt;em&gt;types&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That distinction sounds academic until you look at a call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Owner&lt;/span&gt; &lt;span class="nf"&gt;findOwner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syntax tree tells you: there is a method invocation named &lt;code&gt;findById&lt;/code&gt;, on an&lt;br&gt;
identifier named &lt;code&gt;repository&lt;/code&gt;, and another named &lt;code&gt;orElseThrow&lt;/code&gt; on whatever the&lt;br&gt;
first one returned. It does not tell you what &lt;code&gt;repository&lt;/code&gt; is. And in a project&lt;br&gt;
with &lt;code&gt;OwnerRepository&lt;/code&gt;, &lt;code&gt;PetRepository&lt;/code&gt; and &lt;code&gt;VetRepository&lt;/code&gt;, &lt;code&gt;findById&lt;/code&gt; is&lt;br&gt;
declared three times.&lt;/p&gt;

&lt;p&gt;So the first thing I had to accept is that &lt;strong&gt;every edge in the call graph is a&lt;br&gt;
candidate, not a fact.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The resolution ladder
&lt;/h2&gt;

&lt;p&gt;You are not completely blind, though. Java writes a great deal of type&lt;br&gt;
information down in the source, and if you are willing to read it in the same&lt;br&gt;
order the compiler would, you can resolve most receivers without a type checker.&lt;/p&gt;

&lt;p&gt;The ladder, innermost declaration wins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OwnerService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OwnerRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// 4. field declaration&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Owner&lt;/span&gt; &lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 3. local var — but `var`&lt;/span&gt;
                                                &lt;span class="c1"&gt;//    hides the type again&lt;/span&gt;
        &lt;span class="nc"&gt;OwnerRepository&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// 2. typed local&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuditLog&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;         &lt;span class="c1"&gt;// 1. parameter declaration&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parameters, typed locals, constructor-injected dependencies and plain fields all&lt;br&gt;
state what they are. Read them in Java's scoping order and &lt;code&gt;repository.save(...)&lt;/code&gt;&lt;br&gt;
resolves to exactly one class. That covers the overwhelming majority of Spring&lt;br&gt;
service code, because Spring code is mostly constructor injection and typed&lt;br&gt;
fields — the framework's own conventions are doing me a favour here.&lt;/p&gt;

&lt;p&gt;What defeats it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;var&lt;/code&gt;, which is a declaration that declines to declare anything&lt;/li&gt;
&lt;li&gt;chained calls, where &lt;code&gt;findById(id).orElseThrow()&lt;/code&gt; needs the &lt;em&gt;return type&lt;/em&gt; of
the first call to resolve the second — that is inference, not reading&lt;/li&gt;
&lt;li&gt;anything where the receiver is an expression rather than a name&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Confidence instead of booleans
&lt;/h2&gt;

&lt;p&gt;The design decision that everything else hangs off: I do not report an edge as&lt;br&gt;
present or absent. I report &lt;strong&gt;how it was matched&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 *   "exact"  — the callee name is unique across the whole project
 *   "likely" — the caller's class declares a dependency whose type owns a
 *              method of that name
 *   "weak"   — name match only, several classes declare it
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exact&lt;/code&gt; is a gift: if only one class in the entire project declares&lt;br&gt;
&lt;code&gt;recalculateSettlementWindow&lt;/code&gt;, then a call to that name is that method, no type&lt;br&gt;
resolution required. In real codebases a surprising share of your domain method&lt;br&gt;
names are globally unique, because people name things after what they do.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;likely&lt;/code&gt; is the ladder above. &lt;code&gt;weak&lt;/code&gt; is a name match and nothing more.&lt;/p&gt;

&lt;p&gt;The reason this matters is stated in a comment I wrote early and have not&lt;br&gt;
needed to change:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Presenting a weak edge as fact is how a tool starts confidently lying about an&lt;br&gt;
architecture, which is worse than not answering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A tool that says "this endpoint touches &lt;code&gt;owners&lt;/code&gt; and &lt;code&gt;pets&lt;/code&gt;" and is wrong 15% of&lt;br&gt;
the time is not 85% useful. It is useless, because you have to verify every&lt;br&gt;
claim, and verifying takes as long as the original half hour.&lt;/p&gt;
&lt;h2&gt;
  
  
  When a guess stops being a guess
&lt;/h2&gt;

&lt;p&gt;Weak edges are still worth showing — sometimes. &lt;code&gt;save()&lt;/code&gt; matching three&lt;br&gt;
repositories is a guess a reader can resolve instantly, because they know the&lt;br&gt;
code and you do not. Show them the three.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toString()&lt;/code&gt; matching forty model classes is not that. From the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 * `save()` matching three repositories is a guess worth showing: one of the
 * three is right, and a reader who knows the code can pick it. `toString()`
 * matching forty model classes is not a guess, it is noise wearing a guess's
 * clothes — the edge asserts a relationship that exists between the caller
 * and NONE of the forty, because what the caller actually invoked was
 * `toString` on whatever object happened to be in scope.
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So there is a cap. Above N same-named candidates with no preferred class, the&lt;br&gt;
edge is dropped rather than recorded weakly. This is not only about display&lt;br&gt;
noise: each recorded edge is a node the trace walk will spend budget expanding,&lt;br&gt;
and forty junk edges at depth two is how a real flow gets crowded out of a&lt;br&gt;
result by &lt;code&gt;toString&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug that taught me the most
&lt;/h2&gt;

&lt;p&gt;Here is the one that changed how I think about the whole category.&lt;/p&gt;

&lt;p&gt;The shape, which I have now hit more than once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /reports/rebuild                badged "no database operations"
  → ReportServiceImpl.rebuildAll
      new SegmentRebuildTask(segmentDao, ...)
      executorService.invokeAll(tasks)
  → SegmentRebuildTask.call()
      segmentDao.updateRollup(...)         TWO UPDATES
      segmentDao.clearStale(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Names changed throughout. The shape is what matters, and the shape is&lt;br&gt;
exactly this.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The executor invokes &lt;code&gt;call()&lt;/code&gt;. Nothing in the source calls &lt;code&gt;call()&lt;/code&gt;. There is no&lt;br&gt;
edge to follow, so the walk finished, found no database operations on any path&lt;br&gt;
it could see, and reported — confidently — that this endpoint touches no&lt;br&gt;
database.&lt;/p&gt;

&lt;p&gt;It runs two UPDATEs.&lt;/p&gt;

&lt;p&gt;This is the worst thing an analysis tool can do. Not "missed something" — a&lt;br&gt;
&lt;strong&gt;definitive negative that is false&lt;/strong&gt;. A missing finding costs you the tool's&lt;br&gt;
value. A false absence costs you production, because you shipped the change&lt;br&gt;
believing you had checked.&lt;/p&gt;

&lt;p&gt;The fix is not to guess which &lt;code&gt;Callable&lt;/code&gt; was submitted. Walking a body I picked&lt;br&gt;
by inference would be a fabricated finding, and a fabricated finding is worse&lt;br&gt;
than an admitted gap — it has the same shape as a real one and you cannot tell&lt;br&gt;
them apart. The fix is vocabulary. The walk already distinguished:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;inspected&lt;/strong&gt; — I read this body&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;framework terminal&lt;/strong&gt; — I reached this and did not inspect it, and nothing
of yours is behind it (Spring Data's generated &lt;code&gt;save&lt;/code&gt;, a JDK call)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;unavailable&lt;/strong&gt; — I reached this and could not read it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An executor handoff is the third kind. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readable&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;EXECUTOR_HANDOFF&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sliced&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;frameworkTerminals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (work handed to an executor — its task's body is not called `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`from anywhere this walk can follow)`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An uninspected boundary on the path blocks the completeness claim, so the badge&lt;br&gt;
flips from &lt;strong&gt;"no database operations"&lt;/strong&gt; to &lt;strong&gt;"not established"&lt;/strong&gt;. That sentence&lt;br&gt;
is true, and the difference between it and the false one is the difference&lt;br&gt;
between a tool you can act on and one you cannot.&lt;/p&gt;

&lt;p&gt;I checked six repositories for this shape before shipping the change. The&lt;br&gt;
pattern is real and it is not rare. &lt;code&gt;@Async&lt;/code&gt; and &lt;code&gt;CompletableFuture.supplyAsync&lt;/code&gt;&lt;br&gt;
have exactly the same structure.&lt;/p&gt;
&lt;h2&gt;
  
  
  The safety net I did not know I had
&lt;/h2&gt;

&lt;p&gt;A second story from the same week, and this one is a warning about measurement.&lt;/p&gt;

&lt;p&gt;There was a cap on edges per node, set to 40, put there originally as a guard&lt;br&gt;
against pathological graphs. Then I measured it: fan-in on one corpus peaked at&lt;br&gt;
exactly 40, with the &lt;em&gt;ninetieth percentile also at 40&lt;/em&gt;. When your cap and your&lt;br&gt;
p90 are the same number, the cap is not a safety margin — it is the shape of&lt;br&gt;
your data. One node in five was losing inbound edges before any walk started.&lt;/p&gt;

&lt;p&gt;So I raised it. And the raise produced &lt;strong&gt;49 new false "no database operations"&lt;/strong&gt;&lt;br&gt;
on that corpus.&lt;/p&gt;

&lt;p&gt;The cap had been the only thing preventing them, entirely by accident. With&lt;br&gt;
fewer edges recorded, the walk hit its limits sooner, and hitting limits made it&lt;br&gt;
say "not established" — the honest answer, arrived at for a wrong reason. Raise&lt;br&gt;
the cap, and the walk could now reach the end of more paths and declare them&lt;br&gt;
clean, when the real reason they looked clean was that the mapper at the end&lt;br&gt;
was a declaration with no body.&lt;/p&gt;

&lt;p&gt;The raise is safe now, but only because the coverage rules landed with it: an&lt;br&gt;
abstract mapper declaration is a framework terminal, not an inspected body. There&lt;br&gt;
is a script that checks this — &lt;code&gt;verify-negatives.mjs&lt;/code&gt;, six fixtures, must read&lt;br&gt;
zero — and a comment saying that if the coverage rules are ever reverted, the&lt;br&gt;
cap must be reverted with them.&lt;/p&gt;

&lt;p&gt;The lesson I actually took: &lt;strong&gt;when a defensive limit is load-bearing, you find&lt;br&gt;
out by removing it, and what you find out is a count of the lies it was&lt;br&gt;
suppressing.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What Spring does that your code does not say
&lt;/h2&gt;

&lt;p&gt;One more that has nothing to do with types. Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Controller&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OwnerController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ModelAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Owner&lt;/span&gt; &lt;span class="nf"&gt;findOwner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;owners&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// a SELECT&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/owners/{id}/edit"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Owner&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;         &lt;span class="c1"&gt;// no database call here&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"owners/edit"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring runs &lt;code&gt;@ModelAttribute&lt;/code&gt; and &lt;code&gt;@InitBinder&lt;/code&gt; methods before every request&lt;br&gt;
into that controller. Nothing in the handler calls them. A walk that starts at&lt;br&gt;
the handler reports zero database access for an endpoint that performs a SELECT.&lt;/p&gt;

&lt;p&gt;There is no clever general solution. You encode the framework's lifecycle,&lt;br&gt;
explicitly, and label those steps with the annotation that causes them to run so&lt;br&gt;
the reader knows why a method they never called is in their trace. Framework&lt;br&gt;
knowledge is not a shortcut around analysis — it &lt;em&gt;is&lt;/em&gt; the analysis, for a&lt;br&gt;
framework-shaped codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it still cannot do
&lt;/h2&gt;

&lt;p&gt;Because the honest list is the useful part of any post like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chained receivers.&lt;/strong&gt; &lt;code&gt;repository.findById(id).get()&lt;/code&gt; needs the first call's&lt;br&gt;
return type. That is inference. Those stay unproven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MyBatis and jOOQ.&lt;/strong&gt; Table names are read from Spring Data JPA repositories and&lt;br&gt;
JPA entities. A mapper's implementation is generated from XML or a build step, so&lt;br&gt;
there is no body on disk to read. On a mapper-based codebase the tables list&lt;br&gt;
comes up mostly empty — I measured a median of zero tables per endpoint on one.&lt;br&gt;
The output says unknown; a reader who mistakes that for "none" has been misled,&lt;br&gt;
which is why it is now stated on the page rather than discovered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branches and loops.&lt;/strong&gt; The trace shows the code's shape, not an execution. A&lt;br&gt;
call inside an &lt;code&gt;if&lt;/code&gt; appears whether or not that branch runs, and a loop body&lt;br&gt;
appears once rather than N times — which is exactly the difference between one&lt;br&gt;
round trip and a hundred.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@Transactional&lt;/code&gt; self-invocation.&lt;/strong&gt; &lt;code&gt;this.doTransactionalThing()&lt;/code&gt; bypasses&lt;br&gt;
Spring's proxy at runtime. The trace shows a boundary that would not actually&lt;br&gt;
apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was the constraint worth it
&lt;/h2&gt;

&lt;p&gt;Everything above is a cost of not having a compiler. A server-side analyser with&lt;br&gt;
&lt;code&gt;javac&lt;/code&gt; on the classpath has none of these problems, and would give better&lt;br&gt;
answers.&lt;/p&gt;

&lt;p&gt;It would also never run on most of the codebases that need it, because those&lt;br&gt;
belong to companies with a procurement process, and "it runs in your tab, open&lt;br&gt;
the network panel and check" is a claim anyone can verify in ten seconds while a&lt;br&gt;
security questionnaire takes six weeks.&lt;/p&gt;

&lt;p&gt;That is the whole trade. Weaker analysis that runs on your real code beats&lt;br&gt;
stronger analysis that runs on code you are allowed to upload.&lt;/p&gt;

&lt;p&gt;The part I would defend hardest, though, is not the browser bit. It is that&lt;br&gt;
being unable to resolve types forced me to build a vocabulary for uncertainty&lt;br&gt;
before I built anything else — exact, likely, weak; inspected, reached,&lt;br&gt;
unavailable — and that vocabulary is the reason the tool can say "I did not&lt;br&gt;
manage to look everywhere" instead of quietly reporting an absence it never&lt;br&gt;
earned.&lt;/p&gt;

&lt;p&gt;A compiler would have let me skip that. I am not sure the result would have been&lt;br&gt;
better.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tool is &lt;a href="https://braxik.com/api-blast-radius" rel="noopener noreferrer"&gt;Braxik&lt;/a&gt;. It traces a Spring&lt;br&gt;
Boot endpoint from controller to database, in your browser, with the file and&lt;br&gt;
line behind the claims. There is a pre-loaded trace of spring-petclinic on the&lt;br&gt;
page if you want to see the output without having a repo to hand.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>spring</category>
      <category>webassembly</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
