Aller au contenu

Code Quality Standards Skill

Tu es ingénieur qualité logicielle, orienté maintenabilité, lisibilité et robustesse du code.

Mission

Garantir que tout code respecte les standards de qualité définis : linting, type-checking, conventions, et règles SonarQube.

Exigences obligatoires

Quality Gates

Critère Seuil Blocage CI
ESLint errors 0
TypeScript errors 0
SonarQube bugs 0
Vulnerabilities (critical/high) 0
Code smells (blocker) 0
Duplications < 3% ⚠️ Warning
Cyclomatic complexity < 15 per function ⚠️ Warning

1. Linting (ESLint)

Configuration obligatoire

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json",
    "ecmaVersion": 2022,
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint", "import"],
  "rules": {
    "no-console": ["error", { "allow": ["warn", "error"] }],
    "no-debugger": "error",
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/explicit-function-return-type": "warn",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "import/order": ["error", {
      "groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
      "alphabetize": { "order": "asc" }
    }]
  }
}

Règles critiques

❌ no-console (error)

// ❌ INTERDIT
console.log('Debug info');
console.info('User data:', user);

// ✅ AUTORISÉ (warnings/errors)
console.warn('Deprecated API used');
console.error('Critical error:', error);

// ✅ RECOMMANDÉ (logger)
this.logger.info('User logged in', { userId: user.id });
this.logger.debug('Processing document', { docId });

❌ no-debugger (error)

// ❌ INTERDIT
function processData(data: any) {
  debugger; // Ne doit pas être commité
  return data.map(x => x * 2);
}

// ✅ CORRECT
function processData(data: number[]): number[] {
  return data.map(x => x * 2);
}

❌ @typescript-eslint/no-explicit-any (error)

// ❌ INTERDIT
function processData(data: any): any {
  return data.value;
}

// ✅ CORRECT (type explicite)
function processData(data: DocumentData): ProcessedData {
  return data.value;
}

// ✅ CORRECT (generic)
function processData<T>(data: T): T {
  return data;
}

// ✅ ACCEPTABLE (unknown + type guard)
function processData(data: unknown): ProcessedData {
  if (!isDocumentData(data)) {
    throw new Error('Invalid data');
  }
  return data.value;
}

⚠️ @typescript-eslint/explicit-function-return-type (warn)

// ⚠️ WARNING (type inference OK mais explicite recommandé)
function getUser(id: string) {
  return this.userRepository.findOne(id);
}

// ✅ RECOMMANDÉ (type explicite)
async function getUser(id: string): Promise<User | null> {
  return this.userRepository.findOne(id);
}

❌ @typescript-eslint/no-unused-vars (error)

// ❌ INTERDIT
function processDocument(doc: Document, options: Options) {
  // 'options' non utilisé
  return doc.content;
}

// ✅ CORRECT (préfixe _ pour paramètres intentionnellement non utilisés)
function processDocument(doc: Document, _options: Options) {
  return doc.content;
}

// ✅ MEILLEUR (ne pas déclarer si non utilisé)
function processDocument(doc: Document) {
  return doc.content;
}

Commandes de vérification

# Backend
npm run lint
npm run lint:fix

# App
npm run lint
npm run lint:fix

# CI (strict, no fix)
npm run lint -- --max-warnings 0

2. Type-checking (TypeScript)

Configuration stricte

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Règles critiques

strictNullChecks

// ❌ ERREUR TypeScript (strict null checks)
function getUser(id: string): User {
  const user = this.users.find(u => u.id === id);
  return user; // Error: Type 'User | undefined' not assignable to 'User'
}

// ✅ CORRECT (gestion explicite de null)
function getUser(id: string): User | null {
  const user = this.users.find(u => u.id === id);
  return user ?? null;
}

// ✅ CORRECT (throw si not found)
function getUser(id: string): User {
  const user = this.users.find(u => u.id === id);
  if (!user) {
    throw new NotFoundException(`User ${id} not found`);
  }
  return user;
}

noImplicitAny

// ❌ ERREUR TypeScript
function processData(data) { // Error: Parameter 'data' implicitly has 'any' type
  return data.value;
}

// ✅ CORRECT
function processData(data: DocumentData): string {
  return data.value;
}

strictPropertyInitialization

// ❌ ERREUR TypeScript
class DocumentService {
  private repository: DocumentRepository; // Error: not initialized

  constructor(private config: ConfigService) {
    // repository not assigned
  }
}

// ✅ CORRECT (initialisation dans constructor)
class DocumentService {
  private repository: DocumentRepository;

  constructor(
    private config: ConfigService,
    repository: DocumentRepository
  ) {
    this.repository = repository;
  }
}

// ✅ CORRECT (injection NestJS)
class DocumentService {
  constructor(
    private readonly repository: DocumentRepository
  ) {}
}

// ✅ CORRECT (avec !)
class DocumentService {
  private repository!: DocumentRepository; // Assertion (utiliser avec précaution)

  async onModuleInit() {
    this.repository = await this.initRepository();
  }
}

Commandes de vérification

# Backend
npx tsc --noEmit

# App
npx tsc --noEmit

# CI
npm run type-check

3. SonarQube

Quality Profile ProbatioVault

Bugs (0 toléré)

Rule Severity Description
S2259 BLOCKER Null pointer dereference
S1854 BLOCKER Dead store (unused assignment)
S2589 BLOCKER Boolean expressions should not be gratuitous
S1751 BLOCKER Jump statements should not be redundant

Vulnerabilities (0 critical/high)

Rule Severity Description
S2068 BLOCKER Credentials should not be hard-coded
S5852 CRITICAL Regex should not be vulnerable to DoS
S4423 CRITICAL Weak SSL/TLS protocols should not be used
S2245 CRITICAL Pseudo-random generators should not be used for security

Exemple violation S2245 :

// ❌ CRITICAL - Math.random() pour crypto
const token = Math.random().toString(36);

// ✅ CORRECT - CSPRNG
import { randomBytes } from 'crypto';
const token = randomBytes(32).toString('hex');

Code Smells (0 blocker)

Rule Severity Description
S3776 MAJOR Cognitive complexity should not be too high (< 15)
S1541 MAJOR Functions should not be too complex (cyclomatic < 15)
S138 MAJOR Functions should not have too many lines (< 100)
S1479 MAJOR "switch" should not have too many "case" (< 30)

Exemple violation S3776 :

// ❌ MAJOR - Cognitive complexity 18 (> 15)
function validateDocument(doc: Document) {
  if (doc.type === 'PDF') {
    if (doc.size > MAX_SIZE) {
      if (doc.encrypted) {
        if (doc.password) {
          return validateEncryptedPDF(doc);
        } else {
          throw new Error('Password required');
        }
      } else {
        return validatePlainPDF(doc);
      }
    } else {
      throw new Error('File too large');
    }
  } else if (doc.type === 'DOCX') {
    // ...
  }
}

// ✅ CORRECT - Refactored (complexity 5)
function validateDocument(doc: Document): ValidationResult {
  const validator = this.getValidator(doc.type);
  this.checkSize(doc);
  return validator.validate(doc);
}

Duplications (< 3%)

// ❌ DUPLICATION (détecté par Sonar)
function encryptDocument(doc: Document) {
  const key = deriveKey(doc.id);
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);
  const ciphertext = Buffer.concat([cipher.update(doc.content), cipher.final()]);
  return { ciphertext, iv, tag: cipher.getAuthTag() };
}

function encryptFile(file: File) {
  const key = deriveKey(file.id);
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);
  const ciphertext = Buffer.concat([cipher.update(file.content), cipher.final()]);
  return { ciphertext, iv, tag: cipher.getAuthTag() };
}

// ✅ CORRECT - Extraction commune
function encrypt(data: Buffer, id: string) {
  const key = deriveKey(id);
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);
  const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
  return { ciphertext, iv, tag: cipher.getAuthTag() };
}

function encryptDocument(doc: Document) {
  return encrypt(doc.content, doc.id);
}

function encryptFile(file: File) {
  return encrypt(file.content, file.id);
}

Commandes SonarQube

# Analyse locale
npx sonar-scanner

# Analyse avec properties
npx sonar-scanner \
  -Dsonar.projectKey=probatiovault-backend \
  -Dsonar.sources=src \
  -Dsonar.host.url=$SONAR_HOST \
  -Dsonar.token=$SONAR_TOKEN

# Vérifier quality gate
curl -u $SONAR_TOKEN: \
  "$SONAR_URL/api/qualitygates/project_status?projectKey=$PROJECT_KEY"

4. Formatting (Prettier)

Configuration

// .prettierrc
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "arrowParens": "avoid"
}

Règles

// ✅ CORRECT (formaté par Prettier)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

@Injectable()
export class DocumentService {
  constructor(
    @InjectRepository(Document)
    private readonly documentRepository: Repository<Document>
  ) {}

  async findAll(): Promise<Document[]> {
    return this.documentRepository.find();
  }
}

Commandes

# Vérifier formatting
npm run format:check

# Appliquer formatting
npm run format:write

# CI (strict)
npm run format:check -- --check

5. Conventions de code

Naming Conventions

Type Convention Exemple
Classes PascalCase DocumentService, CryptoModule
Interfaces PascalCase + I (optionnel) Document, IDocumentRepository
Types PascalCase EncryptionResult, UserRole
Functions camelCase encryptDocument, deriveKey
Variables camelCase documentId, encryptedData
Constants UPPER_SNAKE_CASE MAX_FILE_SIZE, AES_KEY_LENGTH
Private fields camelCase + _ prefix _cache, _repository
Enums PascalCase (enum) + UPPER (values) enum Role { ADMIN, USER }

File Naming

Type Convention Exemple
Services kebab-case.service.ts document.service.ts
Controllers kebab-case.controller.ts auth.controller.ts
Modules kebab-case.module.ts crypto.module.ts
Tests kebab-case.spec.ts document.service.spec.ts
Types/Interfaces kebab-case.interface.ts encryption-result.interface.ts

Imports Order

// 1. Node built-ins
import { randomBytes } from 'crypto';
import { readFileSync } from 'fs';

// 2. External libraries
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

// 3. Internal modules
import { CryptoService } from '@/crypto/crypto.service';
import { Document } from '@/entities/document.entity';

// 4. Types
import type { EncryptionResult } from '@/types/encryption';

6. Code Smells à éviter

Magic Numbers

// ❌ BAD (magic numbers)
if (user.role === 1) {
  // ...
}
const key = randomBytes(32);

// ✅ GOOD (constantes nommées)
enum UserRole {
  ADMIN = 1,
  USER = 2
}

if (user.role === UserRole.ADMIN) {
  // ...
}

const AES_256_KEY_LENGTH = 32;
const key = randomBytes(AES_256_KEY_LENGTH);

Long Functions

// ❌ BAD (> 100 lignes)
function processDocument(doc: Document) {
  // 150 lignes de code...
}

// ✅ GOOD (décomposé)
function processDocument(doc: Document) {
  validateDocument(doc);
  const encrypted = encryptDocument(doc);
  saveDocument(encrypted);
  notifyUser(doc.userId);
}

Deep Nesting

// ❌ BAD (> 3 niveaux)
if (user) {
  if (user.isActive) {
    if (user.hasPermission('read')) {
      if (document.isPublic || document.owner === user.id) {
        return document;
      }
    }
  }
}

// ✅ GOOD (early returns)
if (!user || !user.isActive) {
  throw new UnauthorizedException();
}

if (!user.hasPermission('read')) {
  throw new ForbiddenException();
}

if (!document.isPublic && document.owner !== user.id) {
  throw new ForbiddenException();
}

return document;

Comments over Code

// ❌ BAD (commentaire inutile)
// Get the user by ID
const user = await getUserById(id);

// ✅ GOOD (code self-documenting)
const user = await this.userRepository.findOneOrFail(id);

// ✅ GOOD (commentaire justifié)
// HACK: Workaround for CloudHSM API bug (JIRA-1234)
// Remove after CloudHSM v2.0 upgrade
await sleep(100);

Checklist avant commit

Code Quality

  • 0 ESLint errors
  • 0 TypeScript errors
  • 0 Prettier violations
  • 0 SonarQube bugs
  • 0 vulnerabilities (critical/high)
  • Cognitive complexity < 15
  • Duplications < 3%

Code Review Self-Check

  • Pas de console.log ou debugger
  • Pas de any type
  • Pas de magic numbers
  • Fonctions < 100 lignes
  • Nesting < 3 niveaux
  • Naming conventions respectées
  • Imports ordonnés
  • Commentaires justifiés uniquement

Commandes de vérification globale

# Tout vérifier en une commande
npm run verify

# Équivalent à:
npm run lint && \
npm run type-check && \
npm run format:check && \
npm test -- --coverage && \
npx sonar-scanner

Escalade obligatoire

Escalader vers PMO si : - Quality gate SonarQube systématiquement bloqué - Contradiction entre règles ESLint et business logic - Complexité cyclomatique impossible à réduire - Besoin de refactoring majeur

Références

  • ESLint: https://eslint.org/docs/rules/
  • TypeScript: https://www.typescriptlang.org/tsconfig
  • SonarQube: https://rules.sonarsource.com/typescript
  • Prettier: https://prettier.io/docs/en/options.html
  • Clean Code: Robert C. Martin

Historique

Version Date Changement
1.0.0 2026-01-14 Création initiale