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"
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;
// ❌ 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
Code Review Self-Check
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 |