Skip to content

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

  1. Workflow Overview
  2. Using the SonarCloud Web API
  3. Issue Resolution Decision Tree
  4. Fix Patterns by Rule
  5. Won't Fix / Suppression Protocol
  6. Coverage and Duplication
  7. Prevention: What to Pay Attention to When Developing
  8. Pipeline Integration
  9. Pipeline failures and log diagnostics
  10. Lessons Learned from 769 → 0
  11. 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

  1. Bugs (reliability) — always fix, these indicate real logic errors
  2. Vulnerabilities (security) — always fix or suppress with documented reason
  3. Security Hotspots — review and mark as safe, or fix
  4. Code Smells, BLOCKER/CRITICAL — fix first (cognitive complexity, etc.)
  5. Code Smells, MAJOR — fix in batches by rule
  6. Code Smells, MINOR — fix mechanically or suppress project-wide
  7. 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 as SONAR_TOKEN in your shell or CI.
  • Setting the token locally: run export SONAR_TOKEN="<your-token>" in your shell (or add it to a .env file 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:

  1. The code is intentionally written that way (e.g., reversed winding order in 3D geometry)
  2. The framework requires it (e.g., NestJS bootstrap().catch() pattern)
  3. The rule is cosmetic with zero functional value (e.g., alphabetical union sorting)
  4. Fixing would reduce readability (e.g., type aliases from third-party patterns)
  5. The flagged pattern is a standard practice (e.g., void expr for unused params in TypeScript)

A "won't fix" is NOT appropriate when:

  1. The code works but could be objectively improved (fix it)
  2. The fix is trivial (just do it)
  3. 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:

  1. req.session is typed as optional (session?: Session & Partial<SessionData>)
  2. Accessing it without a check causes TS18048 ('req.session' is possibly 'undefined'), failing the build
  3. Adding a guard (if (!req.session) throw) narrows the type in the immediate scope
  4. 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 for req.session!.save()
  5. 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:

  1. Extract shared infrastructure into libs/service-common (auth, retry queue, correlation, health, etc.)
  2. Extract shared DTOs/events into libs/domain-contracts
  3. Designate single owners for domain logic (shipping-service owns SendCloud, print-service owns SimplyPrint)
  4. 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/else chains
  • 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 --unshallow before 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:

  1. Fetch it: curl -s -u "${SONAR_TOKEN}:" "https://sonarcloud.io/api/issues/search?..."
  2. Categorize it: FIX / WON'T FIX / FALSE POSITIVE
  3. If FIX: Look up the rule in the Fix Patterns section, apply the change
  4. If WON'T FIX: Add entry to sonar-project.properties, add inline code comment
  5. Verify: Run pnpm nx run-many --target=typecheck + pnpm nx run-many --target=test
  6. 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.exclusions from 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 have sonar_coverage: 0 (red) or no sonar_coverage attribute 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:

  1. Add the glob pattern to sonar.coverage.exclusions in sonar-project.properties
  2. Add the corresponding regex to coverage_excluded_name or coverage_excluded_path in azure-pipelines.yml
  3. If applicable, add the pattern to coverage.exclude in vitest.config.ts or jest.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):

  1. Download and inspect the cc.json — check for unexpected properties or malformed data
  2. Verify no stray path properties — the jq filter's build_path function adds temporary .path fields that must be cleaned up by clean_paths
  3. Check the sonar_coverage attribute — files should either have a numeric value (colored) or no attribute at all (grey). A value of 0 renders as red.
  4. 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