Violation Catalog
Every violation type detected by Technical Debt Radar with rule IDs, severity levels, code examples, and fix patterns.
Violation Catalog
Complete reference of every violation Radar detects. Each entry includes the rule ID, category, default severity, description, example of the violation, and example of the fix.
Architecture Violations
Architecture violations always block merge. Points: 5 per violation (10 for circular dependencies).
layer-boundary-violation
A file imports from a layer it is not allowed to access according to the architecture rules.
Severity: critical | Points: 5 | Gate: block
// VIOLATION: Domain layer importing from infrastructure
// File: src/orders/domain/order.service.ts
import { PrismaClient } from '@prisma/client';
export class OrderService {
constructor(private prisma: PrismaClient) {}
}
// FIX: Use a repository interface (dependency inversion)
// File: src/orders/domain/order.service.ts
import { OrderRepository } from './order.repository.interface';
export class OrderService {
constructor(private orderRepo: OrderRepository) {}
}
circular-dependency
Two or more modules form a circular import chain, detected using Tarjan's algorithm for strongly connected components.
Severity: critical | Points: 10 | Gate: block
// VIOLATION: Circular dependency
// File: src/orders/orders.service.ts
import { PaymentsService } from '../payments/payments.service';
// File: src/payments/payments.service.ts
import { OrdersService } from '../orders/orders.service';
// orders → payments → orders (cycle)
// FIX: Extract shared logic into a separate module or use events
// File: src/orders/orders.service.ts
import { EventEmitter2 } from '@nestjs/event-emitter';
export class OrdersService {
constructor(private eventEmitter: EventEmitter2) {}
async completeOrder(orderId: string) {
// Instead of calling PaymentsService directly
this.eventEmitter.emit('order.completed', { orderId });
}
}
module-boundary-violation
A file imports internal implementation details from another module, bypassing the module's public API.
Severity: critical | Points: 5 | Gate: block
// VIOLATION: Reaching into another module's internals
// File: src/orders/orders.service.ts
import { calculateDiscount } from '../billing/internal/discount-calculator';
// FIX: Import from the module's public API
// File: src/orders/orders.service.ts
import { BillingService } from '../billing/billing.service';
export class OrdersService {
constructor(private billing: BillingService) {}
}
forbidden-framework-in-layer
A domain or business logic layer imports framework-specific code (ORM entities, HTTP decorators, etc.).
Severity: critical | Points: 5 | Gate: block
// VIOLATION: Framework import in domain layer
// File: src/orders/domain/order.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Order {
@PrimaryGeneratedColumn()
id: number;
}
// FIX: Keep domain models framework-free
// File: src/orders/domain/order.ts
export class Order {
constructor(
public readonly id: string,
public readonly customerId: string,
public readonly total: number,
) {}
}
Runtime Risk Violations
Runtime risk violations are scope-aware --- they only flag patterns inside HTTP request handlers, guards, interceptors, and middleware. The same pattern in a cron job or startup script is not flagged.
sync-fs-in-handler
Synchronous file system operations (readFileSync, writeFileSync, existsSync, etc.) inside a request handler.
Severity: critical | Points: 8 | Gate: block
// VIOLATION: Sync file read in request handler
@Get('export')
async exportData() {
const template = fs.readFileSync('./template.html', 'utf-8'); // blocks event loop
return this.renderTemplate(template);
}
// FIX: Use async file operations
@Get('export')
async exportData() {
const template = await fs.promises.readFile('./template.html', 'utf-8');
return this.renderTemplate(template);
}
sync-crypto
Synchronous cryptographic operations (pbkdf2Sync, scryptSync, randomBytesSync) in a request handler.
Severity: critical | Points: 8 | Gate: block
// VIOLATION: Sync crypto in auth middleware
async validatePassword(input: string, hash: string) {
const derived = crypto.pbkdf2Sync(input, salt, 100000, 64, 'sha512');
return derived.toString('hex') === hash;
}
// FIX: Use async crypto
async validatePassword(input: string, hash: string) {
const derived = await new Promise<Buffer>((resolve, reject) => {
crypto.pbkdf2(input, salt, 100000, 64, 'sha512', (err, key) => {
err ? reject(err) : resolve(key);
});
});
return derived.toString('hex') === hash;
}
sync-compression
Synchronous compression operations (gzipSync, deflateSync, brotliCompressSync) in a request handler.
Severity: critical | Points: 8 | Gate: block
// VIOLATION
@Post('compress')
async compressData(@Body() data: Buffer) {
return zlib.gzipSync(data);
}
// FIX
@Post('compress')
async compressData(@Body() data: Buffer) {
return new Promise((resolve, reject) => {
zlib.gzip(data, (err, result) => (err ? reject(err) : resolve(result)));
});
}
redos-vulnerable-regex
A regular expression with catastrophic backtracking potential (e.g., nested quantifiers).
Severity: critical | Points: 8 | Gate: block
// VIOLATION: ReDoS-vulnerable regex
const emailRegex = /^([a-zA-Z0-9]+)*@[a-zA-Z0-9]+\.[a-zA-Z]+$/;
if (emailRegex.test(userInput)) { /* ... */ }
// FIX: Use a safe regex or a validation library
import { isEmail } from 'class-validator';
if (isEmail(userInput)) { /* ... */ }
busy-wait-loop
A while(true) or polling loop that does not yield the event loop (no await, no setTimeout).
Severity: critical | Points: 8 | Gate: block
// VIOLATION: Busy-wait loop
async waitForResult(id: string) {
while (true) {
const result = cache.get(id);
if (result) return result;
// No yield --- blocks the event loop
}
}
// FIX: Use setTimeout or an event-based approach
async waitForResult(id: string): Promise<Result> {
return new Promise((resolve) => {
const interval = setInterval(() => {
const result = cache.get(id);
if (result) {
clearInterval(interval);
resolve(result);
}
}, 100);
});
}
unbounded-json-parse
JSON.parse() called on user-controlled input without size validation.
Severity: critical | Points: 8 | Gate: block
// VIOLATION: Parsing arbitrary user input
@Post('import')
async importData(@Body('payload') payload: string) {
const data = JSON.parse(payload); // Could be 500MB
}
// FIX: Validate size before parsing
@Post('import')
async importData(@Body('payload') payload: string) {
if (payload.length > 1_000_000) {
throw new BadRequestException('Payload too large');
}
const data = JSON.parse(payload);
}
large-json-stringify
JSON.stringify() called inside a loop on potentially large objects.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
@Get('logs')
async getLogs() {
const logs = await this.getAll();
return logs.map(log => JSON.stringify(log)); // stringify in loop
}
// FIX: Stringify the entire result once, or stream
@Get('logs')
async getLogs() {
const logs = await this.getAll();
return logs; // Let the framework serialize once
}
cpu-heavy-loop-in-handler
A computationally expensive loop (nested loops, large iterations) inside a request handler.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
@Get('report')
async generateReport() {
const data = await this.getData();
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data.length; j++) {
// O(n^2) computation in handler
}
}
}
// FIX: Offload to a worker thread or background job
@Post('report')
async generateReport() {
const job = await this.reportQueue.add('generate', {});
return { jobId: job.id, status: 'processing' };
}
unbounded-array-operation
Array operations (.sort(), .filter(), .reduce()) on unbounded collections in a handler.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
@Get('sorted')
async getSorted() {
const allItems = await this.repo.findAll();
return allItems.sort((a, b) => a.score - b.score); // Sort in memory
}
// FIX: Sort in the database query
@Get('sorted')
async getSorted() {
return this.repo.findAll({ orderBy: { score: 'asc' }, take: 50 });
}
dynamic-buffer-alloc
Buffer.alloc() or Buffer.allocUnsafe() with a user-controlled size parameter.
Severity: critical | Points: 8 | Gate: block
// VIOLATION
@Post('download')
async download(@Body('size') size: number) {
const buffer = Buffer.alloc(size); // User controls allocation size
}
// FIX: Validate and cap the size
@Post('download')
async download(@Body('size') size: number) {
const maxSize = 10 * 1024 * 1024; // 10MB max
if (size > maxSize) throw new BadRequestException('Size too large');
const buffer = Buffer.alloc(size);
}
unhandled-promise
A promise that is neither awaited nor has a .catch() handler (fire-and-forget).
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
async createOrder(dto: CreateOrderDto) {
const order = await this.repo.save(dto);
this.emailService.sendConfirmation(order.email); // No await, no .catch()
return order;
}
// FIX: Await or add error handling
async createOrder(dto: CreateOrderDto) {
const order = await this.repo.save(dto);
await this.emailService.sendConfirmation(order.email);
return order;
}
Performance Violations
Severity scales with the data volume declared in radar.yml. XL/XXL tables produce critical violations that block merge.
n-plus-one-query
A database query executed inside a loop, producing N+1 query patterns.
Severity: critical (XL/XXL), warning (S/M/L) | Points: 8/3 | Gate: block on XL/XXL
// VIOLATION: N+1 query pattern
async getOrdersWithProducts(orderIds: string[]) {
const orders = await this.prisma.order.findMany({ where: { id: { in: orderIds } } });
for (const order of orders) {
order.products = await this.prisma.product.findMany({
where: { orderId: order.id },
});
}
return orders;
}
// FIX: Use include/join or batch query
async getOrdersWithProducts(orderIds: string[]) {
return this.prisma.order.findMany({
where: { id: { in: orderIds } },
include: { products: true },
});
}
unbounded-find-many
A findMany / find call without take/limit/skip on a table with significant data volume.
Severity: critical (XL/XXL), warning (M/L) | Points: 8/3 | Gate: block on XL/XXL
// VIOLATION: No pagination on XL table
const allEvents = await prisma.event.findMany();
// FIX: Add pagination
const events = await prisma.event.findMany({
take: 50,
skip: page * 50,
orderBy: { createdAt: 'desc' },
});
find-many-no-where
A findMany without any where clause on a table with declared volume.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
const users = await prisma.user.findMany({ orderBy: { name: 'asc' } });
// FIX: Add a where clause
const users = await prisma.user.findMany({
where: { active: true },
orderBy: { name: 'asc' },
take: 100,
});
nested-include-large-relation
Deeply nested include statements that eagerly load large related collections.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION: Deep nested include on large tables
const orders = await prisma.order.findMany({
include: {
customer: true,
items: {
include: {
product: {
include: { reviews: true }, // reviews table is XL
},
},
},
},
});
// FIX: Limit nested includes or paginate separately
const orders = await prisma.order.findMany({
include: {
customer: true,
items: { include: { product: true } },
},
take: 20,
});
// Fetch reviews separately with pagination
fetch-all-filter-in-memory
Fetching all records from the database and then filtering in application code.
Severity: critical (XL/XXL), warning (L) | Points: 8/3 | Gate: block on XL/XXL
// VIOLATION: Filter in memory
const allOrders = await prisma.order.findMany();
const activeOrders = allOrders.filter(o => o.status === 'active');
// FIX: Filter in the query
const activeOrders = await prisma.order.findMany({
where: { status: 'active' },
});
missing-pagination-endpoint
An API endpoint that returns a list without pagination support.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION: No pagination in endpoint
@Get('orders')
async getAllOrders() {
return this.prisma.order.findMany();
}
// FIX: Add pagination
@Get('orders')
async getOrders(
@Query('page') page = 1,
@Query('perPage') perPage = 20,
) {
const [data, total] = await Promise.all([
this.prisma.order.findMany({ skip: (page - 1) * perPage, take: perPage }),
this.prisma.order.count(),
]);
return { data, total, page, perPage };
}
unfiltered-count-large-table
A count() operation without a where clause on a large table.
Severity: warning (L), critical (XL/XXL) | Points: 3/8 | Gate: block on XL/XXL
// VIOLATION: Unfiltered count on XXL table
const total = await prisma.auditLog.count();
// FIX: Add a where clause or use an approximate count
const total = await prisma.auditLog.count({
where: { createdAt: { gte: thirtyDaysAgo } },
});
raw-sql-no-limit
A raw SQL query ($queryRaw, query(), raw()) without a LIMIT clause.
Severity: warning (M/L), critical (XL/XXL) | Points: 3/8 | Gate: block on XL/XXL
// VIOLATION: Raw SQL without LIMIT
const results = await prisma.$queryRaw`SELECT * FROM events WHERE type = 'click'`;
// FIX: Add LIMIT
const results = await prisma.$queryRaw`
SELECT * FROM events WHERE type = 'click' LIMIT 100
`;
Reliability Violations
unhandled-promise-rejection
A promise without .catch() or try/catch wrapping.
Severity: critical | Points: 5 | Gate: block
// VIOLATION
async processOrder(orderId: string) {
this.paymentService.charge(orderId); // No await, no .catch()
}
// FIX
async processOrder(orderId: string) {
try {
await this.paymentService.charge(orderId);
} catch (error) {
this.logger.error('Payment failed', error);
throw error;
}
}
missing-try-catch
An async function that performs I/O operations without error handling.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
async fetchUser(id: string) {
const user = await this.httpService.get(`/users/${id}`);
return user.data;
}
// FIX
async fetchUser(id: string) {
try {
const user = await this.httpService.get(`/users/${id}`);
return user.data;
} catch (error) {
this.logger.error(`Failed to fetch user ${id}`, error);
throw new NotFoundException(`User ${id} not found`);
}
}
external-call-no-timeout
An HTTP client call without a timeout configuration.
Severity: critical | Points: 5 | Gate: block
// VIOLATION: No timeout
const response = await axios.get('https://external-api.com/data');
// FIX: Add timeout
const response = await axios.get('https://external-api.com/data', {
timeout: 5000,
});
retry-without-backoff
Retry logic without exponential backoff, risking thundering herd.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION: Retry without backoff
async fetchWithRetry(url: string, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await axios.get(url);
} catch {
// Immediate retry - hammers the server
}
}
}
// FIX: Add exponential backoff
async fetchWithRetry(url: string, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await axios.get(url, { timeout: 5000 });
} catch (error) {
if (i === attempts - 1) throw error;
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
}
}
}
empty-catch-block
A catch block that silently swallows errors.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
try {
await this.save(data);
} catch (e) {
// empty - error silently ignored
}
// FIX: Log the error or rethrow
try {
await this.save(data);
} catch (error) {
this.logger.error('Failed to save data', error);
}
missing-error-logging
Error handling that does not include logging.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
try {
await this.processPayment(amount);
} catch (error) {
throw new InternalServerErrorException('Payment failed');
}
// FIX: Add logging before rethrowing
try {
await this.processPayment(amount);
} catch (error) {
this.logger.error('Payment processing failed', { error, amount });
throw new InternalServerErrorException('Payment failed');
}
transaction-no-timeout
A database transaction without a timeout, risking connection pool exhaustion.
Severity: critical | Points: 5 | Gate: block
// VIOLATION: No timeout on transaction
await prisma.$transaction(async (tx) => {
await tx.order.update({ where: { id }, data: { status: 'shipped' } });
await tx.inventory.decrement({ where: { productId }, data: { quantity } });
});
// FIX: Add transaction timeout
await prisma.$transaction(async (tx) => {
await tx.order.update({ where: { id }, data: { status: 'shipped' } });
await tx.inventory.decrement({ where: { productId }, data: { quantity } });
}, { timeout: 10000 });
missing-null-guard
Accessing properties on potentially null/undefined values without checking.
Severity: warning | Points: 3 | Gate: warn
// VIOLATION
async getOrderTotal(orderId: string) {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
return order.total; // order could be null
}
// FIX: Add null check
async getOrderTotal(orderId: string) {
const order = await this.prisma.order.findUnique({ where: { id: orderId } });
if (!order) {
throw new NotFoundException(`Order ${orderId} not found`);
}
return order.total;
}
Maintainability Violations
Maintainability violations produce warnings by default. They can be configured to block in radar.yml.
high-complexity
A function's cyclomatic complexity exceeds the threshold (default: 10).
Severity: warning | Points: 1 per point above threshold | Gate: warn
// VIOLATION: Cyclomatic complexity = 14
function processOrder(order: Order) {
if (order.type === 'digital') {
if (order.total > 100) {
if (order.customer.isPremium) { /* ... */ }
else if (order.customer.isNew) { /* ... */ }
else { /* ... */ }
} else {
if (order.hasDiscount) { /* ... */ }
else { /* ... */ }
}
} else if (order.type === 'physical') {
if (order.requiresShipping) {
if (order.isInternational) { /* ... */ }
else { /* ... */ }
}
} else if (order.type === 'subscription') { /* ... */ }
}
// FIX: Extract into strategy pattern or smaller functions
function processOrder(order: Order) {
const processors: Record<string, OrderProcessor> = {
digital: new DigitalOrderProcessor(),
physical: new PhysicalOrderProcessor(),
subscription: new SubscriptionProcessor(),
};
return processors[order.type].process(order);
}
code-duplication
Significant code blocks duplicated across files.
Severity: warning | Points: 3 | Gate: warn
missing-test-file
A source file has no corresponding test file (*.spec.ts or *.test.ts).
Severity: warning | Points: 3 | Gate: warn
unused-export
An exported function, class, or constant that is not imported anywhere in the codebase.
Severity: info | Points: 0 | Gate: none
coverage-drop
Test coverage for a file dropped compared to the baseline.
Severity: warning | Points: 2 per percentage point dropped | Gate: warn