# SonarQube, From Zero

> The enterprise code-quality gate: static analysis for bugs, vulnerabilities, and code smells, with coverage and a quality gate that can block a merge.


---

# SonarQube, From Zero

Someone added SonarQube to your pipeline, and now a PR you were proud of is glowing red with words like "code smell" and "security hotspot" and a debt estimate in days. It feels like a robot reviewer that nitpicks while ignoring your actual logic. The relief here is understanding what it really measures, why a *quality gate* exists, and how to make it help instead of nag.

## How to read this

Read in order. Phase 1 builds the mental model: SonarQube is a static analyzer plus a pass/fail gate, and the gate is the part that actually changes your day. Phase 2 is the everyday loop: running a scan, reading the report, and the "clean as you code" idea that keeps it sane on old projects. Phase 3 is where it bites: flaky coverage, false positives, hotspots versus vulnerabilities, and the gate fights that waste afternoons.

## The phases

1. [Phase 1: What SonarQube actually is](01-what-it-is.md) - the analyzer, the five issue types, and the gate.
2. [Phase 2: Scanning and the quality gate](02-scanning-and-the-gate.md) - run a scan, read it, gate new code not old.
3. [Phase 3: Where it nags and how to tame it](03-where-it-nags.md) - false positives, coverage, hotspots, gate fights.


---

# What SonarQube actually is

You open a pull request, the checks spin, and one of them is "SonarQube" with a red X. You click in and there's a dashboard: bugs, vulnerabilities, code smells, a coverage percentage, a duplication percentage, and a number labeled *technical debt* measured in days. Nobody told you what most of those mean. Before you fight any of it, here's the mental model.

SonarQube is two things bolted together. First, a **static analyzer**: it reads your source code without running it and flags patterns that tend to cause trouble. Second, a **quality gate**: a set of pass/fail conditions that turn all those findings into a single green check or red X on your PR. The analyzer is the opinion; the gate is the consequence. Most of the friction you feel comes from the gate, so keep the two separate in your head.

## Static analysis, plainly

Static analysis means reading code as text and structure, never executing it. The analyzer parses your files into a syntax tree, walks that tree, and matches it against a library of **rules**. A rule is a small pattern with a verdict, like "a `catch` block that swallows the exception and does nothing" or "a method with cyclomatic complexity over 15."

Because it never runs your code, it can scan a whole repository fast and find issues that only show up on rare paths. The flip side: it reasons about *shapes* of code, not actual behavior, so it both misses real bugs and flags things that are fine in context. Hold that thought - it explains most of phase 3.

```text
your source files
      │
      ▼
  parse into syntax tree
      │
      ▼
  match against rule set (the "quality profile")
      │
      ▼
  issues: bug | vulnerability | code smell | hotspot
      │
      ▼
  quality gate decides: pass or fail
```

*What just happened:* the analyzer turns text into a tree, runs rules over it, produces a list of issues, and the gate boils that list down to one verdict. Everything else is detail hanging off this pipeline.

## The five things it reports

SonarQube sorts findings into a handful of buckets. Knowing which bucket a finding lands in tells you how seriously to take it.

- **Bugs** - code that is likely wrong: a possible null dereference, an `if` whose two branches are identical, a resource never closed. These are correctness problems.
- **Vulnerabilities** - code that is likely a security hole: SQL built by string concatenation, a hardcoded credential, weak crypto. Treat these as real until proven otherwise.
- **Security hotspots** - code that *might* be a security risk but needs a human to judge. Not the same as a vulnerability (more on that distinction in phase 3).
- **Code smells** - maintainability problems: a 300-line method, a confusing name, dead code, a `TODO` left in. They don't break anything today; they make tomorrow harder.
- **Coverage and duplication** - not issues but measurements. Coverage is the percent of lines exercised by your tests (SonarQube reads this from a report your test runner produces). Duplication is the percent of lines that are copy-pasted blocks.

Each issue also carries a **severity** (from low up to blocker) and an estimated **remediation effort** in minutes. Add all those minutes up across the project and you get the **technical debt** number - the dashboard's headline "X days" is the analyzer's guess at how long fixing every smell would take. It's a rough signal, not a deadline.

> The single most useful habit: read an issue's *type* and *severity* before reacting. A blocker bug and a minor code smell both show up as findings, but only one should hold up your day.

## The quality profile and the gate

Two pieces of config decide what you see. The **quality profile** is which rules are switched on for a language - Sonar ships a sensible default (named "Sonar way") and your team may have its own. The **quality gate** is the set of pass/fail conditions evaluated after analysis, like "coverage on new code must be at least 80%" or "zero new blocker issues."

The gate is what gives Sonar teeth. Without a gate, the dashboard is a wall of advice you can ignore. With a gate wired into CI, a failing gate can mark your PR check red and, if the branch is protected, block the merge. That is the whole reason SonarQube feels like a wall instead of a linter.

```text
Gate: "Sonar way" (default, on new code)
  ✓ New bugs ............... = 0
  ✓ New vulnerabilities .... = 0
  ✗ New code coverage ...... ≥ 80%   (yours: 64%)  ← this one fails
  ✓ New duplicated lines ... ≤ 3%
Result: FAILED
```

*What just happened:* four conditions, three pass, one fails - and a single failing condition fails the whole gate. The red X on your PR almost always traces to one specific unmet condition, so the first move is always to find which one.

For builders: SonarQube and SonarCloud are the same engine in different clothes - SonarQube is the server you (or your platform team) host, SonarCloud is the hosted version. The scanner, rules, and gate concepts are identical, so everything in this guide applies to both.

```quiz
[
  {
    "q": "What does 'static analysis' mean in SonarQube?",
    "choices": ["It runs your tests and measures speed", "It reads your code as text and structure without executing it", "It analyzes production traffic in real time", "It only checks code formatting"],
    "answer": 1,
    "explain": "Static analysis parses source into a tree and matches rules against it, never running the code - which is why it's fast but also why it can produce false positives."
  },
  {
    "q": "Which finding type means 'likely a real correctness problem'?",
    "choices": ["Code smell", "Bug", "Coverage", "Duplication"],
    "answer": 1,
    "explain": "Bugs are likely-wrong code. Code smells are maintainability issues that don't break anything today; coverage and duplication are measurements, not issues."
  },
  {
    "q": "What turns SonarQube's list of findings into a single pass/fail check on a PR?",
    "choices": ["The quality profile", "The quality gate", "The technical-debt number", "The severity label"],
    "answer": 1,
    "explain": "The quality gate is the set of pass/fail conditions evaluated after analysis. The profile only decides which rules run; the gate decides the verdict."
  }
]
```


---

# Scanning and the quality gate

Now the everyday loop: you change some code, a scan runs, you read the result, and either the gate is green or you fix the one thing that's red. Most days you never touch the SonarQube server itself - the scanner runs in CI and posts a verdict. But knowing how to run it locally and read it by hand is what makes the CI result legible instead of mysterious.

## Running a scan

The thing that actually analyzes your code is the **scanner**. It collects your source, computes findings, and uploads them to the server, which stores history and evaluates the gate. There's a generic CLI scanner, and language-native ones (the Maven and Gradle plugins for JVM projects, `dotnet sonarscanner` for .NET). They all do the same job.

A project carries a small config file, `sonar-project.properties`, at its root:

```text
# sonar-project.properties
sonar.projectKey=acme-checkout
sonar.projectName=Acme Checkout
sonar.sources=src
sonar.tests=test
sonar.javascript.lcov.reportPaths=coverage/lcov.info
```

*What just happened:* you told the scanner what to call this project (`projectKey` is its unique id on the server), where the real code lives versus the tests, and where to find the coverage report. SonarQube does not measure coverage itself - it reads a report your test runner already produced.

To run it, you point the scanner at a server and authenticate with a token:

```bash
export SONAR_TOKEN=squ_xxxxxxxxxxxxxxxxxxxx
sonar-scanner \
  -Dsonar.host.url=https://sonar.acme.internal \
  -Dsonar.token=$SONAR_TOKEN
```

```console
INFO: Scanner configuration file: sonar-project.properties
INFO: Analyzing on SonarQube server 10.x
INFO: 412 files indexed
INFO: Sensor JavaScript/TypeScript analysis
INFO: Importing coverage from coverage/lcov.info
INFO: Analysis report uploaded
INFO: ANALYSIS SUCCESSFUL, you can find the results at:
INFO:   https://sonar.acme.internal/dashboard?id=acme-checkout
INFO: QUALITY GATE STATUS: FAILED - View details on the link above
```

*What just happened:* the scanner indexed your files, imported the coverage report, uploaded everything, and the server replied with the gate verdict. "ANALYSIS SUCCESSFUL" only means the scan ran - the line that matters is "QUALITY GATE STATUS." Successful scan, failed gate is the normal state when there's work to do.

> Never hardcode `sonar.token` in the properties file or commit it. It's a credential. Pass it as an environment variable in CI, the same way you'd treat any secret.

## Reading the result without panicking

A failed gate names exactly which condition failed. Open the PR link and you'll see a short summary, not the whole dashboard:

```text
Quality Gate: FAILED
  ✗ Coverage on New Code: 61.0% (required ≥ 80.0%)
  ✓ New Bugs: 0
  ✓ New Vulnerabilities: 0
  ✓ New Security Hotspots Reviewed: 100%
  ✓ Duplicated Lines on New Code: 1.2% (required ≤ 3.0%)
```

*What just happened:* one condition failed - new code coverage. The fix is scoped and obvious: the lines you added or changed aren't tested enough, so add tests for them. You are not on the hook for the project's overall coverage, only the new code. That scoping is the single most important idea in this guide.

## Clean as you code

Here's the idea that makes SonarQube livable on a real, old, large codebase. Picture inheriting a project with 40% coverage and ten thousand existing code smells. If the gate judged the *whole* project, it would be red forever and you'd never dig out. So Sonar's default gate judges **new code** - code added or changed since a baseline (typically the previous release, or for a PR, the lines that differ from the target branch).

This is called **clean as you code**. The legacy heap is frozen as-is; the gate only asks that *what you touch* meets the bar. Write tested, clean new code and the project's overall numbers improve on their own, file by file, as old code gets revisited. No big-bang cleanup project, no blocking the team on debt nobody scheduled.

```text
   Whole project              New code (last 30 days)
   coverage:    41%           coverage:    84%   ← the gate looks here
   smells:      9,812         smells:      3
   bugs:        140           bugs:        0
```

*What just happened:* the project's lifetime numbers are grim, but the gate only evaluates the right-hand column. You can ship today by keeping your slice clean, and the left column drifts in the right direction over time. This is why a healthy team can run a strict gate on a messy codebase without anyone rage-quitting.

## Wiring it into CI

In practice the scan runs on every PR. A typical pipeline step runs your tests (to produce the coverage report), then the scanner, then waits for the gate:

```yaml
# CI step (shape is the same on any CI system)
- run: npm test -- --coverage          # produces coverage/lcov.info
- run: sonar-scanner -Dsonar.token=$SONAR_TOKEN
- run: sonar-scanner -Dsonar.qualitygate.wait=true   # block until gate returns
```

*What just happened:* tests run first so coverage exists, the scanner uploads, and `qualitygate.wait=true` makes the step block until the server finishes evaluating - so the CI step's pass/fail mirrors the gate. Without `wait`, the scanner returns immediately and your pipeline goes green before the gate has even decided.

For the bigger picture of where this step sits in a pipeline, see [/guides/what-cicd-does](/guides/what-cicd-does).

```quiz
[
  {
    "q": "On a default 'clean as you code' gate, what does the gate evaluate for a pull request?",
    "choices": ["The entire project's coverage and issues", "Only the new or changed code", "Only files in the src directory", "Nothing until the next release"],
    "answer": 1,
    "explain": "The default gate judges new code - the lines added or changed since the baseline - so a messy legacy project isn't held against your PR."
  },
  {
    "q": "Where does SonarQube get a project's code coverage number?",
    "choices": ["It runs the tests itself and measures them", "It estimates it from the number of test files", "It reads a coverage report your test runner produced", "It infers it from code complexity"],
    "answer": 2,
    "explain": "Sonar imports a coverage report (like lcov.info) generated by your own test run. If you don't produce and point at that report, coverage shows as 0%."
  },
  {
    "q": "In CI, why add 'sonar.qualitygate.wait=true'?",
    "choices": ["To run the scan faster", "To make the CI step block until the gate verdict is returned", "To skip the gate entirely", "To upload coverage twice"],
    "answer": 1,
    "explain": "Without wait, the scanner returns before the server finishes evaluating, so CI could pass while the gate later fails. wait makes the step reflect the real verdict."
  }
]
```


---

# Where it nags and how to tame it

You now know the model and the loop. This is the part where SonarQube and your afternoon disagree: a finding you're sure is wrong, a coverage number that won't move, a "security hotspot" that demands a meeting, and a gate that blocks a merge you needed an hour ago. None of it is mysterious once you know the moves.

## False positives, and the right way to dismiss

The analyzer reasons about shapes, not behavior, so it sometimes flags code that's correct in context. The wrong reaction is to disable the rule globally - that blinds the whole team to a useful check because one case annoyed you. The right reaction is to resolve the *single issue* with a reason.

In the SonarQube UI you can mark an issue **Won't Fix** or **False Positive**, with a comment. That's the record of a human decision, and it sticks across future scans. As a last resort there's inline suppression in code:

```text
// In Java, the standard suppression:
@SuppressWarnings("java:S2589")   // rule key; condition is always-true by design here

# In a properties/exclusion file, exclude a path from a rule:
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=java:S2589
sonar.issue.ignore.multicriteria.e1.resourceKey=**/LegacyAdapter.java
```

*What just happened:* the first form silences one rule on one spot in code; the second excludes a rule from a file via config. Both are scalpels. Reach for "False Positive" in the UI first - it keeps the reasoning attached to the issue where reviewers can see it, rather than buried in a config file.

> Resist the urge to fix the gate by lowering the bar. Editing the quality gate to demand 50% coverage instead of 80% makes the red turn green and quietly makes the codebase worse for everyone. If a threshold is genuinely wrong, change it in the open with the team, not in a panic on your branch.

## Coverage that won't move

The most common gate failure is coverage on new code, and the most common cause isn't untested code - it's a missing or misconfigured report. If Sonar shows 0% coverage on code you definitely tested, the scanner didn't find your report.

```console
INFO: Sensor JavaScript/TypeScript Coverage
WARN: No coverage report can be found with sonar.javascript.lcov.reportPaths='coverage/lcov.info'
INFO: 0.0% coverage on new code
```

*What just happened:* the path in your config didn't match where tests actually wrote the report, so Sonar imported nothing and reported zero. The fix is almost always the path or the order of steps - tests must run *before* the scanner so the report exists when the scanner looks. Genuinely untested new code is the second cause; the missing report is the first one to rule out.

Note a quirk: SonarQube only counts coverage for lines it also analyzed. Generated files, vendored code, or paths you excluded won't show coverage, which can drag the new-code percentage in surprising ways. Keep your `sonar.sources` and exclusions aligned with what you actually want measured.

## Hotspots are not vulnerabilities

This distinction trips up almost everyone. A **vulnerability** is Sonar saying "this is a security bug, fix it." A **security hotspot** is Sonar saying "this is security-sensitive code that a human must look at and judge." Using a random number generator is a hotspot - fine for a shuffle, dangerous for a token. Sonar can't tell which from the code alone, so it asks you.

The gate condition is usually "Security Hotspots Reviewed = 100%." That does not mean fix them all - it means *review* them all. You open each hotspot, read the explanation, and mark it **Safe** (this use is fine), **Fixed** (you changed it), or **Acknowledged**. Reviewing a hotspot and marking it Safe is a legitimate, expected outcome.

```text
Security Hotspot: Make sure using this pseudorandom number generator is safe here.
  Location: src/util/shuffle.ts:14   Math.random()
  Review options:  [ Safe ]  [ Fixed ]  [ Acknowledged ]
  → marked Safe: "Used only to shuffle a display list, not for security."
```

*What just happened:* you reviewed the hotspot, decided this use is harmless, and recorded why. That satisfies the "100% reviewed" gate condition without changing a line of code - which is exactly how hotspots are meant to clear. For the broader picture of supply-chain and dependency risk that hotspots only scratch, see [/guides/supply-chain-security](/guides/supply-chain-security).

## When the gate blocks a merge you need

Sometimes the gate is right and you still need to ship - a hotfix at 2am, a coverage gap you'll close next sprint. Resist these temptations: do not delete the Sonar check from CI, do not push a config that turns the gate off, do not lower thresholds on the sly. Each one quietly removes the safety net for everyone after you.

The clear moves: fix the real finding if you can; mark a false positive as such with a reason; or, if your team allows it, use the documented override (an admin can manually pass a gate, or an emergency-merge path can require a second approver). The principle is that bypassing a gate should be *visible and accountable*, never a silent edit. A gate that everyone quietly works around is worse than no gate, because it lies about being a safety net.

In the wild: mature teams treat the gate like a flaky test that's usually right. When it fails, the first question is "is this finding real?" - and most of the time the cheapest path to green is to fix the small real thing Sonar caught, not to argue with it.

```quiz
[
  {
    "q": "Sonar flags correct code as a 'code smell'. What's the best response?",
    "choices": ["Disable the rule for the whole project", "Mark that single issue as False Positive with a comment", "Delete the SonarQube check from CI", "Lower the gate threshold so it passes"],
    "answer": 1,
    "explain": "Resolve the single issue as a false positive with a reason. Disabling the rule globally blinds the whole team because one case annoyed you."
  },
  {
    "q": "What does a security hotspot require to satisfy the gate?",
    "choices": ["Every hotspot must be code-fixed", "Every hotspot must be reviewed (and may be marked Safe)", "Hotspots are ignored by the gate", "A penetration test must pass"],
    "answer": 1,
    "explain": "Hotspots are security-sensitive code needing human judgment. The gate asks that they all be reviewed - marking one Safe with a reason is a valid outcome."
  },
  {
    "q": "Sonar reports 0% coverage on code you definitely tested. What's the most likely cause?",
    "choices": ["Your tests are wrong", "The coverage report path is wrong or tests ran after the scanner", "SonarQube doesn't support your language", "The gate is misconfigured"],
    "answer": 1,
    "explain": "Sonar imports a report it didn't generate. A 0% reading usually means the path didn't match, or the scanner ran before tests produced the report."
  }
]
```
