I Built an AI-Powered Dead Code Detector for VS Code (and It Goes Way Beyond Unused Imports)

We’ve all been there. You’re reading through a codebase and you spot it — a feature flag that’s been set to false for two years, a function exported from a module that nothing ever imports, a block of logic sitting underneath a return statement that will never, ever run.

ESLint catches unused variables. TypeScript catches type mismatches. But dead business logic? That’s a different beast — and that’s what I built this extension to hunt.

What Is “Dead Code” Really?

Most linters think dead code = unused variable. But in production codebases the real offenders are:

1. Unreachable Logic
Code that can never execute — not because of a syntax issue, but because of your business logic:

function calculateDiscount(price: number, userType: string): number {
  if (userType === "admin") {
    return price * 0;
  }
  return price * 0.9;

  // 💀 Never reached — the return above always fires
  const bonus = price * 0.05;
  console.log("Applying bonus:", bonus);
  return price - bonus;
}

2. Unused APIs and Functions
Exported functions nobody calls. Classes defined and never instantiated. Entire modules orphaned after a refactor:

// 💀 Exported but nothing in the codebase ever calls this
export class OldReportGenerator {
  generate(data: any[]) {
    return data.map((d) => JSON.stringify(d)).join("n");
  }
  exportToCsv(data: any[]) {
    return data.join(",");
  }
}

3. Obsolete Feature Flags
The slowest-burning dead code. A flag gets hardcoded, the ticket gets closed, and the condition guards code that either always runs or never runs:

const FEATURE_DARK_MODE = false;    // 🚩 Has been false for 18 months
const USE_NEW_CHECKOUT = true;      // 🚩 Always true — the else branch is dead

if (FEATURE_DARK_MODE) {
  applyDarkTheme(); // 💀 Never runs
}

if (USE_NEW_CHECKOUT) {
  showNewCheckout();
} else {
  showLegacyCheckout(); // 💀 Never runs
  trackLegacyCheckoutEvent();
}

Traditional static analysis tools struggle with these because they require understanding intent, not just syntax.

The Solution: AI-Powered Analysis

I built Dead Code Detector — a VS Code extension that sends your file to Claude (via the Anthropic API) with a carefully engineered prompt, gets back a structured JSON response, and renders the results as both inline diagnostics and a filterable results panel.

GitHub: github.com/naimulkarim/dead-code-detector

Here’s what it looks like in action:

  • 🔴 Inline squiggles in the editor (appears in your Problems panel)
  • 📊 A results panel that opens beside your file with filtering, severity badges, and jump-to-line

How It Works

The Prompt Engineering

The heart of the extension is the system prompt in src/analyzer.ts. Getting an LLM to return reliable, parseable structured data requires being very explicit:

const SYSTEM_PROMPT = `You are an expert static analysis tool specializing in dead code detection. 
Analyze the provided source code and identify:

1. **Unreachable Business Logic** — code after unconditional returns, inside impossible 
   conditions (e.g. if (false), if (1 === 2)), catch blocks that swallow everything, etc.

2. **Unused APIs / Functions** — exported or defined functions/classes/variables that appear 
   to never be called or referenced within the file.

3. **Obsolete Feature Flags** — boolean constants or config checks that are hardcoded to 
   true/false, e.g. const FEATURE_X = false used in conditions.

Respond ONLY with a JSON array (no markdown, no preamble). Each item must have:
{
  "category": "unreachable-logic" | "unused-api" | "obsolete-feature-flag" | "dead-code",
  "message": "Short one-line summary (< 80 chars)",
  "explanation": "2-3 sentences explaining why this is dead/obsolete code",
  "suggestion": "What to do — remove, refactor, or clean up",
  "line": ,
  "severity": "high" | "medium" | "low"
}

Return [] if no issues found.`;

Key decisions here:

  • “Respond ONLY with a JSON array” — no preamble, no markdown fences in the model’s thinking. We strip any stray fences client-side as a safety net.
  • Explicit schema — defining every field prevents the model from inventing its own structure.
  • “Return [] if no issues found” — so a clean file doesn’t produce a parse error.

Calling the API

const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": apiKey,
    "anthropic-version": "2023-06-01",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    system: SYSTEM_PROMPT,
    messages: [{ role: "user", content: userMessage }],
  }),
});

const data = await response.json();
const text = data.content
  .filter((b) => b.type === "text")
  .map((b) => b.text)
  .join("");

// Strip possible markdown fences before parsing
const clean = text.replace(/```
{% endraw %}
json|
{% raw %}
```/g, "").trim();
const results = JSON.parse(clean);

We cap the file at 12,000 characters before sending — enough for most real-world files without burning tokens unnecessarily.

Turning Results into VS Code Diagnostics

Once we have the JSON array back, we map each issue to a vscode.Diagnostic so it shows up inline in the editor and the Problems panel:

function applyDiagnostics(doc: vscode.TextDocument, results: DeadCodeResult[]) {
  const diagnostics = results.map((result) => {
    const line = Math.max(0, (result.line ?? 1) - 1);
    const range = doc.lineAt(Math.min(line, doc.lineCount - 1)).range;
    const diag = new vscode.Diagnostic(
      range,
      `[${result.category}] ${result.message}`,
      vscode.DiagnosticSeverity.Warning
    );
    diag.source = "Dead Code AI";
    diag.code = result.category;
    return diag;
  });

  diagnosticCollection.set(doc.uri, diagnostics);
}

The Results Panel

Results also open in a Webview panel beside your editor. It’s filterable by category and sortable by severity, with a Jump to Line button on each card that snaps the cursor to the exact location in your file.

// The webview posts a message back to the extension host
ResultsPanel.currentPanel.webview.onDidReceiveMessage(async (msg) => {
  if (msg.command === "jumpTo") {
    const editor = vscode.window.visibleTextEditors[0];
    const pos = new vscode.Position(Math.max(0, msg.line - 1), 0);
    editor.selection = new vscode.Selection(pos, pos);
    editor.revealRange(
      new vscode.Range(pos, pos),
      vscode.TextEditorRevealType.InCenter
    );
  }
});

Features

  • ✅ Detects unreachable logic, unused APIs, and obsolete feature flags
  • ✅ Works on JS, TS, JSX, TSX, and Python
  • Inline squiggles in the editor via VS Code’s Diagnostics API
  • Filterable results panel with severity badges and jump-to-line
  • Workspace-wide scan (with a confirmation prompt for large repos)
  • ✅ Configurable severity level (error / warning / information / hint)
  • ✅ Toggle individual checks on/off in settings
  • ✅ Works via environment variable (ANTHROPIC_API_KEY) or Settings UI

Installation & Setup

Prerequisites

From GitHub

# Clone the repo
git clone https://github.com/naimulkarim/dead-code-detector.git
cd dead-code-detector

# Install dependencies
npm install

# Open in VS Code
code .

Then press F5 — this opens a new Extension Development Host window with the extension loaded.

Add Your API Key

Option A — Settings UI (recommended):

  1. Open Settings (Cmd+, on Mac / Ctrl+, on Windows)
  2. Search for Dead Code
  3. Paste your key into Dead Code Detector: Anthropic Api Key

Option B — Environment variable:

export ANTHROPIC_API_KEY=sk-ant-api03-...

Usage

Action Shortcut / How
Analyze current file Cmd+Shift+D / Ctrl+Shift+D
Analyze current file Right-click in editor → Dead Code: Analyze Current File
Analyze entire workspace Command Palette → Dead Code: Analyze Entire Workspace
Clear diagnostics Command Palette → Dead Code: Clear All Diagnostics

Test It Immediately

The repo includes sample-test.ts — a file pre-loaded with intentional dead code of all three types. Open it and hit Cmd+Shift+D to see the extension in action right away.

Configuration

// .vscode/settings.json or user settings
{
  "deadCode.anthropicApiKey": "sk-ant-...",
  "deadCode.severity": "warning",
  "deadCode.checks": {
    "unreachableLogic": true,
    "unusedApis": true,
    "obsoleteFeatureFlags": true
  }
}

Architecture Overview

User triggers Cmd+Shift+D
        ↓
extension.ts
  → reads active document text
  → reads enabled checks from config
        ↓
analyzer.ts
  → builds user prompt with language + filename + code
  → POST /v1/messages → Claude Sonnet
  → strips markdown fences, JSON.parse()
  → returns DeadCodeResult[]
        ↓
extension.ts
  → vscode.DiagnosticCollection.set() → squiggles in editor
        ↓
resultsPanel.ts
  → Webview HTML panel with filter buttons + severity cards
  → postMessage() → jump-to-line on click

What’s Next

A few things I want to add:

  • [ ] Quick Fix actions — a code action that lets you delete the dead code with one click
  • [ ] Sidebar tree view — persistent panel showing all issues across the workspace
  • [ ] Git blame integration — show who added the dead code and when
  • [ ] Caching — skip re-analyzing files that haven’t changed since last scan
  • [ ] More languages — Go, Ruby, Rust

PRs and issues are very welcome at github.com/naimulkarim/dead-code-detector.

Built with TypeScript, the VS Code Extension API, and Claude Sonnet. If this helped you ship a cleaner codebase, drop a ⭐ on GitHub!

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Bicep Diagram Generator — Visualize Azure Bicep & ARM Templates Instantly

Related Posts