<?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: endrilickollari</title>
    <description>The latest articles on DEV Community by endrilickollari (@endrilickollari).</description>
    <link>https://dev.to/endrilickollari</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F231087%2Ff1b3bf0f-4754-493f-9235-085d7a43a493.png</url>
      <title>DEV Community: endrilickollari</title>
      <link>https://dev.to/endrilickollari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/endrilickollari"/>
    <language>en</language>
    <item>
      <title>Engineering DebtDrone: Building a High-Performance AST-Based Static Analyzer in Go</title>
      <dc:creator>endrilickollari</dc:creator>
      <pubDate>Tue, 16 Dec 2025 10:13:10 +0000</pubDate>
      <link>https://dev.to/endrilickollari/engineering-debtdrone-building-a-high-performance-ast-based-static-analyzer-in-go-ifd</link>
      <guid>https://dev.to/endrilickollari/engineering-debtdrone-building-a-high-performance-ast-based-static-analyzer-in-go-ifd</guid>
      <description>&lt;h2&gt;
  
  
  The Limitations of Lexical Analysis
&lt;/h2&gt;

&lt;p&gt;In the world of static analysis, there is a distinct hierarchy of capability. At the bottom, you have lexical analysis—tools that treat code as a stream of strings. These are your &lt;code&gt;grep&lt;/code&gt;-based linters. They are incredibly fast (&lt;code&gt;$O(n)$&lt;/code&gt; where &lt;code&gt;$n$&lt;/code&gt; is characters), but they are structurally blind.&lt;/p&gt;

&lt;p&gt;To a regex linter, a function signature is just a pattern to match. It cannot reliably distinguish between a nested closure, a generic type definition, or a comment that &lt;em&gt;looks&lt;/em&gt; like code.&lt;/p&gt;

&lt;p&gt;When I set out to build &lt;strong&gt;DebtDrone&lt;/strong&gt;, I wanted to measure &lt;strong&gt;Cognitive Complexity&lt;/strong&gt;, not just cyclomatic complexity. Cyclomatic complexity counts paths through code (if/else/switch), but it fails to account for &lt;em&gt;nesting&lt;/em&gt;. A flat switch statement with 50 cases is easy to read. A function with 3 levels of nested loops and conditionals is a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;To measure this accurately, lexical analysis is insufficient. We need &lt;strong&gt;Syntactic Analysis&lt;/strong&gt;. We need a tool that understands the code structure exactly as the compiler does.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Engine: Abstract Syntax Trees (AST)
&lt;/h2&gt;

&lt;p&gt;DebtDrone leverages &lt;a href="https://tree-sitter.github.io/tree-sitter/" rel="noopener noreferrer"&gt;Tree-sitter&lt;/a&gt;, an incremental parsing system that builds a concrete syntax tree for a source file. Unlike abstract syntax trees (ASTs) generated by language-specific compilers (like Go's &lt;code&gt;go/ast&lt;/code&gt;), Tree-sitter provides a unified interface for traversing trees across 11+ languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parsing vs. Matching
&lt;/h3&gt;

&lt;p&gt;Consider the following Go snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;              &lt;span class="c"&gt;// +1 Nesting&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// +2 Nesting (1 + 1 penalty)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"stop"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;      &lt;span class="c"&gt;// +3 Nesting (2 + 1 penalty)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&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;A regex tool might count the keywords &lt;code&gt;if&lt;/code&gt; and &lt;code&gt;for&lt;/code&gt;, giving this a score of 3. DebtDrone parses this into a tree structure. By traversing the tree, we can track &lt;strong&gt;nesting depth context&lt;/strong&gt;. Every time we enter a &lt;code&gt;Block&lt;/code&gt; node that is a child of an &lt;code&gt;IfStatement&lt;/code&gt; or &lt;code&gt;ForStatement&lt;/code&gt;, we increment a depth counter.&lt;/p&gt;

&lt;p&gt;The score isn't just &lt;code&gt;1 + 1 + 1&lt;/code&gt;. It is weighted by depth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Level 0&lt;/strong&gt;: Base cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 1&lt;/strong&gt;: Base cost + 1 (Nesting penalty)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 2&lt;/strong&gt;: Base cost + 2 (Nesting penalty)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This yields a "Cognitive Complexity" score that accurately reflects the mental overhead required to understand the function.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural Decision: Why Go?
&lt;/h2&gt;

&lt;p&gt;I chose Go for three primary architectural reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Concurrency Primitives&lt;/strong&gt;: Static analysis is an "embarrassingly parallel" problem. Each file can be parsed in isolation. Go's Goroutines and Channels allow DebtDrone to fan-out parsing tasks across all available CPU cores with minimal overhead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory Safety &amp;amp; Speed&lt;/strong&gt;: While Rust was a contender (and Tree-sitter has excellent Rust bindings), Go provided the fastest iteration loop for the CLI's UX and plumbing, while still offering near-C execution speed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single Binary Distribution&lt;/strong&gt;: The ultimate goal was a zero-dependency binary that could drop into any CI/CD pipeline (GitHub Actions, GitLab CI, Jenkins) without requiring a runtime like Node.js or Python.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Engineering Challenge: CGO and Cross-Compilation
&lt;/h2&gt;

&lt;p&gt;The most significant technical hurdle was the dependency on &lt;code&gt;go-tree-sitter&lt;/code&gt;. Because Tree-sitter is implemented in C for performance, incorporating it requires &lt;strong&gt;CGO&lt;/strong&gt; (&lt;code&gt;CGO_ENABLED=1&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In the Go ecosystem, CGO is often considered a "dealbreaker" for easy distribution. Standard Go cross-compilation (&lt;code&gt;GOOS=linux go build&lt;/code&gt;) is trivial because the Go compiler knows how to generate machine code for different architectures. However, once you enable CGO, you are bound by the &lt;strong&gt;host system's C linker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You cannot compile a macOS binary on a Linux CI runner using the standard &lt;code&gt;gcc&lt;/code&gt;. You need a macOS-compatible linker and system headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: goreleaser-cross
&lt;/h3&gt;

&lt;p&gt;To solve this, I architected the release pipeline around &lt;strong&gt;Dockerized Cross-Compilers&lt;/strong&gt;. Instead of relying on the bare-metal runner, the release process spins up a container (&lt;code&gt;ghcr.io/goreleaser/goreleaser-cross&lt;/code&gt;) that contains a massive collection of cross-compilation toolchains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;o64-clang&lt;/code&gt;&lt;/strong&gt;: For building macOS (Darwin) binaries on Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mingw-w64&lt;/code&gt;&lt;/strong&gt;: For building Windows binaries on Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aarch64-linux-gnu-gcc&lt;/code&gt;&lt;/strong&gt;: For ARM64 Linux builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This configuration is managed via &lt;code&gt;.goreleaser.yaml&lt;/code&gt;, where we dynamically inject the correct C compiler (&lt;code&gt;CC&lt;/code&gt;) based on the target architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;builds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debtdrone-cli&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CGO_ENABLED=1&lt;/span&gt;
      &lt;span class="c1"&gt;# Dynamic Compiler Selection&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CC={{ if eq .Os "darwin" }}o64-clang{{ else if eq .Os "windows" }}x86_64-w64-mingw32-gcc{{ else }}gcc{{ end }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CXX={{ if eq .Os "darwin" }}o64-clang++{{ else if eq .Os "windows" }}x86_64-w64-mingw32-g++{{ else }}g++{{ end }}&lt;/span&gt;
    &lt;span class="na"&gt;goos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;linux&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;darwin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;windows&lt;/span&gt;
    &lt;span class="na"&gt;goarch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;arm64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup allows a standard Ubuntu GitHub Actions runner to produce native binaries for &lt;strong&gt;Mac (Intel/Apple Silicon), Windows, and Linux&lt;/strong&gt; in a single pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  Distribution Strategy: Homebrew Taps
&lt;/h2&gt;

&lt;p&gt;For v1.0.0, accessibility was key. While &lt;code&gt;curl | bash&lt;/code&gt; scripts are common, they lack version management. I implemented a custom &lt;strong&gt;Homebrew Tap&lt;/strong&gt; to treat DebtDrone as a first-class citizen on macOS.&lt;/p&gt;

&lt;p&gt;By adding a &lt;code&gt;brews&lt;/code&gt; section to the GoReleaser config, the pipeline automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generates a Ruby formula (&lt;code&gt;debtdrone.rb&lt;/code&gt;) with the correct SHA256 checksums.&lt;/li&gt;
&lt;li&gt;Commits this formula to a separate &lt;code&gt;homebrew-tap&lt;/code&gt; repository.&lt;/li&gt;
&lt;li&gt;Allows users to install/upgrade via &lt;code&gt;brew install endrilickollari/tap/debtdrone&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Beyond the Code: Impact by Role
&lt;/h2&gt;

&lt;p&gt;While the engineering behind DebtDrone is fascinating, its real value lies in how it empowers different stakeholders in the software development lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  For the Developer: The "Self-Check" Before Commit
&lt;/h3&gt;

&lt;p&gt;We've all been there: you're deep in the zone, solving a complex edge case. You add a flag, then a nested &lt;code&gt;if&lt;/code&gt;, then a loop to handle a collection. It works, but you've just created a "complexity bomb."&lt;/p&gt;

&lt;p&gt;DebtDrone acts as a mirror. By running &lt;code&gt;debtdrone check .&lt;/code&gt; locally, you get immediate feedback:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Warning: &lt;code&gt;processTransaction&lt;/code&gt; has a complexity score of 25 (Threshold: 15)."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This prompts a refactor &lt;em&gt;before&lt;/em&gt; the code even reaches a pull request. It encourages writing smaller, more composable functions, which are inherently easier to test and debug.&lt;/p&gt;

&lt;h3&gt;
  
  
  For the Team Lead: Objective Code Quality
&lt;/h3&gt;

&lt;p&gt;Code reviews can be subjective. "This looks too complex" is an opinion; "This function has a complexity score of 42" is a fact.&lt;/p&gt;

&lt;p&gt;DebtDrone provides an objective baseline for discussions. It helps leads identify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hotspots&lt;/strong&gt;: Which files are the most dangerous to touch?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trends&lt;/strong&gt;: Is the codebase getting cleaner or messier over time?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gatekeeping&lt;/strong&gt;: Preventing technical debt from leaking into the &lt;code&gt;main&lt;/code&gt; branch by setting hard thresholds in CI.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  For DevOps: The Quality Gate
&lt;/h3&gt;

&lt;p&gt;In a CI/CD pipeline, DebtDrone serves as a lightweight, fast quality gate. Because it compiles to a single binary with zero dependencies, it can be dropped into any pipeline (GitHub Actions, GitLab CI, Jenkins) without complex setup.&lt;/p&gt;

&lt;p&gt;It supports standard exit codes (non-zero on failure) and can output results in JSON for integration with dashboarding tools. This ensures that "maintainability" is treated with the same rigor as "passing tests."&lt;/p&gt;

&lt;h3&gt;
  
  
  For the Business Analyst: Velocity &amp;amp; ROI
&lt;/h3&gt;

&lt;p&gt;Why should a business care about Abstract Syntax Trees? Because &lt;strong&gt;complexity kills velocity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;High cognitive complexity directly correlates with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Longer onboarding times&lt;/strong&gt; for new developers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher bug rates&lt;/strong&gt; due to misunderstood logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slower feature delivery&lt;/strong&gt; as developers spend more time deciphering old code than writing new code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By investing in tools like DebtDrone, organizations are investing in their long-term agility. It's not just about "clean code"—it's about sustainable development speed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;DebtDrone v1.0.0 represents a shift from "linting as an afterthought" to "architectural analysis as a standard." By moving from Regex to ASTs, we eliminate false positives. By solving the CGO cross-compilation puzzle, we ensure the tool is available everywhere.&lt;/p&gt;

&lt;p&gt;The result is a CLI that runs locally, respects data privacy, and provides immediate, actionable feedback on technical debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/endrilickollari/debtdrone-cli" rel="noopener noreferrer"&gt;endrilickollari/debtdrone-cli&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Blog&lt;/strong&gt; &lt;a href="https://www.endrilickollari.com/blog" rel="noopener noreferrer"&gt;endrilickollari.com/blog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>codequality</category>
      <category>technicaldebt</category>
      <category>staticanalysis</category>
    </item>
  </channel>
</rss>
