SonarCloud Issue Resolution Guide¶
Purpose: This document is a reusable sub-prompt for AI-assisted SonarCloud issue resolution. When you encounter SonarCloud issues, reference this guide for the correct approach to each issue type.
Project: devgem_forma-3d-connect Organization: devgem Platform: SonarCloud Quality Profile: Sonar way for AI Code Last updated: 2026-03-20
Table of Contents¶
- Workflow Overview
- Using the SonarCloud Web API
- Issue Resolution Decision Tree
- Fix Patterns by Rule
- Won't Fix / Suppression Protocol
- Coverage and Duplication
- Prevention: What to Pay Attention to When Developing
- Pipeline Integration
- Pipeline failures and log diagnostics
- Lessons Learned from 769 → 0
- Keeping SonarCloud and CodeCharta Aligned
Workflow Overview¶
When asked to review and resolve SonarCloud issues, follow this workflow:
1. Fetch open issues via the SonarCloud Web API
2. Categorize each issue: FIX / WON'T FIX / FALSE POSITIVE
3. For FIX: apply the code change, verify with typecheck + tests
4. For WON'T FIX: add suppression in sonar-project.properties + inline code comment
5. For FALSE POSITIVE: suppress via sonar-project.properties + inline code comment
6. Verify the quality gate passes
Prioritization Order¶
- Bugs (reliability) — always fix, these indicate real logic errors
- Vulnerabilities (security) — always fix or suppress with documented reason
- Security Hotspots — review and mark as safe, or fix
- Code Smells, BLOCKER/CRITICAL — fix first (cognitive complexity, etc.)
- Code Smells, MAJOR — fix in batches by rule
- Code Smells, MINOR — fix mechanically or suppress project-wide
- Duplication — refactor into shared libraries (largest effort)
Using the SonarCloud Web API¶
The SonarCloud Web API is the primary tool for fetching and managing issues programmatically.
- API reference (same Web API as SonarQube Server): SonarQube Server — Web API
- Base URL:
https://sonarcloud.io/api/ - Authentication: HTTP Basic Auth with the token as the username and an empty password (equivalent to
curl -u "$SONAR_TOKEN:"). Do not commit tokens; use a user or project token from SonarCloud (My Account → Security) or a secret store, and export it asSONAR_TOKENin your shell or CI. - Setting the token locally: run
export SONAR_TOKEN="<your-token>"in your shell (or add it to a.envfile that is git-ignored). The active project token can be found in SonarCloud under My Account → Security → Tokens.
API troubleshooting¶
| Symptom | Likely cause |
|---|---|
| HTTP 401 with an empty body | Token revoked, mistyped, or wrong auth style (must be -u 'token:', not Bearer-only unless your client maps that to Basic). |
Quality gate JSON shows OK but the pipeline failed |
You queried main (or default branch) instead of the pull request; add pullRequest=<id>. |
issues/search returns nothing for a PR |
Missing pullRequest= / projectKeys= / componentKeys=; ensure the analysis for that PR has finished. |
Essential API Calls¶
Fetch all open issues¶
curl -sS -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/issues/search?componentKeys=devgem_forma-3d-connect&statuses=OPEN,CONFIRMED,REOPENED&ps=100" \
| python3 -m json.tool
To debug auth, repeat the same URL with -w "\nHTTP:%{http_code}\n" (output is no longer pure JSON).
Key response fields:
- total — total number of issues
- effortTotal — SonarCloud's estimated fix time in minutes
- issues[].rule — rule key (e.g., typescript:S6551)
- issues[].component — file path
- issues[].line — line number
- issues[].message — human-readable description
- issues[].severity — BLOCKER, CRITICAL, MAJOR, MINOR, INFO
- issues[].type — BUG, VULNERABILITY, CODE_SMELL
Fetch issues by specific rule¶
curl -s -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/issues/search?componentKeys=devgem_forma-3d-connect&rules=typescript:S6759&ps=100" \
| python3 -m json.tool
Fetch project metrics¶
curl -s -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/measures/component?component=devgem_forma-3d-connect&metricKeys=coverage,duplicated_lines_density,ncloc,bugs,vulnerabilities,code_smells,sqale_rating,reliability_rating,security_rating,duplicated_blocks,duplicated_lines,lines_to_cover,uncovered_lines" \
| python3 -m json.tool
Fetch quality gate status¶
Default branch (e.g. main):
curl -s -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/qualitygates/project_status?projectKey=devgem_forma-3d-connect&branch=main" \
| python3 -m json.tool
Pull request (use the Azure DevOps / GitHub PR number SonarCloud shows in the dashboard URL):
curl -s -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/qualitygates/project_status?projectKey=devgem_forma-3d-connect&pullRequest=584" \
| python3 -m json.tool
The JSON includes projectStatus.status (OK | ERROR) and projectStatus.conditions[] with each quality gate rule and status.
Open issues on a pull request¶
curl -s -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/issues/search?projectKeys=devgem_forma-3d-connect&pullRequest=584&resolved=false&ps=100" \
| python3 -m json.tool
(componentKeys=devgem_forma-3d-connect is often equivalent to projectKeys for this project key; if one form returns no rows, try the other.)
Fetch new code metrics (for quality gate evaluation)¶
curl -s -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/measures/component?component=devgem_forma-3d-connect&metricKeys=new_coverage,new_duplicated_lines_density,new_bugs,new_vulnerabilities,new_code_smells" \
| python3 -m json.tool
Mark an issue as "Won't Fix" via API¶
curl -s -u "${SONAR_TOKEN}:" -X POST \
"https://sonarcloud.io/api/issues/do_transition" \
-d "issue=ISSUE_KEY&transition=wontfix"
Pagination¶
The API returns max 100 results per page. Use p (page number) and ps (page size) for pagination:
# Page 2
curl -s -u "${SONAR_TOKEN}:" \
"https://sonarcloud.io/api/issues/search?componentKeys=devgem_forma-3d-connect&ps=100&p=2"
Useful Filters¶
| Parameter | Example | Description |
|---|---|---|
severities |
BLOCKER,CRITICAL |
Filter by severity |
types |
BUG,VULNERABILITY |
Filter by type |
rules |
typescript:S6759 |
Filter by rule key |
directories |
apps/web/src |
Filter by directory |
createdAfter |
2026-03-13 |
Issues created after date |
facets |
rules,severities,types |
Get counts grouped by facet |
Issue Resolution Decision Tree¶
For each SonarCloud issue, apply this decision tree:
Is it a real problem in our code?
├── YES → Can it be fixed mechanically (search-and-replace)?
│ ├── YES → Fix it. Apply the pattern from the Fix Patterns section below.
│ └── NO → Is it a complex refactoring?
│ ├── YES, and it's CRITICAL+ → Fix now, break into smaller functions.
│ └── YES, and it's MAJOR/MINOR → Track as tech debt, fix when touching the file.
├── NO, it's a false positive → Suppress in sonar-project.properties + inline code comment
└── NO, it's a won't-fix (correct code, rule doesn't apply) → Suppress in sonar-project.properties + inline code comment
Decision Criteria for "Won't Fix"¶
A "won't fix" is appropriate when:
- The code is intentionally written that way (e.g., reversed winding order in 3D geometry)
- The framework requires it (e.g., NestJS
bootstrap().catch()pattern) - The rule is cosmetic with zero functional value (e.g., alphabetical union sorting)
- Fixing would reduce readability (e.g., type aliases from third-party patterns)
- The flagged pattern is a standard practice (e.g.,
void exprfor unused params in TypeScript)
A "won't fix" is NOT appropriate when:
- The code works but could be objectively improved (fix it)
- The fix is trivial (just do it)
- The rule catches a genuine anti-pattern, even if the current code happens to work
Fix Patterns by Rule¶
This section catalogs every SonarCloud rule we've encountered, grouped by fix approach. Use this as a lookup table when resolving issues.
Tier 1: Mechanical Search-and-Replace (minutes per batch)¶
These rules can be fixed across the entire codebase in a single pass.
S7781 — Replace .replace(/pattern/g, ...) with .replaceAll()¶
// Before
const cleaned = input.replace(/\./g, '-');
// After
const cleaned = input.replaceAll('.', '-');
Gotcha: .replaceAll() takes a string, not a regex. Drop the regex delimiters and flags.
S7773 — Use Number.parseInt() / Number.parseFloat() / Number.isNaN()¶
// Before
const port = parseInt(envPort, 10);
if (isNaN(value)) return fallback;
// After
const port = Number.parseInt(envPort, 10);
if (Number.isNaN(value)) return fallback;
S7778 — Use Array#includes() instead of indexOf() !== -1¶
// Before
if (allowedRoles.indexOf(role) !== -1) { ... }
// After
if (allowedRoles.includes(role)) { ... }
S7764 — Use globalThis instead of window¶
// Before
window.addEventListener('resize', handler);
// After
globalThis.addEventListener('resize', handler);
Context: This is about modern JavaScript that works in both browser and Node.js. In React components that are browser-only, both are fine, but globalThis is the modern standard.
S7758 — Use dot notation instead of brackets for static keys¶
// Before
config['apiUrl']
// After
config.apiUrl
S7776 — Use startsWith() / endsWith() instead of indexOf() === 0¶
// Before
if (path.indexOf('/api') === 0) { ... }
// After
if (path.startsWith('/api')) { ... }
S7755 — Use .at() instead of [length - index]¶
// Before
const last = items[items.length - 1];
// After
const last = items.at(-1);
S6606 — Use ??= instead of conditional assignment¶
// Before
if (cache === null || cache === undefined) {
cache = new Map();
}
// After
cache ??= new Map();
S3863 — Merge duplicate imports from the same module¶
// Before
import { Injectable } from '@nestjs/common';
import { Logger, HttpException } from '@nestjs/common';
// After
import { Injectable, Logger, HttpException } from '@nestjs/common';
Gotcha: When merging, check for both value imports and type imports. If one is import type { ... } and the other is import { ... }, they may need to stay separate or use inline type imports: import { type Foo, Bar } from '...'.
S7741 — Compare with undefined directly¶
// Before
if (typeof value !== 'undefined') { ... }
// After
if (value !== undefined) { ... }
S7735 — Flip negated conditions when else clause exists¶
// Before
if (!isAdmin) {
showUserView();
} else {
showAdminView();
}
// After
if (isAdmin) {
showAdminView();
} else {
showUserView();
}
Tier 2: Pattern-Based Fixes (minutes per file)¶
These require understanding the surrounding code but follow a consistent pattern.
S6759 — React props should be read-only¶
// Before
interface Props {
title: string;
onClose: () => void;
}
function Modal(props: Props) { ... }
// After
interface Props {
readonly title: string;
readonly onClose: () => void;
}
function Modal(props: Readonly<Props>) { ... }
Preferred approach: Wrap the entire props type with Readonly<> at the function signature level. This is less invasive than adding readonly to each field.
function Modal({ title, onClose }: Readonly<Props>) { ... }
S4624 — Extract nested template literals¶
// Before
const url = `${baseUrl}/api/${`v${version}`}/orders`;
// After
const versionPath = `v${version}`;
const url = `${baseUrl}/api/${versionPath}/orders`;
S6819 — Use <section> instead of <div role="region">¶
// Before
<div role="region" aria-label="Order details">
// After
<section aria-label="Order details">
S6853 — Associate form labels with controls¶
// Before
<label>Email</label>
<input type="email" />
// After
<label htmlFor="email-input">Email</label>
<input id="email-input" type="email" />
S6582 — Prefer optional chaining¶
// Before
const name = user && user.profile && user.profile.name;
// After
const name = user?.profile?.name;
S6551 — Wrap unknown values in String() for template interpolation¶
// Before
const message = `Error: ${error.message}`; // error.message is typed as unknown
// After
const message = `Error: ${String(error.message)}`;
Note: This rule fires when the interpolated expression has type unknown, object, or a union containing these. Wrap with String() to make the conversion explicit.
S2933 — Add readonly to class fields that are never reassigned¶
// Before (NestJS service with injected dependencies)
constructor(
private prisma: PrismaService,
private logger: Logger,
) {}
// After
constructor(
private readonly prisma: PrismaService,
private readonly logger: Logger,
) {}
Rule of thumb: All NestJS constructor-injected dependencies should be private readonly.
Tier 3: Refactoring Required (minutes to hours per function)¶
These require architectural understanding and careful refactoring.
S3776 — Cognitive complexity too high¶
Approach:
1. Extract helper functions for distinct logical blocks
2. Use early returns (guard clauses) instead of nested if/else
3. Replace complex conditionals with lookup maps
4. For React components: split into sub-components
// Before: deeply nested logic
function processOrder(order: Order) {
if (order.status === 'pending') {
if (order.items.length > 0) {
if (order.paymentVerified) {
// ... 20 lines of processing
} else {
// ... error handling
}
}
}
}
// After: guard clauses + extracted functions
function processOrder(order: Order) {
if (order.status !== 'pending') return;
if (order.items.length === 0) return;
if (!order.paymentVerified) {
handlePaymentNotVerified(order);
return;
}
fulfillOrder(order);
}
S107 — Too many constructor parameters¶
Approach: Group related parameters into an options/config object.
// Before
constructor(
private readonly prisma: PrismaService,
private readonly logger: Logger,
private readonly config: ConfigService,
private readonly eventBus: EventBus,
private readonly mailer: EmailService,
private readonly audit: AuditService,
private readonly cache: CacheService,
private readonly metrics: MetricsService,
private readonly queue: QueueService,
) {}
// After: use NestJS module organization to reduce injections
// Split the service into smaller, focused services
// Or group related services behind a facade
Note: For NestJS services, the real fix is usually to split the service into smaller ones with clearer responsibilities. A service with 9+ injected dependencies is a design smell indicating the service does too much.
S3358 — Nested ternary operators¶
Backend approach: Replace with if/else, lookup map, or extracted helper.
// Before
const level = severity === 'critical' ? 'error' : severity === 'high' ? 'warn' : 'info';
// After (lookup map)
const SEVERITY_LEVELS: Record<string, string> = {
critical: 'error',
high: 'warn',
};
const level = SEVERITY_LEVELS[severity] ?? 'info';
Frontend (React JSX) approach: Nested ternaries are idiomatic in JSX for conditional rendering. We suppress S3358 project-wide for apps/web/src/** in sonar-project.properties. If nesting exceeds 3 levels, extract to a helper component.
S4325 — Unnecessary type assertions¶
This rule covers both as Type assertions and ! non-null assertions.
// Before (as-assertion)
const user = result as User; // result is already typed as User
// After
const user = result;
// Before (non-null assertion on already non-nullable type)
req.session!.save((err) => { ... });
// After
req.session.save((err) => { ... });
Common case: Express req.session after express-session middleware — the type declarations merge session onto Request as a required property, making ! redundant.
The guard-then-closure trap (TS18048 vs S4325)¶
A common pattern that creates a tension between TypeScript strict mode and SonarCloud:
req.sessionis typed as optional (session?: Session & Partial<SessionData>)- Accessing it without a check causes TS18048 (
'req.session' is possibly 'undefined'), failing the build - Adding a guard (
if (!req.session) throw) narrows the type in the immediate scope - But inside a closure (e.g., the callback in
new Promise((resolve, reject) => { ... })), TypeScript doesn't propagate the narrowing for object properties — so developers reach forreq.session!.save() - SonarCloud then flags the
!with S4325 ("This assertion is unnecessary since it does not change the type of the expression") because from Sonar's perspective, the guard already proved it's defined
Wrong fix (fixes TS18048 but introduces S4325):
if (!req.session) {
throw new Error('Session middleware is not configured');
}
await new Promise<void>((resolve, reject) => {
req.session!.save((err) => { ... }); // S4325: unnecessary assertion
});
Correct fix (satisfies both TypeScript and SonarCloud):
if (!req.session) {
throw new Error('Session middleware is not configured');
}
const session = req.session; // capture into const — preserves narrowing in closures
await new Promise<void>((resolve, reject) => {
session.save((err) => { ... }); // no assertion needed
});
Why this works: Assigning the narrowed value to a const local variable gives TypeScript a stable reference it can track into closures. Unlike req.session (a property access that could theoretically change between the guard and the closure execution), a const binding is provably immutable.
CAUTION: Removing type assertions can cause compilation errors if the types don't actually match. Always verify with pnpm nx run-many --target=typecheck after removing assertions. Previous attempts to blindly remove assertions in sendcloud.service.ts and users.service.ts caused TypeScript compilation failures.
Won't Fix / Suppression Protocol¶
When an issue should not be fixed, follow this two-step suppression protocol:
Step 1: Add suppression to sonar-project.properties¶
SonarCloud does NOT support // NOSONAR comments for JavaScript/TypeScript. All suppressions must go in sonar-project.properties via the sonar.issue.ignore.multicriteria mechanism.
Format¶
# Increment the entry list
sonar.issue.ignore.multicriteria=e1,e2,e3,...,eN
# For each suppression:
# Comment: rule description — justification
sonar.issue.ignore.multicriteria.eN.ruleKey=typescript:SXXXX
sonar.issue.ignore.multicriteria.eN.resourceKey=**/path/to/file.ts
Scoping options¶
| Scope | resourceKey Pattern |
When to Use |
|---|---|---|
| Single file | **/audit/audit.service.ts |
False positive in one specific file |
| File pattern | **/users/*.tsx |
Rule doesn't apply to a group of files |
| Directory | apps/web/src/** |
Rule is inappropriate for entire frontend |
| Project-wide | **/* |
Rule has zero value for this codebase |
Naming convention for the comment¶
Use a descriptive comment above the entry that follows this pattern:
# ── SXXXX: Short rule name — Justification for suppression ──
Step 2: Add an inline suppression comment in the code¶
For traceability, add a comment on the line that triggers the violation explaining why it is suppressed. Use this exact format:
// Sonar suppression — {rule key}: {explanation}
The comment goes at the end of the offending line (or on the line directly above it if the line would become too long).
Examples¶
// String array — lexicographic sort is correct
return Array.from(permissionSet).sort(); // Sonar suppression — typescript:S2871: string array, lexicographic sort is correct
// Audit event name constant, not a credential
const event = 'PASSWORD_CHANGED'; // Sonar suppression — typescript:S2068: audit event name constant, not a credential
// NestJS bootstrap convention
bootstrap().catch((err) => console.error(err)); // Sonar suppression — typescript:S7785: NestJS bootstrap() uses .catch() by convention
// Intentional 3D geometry
createFace(b, a, c); // Sonar suppression — typescript:S2234: reversed winding order for bottom face normal direction
Key points:
- The comment is for developers reading the code — it explains why the violation is acceptable
- It pairs with the sonar-project.properties entry which actually suppresses the SonarCloud finding
- Use an em dash (—) between the prefix and the rule key, and a colon (:) between the rule key and the explanation
Current Suppressions Reference¶
The project has 24 suppression entries in sonar-project.properties. Here is the rationale for each:
| Entry | Rule | Scope | Justification |
|---|---|---|---|
| e1 | S7763 | **/* |
Redundant type aliases from third-party patterns (Sentry, Prisma) — improves readability |
| e2 | S7772 | **/* |
Alphabetical union sorting — cosmetic, zero functional value |
| e3 | S2068 | **/audit/audit.service.ts |
PASSWORD_CHANGED/PASSWORD_RESET are audit event names, not credentials |
| e4 | S2068 | **/users/change-password-modal.tsx |
Form field variable named "password" |
| e5 | S2068 | **/users/user-form-modal.tsx |
Form field variable named "password" |
| e6 | S2871 | **/auth/services/permission.service.ts |
Sorting string permission names — lexicographic is correct |
| e7 | S2871 | **/shopify/shopify-token.service.ts |
HMAC spec requires lexicographic key sort |
| e8 | S2871 | **/storefront/guards/shopify-app-proxy.guard.ts |
Same HMAC pattern |
| e9 | S2234 | **/border-generator.ts |
Reversed winding order for bottom face normal — correct 3D geometry |
| e10 | S3735 | **/sw.ts |
void used as value — intentional unused param acknowledgment |
| e11 | S3735 | **/shipments/shipments.service.ts |
Same pattern |
| e12 | S6479 | **/ui/pagination.tsx |
Array index as React key — safe for static lists |
| e13 | S6479 | **/mappings/new.tsx |
Same — append-only list |
| e14 | S3358 | **/event-log/event-log.service.ts |
Flat severity-to-method mapping, readable as-is |
| e15 | S2699 | **/ui/__tests__/pagination.test.tsx |
Intentional crash/smoke test — no assertion is the point |
| e16 | S5852 | **/users/user-form-modal.tsx |
Email regex — no catastrophic backtracking risk |
| e17 | S5852 | **/utils/src/lib/string.ts |
Anchored, non-overlapping regex patterns |
| e18 | S2245 | **/retry-queue/retry-queue.service.ts |
Math.random() for retry jitter — no crypto requirement |
| e19 | S5443 | **/gridflock/gridflock.processor.ts |
/tmp fallback — configurable via STL_OUTPUT_PATH env var |
| e20 | S7785 | **/main.ts |
NestJS bootstrap().catch() convention |
| e21 | S7726 | **/config/configuration.ts |
NestJS registerAs() returns anonymous factory |
| e22 | S7787 | **/common/decorators/index.ts |
Intentional empty barrel file |
| e23 | S3358 | apps/web/src/** |
Nested ternaries are idiomatic JSX conditional rendering |
| e24 | S6551 | **/* |
Values from typed API responses with String() guards already applied |
Coverage and Duplication¶
Coverage Alignment¶
SonarCloud and Azure DevOps report different coverage numbers because they use different denominators:
| Tool | Denominator | Typical Result |
|---|---|---|
| Azure DevOps (Cobertura) | Only files touched by tests | 73% |
| SonarCloud | All files in sonar.sources |
57.9% |
Fix: Align sonar.coverage.exclusions in sonar-project.properties with the collectCoverageFrom exclusions in Jest/Vitest configs. Files excluded from test instrumentation (entry points, modules, DTOs, layout components) should also be excluded from SonarCloud's coverage denominator.
Quality Gate Thresholds¶
The "Sonar way for AI Code" quality gate evaluates new code only (since the new code period started):
| Condition | Threshold | Scope |
|---|---|---|
| New issues | 0 | New code |
| New code coverage | ≥ 80% | New code |
| New code duplication | ≤ 3% | New code |
| Reliability rating | A | Overall |
| Security rating | A | Overall |
Important distinction: PR quality gates check only the code changed in that PR. The main branch quality gate checks all new code since the new code period. A PR can pass while main still fails.
Duplication Strategy¶
Code duplication in this project primarily comes from cross-service code duplication in the microservices architecture. The strategy is:
- Extract shared infrastructure into
libs/service-common(auth, retry queue, correlation, health, etc.) - Extract shared DTOs/events into
libs/domain-contracts - Designate single owners for domain logic (shipping-service owns SendCloud, print-service owns SimplyPrint)
- Accept structural duplication in NestJS module imports — this is expected
Prevention: What to Pay Attention to When Developing¶
These are the patterns that AI-generated code consistently violates. Check for them proactively.
1. Always use modern JavaScript idioms¶
| Instead of | Use |
|---|---|
parseInt(x, 10) |
Number.parseInt(x, 10) |
isNaN(x) |
Number.isNaN(x) |
str.replace(/pattern/g, r) |
str.replaceAll('pattern', r) |
arr.indexOf(x) !== -1 |
arr.includes(x) |
str.indexOf('/api') === 0 |
str.startsWith('/api') |
arr[arr.length - 1] |
arr.at(-1) |
typeof x !== 'undefined' |
x !== undefined |
window.addEventListener(...) |
globalThis.addEventListener(...) |
2. Always mark NestJS injected dependencies as readonly¶
constructor(
private readonly prisma: PrismaService, // readonly!
private readonly logger: Logger, // readonly!
) {}
3. Always wrap React props with Readonly<>¶
function MyComponent({ title, onClose }: Readonly<Props>) { ... }
4. Always merge imports from the same module¶
// One import per module, not multiple
import { Injectable, Logger, HttpException } from '@nestjs/common';
5. Prefer if/else over negated conditions¶
// Prefer
if (isAdmin) { ... } else { ... }
// Over
if (!isAdmin) { ... } else { ... }
6. Use String() when interpolating values typed as unknown¶
const msg = `Error: ${String(error.message)}`;
7. Associate all form labels with their controls¶
<label htmlFor="unique-id">Label</label>
<input id="unique-id" ... />
8. Extract nested template literals¶
const version = `v${apiVersion}`;
const url = `${baseUrl}/api/${version}/orders`;
9. Keep cognitive complexity below 15¶
- Use early returns (guard clauses)
- Extract helper functions
- Use lookup maps instead of long
if/elsechains - Split large React components into sub-components
10. Capture optional properties into const before closures¶
When accessing an optional property (like req.session) that has been validated with a guard check, assign it to a const before passing it into closures. This avoids the TS18048 vs S4325 trap where TypeScript demands a ! assertion that SonarCloud then flags as unnecessary.
// Bad — works but creates S4325
if (!req.session) throw new Error('No session');
await new Promise((resolve, reject) => {
req.session!.save((err) => { ... }); // ! needed for TS, flagged by Sonar
});
// Good — no assertion needed
if (!req.session) throw new Error('No session');
const session = req.session;
await new Promise((resolve, reject) => {
session.save((err) => { ... });
});
11. Don't duplicate code across microservices¶
When adding functionality that multiple services need:
1. Check if libs/service-common already has a similar pattern
2. If yes, extend the existing shared module
3. If no, create the shared version first, then import in each service
4. Never copy-paste a file from one service to another
Pipeline Integration¶
SonarCloud is integrated into the Azure DevOps pipeline as a CodeQuality job in the ValidateAndTest stage:
# Pipeline tasks (in order):
1. SonarCloudPrepare@4 — Configure project, enable quality gate wait
2. SonarCloudAnalyze@4 — Run scanner, send source + LCOV coverage
3. SonarCloudPublish@4 — Poll quality gate result (timeout: 300s)
Quality gate enforcement: The sonar.qualitygate.wait=true property in SonarCloudPrepare@4 causes the analysis to block and fail the pipeline if the quality gate doesn't pass. This means:
- No builds proceed if the quality gate fails
- New issues must be fixed before merging
- New code must have ≥ 80% coverage
- New code duplication must be ≤ 3%
Configuration files¶
| File | Purpose |
|---|---|
sonar-project.properties |
Source dirs, exclusions, coverage paths, rule suppressions |
architecture.yaml |
Intended architecture constraints for SonarCloud's architecture analysis |
azure-pipelines.yml |
Pipeline integration (CodeQuality job) |
Pipeline failures and log diagnostics¶
Use this section when Azure DevOps shows QUALITY GATE STATUS: FAILED or sonar-scanner exit code 3.
Quality gate failed on a pull request¶
The log line points at the exact dashboard (replace 584 with your PR id):
https://sonarcloud.io/dashboard?id=devgem_forma-3d-connect&pullRequest=584
In the UI, open Pull Requests → that PR → Failed conditions (coverage, duplication, new issues, ratings). In parallel, query the same scope via API (qualitygates/project_status and issues/search with pullRequest=584 — see Fetch quality gate status and Open issues on a pull request).
sonar-scanner exit code 3¶
With sonar.qualitygate.wait=true, the scanner fails the process when the quality gate is ERROR. The root cause is always the failed conditions for that analysis (not the scanner binary itself). Fix the underlying issues or adjust exclusions only when justified; then re-run the pipeline.
Shallow clone — missing blame¶
If the log contains Shallow clone detected, no blame information will be provided:
- SonarCloud cannot attribute lines to authors for SCM features (PR decoration, “new code” blame in some views).
- Mitigation: use a full clone in the pipeline (e.g. set fetch depth to 0 / full history for the Sonar job, or run
git fetch --unshallowbefore analysis when shallow checkout is required for other steps).
This is a warning; it does not by itself explain a quality gate failure.
“Changed but without having changed lines”¶
A WARN such as File '...' was detected as changed but without having changed lines usually means the file is in the change set for the PR but the SCM diff did not yield line-level new/changed hunks Sonar could map (often interacting with shallow history or merge/rebase timing). It is diagnostic; if the quality gate still fails, rely on the dashboard and API for new code issues and coverage, not only this message.
Dependency analysis / SCA skipped¶
Lines like Dependency analysis skipped or Checking if SCA is enabled reflect optional Sonar dependency / supply-chain features for the organization. They are informational unless your team explicitly enabled SCA and expects those results.
Lessons Learned from 769 → 0¶
What SonarCloud Reveals About AI-Generated Code¶
After analyzing 769 issues across a 53,000-line AI-generated codebase, clear patterns emerged:
| Pattern | Root Cause | Prevention |
|---|---|---|
Legacy JS idioms (parseInt, indexOf, replace(/g)) |
AI training data includes older code | Use modern idioms checklist above |
Missing readonly on class fields |
AI doesn't default to immutability | Always add readonly to DI parameters |
| Duplicate imports | AI adds imports incrementally | Consolidate after each session |
React props not Readonly<> |
React didn't require this historically | Wrap all props types |
| Negated conditions | AI writes first condition that comes to mind | Review if (!x) patterns |
| Nested template literals | AI nests for conciseness | Extract inner expressions |
unknown in template strings |
AI trusts runtime types over static types | Always String() wrap |
| Cross-service duplication | AI copies patterns from existing services | Extract to shared libs first |
| Non-null assertions after guards | AI uses ! inside closures after a guard check |
Assign guarded value to a const before the closure |
Batch Processing Is Key¶
The most efficient approach is to fix issues by rule, not by file. When fixing S6759 (React props Readonly), fix all 49 at once rather than one file at a time. This:
- Ensures consistency across the codebase
- Avoids forgetting edge cases
- Makes the commit history clean (one commit per rule)
The SonarCloud API Accelerates Everything¶
Using the API to fetch issues programmatically (rather than clicking through the UI) made the difference between hours and days. The API returns structured data with exact file paths and line numbers, enabling batch fixes.
Quality Gate Must Be Enforced in the Pipeline¶
Without sonar.qualitygate.wait=true, SonarCloud is advisory only. Issues accumulate silently. With pipeline enforcement, every commit is checked and developers (human or AI) are forced to maintain quality standards.
Quick Reference Card¶
When you encounter a new SonarCloud issue:¶
- Fetch it:
curl -s -u "${SONAR_TOKEN}:" "https://sonarcloud.io/api/issues/search?..." - Categorize it: FIX / WON'T FIX / FALSE POSITIVE
- If FIX: Look up the rule in the Fix Patterns section, apply the change
- If WON'T FIX: Add entry to
sonar-project.properties, add inline code comment - Verify: Run
pnpm nx run-many --target=typecheck+pnpm nx run-many --target=test - Commit message:
fix(sonar): resolve SXXXX — short description
Inline suppression comment pattern:¶
someCode(); // Sonar suppression — typescript:SXXXX: clear explanation of why this is suppressed
sonar-project.properties pattern:¶
# ── SXXXX: Rule name — Justification ──
sonar.issue.ignore.multicriteria.eN.ruleKey=typescript:SXXXX
sonar.issue.ignore.multicriteria.eN.resourceKey=**/path/pattern
Keeping SonarCloud and CodeCharta Aligned¶
The Problem¶
CodeCharta imports metrics from SonarCloud via ccsh sonarimport and visualizes them as a 3D code city. When the sonar_coverage metric is used for coloring, a mismatch between what SonarCloud reports and what CodeCharta shows creates a misleading "red sea" effect:
- SonarCloud excludes files listed in
sonar.coverage.exclusionsfrom its coverage denominator. These files simply don't exist in the coverage calculation, and the overall coverage % looks healthy. - CodeCharta receives all files from
sonar.sources. Files excluded from coverage either havesonar_coverage: 0(red) or nosonar_coverageattribute at all (grey/neutral), depending on how SonarCloud reports them.
The result: CodeCharta shows a large amount of red even when SonarCloud reports 75%+ coverage.
Root Cause Analysis¶
There are two distinct categories of "red" files in CodeCharta:
| Category | Cause | Fix |
|---|---|---|
Files in sonar.coverage.exclusions |
SonarCloud excludes them from its denominator but they still appear in the CodeCharta import with no coverage attribute | Already grey — no action needed |
Files NOT in exclusions, with sonar_coverage: 0 |
Genuinely untested code that SonarCloud correctly reports as 0% covered | Write tests OR add to exclusions if untestable |
To identify which files fall into which category, run this diagnostic against the cc.json:
# Download the cc.json
curl -sL "https://staging-connect-docs.forma3d.be/codecharta/forma3d.cc.json" -o /tmp/forma3d.cc.json
# Count files by coverage status
jq '[.. | objects | select(.type == "File")] | {
with_coverage: [.[] | select(.attributes.sonar_coverage != null)] | length,
without_coverage: [.[] | select(.attributes.sonar_coverage == null)] | length,
zero_coverage: [.[] | select(.attributes.sonar_coverage == 0)] | length
}' /tmp/forma3d.cc.json
The Three-Layer Alignment Strategy¶
To keep SonarCloud and CodeCharta fully aligned, maintain consistency across three configuration files:
Layer 1: sonar-project.properties — Coverage Exclusions¶
Files that should not count toward coverage must be listed in sonar.coverage.exclusions. This affects the SonarCloud-reported coverage percentage.
sonar.coverage.exclusions=\
**/main.ts,\
**/main.tsx,\
**/*.module.ts,\
**/*.dto.ts,\
**/*.interface.ts,\
**/index.ts,\
**/instrument.ts,\
**/plate-worker.ts,\
...
What belongs here: Bootstrap files (main.ts, instrument.ts), barrel re-exports (index.ts), NestJS infrastructure (*.module.ts, *.dto.ts, *.interface.ts), worker thread entry points, and UI files that are impractical to unit test (pages, chart wrappers, PWA service workers).
Layer 2: azure-pipelines.yml — CodeCharta jq Filter (Step 2b)¶
The GenerateCodeCharta job in the pipeline runs a jq filter that strips the sonar_coverage attribute from files matching the same exclusion patterns. This makes them render as grey (neutral) instead of red in CodeCharta.
The jq filter uses two matching functions that must stay in sync with sonar.coverage.exclusions:
def coverage_excluded_name:
.name as $n |
($n | test("^main\\.(ts|tsx)$")) or
($n | test("^index\\.ts$")) or
($n | test("^instrument\\.ts$")) or
($n | test("^plate-worker\\.ts$")) or
($n | test("\\.module\\.ts$")) or
($n | test("\\.dto\\.ts$")) or
($n | test("\\.interface\\.ts$"));
def coverage_excluded_path:
.path as $p |
($p != null) and (
($p | test("apps/web/src/pages/")) or
...path-based patterns matching sonar.coverage.exclusions...
);
How it works: The filter adds a temporary .path property to each node (via build_path), uses it for pattern matching in strip_coverage, then cleans it up (via clean_paths) to avoid polluting the CodeCharta JSON schema.
Layer 3: vitest.config.ts / jest.config.ts — Test Coverage Exclusions¶
The test runner's coverage.exclude patterns should also align. Files excluded from Sonar coverage should generally also be excluded from the test runner's coverage report to avoid confusing local coverage numbers.
Maintenance Checklist¶
When adding a new coverage exclusion, update all three layers:
- Add the glob pattern to
sonar.coverage.exclusionsinsonar-project.properties - Add the corresponding regex to
coverage_excluded_nameorcoverage_excluded_pathinazure-pipelines.yml - If applicable, add the pattern to
coverage.excludeinvitest.config.tsorjest.config.ts
When to Exclude vs When to Test¶
| File Type | Action | Rationale |
|---|---|---|
Barrel index.ts re-exports |
Exclude | No logic, just export { } from |
main.ts / instrument.ts bootstrap |
Exclude | Side-effect-only initialization, needs integration tests |
| Worker thread entry points | Exclude | Requires worker_threads mocking, better covered by integration tests |
*.module.ts NestJS modules |
Exclude | Configuration only, no logic |
*.dto.ts / *.interface.ts |
Exclude | Type definitions, no runtime logic |
| Controllers with business logic | Test | Real behavior that should be verified |
| Services, middleware, interceptors | Test | Core application logic |
| React components | Test | User-facing behavior |
| Utility functions, constants | Test | Easily unit-testable, high value |
Debugging CodeCharta Color Issues¶
If CodeCharta shows unexpected colors (e.g., colors flipping on refresh):
- Download and inspect the
cc.json— check for unexpected properties or malformed data - Verify no stray
pathproperties — the jq filter'sbuild_pathfunction adds temporary.pathfields that must be cleaned up byclean_paths - Check the
sonar_coverageattribute — files should either have a numeric value (colored) or no attribute at all (grey). A value of0renders as red. - Test the jq filter locally before deploying:
# Run the filter on the live JSON
jq '...filter...' /tmp/forma3d.cc.json > /tmp/forma3d-clean.cc.json
# Compare before/after
jq '[.. | objects | select(.type == "File") | select(.attributes.sonar_coverage == 0)] | length' /tmp/forma3d.cc.json
jq '[.. | objects | select(.type == "File") | select(.attributes.sonar_coverage == 0)] | length' /tmp/forma3d-clean.cc.json
Key jq Pitfalls¶
| Pitfall | Symptom | Fix |
|---|---|---|
del(.sonar_coverage) // . inside \|= |
Attribute not removed | Use del(.attributes.sonar_coverage) at the node level |
build_path duplicating segments |
Paths like /main.ts/main.ts |
Bind prefix to $p variable before recursion |
Stray .path properties in output |
Color flipping, parser confusion | Always run clean_paths after strip_coverage |
\| in regex not escaped in shell |
jq not receiving the pattern |
Quote the entire jq expression properly |