SonarLint best practices for Next.js 16 applications. Covers pre-merge quality checks, issue severity prioritization, common fixes for TypeScript/Next.js, and when to defer minor issues...
Best practices for using SonarLint to maintain code quality in The Simpsons API (Next.js 16 + TypeScript).
Use this skill when:
# Get all TypeScript files modified in PR
git diff --name-only main...your-branch | grep -E "\.(ts|tsx)$"
# Analyze each file (use VS Code SonarLint extension)
# Or use available tools in your environment
| Severity | Action | Timeline |
|---|---|---|
| 🔴 BLOCKER | Must fix | Before merge |
| 🟠 CRITICAL | Must fix | Before merge |
| 🟡 MAJOR | Should fix | Before merge |
| 🔵 MINOR | Can defer | With justification |
| ⚪ INFO | Optional | Per team standards |
When: Type validation errors Fix:
// ❌ Before
if (typeof input !== "number") {
throw new Error("Expected number");
}
// ✅ After
if (typeof input !== "number") {
throw new TypeError("Expected number, got " + typeof input);
}
Exception: Domain validation should use domain exceptions
// ✅ Correct for business rules
if (rating < 1 || rating > 5) {
throw new ValidationException("Rating must be 1-5");
}
Production Code:
// ❌ Wrong
function process(data: any): any {
return data;
}
// ✅ Correct - Use generics
function process<T>(data: T): T {
return data;
}
// ✅ Correct - Use unknown when type is truly unknown
function process(data: unknown): ProcessedData {
if (typeof data !== "object") {
throw new TypeError("Expected object");
}
// ... narrow type and process
}
Test Mocks:
// ✅ Acceptable with comment
// @ts-expect-error - Test mock intentionally incomplete for flexibility
const mockRepo: any = { findById: vi.fn() };
// ✅ Better - Use Partial<T>
const mockRepo: Partial<EpisodeRepository> = {
findById: vi.fn().mockResolvedValue(mockEpisode),
};
Critical Pattern from PR #14:
// ❌ Wrong - Loses type information
catch (error) {
if (error instanceof ValidationException) {
throw new Error(error.message); // Lost field, code, metadata
}
}
// ✅ Correct - Preserves exception type
catch (error) {
if (error instanceof ValidationException || error instanceof DomainException) {
throw error; // Full type info preserved for client
}
if (error instanceof Error) {
throw error; // Preserve stack trace
}
throw new Error("Unexpected error");
}
Why This Matters:
Acceptable deferrals:
any types - If mock needs flexibilityDocument deferrals:
// @ts-expect-error - SonarLint: Using 'any' for test flexibility
// Justification: Mock needs to work with multiple use case types
const mockFactory: any = { create: vi.fn() };
Before PR Creation:
pnpm test # All tests pass
pnpm build # Build succeeds
pnpm tsc --noEmit # Type check clean
# SonarLint analysis # Zero blockers/critical
During Code Review:
Pattern from PR #14 SonarLint fixes:
// app/_actions/episodes.ts
export async function trackEpisode(episodeId: number, rating: number) {
return withAuthenticatedRLS(prisma, async (tx, user) => {
try {
const useCase = UseCaseFactory.createTrackEpisodeUseCase();
await useCase.execute({ episodeId, rating }, user.id);
revalidatePath(`/episodes/${episodeId}`);
return { success: true };
} catch (error) {
// ✅ Preserve all domain exceptions
if (error instanceof ValidationException) {
throw error; // Preserves: field, message, code
}
if (error instanceof NotFoundException) {
throw error; // Preserves: entityType, entityId
}
if (error instanceof DomainException) {
throw error; // Base class for all domain exceptions
}
if (error instanceof Error) {
throw error; // Preserve stack trace
}
throw new Error("Failed to track episode");
}
});
}
Client can now handle specific types:
// app/_components/EpisodeTracker.tsx
try {
await trackEpisode(episodeId, rating);
toast.success("Episode tracked!");
} catch (error) {
if (error instanceof ValidationException) {
// Show field-specific error
toast.error(`${error.field}: ${error.message}`);
} else if (error instanceof NotFoundException) {
toast.error(`${error.entityType} not found`);
} else {
toast.error("Something went wrong");
}
}
any types allowedunknown for truly dynamic data, then narrowPartial<T> for optional fieldsany from missing typesPartial<Interface> for mocks@ts-expect-error only when necessaryany is usedany without commentExamples:
// ✅ Production - Use Partial<T>
function updateUser(id: string, updates: Partial<User>) {
// ...
}
// ✅ Production - Use unknown
function parseJson(input: unknown): ParsedData {
if (typeof input !== "string") {
throw new TypeError("Expected string");
}
return JSON.parse(input);
}
// ✅ Test - Document any usage
// @ts-expect-error - Test mock intentionally uses any for flexibility
const mockUseCase: any = {
execute: vi.fn().mockResolvedValue({ success: true }),
};
// ✅ Test - Better with Partial
const mockUseCase: Partial<TrackEpisodeUseCase> = {
execute: vi.fn().mockResolvedValue({ success: true }),
};
# .github/workflows/quality-check.yml
name: Code Quality
on:
pull_request:
branches: [main]
jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: SonarQube Analysis
uses: sonarsource/sonarqube-scan-action@master
with:
args: >
-Dsonar.projectKey=thesimpsonsapi
-Dsonar.qualitygate.wait=true
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# .git/hooks/pre-push
#!/bin/bash
echo "Running SonarLint analysis..."
# Add SonarLint CLI check here
# Exit 1 if blockers/critical found
Before (loses type):
catch (error) {
if (error instanceof ValidationException) {
throw new Error(error.message);
}
throw new Error("Failed");
}
After (preserves type):
catch (error) {
if (error instanceof ValidationException || error instanceof DomainException) {
throw error;
}
if (error instanceof Error) {
throw error;
}
throw new Error("Failed");
}
Vitest Setup:
// vitest.setup.ts
vi.mock("next/image", () => ({
// ❌ Before: any type
default: (props: any) => props,
// ✅ After: explicit type
default: (props: Record<string, unknown>) => props,
}));