Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SonarCloud: Code Quality Analysis

For .NET engineers who know: SonarQube/SonarLint in Visual Studio, SonarScanner for .NET, quality gates in the Sonar dashboard, and code smell categories from the C# ruleset You’ll learn: How SonarCloud configuration for TypeScript projects mirrors what you already know from C#, and where the TS ruleset is weaker and requires ESLint to fill the gaps Time: 15-20 min read

The .NET Way (What You Already Know)

You have probably run SonarScanner against a C# project and watched the quality gate report appear in the Sonar dashboard. The workflow is: install dotnet-sonarscanner, run dotnet sonarscanner begin with project key and token, run dotnet build, run your tests with coverage collection, then run dotnet sonarscanner end. The scanner uploads the build output, test results, and coverage report to SonarCloud, which analyzes them and applies the quality gate.

The quality gate in C# typically checks: no new blocker or critical issues, code coverage above a threshold on new code, no new duplications above a threshold, and security hotspots reviewed. The Sonar C# ruleset is mature — it catches subtle issues like LINQ misuse, IDisposable not disposed, null reference patterns, and thread-safety violations.

SonarLint in Visual Studio gives you the same rules inline as you type. You can connect it to your SonarCloud project for synchronized rule configuration.

The SonarCloud Way

The transition for TypeScript is genuinely one of the easier ones in this manual. The concepts map directly: project setup, quality gates, code smell categories, security hotspots, and PR decoration all work the same. The main difference is the tool invocation (no dotnet build step) and the coverage report format.

Project Setup

Create a project in SonarCloud at sonarcloud.io. You can auto-import from GitHub, which is the fastest path — Sonar creates the project and configures the GitHub integration automatically.

You need a sonar-project.properties file at the repository root:

# sonar-project.properties
sonar.projectKey=your-org_your-project
sonar.organization=your-org
sonar.projectName=Your Project Name

# Source files to analyze
sonar.sources=src
# Test files — Sonar treats these differently (won't count coverage holes in tests)
sonar.tests=src
sonar.test.inclusions=**/*.spec.ts,**/*.test.ts,**/*.spec.tsx,**/*.test.tsx
# Exclude generated files, node_modules, build output
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.d.ts,**/coverage/**

# Coverage report path — must be lcov format (Vitest and Jest both produce this)
sonar.javascript.lcov.reportPaths=coverage/lcov.info

# Source encoding
sonar.sourceEncoding=UTF-8

For a monorepo with multiple packages, specify multiple source roots:

# Monorepo with separate frontend and backend
sonar.sources=apps/web/src,apps/api/src
sonar.tests=apps/web/src,apps/api/src
sonar.test.inclusions=**/*.spec.ts,**/*.test.ts
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.d.ts
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info,apps/api/coverage/lcov.info

CI/CD Integration (GitHub Actions)

# .github/workflows/sonar.yml
name: SonarCloud Analysis

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  sonar:
    name: SonarCloud Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Shallow clones fail Sonar's blame data — always use full depth

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests with coverage
        run: pnpm test --coverage
        # For NestJS with Jest: pnpm jest --coverage --coverageReporters=lcov

      - name: SonarCloud Scan
        uses: SonarSource/sonarcloud-github-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}   # For PR decoration
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

There is no dotnet sonarscanner begin/end dance. The GitHub Action handles everything — it invokes the scanner, passes the coverage report path from sonar-project.properties, and uploads results. The scanner auto-detects TypeScript.

Coverage Integration

Vitest produces LCOV coverage reports with minimal configuration:

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',          // Or 'istanbul' — both produce lcov
      reporter: ['text', 'lcov', 'html'],
      reportsDirectory: './coverage',
      // Exclude test files and generated types from coverage
      exclude: [
        'node_modules/**',
        'dist/**',
        '**/*.spec.ts',
        '**/*.test.ts',
        '**/*.d.ts',
      ],
    },
  },
});
// package.json — script that CI runs
{
  "scripts": {
    "test": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

For NestJS with Jest:

// jest.config.js
module.exports = {
  moduleFileExtensions: ['js', 'json', 'ts'],
  rootDir: 'src',
  testRegex: '.*\\.spec\\.ts$',
  transform: { '^.+\\.(t|j)s$': 'ts-jest' },
  collectCoverageFrom: ['**/*.(t|j)s', '!**/*.spec.ts', '!**/main.ts'],
  coverageDirectory: '../coverage',
  coverageReporters: ['text', 'lcov'],
  testEnvironment: 'node',
};

The LCOV file at coverage/lcov.info is what Sonar reads. Verify it exists after running pnpm test:coverage before debugging Sonar coverage issues.

Quality Gate Configuration

Quality gates in SonarCloud work exactly as in SonarQube. Navigate to Organization → Quality Gates to create or edit gates. Our recommended thresholds for TypeScript projects:

MetricConditionThreshold
New code coverageLess than80%
New duplicated linesGreater than3%
New blocker issuesGreater than0
New critical issuesGreater than0
New security hotspots reviewedLess than100%
New code smellsRating worse thanA
New reliability issuesRating worse thanA

The “new code” focus is deliberate. Sonar’s default quality gate only checks new code introduced since the last analysis — the same logic as the .NET setup. This prevents the situation where legacy technical debt blocks every PR.

Code Smell Categories in TypeScript

The TypeScript ruleset covers similar categories to C#, with some notable gaps:

Cognitive complexity — Sonar measures function complexity the same way in TS as C#. Functions with complexity above 15 trigger a code smell. The threshold is configurable in the quality profile.

Duplications — Identical to C#. Copy-pasted blocks across files are detected and reported. SonarCloud’s UI shows the duplicated code side-by-side.

Unused variables and imports — Caught by Sonar, but ESLint’s @typescript-eslint/no-unused-vars rule catches these faster, inline in your editor. Let ESLint handle this; Sonar is redundant here.

Type assertions (as any) — Sonar flags as any type assertions, which are the TS equivalent of unsafe casts. Each one is a code smell worth reviewing.

var declarations — Sonar flags var in TypeScript. Use const/let. This should not come up if ESLint is configured with no-var.

Missing await — Sonar catches unhandled Promises and missing await keywords in async functions. This is a reliability issue, not just a style concern — unhandled rejections can crash the process.

// Sonar will flag this as a bug — Promise is not awaited
async function processOrder(id: number) {
  updateAuditLog(id);  // ← Missing await — rejection is silently swallowed
  return getOrder(id);
}

// Fixed
async function processOrder(id: number) {
  await updateAuditLog(id);
  return getOrder(id);
}

console.log in production code — Sonar flags console.log statements. This is a code smell in application code (use a proper logger). It is not flagged in test files.

Security Hotspot Review

Security hotspots in TypeScript match what you see in C#:

Hotspot CategoryTypeScript ExampleWhat to Check
SQL injectionTemplate literals in raw queriesEnsure parameterized queries
XSSinnerHTML, dangerouslySetInnerHTMLVerify input is sanitized
CryptographyUse of weak algorithmsEnsure bcrypt, not md5
AuthenticationJWT validation codeVerify algorithm, expiry, signature
CORScors({ origin: '*' })Ensure restricted origins in production
Environment variablesDirect process.env accessVerify secrets are not logged

Hotspots require a human review decision in the Sonar UI — “Safe,” “Acknowledge,” or “Fixed.” They do not block the quality gate unless “Security Hotspot Review Rate” is in your gate configuration. Add it — unreviewed hotspots are the point of the feature.

PR Decoration

When the CI job runs on a pull request, SonarCloud posts inline comments on the changed lines that have new issues, plus a summary comment with the quality gate result:

Quality Gate: FAILED
New Code Issues: 3 code smells, 1 bug
Coverage on New Code: 74% (required: 80%)

This requires:

  1. GITHUB_TOKEN in your CI environment (GitHub Actions provides this automatically)
  2. SonarCloud GitHub App installed on the repository
  3. The PR trigger in your workflow (on: pull_request)

The PR decoration in SonarCloud is indistinguishable from the SonarQube behavior you know from .NET. The inline comments appear as regular GitHub review comments from the “SonarCloud” bot.

SonarLint IDE Integration

SonarLint for VS Code and WebStorm shows Sonar issues inline as you type, without running the full scanner. Install the SonarLint extension and connect it to your SonarCloud project to synchronize the quality profile (so local rules match what CI will report).

VS Code: Extensions → SonarLint → Connected Mode → Add Connection → SonarCloud
WebStorm: Settings → Tools → SonarLint → Project Binding → Bind to SonarCloud project

Connected mode pulls the quality profile from your project, so you see the same rules locally that CI will enforce.

Key Differences

ConcernSonarQube/Scanner for .NETSonarCloud for TypeScriptNotes
Scanner invocationdotnet sonarscanner begin/endGitHub Action or sonar-scanner CLINo build step needed
Coverage formatOpenCover or Cobertura XMLLCOV (from Vitest or Jest)Both are widely supported
Rule maturityVery mature — catches subtle CLR bugsGood but less matureSupplement TS with ESLint
Language analysisCompiled output analyzedSource files analyzed directlyTypeScript is analyzed without compilation
Ruleset sourceBuilt-in + Roslyn rulesBuilt-in + community TS rulesESLint fills gaps
Build requirementRequires dotnet buildNo build requiredSonar reads TS source directly
Monorepo supportPer-project analysisSingle scanner run with multi-path configConfigure sonar.sources with multiple paths
Fetch depth in CIUsually not an issueMust be full depth (fetch-depth: 0)Shallow clone breaks blame data

Gotchas for .NET Engineers

Gotcha 1: SonarCloud TS Rules Are Good, Not Great — ESLint Is Not Optional

The C# ruleset in Sonar is exceptionally mature. It catches thread-safety bugs, LINQ misuse, and subtle nullability issues that took years of community contribution to encode. The TypeScript ruleset is solid for the basics but misses many patterns that @typescript-eslint catches.

Do not treat SonarCloud as a replacement for ESLint on TypeScript projects. They are complementary:

  • ESLint: Fast, runs in editor, catches type-unsafe patterns, enforces team conventions
  • SonarCloud: Historical tracking, PR decoration, quality gate enforcement, coverage integration, security hotspots

If you only set up one, set up ESLint. If you set up both, configure them to avoid duplicate reporting of the same issues. Disable Sonar rules that are also enforced by ESLint so you only see each issue in one place.

Gotcha 2: Shallow Clone Breaks Sonar Analysis

GitHub Actions uses actions/checkout@v4 which performs a shallow clone by default (fetch-depth: 1). SonarCloud requires the full git history to compute blame data, identify new vs. existing code, and track when issues were introduced. Without full history, Sonar cannot determine what is “new code” — it treats everything as new, which makes the quality gate meaningless.

Always use fetch-depth: 0 in your checkout step:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0   # Required for SonarCloud — never omit this

There is no equivalent issue in the .NET scanner because dotnet sonarscanner is typically run in environments with full checkout configured separately.

Gotcha 3: LCOV Report Path Must Exist Before the Scanner Runs

Sonar will not fail if the coverage report does not exist — it silently reports 0% coverage, which causes the quality gate to fail on coverage. This happens when:

  • The test run fails before writing the coverage file
  • The coverage reporter is not configured to produce LCOV (only HTML or text)
  • The path in sonar-project.properties does not match the actual file location

Debug this by checking whether coverage/lcov.info exists after the test step runs, before the Sonar step runs. Add a verification step in CI:

- name: Run tests with coverage
  run: pnpm test:coverage

- name: Verify coverage report exists
  run: |
    if [ ! -f coverage/lcov.info ]; then
      echo "ERROR: coverage/lcov.info not found"
      exit 1
    fi
    echo "Coverage report size: $(wc -l < coverage/lcov.info) lines"

Gotcha 4: Quality Gate Applies to New Code, Not Total Code

This is the same in .NET but worth repeating because engineers new to Sonar try to apply the gate to total coverage first. The default Sonar way is to measure coverage on new lines only — lines changed or added in the current branch compared to the main branch.

If you configure the gate on “overall” coverage instead of “new code” coverage, you will block every PR until you retrofit tests for all existing code. Use “new code” metrics in your gate, and use the “Coverage Trend” widget in the Sonar dashboard to monitor overall coverage separately.

Gotcha 5: Security Hotspots Do Not Block PRs by Default

Unlike issues (bugs, code smells), security hotspots do not block the quality gate unless you add a “Security Hotspot Review Rate” condition. Developers often assume that a security hotspot showing up in the PR will block merge — it will not, unless you configure it to.

Add this to your quality gate:

  • Security Hotspots Reviewed: is less than100% on new code

Then enforce the review workflow: every hotspot that appears on a PR must be reviewed (marked “Safe,” “Acknowledged,” or “Fixed”) before the quality gate passes. This is the intended workflow and mirrors how security reviewers would handle it in Azure DevOps with SonarQube server.

Hands-On Exercise

Configure SonarCloud for your NestJS or Next.js project from scratch.

  1. Create a SonarCloud account and import your repository from GitHub. Note the project key and organization slug.

  2. Create sonar-project.properties at the repository root with correct source paths, test inclusion patterns, and the LCOV coverage path.

  3. Add the Sonar GitHub Actions workflow. Run it against your main branch and confirm the first analysis completes. Check that the dashboard shows source files and that coverage is not 0%.

  4. Configure a quality gate with the thresholds from the table above. Assign it to your project under Project Settings → Quality Gate.

  5. Create a feature branch, introduce a deliberate code smell (a function with high cognitive complexity — nest six if statements), open a PR, and verify that SonarCloud posts a PR decoration comment identifying the issue.

  6. Install SonarLint in your editor, connect it to your SonarCloud project in connected mode, and confirm that the same issue appears inline in your editor before pushing.

  7. Investigate what issues exist in your current codebase. Read through the Security Hotspots tab and mark each one with a review decision.

Quick Reference

TaskCommand / Config
Run scanner locallynpx sonar-scanner (with sonar-project.properties)
Set project keysonar.projectKey=org_project in sonar-project.properties
Set coverage path (Vitest)sonar.javascript.lcov.reportPaths=coverage/lcov.info
Set source directorysonar.sources=src
Exclude filessonar.exclusions=**/node_modules/**,**/dist/**
Exclude test files from sourcesonar.test.inclusions=**/*.spec.ts,**/*.test.ts
Full git fetch in CIfetch-depth: 0 in actions/checkout
Sonar token env varSONAR_TOKEN in GitHub Secrets
PR decoration tokenGITHUB_TOKEN (auto-provided by GitHub Actions)

sonar-project.properties Template

sonar.projectKey=your-org_your-project
sonar.organization=your-org
sonar.projectName=Your Project

sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.spec.ts,**/*.test.ts,**/*.spec.tsx,**/*.test.tsx
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.d.ts,**/coverage/**,**/*.config.ts

sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.sourceEncoding=UTF-8

Vitest Coverage Configuration

// vitest.config.ts
coverage: {
  provider: 'v8',
  reporter: ['text', 'lcov'],
  reportsDirectory: './coverage',
  exclude: ['**/*.spec.ts', '**/*.test.ts', '**/*.d.ts', 'node_modules/**'],
}

GitHub Actions Workflow

- uses: actions/checkout@v4
  with:
    fetch-depth: 0
- run: pnpm test:coverage
- uses: SonarSource/sonarcloud-github-action@master
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Further Reading