Code Quality Tools: Automated Standards with ESLint, SonarQube, and Analyzers
Introduction
Manual code reviews miss consistency issues and subtle bugs that automated tools catch instantly. This guide covers comprehensive code quality tooling: ESLint and Prettier for JavaScript/TypeScript, SonarQube and SonarLint for multi-language static analysis, .NET analyzers including StyleCop, code coverage measurement with Coverlet and Istanbul, technical debt tracking, and automated quality gates integrated into CI/CD pipelines.
JavaScript/TypeScript Quality: ESLint and Prettier
ESLint Configuration
.eslintrc.json:
{
"env": {
"browser": true,
"es2022": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint",
"react",
"react-hooks",
"import",
"security"
],
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
],
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-explicit-any": "error",
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"newlines-between": "always",
"alphabetize": { "order": "asc" }
}
],
"security/detect-object-injection": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"settings": {
"react": {
"version": "detect"
}
},
"ignorePatterns": ["dist/", "build/", "node_modules/", "*.config.js"]
}
Custom Rules:
// .eslintrc.js
module.exports = {
rules: {
'custom-company/no-legacy-api': 'error',
'custom-company/require-error-logging': 'warn'
},
// Custom rule plugin
plugins: ['custom-company']
};
// eslint-plugin-custom-company/lib/rules/no-legacy-api.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow usage of deprecated legacy API',
category: 'Best Practices'
}
},
create(context) {
return {
CallExpression(node) {
if (node.callee.name === 'legacyAPI') {
context.report({
node,
message: 'Use newAPI instead of deprecated legacyAPI'
});
}
}
};
}
};
Prettier Configuration
.prettierrc.json:
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"overrides": [
{
"files": "*.md",
"options": {
"proseWrap": "always"
}
}
]
}
Integration with ESLint:
// package.json scripts
{
"scripts": {
"lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,json,css,md}\"",
"quality": "npm run lint && npm run format:check"
}
}
Pre-commit Hook (Husky + lint-staged):
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}
# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
SonarQube and SonarLint
SonarQube Setup
Docker Compose:
version: '3.8'
services:
sonarqube:
image: sonarqube:10-community
container_name: sonarqube
ports:
- "9000:9000"
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonar
volumes:
- postgresql_data:/var/lib/postgresql/data
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgresql_data:
Project Configuration (sonar-project.properties):
sonar.projectKey=contoso-app
sonar.projectName=Contoso Application
sonar.projectVersion=1.0.0
sonar.sources=src
sonar.tests=tests
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.test.ts
# Language-specific
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.tsconfigPath=tsconfig.json
# Code coverage
sonar.coverage.exclusions=**/*.test.ts,**/mocks/**
# Quality gate thresholds
sonar.qualitygate.wait=true
Quality Gates
Custom Quality Gate:
# SonarQube UI: Quality Gates → Create
Conditions:
- Coverage: >= 80%
- Duplicated Lines: <= 3%
- Maintainability Rating: A
- Reliability Rating: A
- Security Rating: A
- Security Hotspots Reviewed: 100%
- New Bugs: = 0
- New Vulnerabilities: = 0
- New Code Smells: <= 5
SonarLint IDE Integration
VS Code Configuration:
// .vscode/settings.json
{
"sonarlint.connectedMode.project": {
"projectKey": "contoso-app",
"serverUrl": "http://localhost:9000"
},
"sonarlint.rules": {
"typescript:S1186": {
"level": "off" // Disable specific rule
},
"typescript:S3776": {
"level": "on",
"parameters": {
"threshold": "10" // Cognitive complexity threshold
}
}
}
}
.NET Code Analyzers
Roslyn Analyzers
Project Configuration:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<!-- Enable all analyzers -->
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Treat warnings as errors -->
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<!-- StyleCop analyzers -->
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<!-- Security analyzers -->
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<!-- Additional analyzers -->
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.16.0.82469">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
EditorConfig
.editorconfig:
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.cs]
indent_size = 4
# Naming conventions
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = warning
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.symbols = interface
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.style = begins_with_i
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.capitalization = pascal_case
# Code style
csharp_prefer_braces = true:warning
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = true:suggestion
# Formatting
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
# CA rules (Code Analysis)
dotnet_diagnostic.CA1062.severity = warning # Validate arguments
dotnet_diagnostic.CA1305.severity = warning # Specify IFormatProvider
dotnet_diagnostic.CA2007.severity = none # ConfigureAwait not required
dotnet_diagnostic.CA1031.severity = warning # Do not catch general exception types
# StyleCop rules
dotnet_diagnostic.SA1600.severity = none # Elements should be documented (disable for now)
dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this
dotnet_diagnostic.SA1633.severity = none # File must have header
Rule Suppression
File-level:
#pragma warning disable CA1062 // Validate arguments of public methods
public void ProcessOrder(Order order)
{
// Validation handled by middleware
var total = order.Items.Sum(i => i.Price);
}
#pragma warning restore CA1062
Assembly-level:
// GlobalSuppressions.cs
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage(
"Design",
"CA1062:Validate arguments of public methods",
Justification = "Handled by middleware",
Scope = "member",
Target = "~M:Contoso.OrderService.ProcessOrder(Contoso.Order)"
)]
Code Coverage
JavaScript/TypeScript with Istanbul (nyc)
Configuration:
// package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"coverage:report": "nyc report --reporter=html --reporter=text"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!src/**/*.test.{ts,tsx}",
"!src/**/index.ts"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Jest Configuration:
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.test.{ts,tsx}',
'!src/**/types.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/critical/': {
branches: 95,
functions: 95,
lines: 95,
statements: 95
}
},
coverageReporters: ['text', 'lcov', 'html', 'json-summary']
};
.NET with Coverlet
Configuration:
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
Commands:
# Generate coverage
dotnet test /p:CollectCoverage=true \
/p:CoverletOutputFormat=opencover \
/p:CoverletOutput=./coverage/
# With thresholds
dotnet test /p:CollectCoverage=true \
/p:Threshold=80 \
/p:ThresholdType=line \
/p:ThresholdStat=total
# Generate HTML report
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator \
-reports:coverage/coverage.opencover.xml \
-targetdir:coverage/report \
-reporttypes:Html
Coverage in CI/CD
GitHub Actions:
name: Code Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
fail_ci_if_error: true
- name: Comment PR with coverage
uses: romeovs/lcov-reporter-action@v0.3.1
with:
lcov-file: ./coverage/lcov.info
github-token: ${{ secrets.GITHUB_TOKEN }}
Technical Debt Management
SonarQube Technical Debt
Debt Ratio Calculation:
Technical Debt Ratio = (Remediation Cost / Development Cost) × 100
Example:
- Remediation Cost: 2 days (16 hours)
- Development Cost: 50 days (400 hours)
- Debt Ratio: (16 / 400) × 100 = 4%
Targets:
- A: <= 5%
- B: 6-10%
- C: 11-20%
- D: 21-50%
- E: > 50%
Tracking Debt:
// Mark technical debt in code
// TODO: TECH-DEBT - Replace with proper error handling (Est: 2h)
try {
performOperation();
} catch {
console.log('Failed');
}
// Link to issue tracker
// FIXME: JIRA-1234 - Refactor to use repository pattern (Est: 1 day)
const data = await db.query('SELECT * FROM users');
Code Metrics
Cyclomatic Complexity:
// ❌ High complexity (12) - hard to test
public decimal CalculatePrice(Product product, Customer customer, DateTime date)
{
decimal price = product.BasePrice;
if (customer.IsPremium)
{
if (product.Category == "Electronics")
{
if (date.Month == 12)
price *= 0.7m;
else if (date.DayOfWeek == DayOfWeek.Friday)
price *= 0.85m;
else
price *= 0.9m;
}
else if (product.Category == "Clothing")
{
if (customer.TotalPurchases > 10)
price *= 0.75m;
else
price *= 0.85m;
}
}
else
{
if (date.DayOfWeek == DayOfWeek.Monday)
price *= 0.95m;
}
return price;
}
// ✅ Reduced complexity (2) - testable
public decimal CalculatePrice(Product product, Customer customer, DateTime date)
{
var calculator = PricingStrategy.For(product, customer, date);
return calculator.Calculate(product.BasePrice);
}
CI/CD Quality Gates
GitHub Actions Quality Gate
name: Quality Gate
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Format check
run: npm run format:check
- name: Tests with coverage
run: npm run test:coverage
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- name: Quality Gate Check
uses: sonarsource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Fail on quality issues
if: failure()
run: |
echo "Quality gate failed! Check SonarQube for details."
exit 1
Azure DevOps Quality Gate
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: SonarQubePrepare@5
inputs:
SonarQube: 'SonarQubeConnection'
scannerMode: 'CLI'
configMode: 'file'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
arguments: '/p:CollectCoverage=true /p:CoverletOutputFormat=opencover'
- task: SonarQubeAnalyze@5
- task: SonarQubePublish@5
inputs:
pollingTimeoutSec: '300'
- task: sonar-buildbreaker@8
inputs:
SonarQube: 'SonarQubeConnection'
Best Practices
- Automate Everything: Linting, formatting, and tests in CI/CD
- Fail Fast: Block PRs that don't meet quality standards
- Progressive Enhancement: Gradually increase coverage thresholds
- Context-Specific Rules: Critical code needs higher standards
- Developer Education: Explain why rules exist, not just enforce them
- Regular Updates: Keep analyzer packages current
- Measure and Improve: Track quality metrics over time
Troubleshooting
ESLint Performance:
# Use cache
eslint --cache src/**/*.ts
# Parallel execution
eslint --max-warnings 0 src/**/*.ts
SonarQube Memory:
# Increase Java heap
sonarqube:
environment:
SONAR_CE_JAVAOPTS: "-Xmx2g"
SONAR_WEB_JAVAOPTS: "-Xmx1g"
Analyzer Conflicts:
<!-- Disable conflicting rules -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1062;SA1600</NoWarn>
</PropertyGroup>
Key Takeaways
- ESLint and Prettier enforce consistent JavaScript/TypeScript code style
- SonarQube provides multi-language static analysis with quality gates
- .NET analyzers catch code issues at compile time with Roslyn
- Code coverage metrics ensure adequate test coverage with Istanbul and Coverlet
- Automated quality gates in CI/CD prevent technical debt accumulation
Next Steps
- Implement mutation testing with Stryker for test quality validation
- Set up architecture fitness functions with ArchUnit or NDepend
- Configure security scanning with Snyk or WhiteSource
- Establish code review metrics to track review effectiveness
Additional Resources
Quality code, quality product.