SonarQube for JavaScript: Static Analysis, Code Smells, and CI Gates
Static analysis catches a category of bugs that tests often miss — not because tests are bad, but because they exercise paths through code, while static analysis reads the code itself. SonarQube analyzes your JavaScript and TypeScript for security vulnerabilities, maintainability issues, and reliability problems without running a single test. When you wire it into your CI pipeline with a quality gate, code that introduces new issues simply doesn't merge.
How SonarQube Works
SonarQube runs a server that stores analysis history and exposes a dashboard. The sonar-scanner CLI tool analyzes your codebase locally and ships results to the server. You can run SonarQube Community Edition free on your own infrastructure, or use SonarCloud for a hosted version that integrates directly with GitHub.
The analysis produces findings in four categories:
- Bugs: code likely to behave incorrectly at runtime (null dereferences, resource leaks)
- Vulnerabilities: security weaknesses (SQL injection patterns, hardcoded credentials, insecure use of crypto)
- Code Smells: maintainability issues that slow down development (cognitive complexity, dead code, duplication)
- Security Hotspots: code that needs human review to determine if it's actually a security risk
Each finding carries a severity (Blocker, Critical, Major, Minor, Info) and an estimated remediation time. Quality gates use these to decide pass/fail.
Running SonarQube Locally with Docker
For local development or a team server, Docker is the fastest path:
docker run -d \
--name sonarqube \
-p 9000:9000 \
-v sonarqube_data:/opt/sonarqube/data \
-v sonarqube_extensions:/opt/sonarqube/extensions \
-v sonarqube_logs:/opt/sonarqube/logs \
sonarqube:10-communityWait about 60 seconds for startup, then open http://localhost:9000. Default credentials are admin/admin — change them immediately. Create a project, generate a project token, and keep it somewhere safe.
Configuring sonar-scanner
Install sonar-scanner as a dev dependency:
npm install --save-dev sonar-scannerCreate sonar-project.properties in your project root:
sonar.projectKey=my-project
sonar.projectName=My Project
sonar.projectVersion=1.0
# Source and test directories
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.ts,**/*.spec.ts,**/*.test.tsx
# TypeScript/JavaScript settings
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.tsconfigPath=tsconfig.json
# Exclusions
sonar.exclusions=**/*.test.ts,**/*.spec.ts,**/node_modules/**,**/dist/**,**/generated/**
# Encoding
sonar.sourceEncoding=UTF-8Add a script to package.json:
{
"scripts": {
"sonar": "sonar-scanner -Dsonar.host.url=http://localhost:9000 -Dsonar.token=${SONAR_TOKEN}"
}
}Run your tests with coverage first, then sonar-scanner:
npm test -- --coverage
npm run sonarSonarQube will correlate your LCOV report with its own analysis, so the dashboard shows both test coverage and static analysis findings in one place.
Understanding Code Smell Categories
Not all code smells are equal. SonarQube flags hundreds of patterns; understanding the categories helps you prioritize.
Cognitive Complexity is one of the most actionable metrics. SonarQube measures how hard a function is to understand, not just its cyclomatic complexity. A deeply nested function with multiple early returns scores higher than a flat switch statement with many cases. The default threshold is 15; functions above this are flagged.
// High cognitive complexity — too many nested conditions
function processOrder(order: Order): Result {
if (order.user) {
if (order.user.isActive) {
if (order.items.length > 0) {
if (order.payment) {
if (order.payment.isValid) {
// actual logic buried 5 levels deep
}
}
}
}
}
}
// Refactored — flat and readable
function processOrder(order: Order): Result {
if (!order.user?.isActive) return { error: 'Inactive user' };
if (order.items.length === 0) return { error: 'Empty order' };
if (!order.payment?.isValid) return { error: 'Invalid payment' };
// actual logic at top level
}Duplicated Blocks are another high-value target. SonarQube detects copy-paste code across files and flags blocks above a configurable minimum line count. These are maintenance bombs — a bug fixed in one copy gets missed in three others.
Dead Code — unreachable statements after return, unused parameters, and variables assigned but never read — clutters codebases and confuses future readers. SonarQube catches these without running the code.
Configuring Quality Gates
A quality gate is a set of conditions that must pass for an analysis to be considered a success. The default "Sonar way" gate checks:
- New code coverage ≥ 80%
- New code duplicated lines ≤ 3%
- New code maintainability rating = A
- New code reliability rating = A
- New code security rating = A
"New code" is the key concept. SonarQube compares each analysis against a baseline (usually the previous version or a specific date). Quality gates on new code let you enforce standards for everything going forward without blocking the team on fixing all legacy issues immediately.
To customize a quality gate in the SonarQube UI: Quality Gates → Create → add conditions. For a stricter setup, add:
- New Blocker Issues = 0
- New Critical Issues = 0
- New Vulnerabilities = 0
Integrating with GitHub Actions
# .github/workflows/sonar.yml
name: SonarQube Analysis
on:
push:
branches: [main, develop]
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for SonarQube branch analysis
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Run tests with coverage
run: npm test -- --coverage --coverageReporters=lcov
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- name: SonarQube Quality Gate Check
uses: SonarSource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}The fetch-depth: 0 on the checkout action is important — SonarQube needs full git history to compute new code correctly on branches.
For SonarCloud (the hosted version), the setup is nearly identical but uses SONAR_TOKEN pointing to sonarcloud.io. SonarCloud also posts inline PR comments with findings, which is particularly useful for code review.
Suppressing False Positives
Sometimes SonarQube flags code that's intentionally written a certain way. Suppress individual findings with inline comments:
function hashPassword(password: string): string {
// NOSONAR - MD5 intentionally used for non-security checksum here
return crypto.createHash('md5').update(password).digest('hex');
}For broader suppressions, use the sonar.issue.ignore.multicriteria property in sonar-project.properties:
sonar.issue.ignore.multicriteria=e1,e2
# Ignore cognitive complexity in test files
sonar.issue.ignore.multicriteria.e1.ruleKey=javascript:S3776
sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.test.ts
# Ignore TODO comments
sonar.issue.ignore.multicriteria.e2.ruleKey=javascript:S1135
sonar.issue.ignore.multicriteria.e2.resourceKey=**Use suppressions judiciously. Each suppression is a documented decision, not a dismissal. If you find yourself suppressing the same rule across dozens of files, reconsider whether the rule should be disabled globally in your quality profile instead.
Using SonarLint for Local Feedback
SonarLint is a free IDE plugin (VS Code, IntelliJ, others) that runs the same rules as SonarQube in real time as you type. When connected to your SonarQube server, it syncs your team's quality profile and suppressions, giving every developer identical feedback to what CI will report.
This closes the feedback loop: developers see issues before committing, rather than discovering them when a PR fails the quality gate. The combination of SonarLint locally and SonarQube in CI is one of the most effective static analysis setups available for JavaScript teams.